kernel32.dll是Windows 9x/Me中非常重要的32位动态链接库,属于内核级文件。它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。
PE的意思就是Portable Executable(可移植、可执⾏),它是Win32 可执⾏文件的标准格式。实际上是不可移植的。
RVA(相对虚拟地址)相对虚拟地址是⼀一个相对于PE⽂文件映射到内存的基地址的偏移量。使用RVA 是为了减少PE装载器的负担。因为每个模块都有可能被重载到任何虚拟地址,如果所有重定位项都使用 RVA,那么 PE 装载器只要将整个模块重定位到新的起始 VA。这就像相对路径和绝对路径的概念:虚拟地址VA = 相对虚拟地址 RVA+基地址ImageBase
PE文件结构如下图
由图可知,PE文件由PE文件头标志,映像文件头和可选映像头三部分组成。
通过SizeOfOptionalHeader可以知道节表的起始位置
通过利用NumberOfSections,可以进一步确定后一个节表的末尾地址 (每个节表28H个字节)。这样,在添加新节时,就可以找到新节表应该所在的位置。如果我们要在文件中增加或删除一个节,就需要修改 NumberOfSections。
我们可以发现,需要寻找的数据目录表DATA_DIRECTRORY在可选映像头的最后,也就是离PE文件头74H,文件头标志位占4H,故离PE文件开始位置为78H
数据目录表结构及索引如下图
由图可知,数据目录表的结构为RVA地址与Size大小。在winhex里面即前4个字节与后四个字节,共8个字节大小。前8个字节为导出目录相关信息。后8个字节为导入目录相关信息。
节表其实就是紧挨着 NT 映像头的一结构数组,其成员的数目由映像文件头结构 IMAGE_FILE_HEADER 中 NumberOf Sections 域的域值来决定。节表中每个结构(28H 字节)包含了了该节的具体信息。节表的结构数组成员的数据结构定义如下:
其中,UCHAR = 8个字节, ULONG = 4 个字节 UCHAR = 2 个字节 共40(28H)个字节
VirtualSize:本节的实际字节数,可以⽤用来计算出文件对齐后的节尺寸。
VirtualAddress:本节的RVA,PE装载器将节映射到内存时会读取该值,因此, 如果域值是 1000H,而 PE 文件装载地址是 400000H,那么,本节就被载到 401000H。
SizeOf RawData:经过文件对齐后的节尺寸。经过文件对齐后的节尺寸一般都比该节的实际字节数要多
95
PointerToRawData:节基于文件的偏移量量,PE 装载器器通过本域值找到节数据在文件中的位置,创建新节时应该给出该值。
Characteristics:节属性。
基地址BASE = PointerToRawData - VirtualSize
PE 装载器的工作:
读取IMAGE_FILE_HEADER的NumberOf Sections域,获取⽂文件的节数目。
SizeOf Headers域值作为节表的⽂文件偏移量量,并以此定位节表。
遍历整个结构数组检查各成员值,对于每个结构,读取 PointerToRawData 域值并定位到该⽂文件偏移量量,然后再读取 SizeOf RawData 域值来决定映射内存的字节数。将VirtualAddress域值加上ImageBase域值等于节起始的虚拟地址, 然后就准备把节映射进内存,并根据Characteristics域值设置属性。
遍历整个数组,直⾄至所有节都已处理完毕
导出函数节是本文件向其他程序提供的可调用函数列表。这个节一般⽤用在DLL中, EXE⽂文件中也可以有这个节,但通常很少使⽤用。 当PE装载器器执行一个程序,它将相关DLL都装入该进程的地址空间,然后根据主程序的导入函数信息,查找相关 DLL 中的真实函数地址来修正主程序。PE 装载器搜寻的是DLL中的导出函数。
导出函数表的结构如下:
我们所需要寻找的内容为
AddressOf Functions:模块中有一个指向所有函数/符号的 RVA 数组,本域就是指向该 RVA 数组的 RVA。简而言之,模块中所有函数的 RVA 都保存在一个数组里,本域就指向该数组的首地址。该数组每个成员占 4 个字节,表示相应函数 的入口地址的RVA。数组的项数等于NumberOf Functions字段的值。
AddressOf Names:与以上类似,模块中有一个指向所有函数名的 RVA数组, 本域就是指向该RVA 数组的RVA。该数组每个成员占4个字节,表示相应函数名称字符串的 RVA。数组的项数等于 NumberOf Names 字段的值。这是一个非常重要的域。通常,我们知道要想获取地址的函数的名称,首先要获得这个指针,找到相应的字符串地址数组,再通过数组里面的地址找到相应的字符串进行比较,如果匹配的话就找到了所需要的函数,记住其序号x,然后通过这个序号就可以从AddressOf NameOrdinals指向的序号表中的第x个成员找到所需函数地址在AddressOf Functions字段所指向的数组中的具体位置y,从而找到了所需要的函数地址。
AddressOf NameOrdinals:也是一个RVA,指向包含上述AddressOf Names数组 中相关函数之序数的 16 位数组。数组的项目与文件名地址表 AddressOf Names 中的项目一一对应,项目的值代表函数入口地址表AddressOf Functions的索引, 这样,函数名称就与函数入口地址关联起来了。
已知函数序号获得函数地址如下:① 定位到PE header。 ② 从数据⽬目录表读取导出表的虚拟地址。 ③ 定位导出表获取Base值。 ④ 减掉Base值得到指向AddressOf Functions数组的索引。 ⑤ 将该值与NumberOf Functions作⽐比较,⼤大于等于后者则序数⽆无效。 ⑥ 通过上⾯面的索引就可以获取AddressOf Functions数组中的RVA。
导入函数节包含有从其他DLL(如user32.dll)中导入的函数。该节开始是一个成员为IMAGE_IMPORT_DESCRIPTOR结构的结构数组,即导入地址表(IAT), 简称导入表。数据目录表项结构成员 VirtualAddress 包含导入表地址。该数组的长度不定,但它的后一项是全 0,可以据此判断数组的结束。
导入函数表结构如下:
我们所需要对比的内容为:
FirstThunk和OriginalFirstThunk在文件打开状态下指针指向相同,但是在内存中不一样。
用winhex打开kernal32文件有两种打开方法,一种是在文件状态下打开,一种是在内存状态下打开。两者的区别就在于内存状态下打开时,winhex内显示的文件起始部分为kernal32的基地址。
1、打开kernal32文件:通过工具——打开RAM——选择一个进程中的kernal32.dll文件打开
2、由图可知,
基地址为73D50000 PE文件头标志位 45 50
偏移78h后找到导出表RVA 00 09 10 20 /SIZE 00 00 D8 50
导入表RVA 00 09 E8 70/SIZE 00 00 07 1C
3.1 跳转到导出函数表:
VA = BASE + RVA = 73D50000 + 00091020 = 73DE1020 内容如下
AddressOfFunctions的RVA: 00 09 10 48 VA= 73D50000+00091048=73DE1048
AddressOfNames的RVA: 00 09 29 34 VA=73D50000+00092934=73DE2934
AddressOfNameOrdinals的RVA: 00 09 42 20
3.2 跳转至函数名RVA数组,跳转到函数序号
offset = BASE + RVA = 73D50000 + 00 09 29 34= 73 DE 29 34
offset = 73D50000 + 00 09 42 20 = 73 DE 42 20
函数名RVA数组如下
函数序号如下
3.3 根据函数名RVA跳转至函数名
offset = 73D50000 + 00 09 4F 02 = 73 DE 4F 02
由此,可以通过遍历函数名找到所需要寻找的函数,再根据遍历次数找到函数名序号,找到函数所在地址。
4.1 跳转至导入表
offset = 73 D5 00 00 + 00 09 E8 70 = 73 DE E8 70
OriginalFirstThunk(是一个IMAGE_THUNK_DATA结构数组的RVA,该 RVA 在文件中与FirstThunk域中指针指向相同,但在内存中不一样 ) 00 09 FB 7C
ForwarderChain 00 00 00 00
Name: 00 0A 04 78
FirstThunk:00 08 16 B0
4.2 跳转到Name,得到导入dll名
offset = BASE + RVA = 73 D5 00 00 + 00 0A 04 78 = 73 DF 04 78
1、打开kernal32文件:
2、找到PE文件头,同时在映像头内找到节表的数目和可选头的大小。
PE标志末尾偏移0x04 NumberOfSections : 00 05
PE标志末尾偏移0x10 SizeOfOptionalHeader: 00 E0 –> 224
3、分析可选映像头
SizeOfCode(可执行代码长度): 00 06 10 00 // 偏移4
AddressOfEntryPoint(代码入口RVA): 00 01 06 A0 //偏移16
ImageBase(相对PE头偏移 34): 6B 80 00 00 //偏移28
SectionAlignment(加载后节在内存中的对齐方式 ): 00 01 00 00 //偏移32
FileAlignment(节在文件中的对齐⽅方式 ): 00 00 01 00 //偏移36
SizeOfImage( 程序调入后占用内存大小): 00 0E 00 00 //偏移52
NumberOfRvaAndSizes( 数据目录的项数,): 00 00 00 10
4、从PE文件头起始位置开始偏移78H到达数据目录表
如图,导出函数RVA为00 09 10 20 /大小为00 00 D8 50 导入函数RVA为00 09 E8 70/大小为00 00 07 2C
Offset = 00 00 24 E4 - 00 00 10 00 + 00 00 04 00 = 00 00 18 E4
5、从PE映像头末尾偏移可选映像头大小 224 到达节表
Name: .text
VirtualSize: 00 06 06 16
VirtualAddress(内存对齐后地址): 00 01 00 00
SizeOfRawData(文件对齐后尺寸): 00 06 10 00
PointerToRawData(文件对齐处位置): 00 00 10 00
Name: .rdata
VirtualSize: 00 02 7D 1C
VirtualAddress(内存对齐后地址): 00 08 00 00
SizeOfRawData(文件对齐后尺寸): 00 02 80 00
PointerToRawData(文件对齐处位置): 00 06 20 00
Name: .data
VirtualSize: 00 00 0C 54
VirtualAddress(内存对齐后地址): 00 0B 00 00
SizeOfRawData(文件对齐后尺寸): 00 00 10 00
PointerToRawData(文件对齐处位置): 00 08 A0 00
Name: .rsrc
VirtualSize: 00 00 05 20
VirtualAddress(内存对齐后地址): 00 0C 00 00
SizeOfRawData(文件对齐后尺寸): 00 00 10 00
PointerToRawData(文件对齐处位置):00 08 B0 00
Name: .reloc
VirtualSize: 00 00 46 40
VirtualAddress(内存对齐后地址): 00 0D 00 00
SizeOfRawData(文件对齐后尺寸): 00 00 50 00
PointerToRawData(文件对齐处位置): 00 08 C0 00
6、跳转至导出表
导出函数RVA为00 09 10 20 在节表.rdata里面 节表RVA00 00 08 00 大小00 02 7D 1C
Offset = 00 09 10 20 + 00 06 20 00 - 00 08 00 00 = 00 07 30 20
减数为第二个节表 .rdata的VirtualAddress (00 08 00 00) - PointerToRawData( 00 06 20 00) = 1E000
导出表如下图
AddressOf Functions:00 09 10 48
AddressOf Names: 00 09 29 34
AddressOf NameOrdinals:00 09 42 20
7、跳转至函数名的RVA数组
offset = 00 09 29 34 - 00 01 E0 00 = 00 07 49 34 部分数组指向的函数名RVA如下图,每4个字节一个RVA
8、跳转到函数名
第一个函数名的offset = 00 09 4F 02 - 00 01 E0 00 = 00 07 6F 02 ,由此找到了函数名