0x0 序言
最近在学习《逆向核心工程原理》一书,此贴乃是其书中洞穴代码内容的学习笔记和总结,该章节讲述如何在加了壳的情况下做到不脱壳而更改程序内容,也即给代码打上内嵌补丁,这个补丁代码又称为洞穴代码。因此要求读者需有汇编基础、较为熟悉OD
0x4 何为内嵌补丁
内嵌补丁的代码又被称为洞穴代码,这种补丁以代码的形式直接写在目标的exe代码里,就如同嵌入在exe身上一样,因此叫内嵌补丁。
当然,一般谁会这么蛋疼,用这么麻烦的方式写补丁。用这种方式给程序打补丁的情况一般都是当程序无法直接被修改时才会这么做。例如程序被打了壳,但又想在没脱壳的情况下修改程序的内容,此时才到内嵌补丁上场。所以接下来的例子都是在程序加了壳的情况下进行的。
0x8 原理
当程序打上了壳,在没脱壳的情况下如何修改程序内容呢?有人会说直接用OD载入程序,然后改代码即可。这当然不行,因为被打上了压缩壳的程序会在程序运行时进行解压,倘若在未完全解压程序前修改代码,必定会使代码解压或解密后变形,变成不是我们想要的代码,严重的情况会导致程序崩溃。因此我们要修改代码,必须先找到壳运行结束的地方,将其跳转至我们的空洞代码,最后再跳回源程序。
0x8 实践
程序流程
此贴用的是书中的例子,打开例子exe可以看到大概的程序流程:
我们的目标是更改消息弹窗的信息即可,不必过于复杂。
寻找OEP
一般而言,壳结束最明显的标志就是跳转至程序的真正入口OEP。先看看壳还没结束前搜索字符串会怎样:
除了笔者本人自己加的字符串外,啥都没有。
下面看看壳运行完了,即弹出弹框时搜索字符串:
有是有了,但是如果在这时候改字符串是没啥用的,因为这是解密后的内容。我们必须得做到更改程序后,要让程序解密后能显示我们更改的内容。
所以我们现在要寻找OEP。如同手动脱壳一样,笔者采用单补法,直到有一个较大的跳转:
这个跳转后就看到相关弹窗API的调用,并且字符串也能搜出来,所以可以肯定这里就是壳结束跳转到真正入口的地方:
开辟补丁空间
找到壳结束的地方后,我们可以把这个跳到真正OEP的跳转改成跳到我们补丁代码的地方,然后在补丁代码里跳到真正的OEP。但在此之前,我们需要先找到一个存放补丁代码的空间。
据古典记载,补丁空间的开辟有三种方法:
- 在文件空白处开辟
- 扩展最后节区
- 新添节区
因此处代码较少,故采用第一种方法开辟补丁空间,如果读者们硬要用第二三种方法,笔者也不阻拦,但别问笔者,因为我不会~
在文件空白处开辟补丁空间,我们首先要做的当然是在文件中找到空白的无用空间。那要如何做呢?我们在经常查壳的软件里看各个节区的磁盘偏移和内存偏移以及其大小:
可以看到第一节区中使用的虚拟内存大小为280h字节,而使用的磁盘大小为400h字节。这明显的,磁盘里还剩120h字节尚未被使用。所以我们的补丁代码可安放此处。
为了找到这空白的位置,我们需要找出这个空白空间的地址。这里很简单,只要用磁盘偏移量(400h)加上内存大小(280)就可以了(680h)。
任意使用一个16进制编辑器,笔者这里使用Hex WorkShop:
这里便是我们要使用的补丁空间了。接下来就要把我们的补丁代码放进去。
塞入补丁
那要如何在这个空白空间写入我们的代码呢?当然你可以用16进制写进去,但这任务过于艰辛,因为我们要写入的是若干行汇编代码,用16进制表示的除非是神仙,恐怖难以驾驭。
因此我们可以用OD载入程序,在OD中找到这段空白空间然后写入代码。
但问题来了,OD显示的地址是内存地址,我们刚找的是文件中的地址(磁盘地址),为此这里就需要小小的计算:
Image Base + 内存偏移量 + 内存大小 = 400000 + 1000 + 280 = 401280
所以我们的空间从OD中401280中开始:
从401280空间开始,选一段空白的空间,填写红框中的代码。稍微注意一下几点:
- esi中存放自己自定义的字符串,当然这个字符串自己要先在其他空白地方定义好。
- edi中存放要被替换的字符串,可在壳运行结束后通过搜索字符串找到其地址。
- jmp是跳到真正的OEP即在寻找OEP章节中找到的jmp地址。
当我们把代码填写好之后,下一步就是要将壳结束时原本跳到OEP的地址改成我们这里补丁开始的地址:
这样当程序运行到壳的最后一句代码时就会跳转到我们的补丁。因为此时壳已经结束了,运行补丁更改字符串自然也没事。
但问题是,这样做就可以了吗?不可以。为什么?因为我们最后更改的跳转这句代码,也是在壳之中的。
简单来说,只有壳运行完后,才会显示jmp unpackme.0040121E
这个跳到真正OEP的代码:
所以说我们之前对跳转的修改时没有用的。那我们怎么办呢?找到对jmp解密的那段代码,解析其原理,然后将我们要修改的代码加密,才能让程序解密后变成我们需要的样子。
首先我们记住跳转的内存地址401083,然后重新开始单步运行仔细观察地址的出现。
所幸的是,例子程序的加密很简单。我们可以在程序中大量看到这样类似的代码:
一个循环,将一个地址的字节使用异或。这很明显是用异或解密了。这里是将地址ebx(4010F5)——ebx+ecx(4010F5+154h=401249)中的字节与0x44进行异或(xor)操作。明显,我们的目标地址(401083)并不在这范围,所以继续找,最后发现:
这里是401007——401007+7F = 401086地址范围内字节与7进行异或解密,明显是这里了。所以我们更改后的跳转字节要与7进行异或。(因为用7异或解密意味着用7进行加密,一个数对同一个数进行两次异或操作会变成原来的数,如1 xor 7 = 6, 6xor 7 = 1)
将我们更改后的jmp的字节与7进行异或:
经过计算后结果在图下方。注意后面的0000不用进行异或。因为从字节00已经超出了解密的地址范围。
接着把跳转的字节改成EE0105即可:
会变成两行奇怪的代码,但这不用管。当然你也可以在16进制里找到对应的地址更改。
保存后运行即可见到更改字符串的弹窗:
0xC 结语
其实整个流程下来,我们最终还是得更改壳里的内容,为此还需要找到解密的方法反之推导到加密的方法。倘若这里算法再复杂点恐怕无力回天。所以笔者觉得这里适用性较低了点,但也不是说浪费时间毫无用处。不可否认的是通过这个内嵌补丁的实际动手操作,让我看到了还有这种骚操作。在实践的同时也让我对16进制,节区等内容进一步加深,不再畏惧。