runtime-compressed
进来开例会重新学习了运行时压缩壳,常见的UPX和ASpack壳,在学习了PE结构之后,重新回顾一下
运行时压缩
就是通过在程序结构的结尾,添加一段代码,这行代码可以压缩和解压正常的程序段,在程序执行的时候,优先执行这一壳段,将程序进行恢复,之后开始执行。
压缩壳我认为主要目的是将程序的大小进行压缩,而不是对抗逆向分析。
常见的俩壳
- upx
- Aspack
前者开源,但是压缩效率不如后者,且对于30kb以下的程序不予压缩。后者闭源收费,压缩效率实际高于前者,但是目前只能对32位程序进行压缩。
32位程序
upx壳
这里主要通过ESP定律进行脱壳,因为二次断点方法有的程序找不到.rsrc
段。
ESP定律
ESP定律我认为实质就是壳函数是一个函数,在函数运行之后,会对程序的初始状态进行恢复,而程序的初始状态就通过壳程序的pushad
指令保存在堆栈中。所以可以通过在ESP寄存器下硬件断点的方式,进行脱壳。
硬件断点,不论是多少字节,只要开头的第一个字节是ESP寄存器的内容就可以。
简化
通过ESP定律知道了,将原始状态保存在栈中,那么壳函数结束的时候,就会恢复栈中数据到寄存器,所以可以直接寻找汇编popad
下断点,然后F9执行到断点,进行脱壳,效果一样。
善后
在断点断下之后,需要向下运行找到一个大段的跳转,这里的代码特征是会先经过一个短的循环,然后有个很大的跳转指令,跳转到的目的地就是程序的OEP(初始入口点)
OEP
这里需要注意的是:程序的初始入口点并不是一般的push ebp
指令,而是sub
指令,这里是因为跳转到的实际上是程序的start函数,并不是main函数。如果将OEP跳转到main函数的话,程序则无法正常执行,因为跳过了程序在执行前的必要的初始化。
OEP一般在popad之后的大跳转得到,但是有时候不一定是跳转指令,也可能是ret
指令。
修复IAT
IAT:IMPORT_ADDRESS_TABLE,导入地址表,每一个表记录了一个dll文件的导入函数的地址表,这样就避免了使用每一个函数,都需要手动的使用LoadLibrary()
载入dll和GetProcAddress()
找到对应函数。同时也提高了兼容性。
在加壳的时候,壳修改了原来导入目录表的位置为壳的导入表,这样程序启动后,操作系统就为壳的导入表加载。在壳代码运行完成之后,壳代码会跳到原程序的入口点OEP,壳模拟操作系统填充原来导入表IAT,这样保证代码段能正常调用函数。
ASpack
这里同样可以通过ESP定律来脱壳,但是不能向上述UPX一样,通过寻找POPAD
指令寻找,因为这里有多个POPAD
指令。
64位程序
UPX壳
这里程序基本一样,但是这里pushad
和popad
指令消失了(32位和64位的寄存器的数量不同),无法通过特定指令定位,只能通过ESP定律或者最后一次异常法。
最后
在修复完程序之后,就成功去除了压缩壳子,但是通过EXEINFO,可能还是会识别出UPX壳之类的,这里其实已经去除了,只是还保留了一些特征而已;
在运行时可能无法正常运行,这里可能是程序段里有哪一行没有执行权限,然后LoadPE修改段的权限。