首页 > 技术文档 > A Kernel In NT Kernel :-)
2013
04-05

A Kernel In NT Kernel :-)

很久不玩PE格式了,这次由于要恢复SSDT表的缘故+一个忽然兴起的念头,导致我花了一个小午写了个运行在Ring0的简单PE加载器,并且有意外的收获。

恢复SSDT表手段很多,基本上都是直接从文件中依赖重定位表获取对应数据,重定位后得到相对当前内核加载位置的正确调用地址。大部分的实现代码比较粗糙,因为要不停的在文件地址和内存地址之间进行转换,因此代码繁琐不易读懂。

我在考虑解放方案的时候有这样一个念头:将内核在Ring0重新加载一份,但是在重定位时,不要针对刚刚加载的基址,而是针对内核真实的加载基址来重定位。本来这个念头只是一下闪过,但是我隐约觉着这样是可行的,而且如果成功,那么就不只是恢复SSDT那么简单了。

试验过来,确实可行。重定位结束后,在内存中有了2个内核,一份是原来的(方便起见成为FirstKernel,简写FK),一份是我刚刚加载起来的,在新加载的这份内核中(方便起见称为SecondKernel,简写SK),所有的函数代码都是全新的,干净的,未曾被HOOK代码污染过的,安全的,而所有针对数据的引用,则都因为以FK加载基址为目标重定向的缘故,被正确的指向了FK所在的内存区域,这样一来,SK中的所有导出函数都是可以直接进行调用的!只要通过函数在FK中的偏移就可以简单的得到其在SK中的调用地址,相当于我们拥有了一份完全干净的内核函数集,此时恢复SSDT无论是表项还是代码HOOK,都很容易了,因为我已经有了一份完全干净的内核,我称之为Kernel In Kernel(其名灵感来自 Windows On Windows ),无论在安全还是反安全方面, KIK都有不小的意义!

这样一来SK的加载者就可以使用SK中的内核函数集来进行各种功能调用,从而绕开FK内核中已经存在的各种各样的钩子。 如果再做一些简单处理,就可以轻松的将SSDT表导出变量也指向FK内核,这样的话,所有在内核中存在在的HOOK钩子就彻底的失效了(之所以还要进行一些简单处理,原因在于 SSDT的初始化有一部分是在内核引导时完成的,而不是在编译时完成的,可参考wrk中的内核函数 KiInitSystem(void))。

原理已经给出来了,详细代码我不想贴了,流程很简单,首先读取DosHeader和NtHeader,计算出展开后需要的内存字节数,分配足够的内存,读取SectionHeader,按照SectionHeader中记录的数据,将每一个Seciton从文件中读入内存对应的展开地址处(就是由File::PointerToRawData 读入到 Mem::Base + VirtualAddress处),最后进行重定位处理即可。由于我这里目标在于加载第二个内核(KIK),因此输入输出表之类的不需要我来处理,重定位的过程中,会自动指向到FK内核,而FK早就搞定这些了…… 下面记录的是下代码调试过程中遇到的一些问题,仅供参考,错误之处,请务必拍砖,在下感激不尽:-)

1. PE展开在内存后所需要的实际内存字节数计算方法:
直接读NtHeader中的SizeOfImage可以的,但是还有个计算方法:
首先遍历所有的SectionHeader,记录下具有最大VirtualAddress的节,将该节的VirtualAddress + Aligne(VirtualSize, NtHeader::SectionAligenment) 得到的数值就是PE文件展开后实际所需要的内存字节数

2. 区段展开时到底应该读取多少个字节?
这个问题取决于该区段的 SizeOfRawData和VirtualSize 这两个字段,应该取值min(SizeOfRawData, VirtualSize)。如果 VirtualSize > SizeOfRawData,那么多余的部分用0填充(全局变量们为啥为0?:-) )。如果 VirtualSize < SizeOfRawData,那么读入正确的长度即可,这是因为NtHeader::FileAlignement域中记录的对齐数值引起的文件对齐空洞。

3. 对重定位表结束点的判定:
国内耳熟能详的《加密解密》中提到,会以一个VirtualAddress为0的块结束所有的重定位块。这个值得商榷,Win2k的内核 NTOSKRNL.EXE,由于第一个.text区段是从0x540加载,导致第一个重定位块的VirtualAddress直接为0(每个重定位块负责4K的区域),进而使得重定位操作整体被跨越了(可能很多PE工具也有类似的问题,比如PETOOLS就看不到2k内核的重定位表)。
于是我改为同时针对SizeOfBlock和VirtualAddress进行判断,但是在Vista的内核 NTKRNLPA.EXE 中遇到了反问题,该内核的最后一个重定位表的VIrtualAddress不为0但是SizeOfBlock为0,导致重定位操作溢出越界。
总结了一下,发现最可靠的是SizeOfBlock域,这个域本身的含义就是指明自身块的尺寸(虽然严格按照定义其实这个域该永不为0),因此作为是否最后一个块的判断是比较准确的。改为针对该数值判断后,在2k到Win7(均为32位)的内核上,测试均通过。
因此同重定位表中的 SizeOfBlock进行比较判断,是判断重定位表是否结束的较佳方法。当然也可以通过判断操作内存指针是否越过了重定位区块的结尾来判断,但是较之前一个方法,我个人以为不够优雅。

以上,Kernel In Kernel, Good JOB!! 🙂

 

原文地址:http://yukei.blog.163.com/blog/static/112587703201022262224461/

实现源码:http://bbs.pediy.com/showthread.php?p=1000625

 

最后编辑:
作者:dnybz
这个作者貌似有点懒,什么都没有留下。