在学习《恶意代码分析实战》第19章shellcode分析时,提到了一个根据PEB结构搜索kernel32基址的方法。书中描述的很晦涩难懂,于是花了点时间详细了解了一下这个技术。整理成笔记方便日后查找。
Windows是一个强大的操作系统。为了方便开发者,微软将进程中的每个线程都设置了一个独立的结构数据。这个结构体内存储着当前线程大量的信息。这个结构被称为TEB(线程环境块)
。通过TEB结构内的成员属性向下扩展,可以得到很多线程信息。这其中还包含大量的未公开数据。
TEB结构的其中一个成员为PEB(进程环境块)
,顾名思义,这个结构中存储着整个进程的信息。通过对PEB中成员的向下扩展可以找到一个存储着该进程所有模块数据的链表。
本文共涉及两个资料:
神图:
VergiliusProject结构体查询网站:(下文简称VP)
https://www.vergiliusproject.com/kernels
微软未公开数据:(本文没有用到,但是收藏起来以后会用到,包含API,结构体等信息)
http://undocumented.ntinternals.net/
对模块的遍历是从PEB结构开始的,因此我们需要获取PEB结构指针。PEB的结构指针存储在TEB中。TEB结构指针存储在fs寄存器中。
那么如何在内存中定位TEB结构的位置呢?有三种方法:
查看OD等调试器的寄存器窗口,fs段寄存器的后面会接着TEB结构指针。直接在内存窗口跳过去即可,如下:
通过fs的值拆分成段选择子,通过GDT表查找段描述符,得到一个3环的调用门....#*#%$()#.....
显然这种方式很笨重,而且对于像我这种没学过内核的彩笔来说根本不现实,更别说后面落实代码了。
第三种是最方便的,fs:[0x18]存储着TEB结构指针,fs:[0x30]存储着PEB结构指针。
网上很多文章都会说:fs:[0x30]是PEB fs:[0x18]是TEB
,虽然确实方便,但我们还是要看下原理。
目前我们已经知道了fs就是TEB结构指针,但是不巧,在OD中尝试直接跳转到fs处,OD无法直接找到TEB。这是因为fs只存储了段选择子,而通过段选择子找到的东西才是真正的TEB结构指针(内核知识)。如下图:
这时我们回到VP上,看看TEB结构的具体成员:
这里注意下,系统为64时,VP首页kernels记得选X64,但是当你在64位系统上运行32位程序时,我们要查询的TEB结构为_TEB32
切记不要32位程序去查64位的TEB,会迷失自己。
可以看到TEB结构中第一个成员是另一个结构体TIB(线程信息块)
,它占了1C个字节,我们点击黄色的结构体名
可以直接跳到TIB结构处,看看它的成员。
可以看到,偏移0x18处有个Self成员,它存储着这个TIB结构的首地址。也就是SEH指针
。
回到TEB结构,我们知道TIB是TEB中的第一个成员,那么可以理解为TIB的首地址就是TEB的首地址
。所以TIB的0x18偏移等于TEB的0x18偏移,TIB的Self成员同时也指向TEB首地址
至此,我们得出了一个结论,pTEB->0x18 == *pTEB
还记得上文说的fs就是TEB指针
吗,带入公式,得到:
fs:[0x18] == TEB
同理,我们看到TEB结构0x30偏移处为PEB结构(图上没缩写,我淦),那么同理:
pTEB->0x30 == fs:[0x30] == PEB
SEH:pTEB->0 == fs:[0] == SEH链
===========================================================================
知道了fs:[0x30]的来历那就好办了,我们继续模块遍历的学习。VP上看下PEB结构(我这里直接看X86下的PEB了,不然PEB32里面没有缩写,不能直接跳过去很麻烦):
可以看到PEB偏移为C处存储着LDR指针
,它指向一个_PEB_LDR_DATA结构
,我们点进去看看:
结构中提供了三个链表,链表内的节点都是一样的,只是排序不同。由于我们要寻找kernel32的基址,所以我们选择第三个InInitializationOrderModuleList
,这样kernel32的链表节点会比较靠前。其实选其他两个也一样,就是要找久一点。我们看下这个链表入口结构_LIST_ENTRY
的信息:
可以看到这个结构有两个成员,第一个成员Flink
指向下一个节点,Blink
指向上一个节点。所以这是一个双向链表。接下来的概念很重要:
当我们从_PEB_LDR_DATA
结构中取到InInitializationOrderModuleList
结构时,这个结构中的Flink指向真正的模块链表
,这个真正的链表的每个成员都是一个LDR_DATA_TABLE_ENTRY
结构。
之前的_PEB_LDR_DATA
只是一个入口
,这个结构只有一个
,它不是链表节点,真正的链表节点结构如下图:
_LDR_DATA_TABLE_ENTRY结构中的 _LIST_ENTRY 结构对应下一个 _LDR_DATA_TABLE_ENTRY 节点中的 _LIST_ENTRY 结构。
如:第一个 _LDR_DATA_TABLE_ENTRY 结构中的 InInitializationOrderModuleList 中的 Flink 指向的是第二个 _LDR_DATA_TABLE_ENTRY 结构中 InInitializationOrderModuleList 的首地址。而不是另外两个 _LIST_ENTRY 结构。
第一个 _LDR_DATA_TABLE_ENTRY 结构中的 Blink 指向 PEB_LDR_DATA 中对应成员的 Blink
最后一个 _LDR_DATA_TABLE_ENTRY 结构中的 Flink 指向 PEB_LDR_DATA 中对应的成员的 Flink
PEB_LDR_DATA 结构中的 Blink 指向最后一个 _LDR_DATA_TABLE_ENTRY 中对应成员的 Blink
PEB_LDR_DATA 结构中的 Flink 指向第一个 _LDR_DATA_TABLE_ENTRY 中对应的成员的 Flink
看不懂上面这段话? 我们转换成图:
宏观的体现为下图:
可以看到这是一个以PEB_LDR_DATA
为起点的一个闭合环形双向链表。
每个_LDR_DATA_TABLE_ENTRY
节点结构中偏移为0x30
处的成员为dllName,偏移为0x18
处的成员为DllBase
通过遍历链表,比较dllName字符串内容可以找到目标模块的所属节点。
通过节点成员DllBase可以定位该模块的DOS头起始处。
通过对PE结构的解析可以搜索导出表,从而可以取到指定的导出函数地址。
理论讲的差不多了,下面我们在OD中实战演练一下,手动寻找kernel32的基地址。
打开OD,随便载入一个PE文件。在内存窗口按Ctrl+G
跳转到fs:[0x30]
处。PEB的首地址为0x7EFDE000
对照VP上的结构体信息,我们知道PEB偏移0xC处为成员LDR。继续跳转到LDR结构首地址0x7DF70200
处
MD 属性名搞错了,不是list 是link, 懒得改了
三个成员任选一个,我们这里使用InInitializationOrderModuleList成员,Flink代表第一个节点,我们跳转到0x5E2590
,由于我们是从InInitializationOrderModuleList
中的Flink跳过来的,因此我们向上拉一点,从0x005E2580
处开始为_LDR_DATA_TABLE_ENTRY
结构
我们看到BaseDllName并不是我们想要的kernel32.dll,因此我们继续去看下一个节点。
继续跳转到Flink(0x5E29B8
)处
可以看到此时的Blink已经指向了第一个节点的Blink,dllname依然不是kernel32.dll
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课