-
-
[原创]软件保护壳专题 - 再谈引入表保护
-
发表于:
2009-11-17 13:25
10740
-
整整一天不知道该干什么好。终于想起论坛上的专题好久没更新过了。
写篇文章谈一下我对保护引入表的一些想法。
引入表的结构,以及引入表基本的加密我这里就不废话了。
--------困Zzzzzzzz.....---------
<目录>
0.需要保护哪里
1.最重要的位置
2.为什么不使用自己的映像
3.那个叫做“API抽取”的玩意
4.YY
5.附件
Loding...
<正文>
0.需要保护哪里
让我们先看一个最基本的引入表保护算法:
1.将IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk指向的IMAGE_IMPORT_BY_NAME数组中的函数名记录并进行加密
2.并填充DataDirectory数组的第2项。使得加载器
3.记录原始的IMAGE_IMPORT_DESCRIPTOR.FirstThunk的位置
4.在壳加载时读取1和3中记录的信息,通过LoadLibrary与GetProcess的交互使用
重新填充FirstThunk数组
5.在宿主程序正常运行时当遇到有调用某API的时候
有些直接CALL [FirstThunk数组中某个存储API的地址]
有些CALL [地址D],,,
地址D:JMP [FirstThunk数组中某个存储API的地址]
还有些跳转如:
mov 寄存器, [FirstThunk数组中某个存储API的地址]
call 寄存器
通过这些手段来跳转到真正的API中.
当然这是最初级的手法。但是所有高级的东西都是由基本衍生的,让我们重新来分析以上的算法
首先是第一步保护函数名和库名
将IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk指向的IMAGE_IMPORT_BY_NAME数组中的函数名记录并进行加密
IMAGE_IMPORT_BY_NAME里面最重要的就是其中API的名称,所以要加密但是这里有个非常不爽的事情就是当壳在重构
引入表的时候是要将这个进行解密的,那么API的名称就暴露在光天化日之下。这是我们不愿意看到的,让破解者
一点信息都不知道是最好的选择。那么如何防止呢?反正从原始文件中我们可以得到我们要保护对象是谁。那么最简单
的方式就是通过HASH方式将API的名字记录.并移动到壳的某个部位。如果引入的是直接以ID引入那么我想就不用
管他了。直接记录。这个方式最大的优势就是没有API名称。在壳加载这些API时使用自己编写的GetProcess来遍历库的引出表
并做HASH值对比。但是有一个不可避免就是LoadLibrary的时候,要暴露的库名。库名存在于当前的IMAGE_IMPORT_DESCRIPTOR.Name中。
这个是个问题,解密时肯定会暴露。目前我还真没想到有什么好的方法来弥补这个暴露。有一个不太理想的解决方案是自行编写LoadLibrary
并在R0下做文件操作相关的钩子.读入被加密的文件名时进行解密并工作,但是壳用驱动明显不太安逸。也不能防止R0调试器的调试。这里
我是没办法了,如果谁有好的解决方案可以一起讨论。
这里讨论一下MyGetProcAddress的编写。
具体的编写原理我这里就不废话了。简单的说就是遍历库的引出表,并对每个API名称做HASH,再判断指定的HASH值做判断。
这里要说一个要注意的地方,不要将取出的API地址做为返回地址或者出现在任何MyGetProcAddress结束后的的寄存器中。虽然cracker
在判断了哪个是MyGetProcAddress函数后可以跟进去进行查找。但是我们壳的目的就是延长破解的时间,一定要在细节上注意。有位牛牛和
我说过一句话:"有些人就是你做的越变态,他们越兴奋"。那我们就让他们在兴奋中累死吧。
进一步的细节是把MyGetProcAddress修改为MyGetProcAddressAndSetIt,函数原型类似
VOID MyGetProcAddressAndSetIt(IN HMODULE hModule, IN DWORD dwHash, OUT LPDWORD pSetIt);
最后的pSetIt参数是原FirstThunk的地址或者是某个中间要保存API地址的内存空间地址。在这个函数内部获取并设置。或者直接在函数中
引用一个全局内存地址。总之一个函数能有多复杂就多复杂。
当然还有其他的防护手段。
1.最重要的位置
在0步骤中是读取的部分,有读取必然要有设置,例如设置写入的位置,那个关键的位置是IMAGE_IMPORT_DESCRIPTOR.FirstThunk,我们可以看到
库名和API名是可以任意放置位置。但是最终的要写入的地方就是这个FirstThunk因为他是一个记录了API真正地址的表,所以在流程5中所有
种类的跳转都是围绕的它转。并且这个位置不太容易进行移动但是并非不可能。如果定位了此位置,那么1中做的重要工作就白做了。接下来的
事情就是想个办法将这个位置进行保护。从引入表结构上分析,这个是不太可能的。顺其自然的想法就是做HOOK。
不过具体HOOK哪里就要看对保护的设计思路了。直接去HOOK FirstThunk可以。去HOOK在流程5中每个出现的每条指令也是个办法。我这里推荐
去HOOK FirstThunk,因为HOOK流程5的所有跳转指令是乱序引擎要做的工作。在这里HOOK,先来几段由花指令构成的伪函数在绕几个圈子,
随便你做了,具体威力看你的阴暗程度。再结合上乱序,程序效率应该会影响一些但是强度上却得到了很大的提高。
2.为什么不使用自己的映像
接下来貌似该从那些花指令的伪函数中跳入到真正的入口了。这里就又是一个可以遭到攻击的地方。最好的情况是所有的API都是自己重新
编写的。这个工作量有点大,还是让我们借鉴现成的吧。在这个阶段里我们的程序中只能加载了ntdll和kernel32这两个DLL。
kernel32是我们的目标,从PEB中取得kernel32的映像,只抽取他有用的节。其余无用的东西都CLEAR掉。当然如果还是怕下断点到ntdll,那么还可以
把ntdll的映像在重新加载一遍。通过原始PE重定位表,重新做重定位通过导出表将此函数的地址取出。并且填充到代理函数跳出的函数地址处。
这样相当于一个简单的API抽取。断点除非是下到指定位置,否则直接断API名恐怕是不好使了。其余的DLL可以通过类似方式重新进行映射。
这需要你写自己写一个MyLoadLibrary的函数才可以,避免直接使用LoadLibrary加载。总之一个重要的原则就是能不用标准的API就不用,一切自己来。
这里也有一个容易被攻击的地方。就是在PEB的kernel32上下访问断点。或者在kernel32本身的映射上下访问断点。做好反调试吧。
3.那个叫做“API抽取”的玩意
API抽取貌似近几年的壳都玩这个。其实做这个也比较简单,在上一个小节中的就是一个最简单的抽取。方法也很多,说白了也是HOOK + 变形
这里谈一下我的处理方式,动态抽取。在IAT加载时进行抽取。从要抽取的API地址进行分析。分析出要调用的API的STUB,在这个基础上进行变形
事后修订所有跳转偏移。静态抽取的好处是可以事先准备好的替代函数(自行编写的肯定更猥琐)。不过只能针对某几个指定的函数。
分析出这个STUB最简单的方法不是找结束的RET指令。而是找NOP指令。我们的DLL大多都是用高级编译器编写的,kernel32和user32更是如此
正规的函数之间是用NOP进行对齐的。所以我们只要找到一个函数左右的NOP就能准确的识别出一个函数体。至少我目前没遇到错误。有些情况
我自己也没遇到过,这里就把我的体验写一下。其实我们要抽也就是kernel32与user32里的重要函数抽。也没有必要把所有的都抽掉。
4.YY
IAT是壳保护中很重要的部分。"API抽取"应该是IAT保护最流行的招数了。仔细想想也没什么玩意。还是逃不了HOOK + 识别 + 重定位 + 变形
保护壳果然到最后就是一个体力活。把基础的东西一定型,就是模板和识别。保护强度在于模板的多少,稳定性在于识别的准确。
以下的附录里放了些支持函数的代码。
5.附件
附件中给出一个不完整的加载器代码。其中很些古怪的地方是自定定义的标记用于变形后的重定位。下篇专题在讨论这些吧。代码不是很完整
但是几个关键的点可以看明白。重新映射kernel32这些的代码都有涉及。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)