脱壳手记---Themida(2.1.2.0)
“衣带渐宽终不悔,为伊消得人憔悴”
------柳永《蝶恋花》
王国维先生曾以此比喻治学之第二境,我想以此与仍然苦苦挣扎在KSSD大教室的诸君共勉!
然则高版本的脱壳往往是建立在低版本之上,如果你对低版本已经有所了解可以忽略此处,如果需要了解可以看这里http://bbs.pediy.com/showthread.php?t=172921 或移步KSSD
好了,进入正题!
上OD先直接F9跑一次。
程序结束后,将堆栈往上拉发现(图1):
有个返回到00401510,去反汇编窗口看看:
代码:
0040147E 8AD4 MOV DL,AH
00401480 8915 F4844000 MOV DWORD PTR DS:[4084F4],EDX
00401486 8BC8 MOV ECX,EAX
00401488 81E1 FF000000 AND ECX,0FF
0040148E 890D F0844000 MOV DWORD PTR DS:[4084F0],ECX
00401494 C1E1 08 SHL ECX,8
00401497 03CA ADD ECX,EDX
00401499 890D EC844000 MOV DWORD PTR DS:[4084EC],ECX
0040149F C1E8 10 SHR EAX,10
004014A2 A3 E8844000 MOV DWORD PTR DS:[4084E8],EAX
004014A7 6A 00 PUSH 0
004014A9 E8 A80A0000 CALL sayhello.00401F56
004014AE 59 POP ECX
004014AF 85C0 TEST EAX,EAX
004014B1 75 08 JNZ SHORT sayhello.004014BB
004014B3 6A 1C PUSH 1C
004014B5 E8 9A000000 CALL sayhello.00401554
004014BA 59 POP ECX
004014BB 8365 FC 00 AND DWORD PTR SS:[EBP-4],0
004014BF E8 72070000 CALL sayhello.00401C36
004014C4 E8 A0FA2E02 CALL 026F0F69
004014C9 90 NOP
004014CA A3 F8894000 MOV DWORD PTR DS:[4089F8],EAX
004014CF E8 30060000 CALL sayhello.00401B04
004014D4 A3 D0844000 MOV DWORD PTR DS:[4084D0],EAX
004014D9 E8 D9030000 CALL sayhello.004018B7
004014DE E8 1B030000 CALL sayhello.004017FE
004014E3 E8 90000000 CALL sayhello.00401578
004014E8 A1 04854000 MOV EAX,DWORD PTR DS:[408504]
004014ED A3 08854000 MOV DWORD PTR DS:[408508],EAX
004014F2 50 PUSH EAX
004014F3 FF35 FC844000 PUSH DWORD PTR DS:[4084FC]
004014F9 FF35 F8844000 PUSH DWORD PTR DS:[4084F8]
004014FF E8 FCFAFFFF CALL sayhello.00401000
00401504 83C4 0C ADD ESP,0C
00401507 8945 E4 MOV DWORD PTR SS:[EBP-1C],EAX
0040150A 50 PUSH EAX
0040150B E8 95000000 CALL sayhello.004015A5
00401510 8B45 EC MOV EAX,DWORD PTR SS:[EBP-14]
00401513 8B08 MOV ECX,DWORD PTR DS:[EAX]
00401515 8B09 MOV ECX,DWORD PTR DS:[ECX]
00401517 894D E0 MOV DWORD PTR SS:[EBP-20],ECX
0040151A 50 PUSH EAX
0040151B 51 PUSH ECX
0040151C E8 59010000 CALL sayhello.0040167A
00401521 59 POP ECX
00401522 59 POP ECX
00401523 C3 RETN
可以看出这是一段VC程序的伪OEP,头部被偷取了一些字节
进入CALL sayhello.00401000(图2)
和低版本一样,call都做了处理。
观察数据窗口,我们可以判断00405000为Iat段起始(图3):
好了,重新载入OD,在00405000下内存写入断点:
代码:
004487A0 8F02 POP DWORD PTR DS:[EDX] ;
断在004487A0(这里是VM代码)(图4)
看堆栈返回(图5)
发现调用代码为:
代码:
004C26F9 FF95 7A278906 CALL DWORD PTR SS:[EBP+689277A]
记得低版本的时候,它需要一张表,然后依据表中的数据分别取得原API地址,
IAT地址和CALLAPI地址,这里是不是一样呢,我们在此处下个断点一看便知。
F9后我们查看[esi]发现了非常熟悉的一张表(图6):
找到了这里,我们知道它要取得各种地址就一定需要这里的数据。可以下内存访问断点后跟踪,也可以直接从004C26F9这个断点跟踪,都一样,因为它的处理过程必是一个循环的过程。
那么我们直接跟踪吧,先F8跳过这个CALL,暂时不去关心VM,先弄清楚流程。
很明显这个CALL之后,IAT填入了处理后的地址(图7)
同时,我们的[esi]表也变化了(图8):
可以看出至少获取callapi的步骤也是在VM里面完成了。继续单步,就快到下一个API的处理了,是的,就在这个”DDDDDDDD”之后。单步的过程便不详述,跟低版本相差无几,只不过增加了些垃圾代码。不一会便来到:
代码:
004BF68C C785 2D1D8206 0>MOV DWORD PTR SS:[EBP+6821D2D],0
004BF696 C785 311B8206 0>MOV DWORD PTR SS:[EBP+6821B31],0
004BF6A0 83BD 00B48C06 0>CMP DWORD PTR SS:[EBP+68CB400],0
非常熟悉的循环起始,和低版本一样,下面这段校验也需要修改:
代码:
004BF6E3 83BD 21158206 6>CMP DWORD PTR SS:[EBP+6821521],64
004BF6EA 0F82 09010000 JB sayhello.004BF7F9 ;这里修改成jmp跳过校验
以下一段判断[esi]表处理完毕:
代码:
004BF893 3D EEEEEEEE CMP EAX,EEEEEEEE
004BF898 0F85 AB000000 JNZ sayhello.004BF949
004BF89E F5 CMC
004BF89F 813E DDDDDDDD CMP DWORD PTR DS:[ESI],DDDDDDDD
004BF8A5 0F85 9E000000 JNZ sayhello.004BF949
004BF8AB F8 CLC
004BF8AC F8 CLC
004BF8AD 50 PUSH EAX
004BF8AE B8 00000000 MOV EAX,0
004BF8B3 8906 MOV DWORD PTR DS:[ESI],EAX
004BF8B5 8B0424 MOV EAX,DWORD PTR SS:[ESP]
004BF8B8 83C4 04 ADD ESP,4
004BF8BB E9 08000000 JMP sayhello.004BF8C8
004BF8C0 53 PUSH EBX
004BF8C1 F4 HLT ; 特权命令
004BF8C2 213D 7ADF2136 AND DWORD PTR DS:[3621DF7A],EDI
004BF8C8 52 PUSH EDX
004BF8C9 BA 04000000 MOV EDX,4
004BF8CE 81C6 5C04AE28 ADD ESI,28AE045C
004BF8D4 01D6 ADD ESI,EDX
004BF8D6 81EE 5C04AE28 SUB ESI,28AE045C
004BF8DC 5A POP EDX
004BF8DD E9 0F000000 JMP sayhello.004BF8F1
下面取得[esi]表的值:
代码:
004BF85C AD LODS DWORD PTR DS:[ESI]
继续跟踪到了一个循环:
代码:
004BFC57 3B02 CMP EAX,DWORD PTR DS:[EDX]
004BFC59 0F84 6F000000 JE sayhello.004BFCCE ;找到KEY跳出循环
004BFC5F 0F89 1E000000 JNS sayhello.004BFC83
004BFC65 60 PUSHAD
004BFC66 E8 14000000 CALL sayhello.004BFC7F
004BFC6B AB STOS DWORD PTR ES:[EDI]
004BFC6C F2: PREFIX REPNE: ; 多余前缀
004BFC6D 93 XCHG EAX,EBX
004BFC6E 6B30 A8 IMUL ESI,DWORD PTR DS:[EAX],-58
004BFC71 B6 FD MOV DH,0FD
004BFC73 D19E 2B586176 RCR DWORD PTR DS:[ESI+7661582B],1
004BFC79 B7 07 MOV BH,7
004BFC7B 05 CCF02A5A ADD EAX,5A2AF0CC
004BFC80 60 PUSHAD
004BFC81 61 POPAD
004BFC82 61 POPAD
004BFC83 57 PUSH EDI
004BFC84 893424 MOV DWORD PTR SS:[ESP],ESI
004BFC87 BE 04000000 MOV ESI,4
004BFC8C 01F2 ADD EDX,ESI
004BFC8E 5E POP ESI
004BFC8F FC CLD
004BFC90 52 PUSH EDX
004BFC91 BA 01000000 MOV EDX,1
004BFC96 01D1 ADD ECX,EDX
004BFC98 5A POP EDX
004BFC99 F9 STC
004BFC9A 3B8D B8BF8B06 CMP ECX,DWORD PTR SS:[EBP+68BBFB8]
004BFCA0 ^ 0F85 B1FFFFFF JNZ sayhello.004BFC57
根据低版本的流程,这里是要获取一个偏移值,每个API对应不同的偏移,
这张偏移表可以看做存的是对应API的解密KEY由TMD维护,跟我们脱壳关系不大,我们在004BFCCE下硬件执行断点跳过循环
代码:
004BFCCE 898D E11C8206 MOV DWORD PTR SS:[EBP+6821CE1],ECX
在这行代码后出现了原始api地址:
代码:
004BFED7 01C8 ADD EAX,ECX
004BFED9 2D 190F5568 SUB EAX,68550F19
有的模块的api在此处还只是一个字符串,继续单步一会:
代码:
004C026C 61 POPAD
004C026D FFD3 CALL EBX ;经过这个CALL后得到API原始地址
004C026F 60 PUSHAD ;此时eax为API地址
取得API地址后,它会对API进行处理,处理过程会放在VM中,但是也有一些函数不做处理,那么它将有一个判断,我们继续单步寻找这个判断(单步中会经历一个TMD在自己申请的地址重新构造API的过程,和低版本思路一样,这里就不详述了)。
经历漫长的单步后来到下面(当然,直接在开始的返回后向上找很快就会找到,当然顺序下来有利于我们对流程的了解)
代码:
004C26DF 83BD E9B48C06 0>CMP DWORD PTR SS:[EBP+68CB4E9],0
004C26E6 0F85 91000000 JNZ sayhello.004C277D
这个时候我们还没有发现IAT的地址,可见它也在是VM中处理的。
到VM这里的流程清楚了,我们先直接看看不加密的情况,
004C26DF处下断点,断不加密条件我们开始单步:
代码:
004C279A 0385 AD108206 ADD EAX,DWORD PTR SS:[EBP+68210AD] ;eax为IAT地址
004C27A0 F5 CMC
然后是对CALLAPI地址的处理,和低版本一样,有对”90”填充的处理,我们都可以不管,因为我们将对CALLAPI重新填充:
004C291C 0385 AD108206 ADD EAX,DWORD PTR SS:[EBP+68210AD] ;这里[EAX]callapi地址
004C2922 F9 STC
004C2923 83BD F9148206 0>CMP DWORD PTR SS:[EBP+68214F9],1
004C292A 0F84 F1040000 JE sayhello.004C2E21
004C293E 61 POPAD ;这里[EAX]是CALLAPI地址
004C293F 813E AAAAAAAA CMP DWORD PTR DS:[ESI],AAAAAAAA ;比较是否填充FF25
---------------------------------------------------------------------------------------------------------------------------------
004C293F 也是我们需要的一个断点
代码:
004C2DF4 AB STOS DWORD PTR ES:[EDI] ;这里CALLAPI处理完毕,可以重新写回去
004C2DF5 60 PUSHAD
通过以上的步骤,整个流程我们就清楚了,我们下面就来看看VM吧。所谓VM我理解的是它将原本的指令作了新的约定,并按照新的约定执行变形的代码来替换或者说”等价”的完成原先的指令集,它的每一个新约定指令都有一个指令序列来实现。
稍微跟踪一下,可以发现以下两条重要的代码
代码:
00442221 AC LODS BYTE PTR DS:[ESI] ;这里取得一个VM字节,将通过对这个字节进行计算,获取指令序列的跳转
00442222 30D8 XOR AL,BL
代码:
00447B00 /FF2487 JMP DWORD PTR DS:[EDI+EAX*4] ; 跳转到指令序列
00447B03 |61 POPAD
00447B04 |C3 RETN
跟到[edi]发现一个指令序列表(图9):
当然要仔细分析的话,可以记录以上两个断点比较。当然我们现在不去分析它,我们主要是为了脱壳。
跟踪发现几个关键数据写入都通过
004487A0 8F02 POP DWORD PTR DS:[EDX] ;
通过打印脚本分析004487a0,记录[esp]和edx(因为它们将出现关键数据,这个再跟踪过程中可以发现,TMD差不多都这样,当然不同的VM引擎是可以设置不一样的,具体可以移步KSSD-->保护篇-->虚拟机)
跟踪脚本:
代码:
var vedx
var vesp
var x
bphwc
bphws 004487a0 ,"x"
bphws 004c0169,"x"
mov x,0
esto
loop:
cmp eip,004487a0
jne exit
mov vedx,edx
mov vesp,[esp]
inc x
log x
log vedx
log vesp
cmp x,15
je exit
esto
jmp loop
exit:
ret
通过观察记录得到以下数据:
第0a次出现混淆后的API地址
第016次计算出iat的RVA
第017次计算出iat的VA
第019次填充iat
第0x1f次计算出callapi地址
第0x25次填充callpi偏移 ,填充e8 e9的代码不在此处 须另外寻找
第0x26次比较esi的下一个元素看是否为ffffffff,如果不是则证明还有需要填充的callapi
如果为ffffffff则将在第0x28次之后进入下一个循环
如果不为ffffffff则将在第0x2b处计算出下一个callapi地址
同样6次断之后在第0x31次填充callapi地址
好了 下面分析计算iat的代码 在第0x16次后下断点:
代码:
00444DB0 311C24 XOR DWORD PTR SS:[ESP],EBX
00444DB3 331C24 XOR EBX,DWORD PTR SS:[ESP]
00444DB6 8B2424 MOV ESP,DWORD PTR SS:[ESP]
00444DB9 010424 ADD DWORD PTR SS:[ESP],EAX ; sayhello.00400000
经过跟踪发现00444DB9处代码计算出iat地址,此时iat地址存放在[esp]处
然后继续分析callapi偏移
在第0x1e次后下断点跟踪:
发现写入callapi的地址也是在00444DB9处
可以在虚拟机段找这段特征码(不过不必过于相信特征码,版本不同会略有差异,但方法都是一样的)
下面继续分析比较ffffffff在第0x26次断点后分析结果跟踪到此处:
比较ecx 和eax ,相等则元素结束了不相等证明还有callapi
代码:
0044D099 8B2424 MOV ESP,DWORD PTR SS:[ESP]
0044D09C 3BC8 CMP ECX,EAX
下断点00444DB9 加硬件断点004C026F测试一下
发现00444DB9此处需要判断条件eax==00400000时才得到IAT或者CALLAPI地址,且第一次是IAT地址 (其实就这一个断点已经可以处理VM的IAT地址,第一个是IAT,后面都是callapi地址)
0044D09C 也要条件 eax==ffffffff eax!=ffffffff则证明还有未处理的CALLAPI ,如果相等则证明当前API元素已处理完。
有了以上的条件,我们就可以写脚本来完成脱壳了。
这样整理一下主要断点:
004C026F ----------取得API原始地址
Case1:
004C26F9 ---------这里进入VM,部分函数的IAT和CALLAPI获取填充都在这里实现
Case2:
004C27A0 --------这里断下来则是不需要加密的API的IAT,值在eax中
--------用来做判断之后需要增加一个辅助断点取IAT,单步几行代码找到
004c27ce
Case_VM:
00444DB9 ---------这里断下后通过判断eax==00400000,外加一个数字标识来区分取得的IAT还是callapi
等待执行完毕直接填充正确IAT和FF15call
Case_NOTVM:
004C293F ---------------------这里取得callapi值
004C2DF5 --------------这里填充完毕,重新写回去
代码:
var api
var iat
var callapi
var n
bc
bphwc
bphws 004c026f,"x" ;取得原始API地址
esto
bp 004BF8DD ;处理完毕,可以调到OEP,当然也可以不要这个断点,因为OEP在前面可以通过退出法已经找到了
bp 004C26F9 ;进入VM
bp 00444DB9 ;VM中得到IAT和CALLAPI地址
bp 004C27A0 ;不加密的IAT
bp 004C293F ;不加密的CALLAPI
bp 004C2DF5 ;CALLAPI填充完毕,重新写回
bp 004c27ce ;因为004C27A0作为了是否加密的跳转条件,因此在下面增加一个断点来取不加密的IAT
mov [004bf6ea],#E90A01000090# ;校验处改为JMP略过
loop:
cmp eip,004c026f
jne swtich
mov api,eax
mov n,0 ;判断VM中的IAT和CALLAPI
esto
jmp loop
swtich: ;分别加密处理的和不加密处理的
cmp eip,004C27A0
je notvm
cmp eip,004C26F9
je vm
jmp exit
notvm:
cmp eip,004C27A0
jne filliat
mov iat,eax
esto
jmp notvm
filliat:
cmp eip,004C27CE
jne callapi
mov [iat],api ;得到IAT,有些加密API也在这里处理IAT
esto
jmp notvm
callapi:
cmp eip,004C293F
jne fillcallapi
mov callapi,eax ;得到CALLAPI地址
esto
jmp notvm
fillcallapi:
cmp eip,004C2DF5
jne loop
find callapi,#E9#,2 ;比较是否填充FF25
cmp $RESULT,0
jne fillFF25A
mov [callapi],#ff15#
add callapi,2
mov [callapi],iat
esto
jmp notvm
fillFF25A:
mov [callapi],#ff25#
add callapi,2
mov [callapi],iat
esto
jmp notvm
vm:
esto
cmp eip,00444DB9
jne loop
cmp eax,00400000 ;条件eax==00400000时才可能是ITA或者CALLAPI
jne vm
inc n
cmp n,1 ;n来判断当前是处理IAT还是处理CALLAPI
jne callapifun
mov iat,[esp]
add iat,00400000 ;得到IAT或者CALLAPI的VA
bp 004487a0 ;新增断点,此处的代码将填充IAT
esto
esto
esto
esto
mov [iat],api ;四次F9后IAT填充完毕
BC 004487a0
jmp vm
callapifun:
mov callapi,[esp]
add callapi,00400000
bp 004487a0 ;新增断点,此处的代码将填充CALLAPI
esto
esto
esto
esto
esto
esto
esto
esto
esto
esto ;十次F9后CALLAPI填充完毕
find callapi,#E9#,2 ;比较是否填充FF25
cmp $RESULT,0
bc 004487a0
jne fillFF25b
mov [callapi],#ff15#
add callapi,2
mov [callapi],iat
jmp vm
fillFF25b:
mov [callapi],#ff25#
add callapi,2
mov [callapi],iat
jmp vm
exit:
bc
bphwc
bprm 00401000,4000 ;处理完毕后下内存访问断点,将断在OEP
esto
ret
后面按照常规方法恢复OEP即可,和低版本一样。TMD版本会有所不同,加壳方式也会略有差异,
但总体流程都是这样,按照这个思路去分析,大体脱壳是没有问题的!
2013年6月5日