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壳

这里程序基本一样,但是这里pushadpopad指令消失了(32位和64位的寄存器的数量不同),无法通过特定指令定位,只能通过ESP定律或者最后一次异常法。

最后

在修复完程序之后,就成功去除了压缩壳子,但是通过EXEINFO,可能还是会识别出UPX壳之类的,这里其实已经去除了,只是还保留了一些特征而已;

在运行时可能无法正常运行,这里可能是程序段里有哪一行没有执行权限,然后LoadPE修改段的权限。