用winhex解析kernal32.dll文件

kernel32.dll是Windows 9x/Me中非常重要的32位动态链接库,属于内核级文件。它控制着系统的内存管理、数据的输入输出操作和中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。


PE文件的基本格式

PE的意思就是Portable Executable(可移植、可执⾏),它是Win32 可执⾏文件的标准格式。实际上是不可移植的。

RVA(相对虚拟地址)相对虚拟地址是⼀一个相对于PE⽂文件映射到内存的基地址的偏移量。使用RVA 是为了减少PE装载器的负担。因为每个模块都有可能被重载到任何虚拟地址,如果所有重定位项都使用 RVA,那么 PE 装载器只要将整个模块重定位到新的起始 VA。这就像相对路径和绝对路径的概念:虚拟地址VA = 相对虚拟地址 RVA+基地址ImageBase

PE文件结构如下图

         

由图可知,PE文件由PE文件头标志,映像文件头和可选映像头三部分组成。

             

        通过SizeOfOptionalHeader可以知道节表的起始位置

        通过利用NumberOfSections,可以进一步确定后一个节表的末尾地址 (每个节表28H个字节)。这样,在添加新节时,就可以找到新节表应该所在的位置。如果我们要在文件中增加或删除一个节,就需要修改 NumberOfSections。 

  1. PE文件标志 PE00(4H字节) PE ⽂文件标志 0x50450000 即 PE00,标志着 NT 映像头的开始,也是 PE ⽂文件中与 Windows 有关内容的开始。我们可以在DOS程序头中的偏移3CH处的4个字节找到定位该字符串的偏移位置。
  2. 映像文件头( 14H字节) 紧跟着“ PE00” 的是映像⽂文件头。映像⽂文件头是映像头的主要部分,它包含有PE⽂文件的基本信息。(注释符号后面的十六进制数是域在 NT 映像头中的偏 移量量,从4H开始,是因为PE标志字符串串占4H字节)  
  3. 可选映像头  紧跟映像文件头后面,尽管叫可选映像头,实际上是不可选的, 是“ 必选” 的,包含了PE 文件的逻辑分布信息。可选映像头的结构定义如下:( 1DWORD=2WORD=4BYTE=16BIT)

        我们可以发现,需要寻找的数据目录表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域值设置属性。 

遍历整个数组,直⾄至所有节都已处理完毕

 


导出函数目录与导入函数目录

1、导出函数

导出函数节是本文件向其他程序提供的可调用函数列表。这个节一般⽤用在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。 

2、导入函数

导入函数节包含有从其他DLL(如user32.dll)中导入的函数。该节开始是一个成员为IMAGE_IMPORT_DESCRIPTOR结构的结构数组,即导入地址表(IAT), 简称导入表。数据目录表项结构成员 VirtualAddress 包含导入表地址。该数组的长度不定,但它的后一项是全 0,可以据此判断数组的结束。

导入函数表结构如下:

我们所需要对比的内容为:

FirstThunk和OriginalFirstThunk在文件打开状态下指针指向相同,但是在内存中不一样。


用winhex解析kernal32.dll文件

用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 ,由此找到了函数名

 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
漫谈兼容内核之一:ReactOS怎样实现系统调用 漫谈兼容内核之二:关于kernel-win32的对象管理 漫谈兼容内核之三:Kernel-win32的文件操作 漫谈兼容内核之四:Kernel-win32的进程管理 漫谈兼容内核之五:Kernel-win32的系统调用机制 漫谈兼容内核之六:二进制映像的类型识别 漫谈兼容内核之七:Wine的二进制映像装入和启动 漫谈兼容内核之八:ELF映像的装入(一) 漫谈兼容内核之九:ELF映像的装入(二) 漫谈兼容内核之十:Windows的进程创建和映像装入 漫谈兼容内核之十一:Windows DLL的装入和连接 漫谈兼容内核之十二:Windows的APC机制 漫谈兼容内核之十三:关于“进程挂靠” 漫谈兼容内核之十四:Windows的跨进程操作 漫谈兼容内核之十五:Windows线程的等待、唤醒机制 漫谈兼容内核之十六:Windows的进程间通信 漫谈兼容内核之十七:再谈Windows的进程创建 漫谈兼容内核之十八:Windows的LPC机制 漫谈兼容内核之十九:Windows线程间的强相互作用 漫谈兼容内核之二十:Windows线程的系统空间堆栈 漫谈兼容内核之二十一:Windows进程的用户空间 漫谈兼容内核之二十二:Windows线程的调度和运行 漫谈兼容内核之二十三:关于TLS 漫谈兼容内核之二十四:Windows的结构化异常处理(一) 漫谈兼容内核之二十五:Windows的结构化异常处理(二) 漫谈兼容内核之二十六:Windows的结构化异常处理(三)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值