-
-
[原创]手动脱壳:如何寻找并修复 Stolen Code
-
发表于: 2026-10-16 21:09 784
-
0x01.解释
网上关于 32 位壳的脱壳教程大多使用 OD,我想用 x32dbg 演示。本文会完整走一遍脱壳流程,虽然网上有很多相似的教程,但本题的核心不在流程本身,而在于:当程序到达假 OEP 后,如何定位并还原被偷取的 Stolen Code,最终找到真正的 OEP。
0x02.了解Stolen Code
经典的 Stolen Code(被偷取的代码):壳把程序原始入口点(OEP)处的若干条指令"搬走"到壳自己的内存空间里执行,然后在原位置留下一个跳转指令,指向壳中保存的这些指令。这种做法比较好追踪,因为在假 OEP 处会留下明显的跳转痕迹。
而这里讲述的是稍难得一种,不会在假OEP留下痕迹,这样太明显了,OEP 处的原始指令完全在壳内部执行,执行完毕后壳将相关痕迹清理掉,跳到假OEP。所以这里我们就要找到合适得时机抓到这个指令。
0x03.查壳
相信大家脱壳的第一步都是查壳,这题也不例外。这是一个ACProtect的壳。
0x04.寻找OEP
这里用不了esp定律法,因为会跑飞,尽管这里有pushad。这里我用的是最后一次异常法。

当按三次F9后,程序就跑起来了,所以我们这里按两次F9,停再最后一次异常,两次异常后就会停在这里。

然后我们到栈里查看 SEH 链,找到异常发生时系统会调用的那个 handler

很显然,这里的hanlder的地址在00415938位置,最好不要直接跳到该地址下软件断点(小心断点检测)。下硬件断点或者在该内存下内存访问断点。再F9,停在这里。
内存断点
或者
硬件断点

养成好习惯,把我们下过的断点给全部删除掉。
之后我们不能直接在.text段下内存执行断点,要不然会触发异常奔溃。这时我们得先让壳运行一会儿。我们可以往这块内存下内存读取断点,因为这个壳会读取自己这块内存的代码,然后便运行边解密。注意这里要重复设置。
按F9停在这里,这里还不能直接给.text段下内存执行断点,要按几次F9让它多运行几次,确定不会干扰到壳运行。
然后删除之前的断点,再给.text下内存执行断点。按一次F9,我们就可以到达假的OEP了。
这个OEP一看就是假的,通常OEP都是push ebp开头的。但是这里我们不要着急,先尝试修复。
0x05修复程序
我们这里直接用x32dbg自带的Scylla工具dump出程序,尝试获取导入表,这里我试过不管用Scylla还是ImportREC工具获取导入表,他总是修复不完全或者干脆修复不了(因为这里的导入表稍微做了加密的),所以我这里直接写了一个工具,功能:输入导入表地址,模拟跟踪导入表,获取真实API地址,然后再写会导入表,这样Scylla工具就可以直接识别到导入表。(工具我放到下面了,需要源代码的伙伴可以私信我)。


还有一个无法识别的直接剪切掉,这样scylla就可以识别到了。接下来我们直接修复导入表。但是这样虽然修复了导入表,但是程序还是运行不起来,因为OEP头部的指令被偷走了,所以接下来就是重头戏,如何抓到真实OEP。(修复完后不要急着把该程序结束掉,后面会用到)
0x06寻找Stolen Code
众所周知程序OEP的开头一般是push ebp开头,这时我们就会想到直接特征码搜索,但是对于这个壳是行不通的,因为这个壳它是边解密边运行,运行完再销毁掉。等我们去搜索 Stolen Code 的特征码时,壳早就把它销毁了。所以这个时候我们就想到push ebp这个操作会压栈,我们找到这个栈位置然后再下硬件写入断点是不是可以抓到了。但是我们怎么找到那个栈的位置呢?这时我们就要用到前面修复的那个运行不起来的程序(这里我称之为假程序)。
壳在做完自己的工作后,必须保证栈的状态和原程序执行到同一位置时一致。否则控制权交还给程序后,程序会立刻崩溃。所以不管壳在中间做了多少事——搬走 OEP 处的指令、执行完被搬走OEP指令、再执行自己的代码、跳到假OEP——最终它都得把栈恢复成“被偷走的那些代码执行完毕之后”应有的状态。

有图可知,不管阶段三做了什么,到达阶段四(假的OEP)的状态和阶段二的栈的状态是一样的。但是我们修复的那个假程序初始状态是直接从阶段一开始的,他是跳过了阶段二和阶段三,栈的状态是错误的。
那么接下来思路就很清晰了,我们可以直接到阶段一或者阶段一前面的时候,在return addres1的上面下一个硬件写入断点,就可以停到阶段二,而阶段二的代码也就刚刚执行push ebp,这样我们就抓到了Stolen Code。
我们该如何找到call's ebp1栈的位置呢?别忘了我们先前的那个假OEP,虽然跑不起来。但是我们只要他到假入口点时的状态。记住x32dbg只要设置入口断点,我们把这个假程序加载到x32dbg中。这时,假程序的栈状态就是上图中阶段一,记录下来。
还记得我们第一次加载的那个带壳程序吗(不会直接把他关掉了吧?),他现在处于上图的阶段四,栈的状态是和阶段二是相等的。
观察两处栈的状态,可以发现 return address 为 75C65D49。这里两处的栈地址虽然相同,但我不能保证脱其他壳时也会一样,所以不能依赖这个固定的绝对地址。我们应该以 75C65D49 这个 return address 为特征,反推出 call's ebp 所在的栈地址。
既然已经获取道路75c65D49这个特征,那么我们可以通过此特征找到call's ebp的栈地址了,下硬件写入断点,抓取push ebp(Stolen Code)。
0x07抓取Stolen Code
下断点的时机同样重要。如前面所述,可以在阶段一之前下断点,但并非什么时候都能下,最好是在接近阶段二的那个时间点下断点。这里我可以提供一个时机,可以重复我上面的脱壳操作,直到执行了多次内存读取断点后那一步下断点。
接下来演示下断点的时机和抓取Stolen Code。重新加载带壳程序,重复脱壳操作,两次异常,跳到hanlder,下重复内存读取断点,多次F9,删除断点,停!!!然后就可以到将要push ebp那处的栈地址下硬件写入断点。
然后点击F9,我们就到了被偷走的Stolen Code处了。
我们观察此处的代码特征,很想被偷走的OEP指令,popad还原寄存器指令,然后执行Stolen Code,再pushad,保存寄存器状态方便后续销毁此处代码。如果觉得还不保险,可以执行完这几处的代码,看看栈的状态与你将要到达的假OEP栈的状态是否一样。
0x08真正修复程序
最后也就是将此处的代码添加到假OEP上面,打好补丁,修改好入口地址(记得要用相对地址)脱壳成功了。


[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)