运行时压缩(UPX)

任何文件都是由二进制组成的,因而只要使用合适的压缩算法,就可以是文件大小进行压缩。

无损压缩:经过压缩的文件能完全恢复。如7-zip、面包房等压缩程序。

有损压缩:经过压缩的文件不能完全恢复。压缩多媒体文件时大部分使用有损压缩。

运行时压缩器:

运行时压缩器(Run-Time Packer)是针对可执行文件而言的,可执行文件内部含有解压缩代码,文件在运行瞬间在内存中解压缩后执行。

运行时压缩文件也是PE文件,内部含有原PE文件与解码程序,在程序的EP代码中执行解码程序,同时在内存中解压缩后执行。

与普通压缩器相比,运行时压缩器的明显区别是PE文件的可运行性。

把普通的PE文件创建成运行时压缩文件的程序称为压缩器,经反逆向技术处理的压缩器称为保护器。

PE压缩器是指可执行文件的压缩器,其目的主要是缩减PE文件大小、隐藏PE文件内部代码与资源。

PE保护器是一类保护PE文件免受代码逆向分析的实用程序,应用了如反调试、反模拟、代码混乱、多态代码、垃圾代码、调试器监视等技术,通常用于防止破解、保护代码与资源等。

普通压缩和运行时压缩的比较如下表:


这里使用UPX压缩器,对notepad程序进行压缩:


可以看到,notepad文件经压缩后大小缩减了35231。

运行时压缩比普通的ZIP压缩的压缩率较低,原因在于压缩后仍是PE文件,需要添加PE头和放入解压缩代码。

使用PEView查看该文件:


可以看到,第一个节区的RawDataSize为0,即第一节区在磁盘文件中是不存在的,但第一节区的VirtualSize为00010000,即第一节区在内存中是存在的。由此可知,经过UPX压缩后的PE文件在运行瞬间将压缩的代码解压到内存中的第一节区中。也就是说,解压缩代码和压缩的源代码都在第二节区,文件运行时首先执行解压缩代码,将处于压缩状态的源代码解压到第一节区,解压结束后再运行源文件的EP代码。


调试UPX压缩的notepad.exe程序

目标是查找UPX压缩的notepad程序的OEP(源文件的EP称为OEP)。

先打开正常的notepad程序的EP代码,以便于后面辨认出OEP:


在010073B2地址处调用了GetModuleHandleA() API来获取notepad.exe程序的ImageBase,然后在下面的地方可以看到MZ和PE签名。

重命名使用UPX压缩的notepad程序为notepad_upx,用Ollydbg打开该文件时会弹出警告框:


点击是或否之后,显示出UPX的EP代码:


EP地址为01015330,其为第二节区末端位置,实际压缩的源代码处于该EP地址上方。

PUSHAD指令将EAX~EDI寄存器的值保存进栈,然后分别把第二节区的起始地址01011000与第一节区的起始地址01001000(01011000 + FFFF0000)设置到ESI与EDI寄存器中。

当遇到如上同时设置ESI与EDI时,即为内存复制,从ESI所指缓冲区到EDI所指缓冲区的内存进行了复制。从ESI读取数据,解压缩后保存到EDI中。


接着开始跟踪代码,期间整个解压缩过程由无数个循环组成,在恰当的时候需要跳出循环才能提高调试效率。

在EP处执行Crtl+F8,即反复执行Step Over命令,若想停止按F7即可,运行一会后程序进入如下一段小循环,停止运行并在该循环的起始地址处设置断点,重新运行:


其中ECX值为36B,则根据上述代码可知循环次数为ECX的值36B。循环内容为从EDX(01001000)中读取一个字节(byte)到EDI(01001001)中。EDI的值即为第一节区的起始地址,其内存中的内容全为NULL,如下图。会发现,EDX所指地址为EDI所指地址的前一位。调试经过运行时压缩的文件时,当遇到这样的循环应当跳过,在循环结尾的下一条指令处(即010153E6处的JMP指令)设置断点,取消循环起始地址的断点,再F9即可跳出。



再次Ctrl+F8跟踪代码,不久便遇到了如下较大的循环,该循环包含了上面的短循环:



这段循环是解码循环,先从ESI所指的第二节区中一次读取值,经过解压缩后,将值写入EDI所指的第一节区中。

在循环结尾的下一条指令处即01015402地址的POP指令处设置断点再运行即可跳出该循环。

可以在Dump窗口中看到,原本全是NULL填充的第一节区区域现在已经写入了解压缩的代码:



继续往下调试,遇到如下循环:


该段代码用于恢复源代码的CALL/JMP指令(操作码:E8/E9)的destination地址。

在01015436地址处设置断点运行即可退出该循环。


接着调试至如下循环:


该部分循环代码是循环设置IAT,01015436地址的指令将EDI设置为01014000,即指向第二节区区域,该区域中保存着原notepad程序调用的API函数名称的字符串:


UPX压缩原notepad文件时,会分析其IAT,提取出程序中调用的API名称列表,形成API名称字符串。使用这些字符串调用01015467地址处的GetProcAddress()函数,获取API的起始地址,再将API地址输入EBX寄存器所指的原notepad的IAT区域,反复进行直至恢复完整。

逐一往下调试。

可以发现,EAX的值设置为了第一个DLL文件名称字符串“kernel32.dll”,EBX的值被设置为指向第一节区中的IAT区域:


接着为调用LoadLibraryA()函数加载相应的库:


接着将EDI的值API名称字符串“GetCurrentThreadId”放入栈中:


接着为调用一个函数,通常而言,获取了API名称字符串之后便开始查找该API函数的地址,因而可以推测该函数为GetProcAddress()函数,查看验证:



总的来说,就是用保存在第二节区的API名称字符串调用GetProcAddress()函数查找这些API的地址,再将API地址输入EBX寄存器所指的原notepad.exe的IAT区域。

直接运行到后面,notepad.exe全部解压后,程序会返回至OEP处:


POPAD指令与PUSH指令对应,将PUSHAD指令存储在栈中的值再次恢复到各个寄存器中。最后有个JMP指令,F7跟踪查看:


可以发现,跳转的地址为OEP代码。


小结快速查找UPX OEP的方法

1、在POPAD指令后的JMP指令处设置断点:

UPX压缩器的一个重要特征是,EP代码位于PUSHAD和POPAD指令之间,跳转到OEP代码的JMP指令紧跟在POPAD指令之后。

因此只需在POPAD指令后的JMP指令处设置好断点后运行即可找到OEP。



2、在栈中设置硬件断点:

硬件断点是CPU支持的断点,最多可设置4个,其和普通断点的区别在于,硬件断点会执行完断点处的指令再暂停执行。

同样也是利用UPX的PUSHAD和POPAD指令的特点,在PUSHAD指令将寄存器的值存入栈时,对该栈地址设置硬件断点,当执行POPAD指令时会访问该内存地址来获取寄存器的值,从而触发了该断点。

执行01015330地址处的PUSHAD指令后,查看栈,在Dump窗口访问栈顶地址,选中第一个字节,右键>Breakpoint>Hardware, on access>Byte设置硬件断点:


设置完硬件断点后,F9运行,会直接停在POPAD指令的下一条指令中:


猜你喜欢

转载自blog.csdn.net/SKI_12/article/details/80611969