1、call ds:[xxx]
2、jmp ds:[xxx]
3、mov reg,ds:[xxx] + call reg
4、壳开始运行时填充到原来的导入表地址,不改变指令。
在1和2的情况下,原来都是FF 15或FF 25的6字节,vmp iat保护后,代码中也会变为1字节push/pop reg+5字节E8 call vmp0或5字节E8 call vmp0+1字节的的两种类型:
第一种类型:
原来:
保护后变为call vmp0 + 1字节,这里call 后面不全是填充的retn,有可能是其他字节:
第二种类型:
原来:
保护后变为push/pop reg + call vmp0:
在3的情况下,根据原来对寄存器是否为eax的赋值,也会有两种类型:
第一种:
5字节A1 开头的mov eax,ds:[xxx] 直接变为5字节的E8 call vmp0
第二种:
6字节8B 开头的mov reg,ds:[xxx],则变为5+1或1+5类型的E8 call vmp0
在4的情况下,原始的指令代码call ds:[xxx],jmp ds:[xxx],push ds:[xxx],mov reg,ds:[xxx]不改变,壳在启动时候直接填充函数地址到xxx。这类情况直接搜代码段FF??,8B??,A1????,获取到对应内存的函数地址进行修复。
这里我直接对text段进行爆搜E8字节,然后判断是否是call到的vmp0区段,这样筛选出一堆地址,分析时候发现call vmp0区段后是以nop 0x90开头,之后再通过unicorn模拟获取在retn指令时候的返回地址,如果返回地址是当前其他模块的导出函数,则认为是没加壳程序原来调用导入函数的地址
1、对于jmp ds:[xxx]类型,由于本身不会对堆栈进行操作,而保护后改为了E8 Call方式,所以vmp为了恢复堆栈在最后retn时候是采用retn 0x4方式,对应64位为retn 0x8。
2、对于mov reg,ds:[xxx]方式,通过unicorn模拟可以发现返回地址是在E8 call vmp0导致的+5或者+6位置,对应上面1+5和5+1情况,同时只会写入除了ESP外的一个寄存器。
3、对于call ds:[xxx]情况,返回时候是以retn返回,并且返回的地址不在调用call的地址附近。
判断这个的目的是确定还原代码开始的地址,最开始我这里是通过模拟得到retn时候的返回地址和E8 call的地址进行对比是+6还是+5从而判断是5+1还是1+5情况,但是这种方式对于jmp ds:[xxx]类型不能取到返回地址,对于mov reg,ds:[xxx]方式也会因为有5字节的mov eax,ds:[xxx]而判断错误。
后面分析的时候发现对于1+5情况使用push/pop reg + E8 call方式,模拟时候把除了esp外的寄存器值都初始化为0:
1、对于push reg +E8 call方式在 call内会通过pop reg来恢复call前因为push reg减少的堆栈,比如如下调用,模拟开始给的esp是0x1000,恢复堆栈的指令为:
所以此时会出现esp = 0x1004,并且堆栈为:
[0x1000+4] = 返回地址
[0x1000+8] = 0
2、pop reg+E8 call方式会在call中进行xchg reg,[esp],push reg操作,将原先保存在reg中的参数和返回地址互换,并且重新压入返回地址:
所以当xchg指令执行后会出现esp = 0x1000,并且堆栈为:
[0x1000] = 0
[0x1000+4] = 0
3、其他除了5字节的mov eax,ds:[xxx]外都是E8 call + 1字节的情况
代码直接在这个基础上改动https://github.com/mike1k/VMPImportFixer,原代码把所有的指令都判断为FF 15类型,并且没有判断5+1、1+5和5的模式,以及mov reg,xxx类型和直接填充的类型。
参考:
1、https://github.com/mike1k/VMPImportFixer
2、使用模拟器进行x64驱动的Safengine脱壳+导入表修复
3、手动分析VMP加密的x64驱动导入表
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)