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