第三章:PE文件头
PE中涉及的地址有四类,它们分别为:
虚拟内存地址(VA)
相对虚拟内存地址(RVA)
文件偏移地址(FOA)
特殊地址
没有物理内存对应的页面被标记为dirty的页面,一般存储在一个名为“交换文件”的磁盘文件中。在WINDOWS XP系统中,交换文件为pagefile.sys
进程本身的VA被解释为:进程的基地址+相对虚拟内存地址
RVA是相对基地址的偏移,即RVA是虚拟内存中用来定位某个特定位置的地址,该地址的值是这个特定位置距离某个模块基地址的偏移量,所以说RVA是针对某个模块而存在的。
RVA是相对于模块而言的,VA是相对于整个地址空间而言的。
文件偏移地址是指某个位置距离文件头的偏移。
数据目录
PE中的数据目录这个数据结构记录了所有可能的数据类型,目前已定义的有15种:包括导出表,导入表,资源表,异常表,属性证书表,重定位表,
调试数据,architecture,Global Ptr,线程局部存储,加载配置表,绑定导入表,IAT,延迟导入表和CLR运行时头部。
//属性证书表验证PE合法性 ;IAT 导入地址表定义所有导入函数的VA,程序可以直接跳转到此处执行。
//CLR运行时头部:.NET框架的重要信息,加载器通过此处信息来加载托管代码所需的dll.
节:存放不同类型数据(比如代码,数据,常量,资源等)的地方,不同的节具有不同的访问权限。节是PE文件中存放代码或数据的基本单元。
一个目标文件中的所有代码可以组合成单个节,或者每个函数占一个节,一个节中的所有原始数据必须被加载到连续的内存空间中。
数据类型不同,分属不同的数据目录,但其访问属性相同,便被归类到同一个节中,这个节最终可能会占用一个或多个页面。
.data 声明的是初始化的数据;.data? 声明的是未初始化的数据(.data?的数据在磁盘中不存在,但在内存中存在);.code声明的是可执行的代码
对齐:PE中规定三类对齐:数据在内存中对齐,数据在文件中对齐,资源文件中资源数据对齐
节在内存的对齐单位必须至少是一个页的大小,32bit OS,这个值是4KB,64bit 8KB
提高磁盘利用率,通常会以一个物理扇区的大小作为对齐粒度的值,512B=200H,所以数据段,代码段等起始地址都是200h的倍数原因。
资源字节码部分一般要求以双字方式对齐
16位系统下PE结构分两部分:DOS头(DOS MZ头 + DOS Stub)和冗余数据(PE头 + PE数据区)
32位OS下,正好相反,DOS头成为冗余数据 PE文件结构:DOS头(DOS MZ头 + DOS Stub)和冗余数据(PE头 + 节表 + 节内容)
PE文件头部(DOS头 + PE头) PE数据区( 节表 + 节内容)
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //汇编伪指令 .386来生成相应平台上的汇编代码
WORD NumberOfSections; //始终等于节表中的节表项数量 否则加载会失败
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //确定IMAGE_OPTIONAL_HEADER32的大小
WORD Characteristics; //common pe file is 010fh, due to DLL file is 210eh
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
节表数量不确定,由_IMAGE_FILE_HEADER.NumberOfSections确定,但每个节的大小为40B
typedef struct _IMAGE_NT_HEADERS
{
+00h DWORD Signature
+04h IMAGE_FILE_HEADER FileHeader
+18h IMAGE_OPTIONAL_HEADER32 OptionalHeader
} IMAGE_NT_HEADERS ENDS, *PIMAGE_NT_HEADERS32;
IMAGE_NT_HEADERS=4BYTEPE标识符 + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER32 总共240字节 = 4 + 20 + 216
4BYTEPE标识符:00004550h,修改任何字节,32bit OS将无法加载些文件,对病毒文件可以修改下来预防启动
IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint 相当PE头 28h偏移量,用IDA可以自动定位这个地址,许多病毒就是劫持这个值
IMAGE_OPTIONAL_HEADER32.CheckSum 对大多数这个数为0,但对一些内核模式的驱动程序和系统dll,必须存在并正确。
代码段一般不可写,如在代码段中写数据,运行会出异常,压缩软件因为要回写代码段,它会设置为可写,压缩解压后再运行就不会异常了。
16个数据目录, 节表放的就是所谓的段(数据段,代码段什么的)
.data 数据节或叫数据段, .rdata 只读数据段,部分用来存放数据目录中的IAT。
内存映射文件字节码在内存映射大致为:
PE文件头(pe header) + 代码(.code) + 导入表(.rdata) + 数据(.data)
PE映像
【PE映像头】PE文件头部(DOS头 + PE头) PE数据区( 节表 + 节内容)
ollyICE里 memory map window
PE文件头(pe header)【确切叫内存映射文件头】 = PE文件头部(DOS头 + PE头) + 节表
代码(.code) + 导入表(.rdata) + 数据(.data) = 节内容
FOA 与 RVA 转换
FOA 节表中PointerToRawData + offset
RVA 节表中VirtualAddress + offset
VA IMAGE_OPTIONAL_HEADER32中ImageBase + RVA
;------------------------
; PE文件头中的定位
; 戚利
; 2006.2.28
;------------------------
.386
.model flat,stdcall
option casemap:none
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include imagehlp.inc
includelib imagehlp.lib
;数据段
.data
szText db 'HelloWorld',0
szOut db '地址为:%08x',0
szOutIAT db 'IAT地址为:%08x',0
szBuffer db 256 dup(0)
szExeFile db 'c:\windows\system32\kernel32.dll',0
szOut1 db 'MapFileAndCheckSum 函数与算法函数相等 kernel32.dll的校验和为:%08x',0
dwCheckSum dd ?
;代码段
.code
;---------------------
; 将内存偏移量RVA转换为文件偏移
; lp_FileHead为文件头的起始地址
; _dwRVA为给定的RVA地址
;---------------------
_RVAToOffset proc _lpFileHead,_dwRVA
local @dwReturn
pushad
mov esi,_lpFileHead
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew
assume esi:ptr IMAGE_NT_HEADERS
mov edi,_dwRVA
mov edx,esi
add edx,sizeof IMAGE_NT_HEADERS
assume edx:ptr IMAGE_SECTION_HEADER
movzx ecx,[esi].FileHeader.NumberOfSections
;遍历节表
.repeat
mov eax,[edx].VirtualAddress
add eax,[edx].SizeOfRawData
;计算该节结束RVA,
;不用Misc的主要原因是有些段的Misc值是错误的!
.if (edi>=[edx].VirtualAddress)&&(edi<eax)
mov eax,[edx].VirtualAddress
sub edi,eax ;计算RVA在节中的偏移
mov eax,[edx].PointerToRawData
add eax,edi ;加上节在文件中的的起始位置
jmp @F
.endif
add edx,sizeof IMAGE_SECTION_HEADER
.untilcxz
assume edx:nothing
assume esi:nothing
mov eax,-1
@@:
mov @dwReturn,eax
popad
mov eax,@dwReturn
ret
_RVAToOffset endp
;-------------------
; 定位到指定索引的数据目录项所在数据的起始地址
; _lpHeader 头部基地址
; _index 数据目录表索引,从0开始
; _dwFlag1
; 为0表示_lpHeader是PE映像头
; 为1表示_lpHeader是内存映射文件头
; _dwFlag2
; 为0表示返回RVA+模块基地址
; 为1表示返回FOA+文件基地址
; 为2表示返回RVA
; 为3表示返回FOA
; 返回eax=指定索引的数据目录项的数据所在地址
;-------------------
_rDDEntry proc _lpHeader,_index,_dwFlag1,_dwFlag2
local @ret,@ret1,@ret2
local @imageBase
pushad
mov esi,_lpHeader
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew ;PE标识
assume esi:ptr IMAGE_NT_HEADERS
mov eax,[esi].OptionalHeader.ImageBase ;程序的建议装载地址
mov @imageBase,eax
add esi,0078h ;指向DataDirectory
xor eax,eax ;索引*8
mov eax,_index
mov bx,8
mul bx
mov ebx,eax
; 取出指定索引数据目录项的位置,是RVA
mov eax,dword ptr [esi][ebx]
mov @ret1,eax
.if _dwFlag1==0 ;_lpHeader是PE映像头
.if _dwFlag2==0 ;RVA+模块基地址
add eax,_lpHeader
mov @ret,eax
.elseif _dwFlag2==1 ;无意义,返回FOA
invoke _RVAToOffset,_lpHeader,eax
mov @ret,eax
.elseif _dwFlag2==2 ;RVA
mov @ret,eax
.elseif _dwFlag2==3 ;FOA
invoke _RVAToOffset,_lpHeader,eax
mov @ret,eax
.endif
.else ;_lpHeader是内存映射文件头
.if _dwFlag2==0 ;RVA+模块基地址
add eax,@imageBase
mov @ret,eax
.elseif _dwFlag2==1 ;FOA+文件基地址
;先将RVA转换为文件偏移
invoke _RVAToOffset,_lpHeader,eax
mov @ret2,eax
add eax,_lpHeader
mov @ret,eax
.elseif _dwFlag2==2 ;RVA
mov @ret,eax
.elseif _dwFlag2==3 ;FOA
;先将RVA转换为文件偏移
invoke _RVAToOffset,_lpHeader,eax
mov @ret,eax
.endif
.endif
popad
mov eax,@ret
ret
_rDDEntry endp
;-------------------
; 定位到指定索引的节表项
; _lpHeader 头部基地址
; _index 表示第几个节表项,从0开始
; _dwFlag1
; 为0表示_lpHeader是PE映像头
; 为1表示_lpHeader是内存映射文件头
; _dwFlag2
; 为0表示返回RVA+模块基地址
; 为1表示返回FOA+文件基地址
; 为2表示返回RVA
; 为3表示返回FOA
; 返回eax=指定索引的节表项所在地址
;-------------------
_rSection proc _lpHeader,_index,_dwFlag1,_dwFlag2
local @ret,@ret1,@ret2
local @imageBase
pushad
mov esi,_lpHeader
assume esi:ptr IMAGE_DOS_HEADER
add esi,[esi].e_lfanew ;PE标识
assume esi:ptr IMAGE_NT_HEADERS
mov eax,[esi].OptionalHeader.ImageBase ;程序的建议装载地址
mov @imageBase,eax
mov eax,[esi].OptionalHeader.NumberOfRvaAndSizes
mov bx,8
mul bx
add esi,0078h ;指向DataDirectory
add esi,eax ;加上DataDirectory的大小,指向节表开始
xor eax,eax ;索引*40
mov eax,_index
mov bx,40
mul bx
add esi,eax ;索引项所在地址
.if _dwFlag1==0 ;_lpHeader是PE映像头
.if _dwFlag2==0 ;RVA+模块基地址
mov eax,esi
mov @ret,eax
.else
sub esi,_lpHeader
mov eax,esi
mov @ret,eax
.endif
.else ;_lpHeader是内存映射文件头
.if _dwFlag2==0 ;RVA+模块基地址
sub esi,_lpHeader
add esi,@imageBase
mov @ret,eax
.elseif _dwFlag2==1 ;FOA+文件基地址
mov eax,esi
mov @ret,eax
.else
sub esi,_lpHeader
mov eax,esi
mov @ret,eax
.endif
.endif
popad
mov eax,@ret
ret
_rSection endp
;-------------------
; 通过调用API函数计算校验和
; kernel32.dll的校验和为:0011e97e
;-------------------
_checkSum1 proc _lpExeFile
local @cSum,@hSum
local @ret
pushad
invoke MapFileAndCheckSum,_lpExeFile,\
addr @hSum,addr @cSum
mov eax,@cSum
mov @ret,eax
popad
mov eax,@ret
ret
_checkSum1 endp
;-------------------
; 自己编写程序计算校验和
;-------------------
_checkSum2 proc _lpExeFile
local hFile,dwSize,hBase
local @size
local @ret
pushad
;打开文件
invoke CreateFile,_lpExeFile,GENERIC_READ,\
FILE_SHARE_READ,NULL,OPEN_EXISTING,\
FILE_ATTRIBUTE_NORMAL,0
mov hFile,eax
invoke GetFileSize,hFile,NULL
mov dwSize,eax
;为文件分配内存,并读入
invoke VirtualAlloc,NULL,dwSize,\
MEM_COMMIT,PAGE_READWRITE
mov hBase,eax
invoke ReadFile,hFile,hBase,dwSize,addr @size,NULL
;关闭文件
invoke CloseHandle,hFile
;第一步,将CheckSum清零
mov ebx,hBase
assume ebx:ptr IMAGE_DOS_HEADER
mov ebx,[ebx].e_lfanew
add ebx,hBase
assume ebx:ptr IMAGE_NT_HEADERS
mov [ebx].OptionalHeader.CheckSum,0
assume ebx:ptr nothing
mov ecx,dwSize
mov esi,hBase
;第二步,按字进位加,溢出忽略
push ecx
inc ecx
shr ecx,1
xor ebx,ebx
clc
loc1:
lodsw ;load a word from esi to ax
adc bx,ax
loop loc1
invoke VirtualFree,hBase,dwSize,MEM_DECOMMIT
;第三步,加文件长度
pop eax
add eax,ebx
mov @ret,eax
@exit:
popad
mov eax,@ret
ret
_checkSum2 endp
start:
;PEHeader.exe导入表数据所在VA
invoke _rDDEntry,00400000h,01h,0,0
invoke wsprintf,addr szBuffer,addr szOutIAT,eax
invoke MessageBox,NULL,offset szBuffer,NULL,MB_OK
;PEHeader.exe导入表数据在文件地址FOA
invoke _rDDEntry,00400000h,01h,0,3
invoke wsprintf,addr szBuffer,addr szOut,eax
invoke MessageBox,NULL,offset szBuffer,NULL,MB_OK
;PEHeader.exe第2个节表项在内存的VA地址
invoke _rSection,00400000h,01h,0,0
invoke wsprintf,addr szBuffer,addr szOut,eax
invoke MessageBox,NULL,offset szBuffer,NULL,MB_OK
;PEHeader.exe第2个节表项在文件的偏移
invoke _rSection,00400000h,01h,0,3
invoke wsprintf,addr szBuffer,addr szOut,eax
invoke MessageBox,NULL,offset szBuffer,NULL,MB_OK
;计算校验和
invoke _checkSum1,addr szExeFile
mov dwCheckSum,eax
invoke _checkSum2,addr szExeFile
.if eax==dwCheckSum
invoke wsprintf,addr szBuffer,addr szOut1,eax
invoke MessageBox,NULL,offset szBuffer,NULL,MB_OK
.endif
invoke ExitProcess,NULL
end start