Win32汇编 - PE结构解析器


PE格式是Windows系统下最常用的可执行文件格式,有些应用必须建立在了解PE文件格式的基础之上,如可执行文件的加密与解密,文件型病毒的查杀等,熟练掌握PE文件结构,有助于软件的分析.

在PE文件中,代码,已初始化的数据,资源和重定位信息等数据被按照属性分类放到不同的Section(节区/或简称为节)中,而每个节区的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成了一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面.

在Win32系统中,当我们执行了可执行文件之后,可执行文件会被映射到内存,并且以4kb的粒度进行对齐,这个4kb也就是一个页面的大小,而每个页面又分别具有,可执行,可读写等属性.

PE格式中的DOS部分由MZ格式的文件头和可执行代码部分组成,可执行代码被称为DOS块(DOS stub).MZ格式的文件头由IMAGE_DOS_HEADER结构定义,以下就是DOS头部分的关键属性.

		mov esi,lpMemory
		assume esi:ptr IMAGE_DOS_HEADER
		movzx eax,[esi].e_magic         ; 读取DOS的头部
		movzx eax,[esi].e_ss            ; DOS代码段的初始堆栈段
		movzx eax,[esi].e_sp            ; DOS代码段的初始堆栈指针
		movzx eax,[esi].e_cs            ; DOS代码的入口地址
		movzx eax,[esi].e_ip            ; DOS代码的入口IP
		movzx eax,[esi].e_lfanew        ; 指向了PE文件的开头(重要)

第一个字段e_magic被定义为MZ,标志着DOS文件的开头部分,最后一个字段e_lfanew则指明了PE文件的开头位置,现在来说除了第一个字段和最后一个字段有些用处,其他的字段几乎已经废弃了,这里也不再介绍了.

解析PE头结构

从DOS文件头的e_lfanew字段(文件头偏移003ch),PE文件格式排列在DOS头的后面,也就是e_lfanew指针所指向的地址,而PE文件的第一个字节就是PE这两个字符,有了这些信息,我们就可以写一个小工具,来检测指定一个程序是否是可执行文件啦.

.data
	szFileName db "lyshark.exe",0h
	hFile dd ?
	hMapFile dd ?
	lpMemory dd ?
	szText db "这是一个PE可执行文件 !",0h
.code
	main PROC
	; 打开文件,并创建内存映射镜像
		invoke	CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
			FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
		mov hFile,eax
		invoke CreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,NULL
		mov hMapFile,eax
		invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
		mov lpMemory,eax
; -------------------------------------------------------------------
	; 检测PE文件是否有效,是否是一个正常的PE
		mov esi,lpMemory
		assume esi:ptr IMAGE_DOS_HEADER
		; 判断是否为DOS文件头部
		.if [esi].e_magic == IMAGE_DOS_SIGNATURE
			add esi,[esi].e_lfanew              ; 递增指针
			assume esi:ptr IMAGE_NT_HEADERS
			; 判断是否为PE可执行文件
			.if [esi].Signature == IMAGE_NT_SIGNATURE
				invoke MessageBox,NULL,addr szText,0,MB_OK
			.endif
		.endif
; -------------------------------------------------------------------
		invoke UnmapViewOfFile,addr lpMemory
		invoke ExitProcess,NULL
	main ENDP
END main

上面的核心代码原理也非常的简单,过程:读入文件,判断第一个字符是不是MZ,如果是MZ,则在判断e_lfanew指针指向的地址是不是PE如果是,则说明这是PE文件.

解析各区块信息

下面的代码,则用于读取PE文件的一些关键区块信息.

	.386
	.model flat,stdcall
	option casemap:none

include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include masm32.inc
includelib masm32.lib

.data
	szFileName db "lyshark.exe",0h
	hFile dd ?
	hMapFile dd ?
	lpMemory dd ?
	lpBuffer db 2048 dup(?)
.const
	szMsg	db	"----------------------------------------",0dh,0ah
		db	"运行平台:           0x%04X",0dh,0ah
		db	"节区数量:           %d",0dh,0ah
		db	"文件属性:           0x%04X",0dh,0ah
		db	"时间标记:            %d",0dh,0ah
		db	"镜像装入基址:       0x%08X",0dh,0ah
		db	"程序的入口RVA:      0x%08X",0dh,0ah
		db	"代码节起始RVA:      0x%08X",0dh,0ah
		db	"数据节起始RVA:      0x%08X",0dh,0ah
		db	"----------------------------------------",0dh,0ah,0
.code
	main PROC
	; 打开文件,并创建内存映射镜像
		invoke	CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
			FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
		mov hFile,eax
		invoke CreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,NULL
		mov hMapFile,eax
		invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
		mov lpMemory,eax
		mov esi,lpMemory

		assume	esi:ptr IMAGE_DOS_HEADER
		add esi,[esi].e_lfanew
		assume esi:ptr IMAGE_NT_HEADERS
		invoke wsprintf,addr lpBuffer,addr szMsg,\
			[esi].FileHeader.Machine, \                 ; 运行平台
			[esi].FileHeader.NumberOfSections, \        ; 节区数目
			[esi].FileHeader.Characteristics, \         ; 文件属性
			[esi].FileHeader.TimeDateStamp, \           ; 时间标记
			[esi].OptionalHeader.ImageBase, \           ; 镜像基址
			[esi].OptionalHeader.AddressOfEntryPoint, \ ; 入口RVA地址
			[esi].OptionalHeader.BaseOfCode, \          ; 代码节起始RVA
			[esi].OptionalHeader.BaseOfData
		invoke StdOut,addr lpBuffer
		invoke ExitProcess,NULL
	main ENDP
END main

解析节与节表

系统装载可执行文件并不等同于内存映射,内存映射是将整个磁盘文件原封不动的搬到内存中去,而PE的加载则会处理一些其他数据,例如预处理,重定位等,装入以后页面位置,偏移等都会随之发生改变,Windows装载器在装载DOS部分,PE文件头部分和节表部分时不进行任何处理,而装载节的时候将根据节的属性做不同的处理.

	.386
	.model flat,stdcall
	option casemap:none

include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include masm32.inc
includelib masm32.lib

.data
	szFileName db "lyshark.exe",0h
	hFile dd ?
	hMapFile dd ?
	lpMemory dd ?
	lpBuffer db 2048 dup(?)
.const
	szMsg	db	"----------------------------------------------------------",0dh,0ah
		db	"节区名称  节区大小  虚拟地址  Raw_尺寸  Raw_偏移  节区属性",0dh,0ah
		db	"----------------------------------------------------------",0dh,0ah,0
	szFmt	db	"%s  %08X  %08X  %08X  %08X  %08X",0dh,0ah,0
.code
	main PROC
		invoke	CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
			FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
		mov hFile,eax
		invoke CreateFileMapping,hFile,NULL,PAGE_READONLY,0,0,NULL
		mov hMapFile,eax
		invoke MapViewOfFile,eax,FILE_MAP_READ,0,0,0
		mov lpMemory,eax
		mov esi,lpMemory

		assume	esi:ptr IMAGE_DOS_HEADER     ; 指向DOS开头
		add esi,[esi].e_lfanew               ; 递增指针到PE结构开头
		assume esi:ptr IMAGE_NT_HEADERS
		
		invoke StdOut,addr szMsg                        ; 输出提示信息
		movzx ecx,[esi].FileHeader.NumberOfSections     ; 取出节的数量,作为循环条件
		add esi,sizeof IMAGE_NT_HEADERS      ; 指向.text节
		assume esi:ptr IMAGE_SECTION_HEADER  ; 指向节中的SECTION
		.repeat
			push ecx      ; wsprintf影响ecx寄存器,所以这里必须压栈保存数据
			mov eax,[esi].VirtualAddress
			
			invoke wsprintf,addr lpBuffer,addr szFmt,esi, \   ; 节区名称
				[esi].Misc.VirtualSize, \                 ; 节区大小
				[esi].VirtualAddress, \                   ; 虚拟地址
				[esi].SizeOfRawData, \                    ; Raw_尺寸
				[esi].PointerToRawData, \                 ; Raw_偏移
				[esi].Characteristics                     ; 节区属性
			invoke StdOut,addr lpBuffer                       ; 打印节区信息
			pop ecx
			add esi,sizeof IMAGE_SECTION_HEADER
		.untilcxz
		invoke ExitProcess,NULL
	main ENDP
END main

相关