⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 017.txt

📁 会变语言实现的一些程序
💻 TXT
📖 第 1 页 / 共 5 页
字号:

  db   ~节区数量:   %d~,0dh,0ah

  db   ~文件标记:   0x%04X~,0dh,0ah

  db   ~建议装入地址:   0x%08X~,0dh,0ah,0ah,0

szMsgSection db ~--------------------------------------------~,0dh,0ah

  db   ~节区名称  节区大小  虚拟地址  Raw_尺寸  Raw_偏移  节区属性~,0dh,0ah

    db  ~-----------------------------------------------~,0dh,0ah,0

szFmtSection db ~%s  %08X  %08X  %08X  %08X  %08X~,0dh,0ah,0

 

    .code

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

_ProcessPeFile  proc     _lpFile,_lpPeHead,_dwSize

    local @szBuffer[1024]:byte,@szSectionName[16]:byte

 

    pushad

    mov   edi,_lpPeHead

    assume  edi:ptr IMAGE_NT_HEADERS

;********************************************************************

; 显示 PE 文件头中的一些信息

;********************************************************************

    movzx ecx,[edi].FileHeader.Machine

    movzx edx,[edi].FileHeader.NumberOfSections

    movzx ebx,[edi].FileHeader.Characteristics

    invoke  wsprintf,addr @szBuffer,addr szMsg,\

    addr szFileName,ecx,edx,ebx,\

    [edi].OptionalHeader.ImageBase

    invoke  SetWindowText,hWinEdit,addr @szBuffer

;********************************************************************

; 循环显示每个节区的信息

;********************************************************************

    invoke  _AppendInfo,addr szMsgSection

    movzx ecx,[edi].FileHeader.NumberOfSections

    add   edi,sizeof IMAGE_NT_HEADERS

    assume  edi:ptr IMAGE_SECTION_HEADER

    .repeat

    push     ecx

;********************************************************************

; 获取节的名称,由于节名称不一定是以0结尾的,所以要进行一些处理

;********************************************************************

    invoke  RtlZeroMemory,addr @szSectionName,\

      sizeof @szSectionName

      push     esi

      push     edi

    mov   ecx,8

    mov   esi,edi

    lea   edi,@szSectionName

      cld

    @@:

      lodsb

      .if   ! al

    mov   al,~ ~

      .endif

    stosb

    loop     @B

      pop   edi

    pop   esi

;********************************************************************

    invoke  wsprintf,addr @szBuffer,addr szFmtSection,\

      addr @szSectionName,[edi].Misc.VirtualSize,\

      [edi].VirtualAddress,[edi].SizeOfRawData,\

    [edi].PointerToRawData,[edi].Characteristics

    invoke  _AppendInfo,addr @szBuffer

    add   edi,sizeof IMAGE_SECTION_HEADER

;********************************************************************

      pop   ecx

    .untilcxz

    assume  edi:nothing

    popad

    ret

 

_ProcessPeFile    endp

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

程序首先显示了PE文件头中一些重要字段的数值,如Machine,NumberOfSections和Characteristics等字段,然后根据NumberOfSections的数值构造一个循环,并在循环中显示每个节的信息。

在子程序开始的地方,edi寄存器被赋值为PE文件头指针,当在循环开始前将edi加上IMAGE_NT_HEADERS结构的长度后,edi指向的就是节表的起始地址了,使用edi作指针就可以将每个节的名称、尺寸、RVA地址、在文件中的偏移以及大小显示出来。

读者可以用这个PEInfo程序打开不同的文件来验证一下本节中叙述的一些内容,并思考下面的问题:

(1)比较EXE文件和DLL文件的Characteristics字段的差异。

(2)EXE文件往往没有重定位节(一般名称为.reloc),而DLL文件中总是有这个节。

(3)看看代码节和数据节的属性有什么不同,再查看第11章中KeyHook例子中的DLL文件,看看包含共享数据的节的属性又有什么不同。

(4)编写一个有.data?段却没有.data段的程序并用PEInfo去查看,可以发现数据节在磁盘文件中的长度为0,但是被映射到内存中以后却不为0。

 


17.2 导 入 表


在开始下面几节的介绍前,先来复习一下17.1节中提出的两个概念。

首先,PE文件中的数据按照装入内存后的页面属性被划分成多个节,并由节表中的数据来描述这些节。一个节中的数据仅仅是属性相同而已,并不一定就是同一种用途的,比如导入表、导出表等就有可能和只读常量一起被放在同一个节中,因为它们的属性同是可读不可写的。

其次,由于不同用途的数据可能被放在同一个节中,仅仅依靠节表是无法确定它们的存放位置的,PE文件中依靠文件头中IMAGE_OPTIONAL_HEADER32结构内的数据目录表来指出它们的位置,可以由数据目录表来定位的数据包括导入表、导出表、资源、重定位表和TLS等15种数据。

好了,现在要引出这几节将要讲述的内容了:从数据目录表得到的是这些数据的RVA和数据块的尺寸,很明显,不同的数据块中的数据组织方式是不同的,比如导入表和资源数据块中的数据就完全是两码事情,要想深入了解PE文件就必须了解这些数据的组织方式以及系统是如何处理它们的,这就是本节以及下面几个小节的内容。

本节将首先介绍导入表的格式,下面的几个小节将逐一介绍导出表、资源和重定位表的格式和使用方法。

17.2.1  导入表简介

在Win32编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。

对于存储在磁盘上的PE文件来说,它无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存的时候,Windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。动态链接是通过PE文件中定义的“导入表”(Import Table)来完成的,导入表中保存的正是函数名和其驻留的DLL名等动态链接所必需的信息。

1. 调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?先将第3章中那个简单的Hello World程序反汇编一把,看看调用导入函数的指令都是什么样子的,需要反汇编的两句源代码如下:

  invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK

  invoke  ExitProcess,NULL

当使用W32Dasm反汇编以后,这两句代码变成了以下的指令:

:00401000 6A00     push 00000000

:00401002 6800304000     push 00403000

:00401007 680F304000     push 0040300F

:0040100C 6A00   push 00000000

:0040100E E807000000     Call 0040101A ;MessageBox

:00401013 6A00   push 00000000

:00401015 E806000000     Call 00401020 ;ExitProcess

:0040101A FF2508204000   Jmp dword ptr [00402008]

:00401020 FF2500204000   Jmp dword ptr [00402000]

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是在DLL模块中的,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr [xxxxxxxx]类型的指令,这个指令是一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那么在没有装载到内存之前,PE文件中的00402000地址处的内容是什么呢?使用在17.1.4节中了解的方法来查看一下。

首先,使用17.1.4节的例子文件PEInfo.exe去查看一下Hello.exe文件,会得到以下的信息:

文件名:C:\Documents and Settings\Administrator\桌面\Hello.exe

----------------------------------------------------------

运行平台:   0x014C

节区数量:   3

文件标记:   0x010F

建议装入地址:   0x00400000

----------------------------------------------------------

节区名称  节区大小  虚拟地址  Raw_尺寸  Raw_偏移  节区属性

----------------------------------------------------------

.text 00000026  00001000  00000200  00000400  60000020

.rdata   00000092  00002000  00000200  00000600  40000040

.data 00000022  00003000  00000200  00000800  C0000040

由于建议装入地址是00400000h,所以00402000h地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移项目为600h,也就是说00402000h地址的内容实际上对应PE文件中偏移600h处的数据。

现在随便找一个16进制编辑器来看看文件0600h处的内容是什么:

0600  76 20 00 00 00 00 00 00-5C 20 00 00 00 00 00 00 v ......\ ......

0610  54 20 00 00 00 00 00 00-00 00 00 00 6A 20 00 00 T ..........j ..

0620  08 20 00 00 4C 20 00 00-00 00 00 00 00 00 00 00 . ..L ..........

0630  84 20 00 00 00 20 00 00-00 00 00 00 00 00 00 00 . ... ..........

0640  00 00 00 00 00 00 00 00-00 00 00 00 76 20 00 00 ............v ..

0650  00 00 00 00 5C 20 00 00-00 00 00 00 BB 01 4D 65 ....\ ........Me

0660  73 73 61 67 65 42 6F 78-41 00 55 53 45 52 33 32 ssageBoxA.USER32

0670  2E 64 6C 6C 00 00 75 00-45 78 69 74 50 72 6F 63 .dll..u.ExitProc

0680  65 73 73 00 4B 45 52 4E-45 4C 33 32 2E 64 6C 6C ess.KERNEL32.dll

0690  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................

查看的结果是00002076h,这显然不会是内存中的ExitProcess函数的地址,慢着!将它作为RVA看会怎么样呢?查看节表可以发现RVA地址00002076h也处于.rdata节内,减去节的起始地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是说它对应文件0676h开始的地方,接下来可以惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

这都有点搞糊涂了,Call ExitProcess指令被编译成了Call aaaaaaaa类型的指令,而aaaaaaaa处的指令是Jmp dword ptr [xxxxxxxx],而xxxxxxxx地址的地方只是一个似乎是指向函数名字符串的RVA地址,这一系列的指令显然是无法正确执行的!

但如果告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。

接下来看看如何去获取导入表的位置,以及导入表中的数据是如何组织以便Windows装载器能够顺利地进行上面的转换工作的。

2. 获取导入表的位置

导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构(见表17.4)。

从IMAGE_DATA_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址;如果在PE文件中查找导入表,那么需要首先使用17.1.4节中例举的_RVAToOffset子程序将RVA首先转换成文件偏移。

 


17.2.2  导入表的结构 
1. PE文件中的导入表

现在得到了包含导入表的数据块,导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个DLL文件,例如,如果一个PE文件从10个不同的DLL文件中引入了函数,那么就存在10个IMAGE_IMPORT_DESCRIPTOR结构来描述这些DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。

IMAGE_IMPORT_DESCRIPTOR结构的定义如下:

IMAGE_IMPORT_DESCRIPTOR STRUCT

  union

  Characteristics  dd ?

  OriginalFirstThunk dd ?

  ends

  TimeDateStamp dd   ?

  ForwarderChain dd ?

  Name1 dd   ?

  FirstThunk dd ?

IMAGE_IMPORT_DESCRIPTOR ENDS

结构中的Name1字段(使用Name1作为字段名同样是因为Name一词和MASM的关键字冲突)是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。

OriginalFirstThunk字段和FirstThunk字段的含义现在可以看成是相同的(使用“现在”一词的含义马上会见分晓),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组的最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。

一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同的时刻有不同的含义,结构的定义如下:

IMAGE_THUNK_DATA STRUCT

  union u1

  ForwarderString dd ?

  Function dd     ?

  Ordinal dd   ?

  AddressOfData dd     ?

  ends

IMAGE_THUNK_DATA ENDS

一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?当双字(就是指结构!)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。读者可以用预定义值IMAGE_ORDINAL_FLAG32(或80000000h)来对最高位进行测试,当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构的定义如下:

IMAGE_IMPORT_BY_NAME STRUCT

  Hint dw ?

  Name1 db   ?

IMAGE_IMPORT_BY_NAME ENDS

结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name1字段定义了导入函数的名称字符串,这是一个以0为结尾的字符串。

整个过程听起来有些复杂,其实再看一下图17.5就很清楚了,图中示意了可执行文件导入了Kernel32.dll中的ExitProcess,ReadFile,WriteFile和lstrcmp函数的情况,其中,前面3个函数按照名称方式导入,最后的lstrcmp函数按照序号导入,这4个函数的序号分别是02f6h,0111h,002bh和0010h。



图17.5  函数的导入方法举例

现在来分析一下图17.5中的示例,导入表中IMAGE_IMPORT_DESCRIPTOR结构的Name1字段指向字符串“Kernel32.dll”,表明当前要从Kernel32.dll文件中导入函数,OriginalFirstThunk和FirstThunk字段指向两个同样的IMAGE_THUNK_DATA数组,由于要导入的是4个函数,所以数组中包含4个有效项目并以最后一个内容为0的项目作为结束。

第4个函数lstrcmp函数是以序号导入的,与其对应的IMAGE_THUNK_DATA结构的最高位等于1,和函数的序号0010h组合起来的数值就是80000010h,其余的3个函数采用的是以函数名导入的方式,所以IMAGE_THUNK_DATA结构的数值是一个RVA,分别指向3个IMAGE_IMPORT_BY_NAME结构,每个IMAGE_IMPORT_BY_NAME结构的第一个字段是函数的序号,后面就是函数的字符串名称了,一切就是这么简单!

2. 内存中的导入表

为什么需要两个一模一样的IMAGE_THUNK_DATA数组呢?答案是当PE文件被装入内存的时候,其中一个数

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -