脱壳系列_2_IAT加密壳_详细分析(含脚本)
1 查看壳程序信息
-
使用ExeInfoPe
分析:
发现这个壳的类型没有被识别出来,Vc 6.0倒是识别出来了,Vc 6.0的特征是 入口函数先调用GetVersion()
2 用OD找OEP
- 拖进OD
发现 这个壳和我们的正常程序很像。但是并不是我们的真正程序入口
-
因为vc6.0特征的第一个调用的是GetVersion(),给GetVersion()下 硬件断点
//第一次断下来,但是根据栈回溯,调用者并不是我们的模块
//第二次断下来,就应该是了
//找到入口后 栈上右键 反汇编窗口跟随
//如下
-
在OD看内存布局,一般.rdata的最前面是放的IAT,而且OD数据窗口默认就是.rdata的起始位置。
-
也可以点一个call /jmp [];看一下来找IAT表
-
-
对那个地方下一个硬件写入断点 --DWORD,即当前面的壳程序在修改的时候就能段下来找到壳的加密算法的地方
- 这里只是为了快速脱壳所以下硬件断点,快速定位加密修改IAT的地方,但后面部分将对整个壳详细分析:。
3 对壳详细分析
这个分析的过程,需要自己去啃,是分享不了的。在分析的时候遇到不知道的变量、地址这些先标记留着,后面分析着分析着就知道了。
00438450 > 55 PUSH EBP 00438451 8BEC MOV EBP,ESP 00438453 83EC 0C SUB ESP,0xC 00438456 E8 45FFFFFF CALL 02.004383A0 0043845B A1 40804300 MOV EAX,DWORD PTR DS:[<程序基址>] 00438460 0305 44804300 ADD EAX,DWORD PTR DS:[<代码段偏移>] 00438466 8945 F8 MOV DWORD PTR SS:[EBP-0x8],EAX 00438469 C745 FC 00000000 MOV DWORD PTR SS:[EBP-0x4],0x0 00438470 8D4D FC LEA ECX,DWORD PTR SS:[EBP-0x4] 00438473 51 PUSH ECX 00438474 6A 40 PUSH 0x40 00438476 8B15 4C804300 MOV EDX,DWORD PTR DS:[0x43804C] 0043847C 52 PUSH EDX 0043847D 8B45 F8 MOV EAX,DWORD PTR SS:[EBP-0x8] 00438480 50 PUSH EAX 00438481 FF15 C0924300 CALL DWORD PTR DS:[<virtualProtect>] ; kernel32.VirtualProtect 00438487 E8 04FEFFFF CALL 02.00438290 0043848C 8D4D FC LEA ECX,DWORD PTR SS:[EBP-0x4] 0043848F 51 PUSH ECX 00438490 8B55 FC MOV EDX,DWORD PTR SS:[EBP-0x4] 00438493 52 PUSH EDX 00438494 A1 4C804300 MOV EAX,DWORD PTR DS:[0x43804C] 00438499 50 PUSH EAX 0043849A 8B4D F8 MOV ECX,DWORD PTR SS:[EBP-0x8] 0043849D 51 PUSH ECX 0043849E FF15 C0924300 CALL DWORD PTR DS:[<virtualProtect>] ; kernel32.VirtualProtect 004384A4 6A 04 PUSH 0x4 004384A6 68 2C814300 PUSH 02.0043812C ; Hello 15PB 004384AB 68 38814300 PUSH 02.00438138 ; 欢迎使用免费加壳程序,是否运行主程序? 004384B0 6A 00 PUSH 0x0 004384B2 FF15 BC924300 CALL DWORD PTR DS:[<user.MessageBoxA>] ; user32.MessageBoxA 004384B8 8945 F4 MOV DWORD PTR SS:[EBP-0xC],EAX 004384BB 837D F4 06 CMP DWORD PTR SS:[EBP-0xC],0x6 ; 当点击提示框的 IDYES 后 跳转 004384BF 75 0B JNZ SHORT 02.004384CC ; 如果不是 IDYES 那么就跳到退出程序标签 004384C1 E8 1A000000 CALL <02.选择IDYES_THEN> 004384C6 - FF25 3C804300 JMP DWORD PTR DS:[<拟定的真实入口>] ; 02.00409486 004384CC 6A 00 PUSH 0x0 004384CE FF15 B8924300 CALL DWORD PTR DS:[<MessageBoxA>] ; kernel32.ExitProcess 004384D4 8BE5 MOV ESP,EBP 004384D6 5D POP EBP 004384D7 C3 RETN 004384D8 CC INT3 004384D9 CC INT3 004384DA CC INT3 004384DB CC INT3 004384DC CC INT3 004384DD CC INT3 004384DE CC INT3 004384DF CC INT3 004384E0 > 53 PUSH EBX ; 1. 将上一个函数的EBX保存 004384E1 8BDC MOV EBX,ESP ; /将当前main栈顶 保存到 EBX 004384E3 83EC 08 SUB ESP,0x8 ; 2.开辟 8字节的局部空间 作用:将esp 4位对齐 ↓ 004384E6 83E4 F0 AND ESP,0xFFFFFFF0 ; /将ESP -- 4位 对齐 004384E9 83C4 04 ADD ESP,0x4 ; /平4bytes,这三句的作用:将原来的ESP 4位对齐 ↑ 004384EC 55 PUSH EBP ; 3.压入main的栈底 004384ED 8B6B 04 MOV EBP,DWORD PTR DS:[EBX+0x4] ; 4.将return 地址给 EBP 004384F0 896C24 04 MOV DWORD PTR SS:[ESP+0x4],EBP ; 5.将ebp再赋值给return,这两句其实是 mov esp+0x4,ebx+0x4. 也就是将原来未4位对齐之前的return 地址赋值给对齐之后理论应该存放(原对应)的地址 004384F4 8BEC MOV EBP,ESP ; *1 平栈,开辟新的栈帧 004384F6 83EC 48 SUB ESP,0x48 ; *2 开辟局部空间 004384F9 A1 20804300 MOV EAX,DWORD PTR DS:[<第一个未知使用:0x4384f9 -- [0x438020]>] ; 0x438020 -- 是啥? 74062457 -->eax 004384FE 33C5 XOR EAX,EBP ; 和return 地址异或 -- 应该是 判断是否相等 00438500 8945 FC MOV DWORD PTR SS:[EBP-0x4],EAX ; 把 函数返回地址 xor 未知数相与之后 -》.local1 像在算cookie 一样 00438503 56 PUSH ESI ; 保存ESI 00438504 8B35 40804300 MOV ESI,DWORD PTR DS:[<程序基址>] ; 把程序基地址 放入ESI 0043850A 8D45 C8 LEA EAX,DWORD PTR SS:[EBP-0x38] ; & local.14 -->eax 0043850D 57 PUSH EDI ; 保存EDI 0043850E 8B3D 54804300 MOV EDI,DWORD PTR DS:[<遍历的动态偏移>] ; 将[0X43805]放入 EDI 当前是:28c00 看样子是一个PE偏移RVA 00438514 50 PUSH EAX ; 压入EAX 即 &local.14 : 经过后面分析 这是用来保存以前.rdata区段属性的局部变量 00438515 A1 5C804300 MOV EAX,DWORD PTR DS:[<.rdata的RVA>] ; 将[0x43805c]放入EAX 当前是 22000 -- .rdata数据段RVA 0043851A 6A 40 PUSH 0x40 ; push 0x40 0043851C FF35 60804300 PUSH DWORD PTR DS:[<.rdata的SIZE>] ; 退 00438522 03C6 ADD EAX,ESI ; .rdata的真实VA eax = imageBase + EAX ---*****----到这儿恍然大悟:前面的局部数据是区段信息rva 、size 00438524 8975 CC MOV DWORD PTR SS:[EBP-0x34],ESI ; local.13 程序的基地址 00438527 50 PUSH EAX ; .rdata的VA 即IAT数组的地址 -- push &IAT 00438528 C745 C8 00000000 MOV DWORD PTR SS:[EBP-0x38],0x0 ; local.14 = 0 0043852F FF15 C0924300 CALL DWORD PTR DS:[<virtualProtect>] ; kernel32.VirtualProtect 00438535 833C37 00 CMP DWORD PTR DS:[EDI+ESI],0x0 ; 比较在程序虚拟空间偏移为0x28c00的地方的值 是否为0 00438539 0F84 D7000000 JE <02.遍历结束恢复区段保护属性> 0043853F 8B55 CC MOV EDX,DWORD PTR SS:[EBP-0x34] 00438542 83C6 10 ADD ESI,0x10 ; 程序基地址 + 16个字节? IMPORT_DESCRIPTOR 的FirstThunk字段的起始地址,也就是 IAT的指针 00438545 03F7 ADD ESI,EDI ; 程序基地址 + DLL模块 当前的偏移 00438547 8975 C4 MOV DWORD PTR SS:[EBP-0x3C],ESI ; 把IAT 的地址 放入 local.15 0043854A 8D9B 00000000 LEA EBX,DWORD PTR DS:[EBX] ; 这句 》混淆视听? 00438550 8B46 FC MOV EAX,DWORD PTR DS:[ESI-0x4] ; dllName字段的RVA 00438553 03C2 ADD EAX,EDX ; dllName的VA 00438555 50 PUSH EAX ; 获取模块基址 第一个dll 从010Editor中查看来是 KERNEL32.DLL 00438556 FF15 C8924300 CALL DWORD PTR DS:[<LoadLibraryA>] ; kernel32.LoadLibraryA 0043855C 8B3E MOV EDI,DWORD PTR DS:[ESI] ; IAT表的RVA 放在EDI 0043855E 8B55 CC MOV EDX,DWORD PTR SS:[EBP-0x34] ; 程序基地址 放入 EDX 00438561 03FA ADD EDI,EDX ; IAT 表的起始VA 00438563 8945 C0 MOV DWORD PTR SS:[EBP-0x40],EAX ; DLL 模块基地址 放在 local.16的位置上 00438566 8B0F MOV ECX,DWORD PTR DS:[EDI] ; 获取第一个IAT 项 -- 放在 ECX 00438568 85C9 TEST ECX,ECX ; 判断是否为0 ,为0 就跳转当前模块的IAT 遍历结束 0043856A 0F84 93000000 JE <02.模块遍历结束> 00438570 8BF7 MOV ESI,EDI 00438572 > 8B07 MOV EAX,DWORD PTR DS:[EDI] ; 还是第一个IAT 项 -- 第一个API地址 --》EAX 和前面ECX一样 00438574 83C0 02 ADD EAX,0x2 ; EAX +=2 --- 函数地址 +=2 跳过import_by_name结构体 Hint字段 直接是 name 字段的地址 00438577 85C9 TEST ECX,ECX 00438579 78 75 JS SHORT <02.序号导出的函数> ; 判断最高位是否为1 SF =1 则代表着是序号导出 0043857B 03C2 ADD EAX,EDX ; 如果不是序号导出 计算函数名称的 VA 0043857D C745 D0 E8010000 MOV DWORD PTR SS:[EBP-0x30],0x1E8 00438584 50 PUSH EAX ; 函数字符串的地址 --押入站 00438585 FF75 C0 PUSH DWORD PTR SS:[EBP-0x40] ; dll模块基地址 入站 00438588 C745 D4 00E958EB MOV DWORD PTR SS:[EBP-0x2C],0xEB58E900 0043858F 66:C745 D8 01E8 MOV WORD PTR SS:[EBP-0x28],0xE801 ; 0x25在下面一点,用来写入GetProcAddress的返回地址 00438595 C645 DA B8 MOV BYTE PTR SS:[EBP-0x26],0xB8 00438599 C745 DF EB011535 MOV DWORD PTR SS:[EBP-0x21],0x351501EB 004385A0 C745 E3 15151515 MOV DWORD PTR SS:[EBP-0x1D],0x15151515 004385A7 C745 E7 EB01FF50 MOV DWORD PTR SS:[EBP-0x19],0x50FF01EB 004385AE C745 EB EB02FF15 MOV DWORD PTR SS:[EBP-0x15],0x15FF02EB 004385B5 C645 EF C3 MOV BYTE PTR SS:[EBP-0x11],0xC3 004385B9 FF15 CC924300 CALL DWORD PTR DS:[<GetProcAddress>] ; kernel32.GetProcAddress 004385BF 6A 40 PUSH 0x40 004385C1 68 00300000 PUSH 0x3000 ; 当IDYES 后那个CALL结束后 004385C6 6A 20 PUSH 0x20 004385C8 35 15151515 XOR EAX,0x15151515 004385CD 6A 00 PUSH 0x0 004385CF 8945 DB MOV DWORD PTR SS:[EBP-0x25],EAX ; 放入地址 -- 0x25刚好是用来存放地址的 004385D2 FF15 B4924300 CALL DWORD PTR DS:[<VirtualAlloc>] ; 申请32字节来存放硬编码(除了那4个地址字节都是死的,) 004385D8 F30F6F45 D0 MOVDQU XMM0,DQWORD PTR SS:[EBP-0x30] 004385DD 8B55 CC MOV EDX,DWORD PTR SS:[EBP-0x34] 004385E0 F30F7F00 MOVDQU DQWORD PTR DS:[EAX],XMM0 004385E4 F30F6F45 E0 MOVDQU XMM0,DQWORD PTR SS:[EBP-0x20] 004385E9 F30F7F40 10 MOVDQU DQWORD PTR DS:[EAX+0x10],XMM0 ; 这几句是把解密IAT的opcode 写入申请的内存中 004385EE 8907 MOV DWORD PTR DS:[EDI],EAX ; 申请的内存地址 放入IAT中 004385F0 > 8B4E 04 MOV ECX,DWORD PTR DS:[ESI+0x4] ; 下一个IAT表项 004385F3 83C6 04 ADD ESI,0x4 ; esi 此时指向下一个 IAT表项 004385F6 8BFE MOV EDI,ESI ; 把EDI 指向下一个表项 004385F8 85C9 TEST ECX,ECX ; 判断是否结束 004385FA ^ 0F85 72FFFFFF JNZ <02.IAT 遍历循环起始处> ; 还没有结束的时候跳 回去继续遍历IAT↑ 00438600 8B75 C4 MOV ESI,DWORD PTR SS:[EBP-0x3C] ; IAT 如果结束了 就把上一个IMP_DESCRIPTOR的最后一个地址,放入ESI 00438603 > 83C6 14 ADD ESI,0x14 ; ESI + 一个IMP_DESCRIPTOR结构体的大小 相当于解析下一个dll 00438606 8975 C4 MOV DWORD PTR SS:[EBP-0x3C],ESI ; 再把新的当前的位置存回去0x3c -- local.15的位置 00438609 837E F0 00 CMP DWORD PTR DS:[ESI-0x10],0x0 0043860D ^ 0F85 3DFFFFFF JNZ 02.00438550 00438613 8B75 CC MOV ESI,DWORD PTR SS:[EBP-0x34] 00438616 > 8D45 C8 LEA EAX,DWORD PTR SS:[EBP-0x38] 00438619 50 PUSH EAX ; 把 原来的区段属性弹出 0043861A FF75 C8 PUSH DWORD PTR SS:[EBP-0x38] 0043861D A1 5C804300 MOV EAX,DWORD PTR DS:[<.rdata的RVA>] 00438622 FF35 60804300 PUSH DWORD PTR DS:[<.rdata的SIZE>] ; 退 00438628 03C6 ADD EAX,ESI 0043862A 50 PUSH EAX 0043862B FF15 C0924300 CALL DWORD PTR DS:[<virtualProtect>] ; 恢复原来的区段属性 00438631 8B4D FC MOV ECX,DWORD PTR SS:[EBP-0x4] ; 就是前面ebp 与一个未知的数的异或加密 类似cookie 00438634 5F POP EDI 00438635 33CD XOR ECX,EBP 00438637 5E POP ESI
//开始怀疑修改进IAT的函数内容--加密代码前面几句--写死的硬编码opcode 到底意欲何为:
在内存窗口跳到[EBX-0X30]选择反汇编观察一下:
- 发现 这就是解密IAT代码。
总结:用硬编码 opcode (用于解密IAT)的首地址替代IAT中的函数地址,然后每次IAT调用的时候都会去调用这个代码块使用。
填入IAT的解密函数 详解:
进入填充硬编码后的 EBP-0X30 , 进去后发现里面包含了很多花指令,我一个个提取出来组合分析:
注意: 数字标号,代表着我的分析顺序
;1.解密程序第一句: 0012FF30 E8 01000000 CALL 0012FF36 ;-------------------------------- ;2.跳转到 0012ff36后 0012FF36 58 POP EAX;其实这儿主要是为了弹出前面的返回地址,好自己push返回地址 0012FF37 EB 01 JMP SHORT 0012FF3A ;---------------------------------- ;3.简单pop EAX 后又添砖0012ff3a: 0012FF3A B8 00040000 MOV EAX,0x????;8.结合后面的代码,发现是取得的代码0x15异或加密后的代码 0012FF3F EB 01 JMP SHORT 0012FF42 ;将EAX 赋值为0x400 ,然后又跳转 ;---------------------------------- ;4.跳转到 0012FF42: 0012FF42 35 15151515 XOR EAX,0x15151515;9.这样就迎刃而解了,这儿是解密。 0012FF47 EB 01 JMP SHORT 0012FF4A ;将EAX和0x 15151515 异或计算加密后 继续跳转 0012FF4A ;------------------------------------- ;5.跳转到 0012ff4a: 0012FF4A 50 PUSH EAX;10.把解密的真正地址作为返回地址 0012FF4B EB 02 JMP SHORT 0012FF4F ;push了现在的 40000 XOR 0X15151515后的值作为返回地址,又跳转 ;-------------------------------------- ;6.再结合后面一点的代码: 004385B9 FF15 CC924300 CALL DWORD PTR DS:[<GetProcAddress>]; kernel32.GetProcAddress 004385BF 6A 40 PUSH 0x40 004385C1 68 00300000 PUSH 0x3000 004385C6 6A 20 PUSH 0x20 004385C8 35 15151515 XOR EAX,0x15151515 004385CD 6A 00 PUSH 0x0 004385CF 8945 DB MOV DWORD PTR SS:[EBP-0x25],EAX ; 7.放入地址 -- 0x25刚好是用来存放地址的 ;------------------------------------------------------------
综述:
这段opcode 留了 EBP-0X25这个位置 来填充 xor 0x15 加密后的地址值,待真正call IAT[index]的时候,把call的返回地址pop弹出,把那个亦或的加密后的地址值用 xor 15 来解密,并且push进去代替前面pop出的地址,并且返回。
4 修正前面分析的加密算法
- 这时候我们只需要保存正确的函数地址值到IAT,那么这个程序就能脱掉了。
然后和前面几篇一样的流程dump到本地,用impREC修复一下IAT
运行没毛病!!
附:还可以使用OD脚本修正IAT
思路:
调用getprocAddress之后,立刻使用一个临时变量保存起来,
再待壳修改IAT后,立刻修改回正确的函数地址(前面保存的临时变量)
脚本如下:
//1.定义变量 MOV dwGetApiAddr,004385bf MOV dwWriteAddr,004385f0 MOV dwOEP,00409486 //2.初始化环境 BC //清除软件断点 BPHWC //清除硬件断点 BPMC //清除内存断点 //下硬件执行断点 BPHWS dwGetApiAddr,"x" BPHWS dwWriteAddr,"x" BPHWS dwOEP,"x" //3.构建逻辑 /* -- 用一个临时变量来存储 真正的函数地址 -- 在加密逻辑代码执行完,并且写入IAT后,立刻改回 */ LOOP0: RUN //相当于od -- F9 CMP dwGetApiAddr,eip//如果是执行完GetProcAddress后 JNZ CASE1 mov dwTemp,eax JMP LOOP0 CASE1: CMP dwWriteAddr,eip MOV [edi],dwTemp JMP LOOP0 CASE2: CMP dwOEP,eip JNZ LOOP0 MSG "改回来了哈哈"我发现另外一种解法,待会儿送上!