0%

手动脱UPX壳

加壳脱壳对比

我们先看没有加壳的pe文件,PE文件入口是401000

image-20211214162015301

加壳后入口地址如下,入口地址变成了409c80

image-20211214170024538

查看401000位置,可见代码段是空的,即加壳程序将原程序代码段加密/压缩后保存到其他地方,并且将原程序代码段清空了。

image-20211214170853897

继续往下看,可以看到跳转到OEP的指令,设置一个断点运行起来。

image-20211214171034553

运行后如下,可看到原程序。加壳程序执行的整个流程如下

1.执行解压/解密例程

2.解压/解密原程序的各个区段的数据

3.跳往OEP处

4.执行原程序代码

image-20211214173928228

原代码段是空的,在合适的时间,解压/解密程序会将解压/解密后的代码段数据重新写到这里,所以我们可以对这里设置内存访问断点,当解压/解密例程向这里写入数据的时候就会断下来。(点击M->如下图下断点)

img

断下来以后看到这里401000开始处内存单元第一个字节将被赋值为AL的值,此时AL值为6A,我们再来看看原程序401000处的第一个字节,可以看到也是6A

image-20211214195748271

image-20211214200245457

​ 如果我们逐字节的跟踪,可以看到解密/解压例程将原程序代码恢复的整个过程。但是并不是所有的壳都是按照这个顺序来解密/解压原程序区段的,这里是最常规的情况。

​ 如果大家仔细跟踪这个流程的话,就会发现这是一个循环,逐字节读取加密过的字节,接着进行数学运算(例如说:加法,乘法,等等)来解密,运算完毕得到原始字节值,然后将其恢复到原处。

​ 接下来,我们清除掉前面设置的内存访问断点(右键breakpoint->remove memory breakpoint),单击菜单项中Animate into(自动步入ctrl+f7)选项,将能看到原程序代码段被逐字节还原的动画过程,原程序各个区段被还原后,就会断在我们前面设置的jmp OEP处的断点处。

脱壳

ESP定律

​ 又称堆栈平衡定律**(如果要返回父程序,则当我们在堆栈中进行堆栈的操作的时候,一定要保证在RET这条指令之前,ESP指向的是我们压入栈中的地址),是应用频率最高的脱壳方法之一。ESP(Extended Stack Pointer)为扩展栈指针寄存器,是指针寄存器的一种,用于存放函数栈顶指针。与之对应的是EBP(Extended Base Pointer),扩展基址指针寄存器,用于存放函数栈底指针**。ESP为栈指针,用于指向栈的栈顶(下一个压入栈的活动记录的顶部),而EBP为帧指针,指向当前活动记录的底部。

​ 加壳后的程序入口点最开始是pushad,就是把所有寄存器压入栈,在壳的最后有一个popad将所有寄存器出栈,对ESP的0019FF54下硬件访问断点。也就是说当程序要访问这些堆栈,从而恢复原来寄存器的值,准备跳向苦苦寻觅的OEP的时候,OD帮助我们中断下来。

总结:我们可以把壳假设为一个子程序,当壳把代码解压前和解压后,他必须要做的是遵循堆栈平衡的原理,让ESP执行到OEP的时候,使ESP=0019FF54。载入程序后只有esp寄存器内容发生变化,那么这个程序多半可以用ESP定律。

​ 首先f7执行pushad,然后在ESP处选择Follow in dump,就可以在数据窗口中定位到刚刚PUSHAD指令保存到堆栈中的寄存器环境了,我们选中前4个字节,我们通过单击鼠标右键选择Breakpoint-Hardware,on access-Dword给这4个字节设置硬件访问断点。

image-20211214204923883

​ 然后点击f9我们可以看到,有一个判断。如果不为0 那么就跳向 00409DFA,我们在这一行的下面一行按F4,直接跳过这个判断,然后继续按F8,就到了程序入口,或者直接在最后的jmp下断点,然后f7到OEP。

image-20211214171034553

dump

​ 然后使用工具PETools将程序dump出来,双击程序会显示不是一个有效的win32程序,这时候我们就需要修复IAT表了。

​ 下图中403238是IAT的其中一个元素,里面保存的是GetModuleHandleA这个API函数的入口地址,我们dump出来的程序的IAT部分跟这里是一样的,属于同一个动态库的API函数地址都是连续存放的,不同的动态库函数地址列表是用零隔开的。

image-20211219110628313

​ 上图中有三个77开头的地址,我们点击窗口M按钮,看看这个三个地址属于什么dll

image-20211219111016142

​ 要修复IAT,我们需要知道的是开始地址和结束地址,IAT起始地址是403184,有些强壳可能会将IAT前后都填充上垃圾数据,让我们定位IAT的起始位置和结束位置更加困难。但是我们知道IAT中的数值都是属于某个动态库代码段范围内的,如果我们发现某数值不属于任何一个动态库的代码段的话,就说明该数值是垃圾数据。

image-20211219111712077

我们看到最后一个元素地址是75xxxxxx,属于COMDLG32.DLL,所以40328c是IAT结束地址,

begin:403184

end:40328c

image-20211219112120480

1)IAT的起始地址,这里是403184,减去映像基址400000就得到了3184(RVA:相对虚拟地址)。

2)IAT的大小

IAT的大小 = 40328C - 403184 = 108(十六进制)

3)OEP = 401000(虚拟地址)- 映像基址400000 = 1000(OEP的RVA)。

然后点击Get Imports,不知道为啥userdll会显示无效,先点击fix dump,然后选择dumped.exe

image-20211219113450399

依然显示无效32文件,使用PEtools rebuildPE,就可以运行起来了。

最后一次异常法

​ 新开一个例子,在debug options-exception,各个选项全部选择然后运行起来

image-20211216184247952

img

​ 这里我们可以看到产生了好几处异常,但是都不是位于第一个区段,说明这些异常不是在原程序运行期间发生的,是在壳的解密例程执行期间产生的异常,最后一次是46e88f处的这个异常。

​ 好,现在我们重新启动OD,将EXCEPTIONS菜单项中忽略的异常选项的对勾都去掉,仅保留Ignore memory access violations in KERNEL32这个选项的对勾。

img

​ 这里不是,我们按SHIFT + F9忽略异常继续运行,我们知道最后一次异常是46E88F处的INT 3指令引发的。

img

​ 这里是壳的解密例程执行过程中产生的最后一次异常,接着就是执行原程序的代码了。

​ 接着我们可以对代码段设置内存访问断点,为什么不在一开始设置内存访问断点呢?原因是很多壳会检测程序在开始时是否自身被设置内存访问断点,如果执行到了最后一次异常处的话,很可能已经绕过了壳的检测时机,我们来试一试。

img

​ 我们按SHIFT + F9忽略该异常运行起来。

img

​ 我们可以看到断在了OEP处,下面我们来看看该壳在开始的时候是否有检测内存访问断点。

​ 我们重新加载该程序,将忽略的异常选项都勾选上,接着打开区段列表窗口,给第一个区段设置内存访问断点,过了很久断在了OEP处。

img

​ 虽然这里我们直接给第一个区段设置内存访问断点直接定位到了OEP。

利用壳最常用的API函数来定位OEP

 将忽略的异常选项都勾选上,我们来定位一下壳最常用的API函数,比如GetProcAddress,LoadLibrary。ExitThread有些壳会用。我们首先来看看GetProcAddress。

img

​ 我们可以看到该壳使用了GetProcAddress,接着使用bp GetProcAddress命令给该API函数设置一个断点。

img

​ 如果在命令栏中使用bp命令设置断点失败的话,可以尝试手工设置断点,运行起来。

img

​ 这里我们并不需要其断下来,我们只需要知道壳在哪些地方调用GetProcAddress,所以我们在断下来的这一行上面单击鼠标右键选择-Breakpoint-Conditional log,来设置条件记录。

img

​ 这里我们将Pause program这一项勾选上Never,记录的表达式设置为[ESP],也就是记录返回地址,这样我们就能知道哪些地方调用GetProcAddress。接着在日志窗口中单击鼠标右键选择-Clear Log(清空日志)。

img

​ 运行起来,我们可以看到程序的主窗口弹了出来,打开日志窗口,看看最后一次GetProcAddress(排除掉第一个区段中调用的位置)是在哪里被调用的。

img

​ 我们可以看到基本上GetProcAddress都是解密例程中调用的,除了428C2B这一处以外(这里是第一个区段中调用的,也就是原程序本身调用的)。所以我们要定位的应该是47009A这一处。接下来我们重新来编辑一下条件断点中断的条件,将中断条件设置为[ESP] == 47009A。

​ 并且将Pause program这一项勾选上On condition,重新启动OllyDbg。

img

​ 编辑条件断点,设置Condition为[ESP] == 47009A,接着将Pause program这一项勾选上On condition,运行起来。

img

img

​ 断了下来。我们可以在对代码段设置内存访问断点之前尝试一下这种方法,这样就可以绕过很多壳对内存断点的检测,但是有一些壳也会对API函数断点进行检测,所以说我们需要各种方式都尝试一下,找到最合适的。

​ 对当前这个壳定位GetProcAddress的调用处是可行的,我们现在已经在OEP附近了。如果定位GetProcAddress的调用处失败的话,我们可以换其他的API函数,这里我们再来看看日志窗口,可以看到一处线程结束记录。

img

​ 因此接下来给ExitThread设置断点,并且将菜单项Debugging options-Events中的Break on thread end(在线程结束位置中断下来)勾选上。

img

​ 运行起来。

img

​ 断在了线程结束的位置。

img

​ 接着我们给代码段设置内存访问断点就能够马上定位到OEP。

利用应用程序调用的第一个API函数来定位OEP

​ 这种方法就是直接给应用程序调用的第一个API函数设置断点,比如说,很多程序(VC++)一开始会调用GetVersion,GetModuleHandleA,对于bitarts_evaluations.c来说我们可以断GetVersion,对于CRACKME UPX来说我们可以断GetModuleHandleA。

img

运行起来。

img

这里我们断在了GetVersion的入口点处,从堆栈中我们可以看到返回地址位于第一个区段。我们直接在返回地址上面单击鼠标右键选择-Follow in Disassembler。

img

​ 这里我们又定位到了OEP。以上就给大家演示的如何利用应用程序调用的第一个API函数来定位OEP了。如果我们遇到有的壳检测GetVersion入口处的INT 3断点的话,我们可以尝试在该API函数的返回指令RET处下断。

​ 其实还有很多适用于特定壳定位OEP的方法,基本上也是根据上面的这些基本方法变通来的。

​ 如果壳检测INT 3断点或者硬件断点的话,使用ESP定律给堆栈中的寄存器初始值设置硬件断点也是不起作用的,只能换其他方法。