《Windows PE权威指南》学习之第2章 三个小工具的编写(1)

本文介绍了一种解决未设置到/Framework里的dylib库无法被程序包正确调用的问题的方法。通过使用otool查看链接位置,并利用install_name_tool修改dylib自身的链接位置,可以将dylib复制到程序的Frameworks目录实现正常调用。

俗话说:“工欲善其事,必先利其器。”本章将完成与Windows PE有关的三个小工具的开发。这三个小工具分别是:

        ❑ PEDump:PE文件字节码查看器

        ❑ PEComp:PE文件比较器

        ❑ PEInfo:PE文件结构查看器

首先,让我们从编写最基本的汇编窗口程序开始。该窗口程序是编写本章的三个小工具的基础,也是编写后续大部分章节中其他程序的基础。

2.1 构造基本窗口程序

本节我们将构造一个具有基本窗口元素(含标题栏、菜单栏、工作区域)的窗口程序,后续大部分的程序开发都将以这个基本窗口程序作为基础进行扩展。

2.1.1 构造窗口界面

要构造的窗口程序具备窗口图形界面的大部分元素,包含窗口、菜单、图标、工作区域等。通常的做法是:首先,根据程序功能对程序的界面进行构思;然后,在纸上画出大致的结构图;最后,通过资源脚本来定义并实现界面中的每一部分。当然,读者也可以使用一些辅助的软件(如RADAsm中的资源编辑器,或者VS中的资源编辑器)根据构思好的界面,在所见即所得的资源编辑器图形界面中直接构造程序窗口界面。该程序最终显示的效果如图2-1所示。

                                                                 图2-1 基本窗口界面

2.1.2 编写相关的资源文件

构造完窗口界面以后,需要依据界面编写对应的资源文件,一般以“.rc”为扩展名;这有点类似于工程建设里的依照图纸施工。资源文件编写完成后,还必须通过资源编译器对资源文件实施编译,以生成资源目标文件。资源文件是文本文件,由定义资源的一些脚本语句组成,可以使用文本编辑软件(如记事本)查看和修改。资源目标文件是对这些脚本的一种再组织,根据脚本描述将脚本涉及的所有资源编译到一起,形成二进制字节码。资源目标文件无法通过文本编辑软件查看。

整个过程分为两个阶段:

        ❑ 创建资源文件pe.rc

        ❑ 生成资源目标文件pe.res

下面分别来介绍这两个阶段的内容。

1.创建资源文件pe.rc

在编写资源文件时,需要定义图形中出现的所有菜单项、对话框、图标等。资源文件的详细编码如代码清单2-1所示。

代码清单2-1 资源文件的详细编码(chapter2\pe.rc)

#include <resource.h>

#define ICO_MAIN 1000
#define DLG_MAIN 1000
#define IDC_INFO 1001
#define IDM_MAIN 2000
#define IDM_OPEN 2001 
#define IDM_EXIT 2002 

#define IDM_1 4000
#define IDM_2 4001
#define IDM_3 4002 
#define IDM_4 4003 


ICO_MAIN ICON "main.ico"

DLG_MAIN DIALOG 50, 50, 544, 199
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU 
CAPTION "PE文件基本信息 by Scott"
MENU IDM_MAIN 
FONT 9,"宋体"
BEGIN 
	CONTROL "", IDC_INFO, "RichEdit20A", 196 | ES_WANTRETURN | WS_CHILD| ES_READONLY
					| WS_VISIBLE | WS_BORDER | WS_VSCROLL | WS_TABSTOP, 0,0, 540, 396
END 

IDM_MAIN menu discardable 
BEGIN 
	POPUP " 文件(&F)"
	BEGIN 
		menuitem "打开文件(&O)...", IDM_OPEN 
		menuitem separator 
		menuitem "退出(&X)", IDM_EXIT 
	END 
	
	POPUP "查看"
	BEGIN 
		menuitem "源文件", IDM_1 
		menuitem "窗口透明度", IDM_2 
		menuitem separator 
		menuitem "大小", IDM_3 
		menuitem "宽度", IDM_4 
	END 
	
END 

第1~13行定义了各元素常量,第16行指定了显示在窗口标题栏的图标为main.ico。需要注意的是,必须保证图标文件与资源文件在同一个目录中,如果指定的图标文件在其他目录中,则需要使用绝对路径,语法如下:

ICO_MAIN   ICON   "C:\source\icon\main.ico"

第18~26行定义了对话框DIALOG,该对话框最终的显示效果如图2-1所示。窗口定义中包含了窗口的显示样式、标题栏文字、窗口中包含的菜单IDM_MAIN,以及窗口字体格式。窗口工作区域中只包含了一个富文本框控件IDC_INFO(在第24行和第25行定义)。

第28~46行定义了菜单IDM_MAIN。它包含两个弹出式下拉菜单,分别命名为“文件”和“查看”,每个菜单中又各包含了多个菜单选项。

2.生成资源目标文件pe.res

接下来编译资源文件,生成资源目标文件(扩展名为res)。在命令提示符下输入以下命令(加粗部分):

D:\masm32\source\chapter2>rc -r pe.rc

如果执行编译时没有错误发生(如资源脚本中定义的相关文件不存在就会抛出错误提示),则命令执行后会在当前目录下生成资源目标文件pe.res。该资源目标文件最终要被链接程序嵌入到PE文件中,构成PE资源表所描述的数据的一部分。

2.1.3 通用程序框架的实现

资源目标文件生成以后,接下来的工作就是实现通用程序框架。主要分为三个阶段:

        ❑ 编写源程序pe.asm

        ❑ 编译生成目标文件pe.obj

        ❑ 链接生成可执行文件pe.exe

下面分别介绍各阶段的详细内容。

1.编写源程序pe.asm

首先,打开记事本,输入代码清单2-2所示的内容(去掉前面的行号)。

;pe.asm   通用程序框架
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff pe.asm
;rc -r pe.rc
;link -subsystem:windows pe.obj pe.res
.386
.model flat, stdcall 
option casemap:none 

include		c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
include 	c:/masm32/include/comdlg32.inc 
includelib 	c:/masm32/lib/comdlg32.lib 

ICO_MAIN	equ 1000 
DLG_MAIN 	equ 1000
IDC_INFO	equ 1001 
IDM_MAIN 	equ 2000
IDM_OPEN 	equ 2001 
IDM_EXIT 	equ 2002 
IDM_1		equ 4000
IDM_2 		equ 4001 
IDM_3 		equ 4002 

.data 
hInstance 	dword ?
hRichEdit 	dword ?
hWinMain	dword ?
hWinEdit	dword ?
szFileName 	byte MAX_PATH dup(?)

.const 
szDllEdit 	byte 'RichEd20.dll', 0
szClassEdit byte 'RichEdit20A', 0
szFont 		byte '宋体', 0

.code
;初始化窗口程序
_init proc 
	local @stCf:CHARFORMAT 
	
	invoke GetDlgItem, hWinMain, IDC_INFO 
	mov hWinEdit, eax 
	
	;为窗口设置图标
	invoke LoadIcon, hInstance, ICO_MAIN 
	invoke SendMessage, hWinMain, WM_SETICON, ICON_BIG, eax 	

	;设置编辑控件	
	invoke SendMessage, hWinEdit, EM_SETTEXTMODE, TM_PLAINTEXT, 0 		
	invoke RtlZeroMemory, addr @stCf, sizeof @stCf 					;初始化 
	mov @stCf.cbSize, sizeof @stCf 
	mov @stCf.yHeight, 9*20
	mov @stCf.dwMask, CFM_FACE or CFM_SIZE or CFM_BOLD 
	invoke lstrcpy, addr @stCf.szFaceName, addr szFont 
	invoke SendMessage, hWinEdit, EM_SETCHARFORMAT, 0, addr @stCf 
	invoke SendMessage, hWinEdit, EM_EXLIMITTEXT, 0, -1 
	ret 
_init endp 


;-------------------------------
;窗口程序
;--------------------------------
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	mov eax, wMsg 
	.if eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG				;初始化 	
		push hWnd 
		pop hWinMain 
		call _init 
	.elseif eax == WM_COMMAND 					;菜单
		mov eax, wParam 
		.if eax == IDM_EXIT					;退出
			invoke EndDialog, hWnd, NULL 
		.elseif eax == IDM_OPEN 			;打开文件 
		.elseif eax == IDM_1 
		.elseif eax == IDM_2 
		.elseif eax == IDM_3 
		.endif 
	.else
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

start:
	invoke LoadLibrary, offset szDllEdit 
	mov hRichEdit, eax 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, \
			DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke FreeLibrary, hRichEdit 
	invoke ExitProcess, NULL 
end start 

代码清单2-2中的第98行通过调用DialogBoxParam函数创建了一个弹出式窗口作为整个程序的主窗口,并将内部函数_ProcDlgMain的地址当成该函数的参数之一传入该函数。函数_ProcDlgMain是弹出窗口的回调函数,如果要对发生在窗口中的消息进行捕获,则需要在此函数中设置对不同的消息进行响应的代码。由于本实例只是一个基本程序框架,所以回调函数只对菜单中的“退出”选项做了响应,如下所示:

70      .if eax==WM_CLOSE
71                 invoke EndDialog,hWnd,NULL

代码编写完成以后保存为pe.asm文件,然后编译生成目标文件。

2.编译生成目标文件pe.obj

在编写较大的程序时,通常会根据功能将源代码分别写到不同的文件里。有时为了分工合作,同一个项目中还会出现使用不同语言编写的源代码。这些源代码文件都需要在各自独立的环境中被编译成各自的目标文件。目标文件符合通用对象文件格式(COFF),该格式的定制主要是为了方便混合编程。生成目标文件的过程是处理源代码中可能会出现错误(如引入外部符号错误、源代码语法错误等)的过程,生成的目标文件最终会被链接程序拼接到最终的可执行文件中,当然,除了编译源代码生成的目标文件外,可执行文件还包含资源目标文件、外部引入的符号等信息。

在命令提示符下输入以下命令,编译源文件pe.asm:

D:\masm32\source\chapter2>ml -c -coff pe.asm

如果没有错误,则会在当前目录下生成目标文件pe.obj。接下来链接所有的目标文件(包括资源目标文件和源代码目标文件),生成最终的可执行文件。

3.链接生成可执行文件pe.exe

在命令提示符下输入以下命令:

D:\masm32\source\chapter2>link -subsystem:windows pe.obj pe.res

上述命令指定了最终生成的EXE文件的运行平台为Windows,链接程序将根据pe.obj中的描述构造PE文件,并将相关资源内容附加到PE文件里,最终生成可执行的pe.exe。在命令提示符下输入“pe”,然后回车,即可看到最终的运行效果,如图2-1所示。

至此,一个基本的基于汇编语言的窗口程序就编写完成了。接下来的工作就是在此基础上进行扩展,开发三个基于Windows PE的小工具,首先来看字节码查看器PEDump的编写。


笔记:

编写Makefile文件,使用nmake编译,内容如下

NAME = pe
OBJS = $(NAME).obj
RES  = $(NAME).res

LINK_FLAG = /subsystem:windows
ML_FLAG = /c /coff

$(NAME).exe: $(OBJS) $(RES)
	Link $(LINK_FLAG) $(OBJS) $(RES)

.asm.obj:
	ml $(ML_FLAG) $<
.rc.res:
	rc $<

clean:
	del *.obj
	del *.res

执行nmake命令编译

运行:


2.2 PEDump的实现

PEDump是PE文件字节码查看器,利用它可以查看和阅读指定PE文件的十六进制字节码,帮助我们更好地分析PE结构。

2.2.1 编程思路

编写PEDump的重点在于显示功能,首先来看一看最终可能的输出效果,如图2-2所示。

                                                                图2-2 PEDump的输出效果

如图所示,最终输出包含三列内容。

第一列是地址。地址的值是第(n×16+1)个字节在文件中的位置。

第二列是由空格分隔符分隔的16个字节的十六进制显示。

第三列是这16个字节对应的ASCII码值。如果ASCII码中无对应值,或者这些值是一些功能键,则以“.”代替。

注意 有的查看器(如FlexHex)还增加了Unicode字符一列,用来显示字节码中包含的Unicode字符。

编程时要考虑到最后一行可能会少于16个字节,这时候第二列和第三列不足的地方就可以使用空格补足。程序编写的流程如图2-3所示。

                                                         图2-3 编写PEDump程序的流程

步骤1 打开PE文件。需要说明的是,打开的PE文件会被映射为内存文件。因为内存文件中的内容是线性存放的,存取方便,速度也快,并且操作起来比在文件中使用指针定位要更容易些。

步骤2 使用API函数GetFileSize得到该PE文件的大小。

步骤3~5 将第2步获取的值与16相除,商作为循环计数,余数则是字节码查看器最后一行的字节个数。在程序中构造一个循环,用来显示PE文件除最后一行外其他行的字节内容。

为了更好地理解该开发过程,我们需要理解两个概念:内存映射文件与PE内存映像。

内存映射文件是指将硬盘上的文件不做修改地装载到内存中。这样,文件中字节与字节之间就是顺序排列的了。在硬盘上,文件被分割成若干簇,这些簇不一定会按照文件内容顺序排列在一起,当我们访问磁盘上的文件时,需要计算机首先将不同位置的内容读取到内存。有了内存映射文件,访问就会变得更轻松和快捷,由于读取磁盘的操作集中到了一起执行,读写效率会提高很多。被一次性读取到内存的文件字节按线性排序,访问相对简单,速度也提升了不少。所以,许多大型的编辑软件在设计中经常会使用内存映射文件存取磁盘文件。

PE内存映像是指将PE文件按照一定的规则装载到内存中,装入后的整个文件头内容不会发生变化,但PE文件的某一部分如节的内容会按照字段中的对齐方式在内存中对齐,从而使得内存中的PE映像与装载前的PE文件不同。那么,为什么PE内存映像不能和一般的内存映射文件一样呢?答案很简单:PE文件是由操作系统装载进内存的,其目的是为了运行。为了配合操作系统的运行,方便调度,提高运行效率,PE映像必须按照一定的格式对齐,所以内存中的PE映像和原来硬盘上的文件是不同的,当然与内存映射文件也就不一样了。

2.2.2 PEDump编码

前面简单了解了程序的开发流程,接下来进入编码阶段。此处将会用到2.1节中的源程序文件pe.asm。

PEDump.asm在pe.asm的基础上增加了对菜单项IDM_OPEN的响应代码,如下所示:

.elseif eax==IDM_OPEN    ;打开文件
      invoke _openFile

函数_openFile的实现如代码清单2-3所示。

代码清单2-3 PEDump主要函数_openFile实现(chapter2\pedump.asm)

1 ;--------------------
2 ; 打开PE文件并处理
3 ;--------------------
4 _openFile proc
5 local @stOF:OPENFILENAME
6 local @hFile,@hMapFile
7 local @bufTemp1 ; 十六进制字节码
8 local @bufTemp2 ; 第一列
9 local @dwCount ; 计数,逢16则重新计
10 local @dwCount1 ; 地址序号
11 local @dwBlanks ; 最后一行空格数
12
13 invoke RtlZeroMemory,addr @stOF,sizeof @stOF
14 mov @stOF.lStructSize,sizeof @stOF
15 push hWinMain
16 pop @stOF.hwndOwner
17 mov @stOF.lpstrFilter,offset szExtPe
18 mov @stOF.lpstrFile,offset szFileName
19 mov @stOF.nMaxFile,MAX_PATH
20 mov @stOF.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST
21 invoke GetOpenFileName,addr @stOF ;让用户选择打开的文件
22 .if !eax
23 jmp @F
24 .endif
25 invoke CreateFile,addr szFileName,GENERIC_READ,\
26 FILE_SHARE_READ or FILE_SHARE_WRITE,NULL,\
27 OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
28 .if eax!=INVALID_HANDLE_VALUE
29 mov @hFile,eax
30 invoke GetFileSize,eax,NULL ;获取文件大小
31 mov totalSize,eax
32
33 .if eax
34 invoke CreateFileMapping,@hFile,\ ;内存映射文件
35 NULL,PAGE_READONLY,0,0,NULL
36 .if eax
37 mov @hMapFile,eax
38 invoke MapViewOfFile,eax,\
39 FILE_MAP_READ,0,0,0
40 .if eax
41 mov lpMemory,eax ;获得文件在内存的映像起始位置
42 assume fs:nothing
43 push ebp
44 push offset _ErrFormat
45 push offset _Handler
46 push fs:[0]
47 mov fs:[0],esp
48
49 ;开始处理文件
50

177
178 ;处理文件结束
179
180 jmp _ErrorExit
181
182 _ErrFormat:
183 invoke MessageBox,hWinMain,offset szErrFormat,NULL,MB_OK
184 _ErrorExit:
185 pop fs:[0]
186 add esp,0ch
187 invoke UnmapViewOfFile,lpMemory
188 .endif
189 invoke CloseHandle,@hMapFile
190 .endif
191 invoke CloseHandle,@hFile
192 .endif
193 .endif
194 @@:
195 ret
196 _openFile endp
 

子程序_openFile首先调用GetOpenFileName,显示一个文件选择对话框,让用户选择要打开的PE文件;然后,获取指定文件的大小,并利用这个值通过函数CreateFileMapping在内存中建立该文件的映像;全局变量lpMemory指向了内存映像的起始地址。有了这个地址以后,对文件进行各种操作就简单多了。

第50~177行是对内存映像文件的处理过程。这个过程如果太复杂,可以继续使用子程序;如果不是很复杂,则可以直接在此编写处理代码。十六进制字节码查看器的主要代码如下:

51 ;缓冲区初始化
52 invoke RtlZeroMemory,addr @bufTemp1,10
53 invoke RtlZeroMemory,addr @bufTemp2,20
54 invoke RtlZeroMemory,addr lpServicesBuffer,100
55 invoke RtlZeroMemory,addr bufDisplay,50
56
57 mov @dwCount,1
58 mov esi,lpMemory
59 mov edi,offset bufDisplay
60
61 ; 将第一列写入lpServicesBuffer
62 mov @dwCount1,0
63 invoke wsprintf,addr @bufTemp2,addr lpszFilterFmt4,@dwCount1
64 invoke lstrcat,addr lpServicesBuffer,addr @bufTemp2
65
66 ;求最后一行的空格数(16-长度% 16)*3
67 xor edx,edx
68 mov eax,totalSize
69 mov ecx,16
70 div ecx
71 mov eax,16
72 sub eax,edx
73 xor edx,edx
74 mov ecx,3
75 mul ecx
76 mov @dwBlanks,eax
77
78 ;invoke wsprintf,addr szBuffer,addr lpszOut1,totalSize
79 ;invoke MessageBox,NULL,addr szBuffer,NULL,MB_OK
80
81 .while TRUE
82 .if totalSize==0 ;最后一行
83 ;填充空格
84 .while TRUE
85 .break .if @dwBlanks==0
86 invoke lstrcat,addr lpServicesBuffer,addr lpszBlank
87 dec @dwBlanks
88 .endw
89 ;第二列与第三列中间的空格
90 invoke lstrcat,addr lpServicesBuffer,addr lpszManyBlanks
91 ;第三列内容
92 invoke lstrcat,addr lpServicesBuffer,addr bufDisplay
93 ;回车换行符号
94 invoke lstrcat,addr lpServicesBuffer,addr lpszReturn
95 .break
96 .endif
97 ;将al翻译成可以显示的ASCII码字符,注意不能破坏al的值
98 mov al,byte ptr [esi]
99 .if al>20h && al<7eh
100 mov ah,al
101 .else ;如果不是ASCII码值,则显示“.”
102 mov ah,2Eh
103 .endif
104 ;写入第三列的值
105 mov byte ptr [edi],ah
106
107 ;Windows 2000不支持al字节级别,经常导致程序意外终止
108 ;因此用以下方法替代
109 ;invoke wsprintf,addr @bufTemp1,addr lpszFilterFmt3,al
110
111 mov bl,al
112 xor edx,edx
113 xor eax,eax
114 mov al,bl
115 mov cx,16
116 div cx ;结果高位在al中,余数在dl中
117
118 ;组合字节的十六进制字符串到@bufTemp1中,类似于:“7F \0”
119 push edi
120 xor bx,bx
121 mov bl,al
122 movzx edi,bx
123 mov bl,byte ptr lpszHexArr[edi]
124 mov byte ptr @bufTemp1[0],bl
125
126 xor bx,bx
127 mov bl,dl
128 movzx edi,bx
129 mov bl,byte ptr lpszHexArr[edi]
130 mov byte ptr @bufTemp1[1],bl
131 mov bl,20h
132 mov byte ptr @bufTemp1[2],bl
133 mov bl,0
134 mov byte ptr @bufTemp1[3],bl
135 pop edi
136
137 ; 将第二列写入lpServicesBuffer
138 invoke lstrcat,addr lpServicesBuffer,addr @bufTemp1
139
140 .if @dwCount==16 ;已到16个字节
141 ;第二列与第三列中间的空格
142 invoke lstrcat,addr lpServicesBuffer,addr lpszManyBlanks
143 ;显示第三列字符
144 invoke lstrcat,addr lpServicesBuffer,addr bufDisplay
145 ;回车换行
146 invoke lstrcat,addr lpServicesBuffer,addr lpszReturn
147
148 ;写入内容
149 invoke _appendInfo,addr lpServicesBuffer
150 invoke RtlZeroMemory,addr lpServicesBuffer,100
151
152
153 ;显示下一行的地址
154 inc @dwCount1
155 invoke wsprintf,addr @bufTemp2,addr lpszFilterFmt4,\
156 @dwCount1
157 invoke lstrcat,addr lpServicesBuffer,addr @bufTemp2
158 dec @dwCount1
159
160 mov @dwCount,0
161 invoke RtlZeroMemory,addr bufDisplay,50
162 mov edi,offset bufDisplay
163 ;为了能与后面的inc edi配合,使edi正确定位到bufDisplay处
164 dec edi
165 .endif
166
167 dec totalSize
168 inc @dwCount
169 inc esi
170 inc edi
171 inc @dwCount1
172 .endw
173
174 ;添加最后一行
175 invoke _appendInfo,addr lpServicesBuffer
176
 

完整的源代码请查看随书文件chapter2\pedump.asm。在该部分代码中,每取一个字节,都会将其ASCII码的值写入bufDisplay。如果字节的值在20h和7eh之间,则显示相应的ASCII码,否则显示“.”。每计数16个字节,就会重新初始化bufDisplay。

每取一个字节,都会将该字节的十六进制字符表示形式加上后面的空格作为一个完整单位,附加到lpServicesBuffer。每计数16个字节,就会将lpServicesBuffer中存放的完整的一行内容写入到富文本框中。

2.2.3 PEDump代码中的数据结构

为了帮助大家更好地阅读PEDump的实现代码,在此分别列出本程序中用到的全局变量和局部变量。

(1)程序中用到的全局变量

totalSize    dd ?    ; 文件大小
lpMemory     dd ?    ; 内存映像文件在内存的起始位置
szFileName   db MAX_PATH dup(?)   ;要打开的文件名

lpServicesBuffer           db 100 dup(0)        ;每行的缓冲区
bufDisplay                  db 50 dup(0)         ;第三列ASCII码字符显示
szBuffer                     db 200 dup(0)        ;临时缓冲区
lpszFilterFmt4              db   '%08x   ',0      ;第一列地址+两个空格
lpszManyBlanks              db   '   ',0           ;列间空格
lpszBlank                    db   ' ',0             ;空格符
lpszSplit                    db   '-',0
lpszScanFmt                  db   '%02x',0
lpszHexArr                   db   '0123456789ABCDEF',0
lpszReturn                   db   0dh,0ah,0                ;一个回车换行符
lpszDoubleReturn           db   0dh,0ah,0dh,0ah,0      ;两个回车换行符

(2)程序中用到的局部变量

local @bufTemp1    ; 十六进制字节码
local @bufTemp2    ; 第一列缓冲区
local @dwCount     ; 计数,逢16则重新计
local @dwCount1    ; 地址序号
local @dwBlanks    ; 最后一行的空格数

结合代码清单2-3,对生成PE字节码的过程解释如下:

第一列内容的生成(第63~64行)程序通过组合字符串函数wsprintf构造第一列内容,生成的字符串被存储在@bufTemp2中,使用lstrcat函数将该内容加到lpServicesBuffer中。如下所示:

63 invoke wsprintf,addr @bufTemp2,addr lpszFilterFmt4,@dwCount1
64 invoke lstrcat,addr lpServicesBuffer,addr @bufTemp2

第二列内容的生成 (第111~135行)通过循环构造第二列字节码的内容,把每一个字节的字节码字符串写入缓冲区@bufTemp1,然后使用lstrcat函数将这个字节的内容加到lpServicesBuffer中。

注意 @bufTemp1中存放的并不是第二列所有的内容,而是一个字节的内容。内容包括字节十六进制表示+空格+结尾的“\0”字符。假设该字节为80h,则存储在@bufTemp1中的内容为“80 \0”。

第三列内容的生成(第97~105行)每取一个字节,就会判断该字节的值是否介于20h和7eh之间,如果是则将相应的ASCII码写入变量bufDisplay,否则写入“.”到变量bufDisplay。

每行的生成 lpServicesBuffer代表每行的缓冲区,每读取16个字节,就会将该缓冲区中的内容写入到新文件中。如果是最后一行,则在循环中单独处理(见代码清单2-3的第82~96行)。

2.2.4 运行PEDump

打开命令提示符窗口,在D:\masm32\source\chapter2目录下执行如下命令:

❑ rc-r pedump.rc                         (编译资源脚本文件)
❑ ml-c-coff pedump.asm                   (编译PEDump.asm)
❑ link-subsystem:windows pedump.res pedump.obj  (链接生成可执行程序)

运行PEDump.exe,最终效果如图2-4所示。

                                                           图2-4 PEDump的运行效果

在测试多个打开的PE文件后你会发现程序存在一个问题,当程序在打开比较大的PE文件显示字节码时界面会发生假死,这是由于程序主线程的循环造成了系统消息堵塞,从而无法完成界面更新所致。要解决这个问题,应该把事件响应代码_openFile单独放到一个开启的线程中执行,方法如下。

将以下菜单响应代码:

invoke _openFile

更改为:

invoke CreateThread,NULL,0,addr _openFile,addr @sClient,0,NULL

为了能随时终止滚动显示,可以在主程序中增加一个标志字节,然后在查看菜单中增加一个“停止dump...”菜单选项,该选项的响应代码为:

.elseif eax==IDM_1 ;停止dump
        mov dwStop,1

线程函数_openFile的循环中也有对应的检测dwStop的代码,如下所示:

.while TRUE
        ......
        .break .if dwStop==1
        ......
.endw

经过如上设计,在程序显示字节码的过程中,任意拖曳运行窗口都不会出现界面假死的现象。同时,用户也可以通过菜单项“查看”|“停止Dump...”随时终止字节码的显示,退出循环。

至此,PE文件字节码查看器的编写完成。可以看出,在通用程序框架的基础上实施再编码,不仅为开发者节省了很多的时间,提高了开发效率,同时也有利于开发者把主要精力集中到关键代码的编写上。


笔记:

pedump.rc文件:

#include <resource.h>

#define ICO_MAIN  1000
#define DLG_MAIN  1000
#define IDC_INFO  1001
#define IDM_MAIN  2000
#define IDM_OPEN  2001
#define IDM_EXIT  2002

#define IDM_1    4000
#define IDM_2    4001
#define IDM_3    4002
#define IDM_4    4003


ICO_MAIN  ICON  "main.ico"

DLG_MAIN DIALOG 50,50,544,399
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "PEDump by Scott"
MENU IDM_MAIN
FONT 9,"宋体"
BEGIN
   CONTROL "",IDC_INFO,"RichEdit20A",196 | ES_WANTRETURN | WS_CHILD | ES_READONLY
               | WS_VISIBLE |WS_BORDER | WS_VSCROLL | WS_TABSTOP,0,0,540,396
END

IDM_MAIN menu discardable
BEGIN
  POPUP "文件(&F)"
  BEGIN
    menuitem "打开文件(&O)...",IDM_OPEN
    menuitem separator
    menuitem "退出(&x)",IDM_EXIT
  END

  POPUP "编辑(&E)"
  BEGIN
    menuitem separator
  END

  POPUP "格式(&O)"
  BEGIN
    menuitem separator
  END

  POPUP "查看(&V)"
  BEGIN
    menuitem "停止Dump...",IDM_1
    menuitem "窗口透明度",IDM_2
    menuitem separator
    menuitem "大小",IDM_3
    menuitem "宽度",IDM_4
  END

  POPUP "帮助(&H)"
  BEGIN
    menuitem separator
  END


END
 

pedump.asm文件

;pedump.asm   通用程序框架
;使用 nmake 或下列命令进行编译和链接:
;ml -c -coff pedump.asm
;rc -r pedump.rc
;link -subsystem:windows pedump.obj pedump.res
.386
.model flat, stdcall 
option casemap:none 

include		c:/masm32/include/windows.inc 
include 	c:/masm32/include/user32.inc 
includelib 	c:/masm32/lib/user32.lib 
include 	c:/masm32/include/kernel32.inc 
includelib 	c:/masm32/lib/kernel32.lib 
include 	c:/masm32/include/comdlg32.inc 
includelib 	c:/masm32/lib/comdlg32.lib 

ICO_MAIN	equ 1000 
DLG_MAIN 	equ 1000
IDC_INFO	equ 1001 
IDM_MAIN 	equ 2000
IDM_OPEN 	equ 2001 
IDM_EXIT 	equ 2002 
IDM_1		equ 4000
IDM_2 		equ 4001 
IDM_3 		equ 4002 

.data 
hInstance 	dword ?					;进程句柄
hRichEdit 	dword ?
hWinMain	dword ?					;弹出窗口句柄
hWinEdit	dword ?					;富文本框句柄
totalSize	dword ?					;文件大小
lpMemory	dword ?					;内存映像文件在内存的起始位置
szFileName 	byte MAX_PATH dup(?)		;要打开的文件路径及名称名

lpServicesBuffer	byte 100 dup(0)	;所有内容
bufDisplay 			byte 50 dup(0)		;第三列ASCII码字符显示
szBuffer 			byte 200 dup(0)	;临时缓冲区
lpszFilterFmt4 	byte '%08x ', 0
lpszManyBlanks 		byte '  ', 0
lpszBlank 			byte ' ', 0
lpszSplit 			byte '-', 0
lpszScanFmt 		byte '%02x', 0 
lpszHexArr 			byte '0123456789ABCDEF', 0
lpszReturn 			byte 0dh,0ah,0
lpszDoubleReturn	byte 0dh,0ah,0dh,0ah,0
lpszOut1			byte '文件大小:%d',0
dwStop		dword 0

.const 
szDllEdit 	byte 'RichEd20.dll', 0
szClassEdit byte 'RichEdit20A', 0
szFont 		byte '宋体', 0
szExtPe		byte 'PE File',0,'*.exe;*.dll;*.scr;*.fon;*.drv',0
			byte 'All Files(*.*)',0,'*.*',0,0
szErr		byte '文件格式错误!',0
szErrFormat byte '操作文件时出现错误!',0

.code
;初始化窗口程序
_init proc 
	local @stCf:CHARFORMAT 
	
	invoke GetDlgItem, hWinMain, IDC_INFO 
	mov hWinEdit, eax 
	
	;为窗口设置图标
	invoke LoadIcon, hInstance, ICO_MAIN 
	invoke SendMessage, hWinMain, WM_SETICON, ICON_BIG, eax 	

	;设置编辑控件	
	invoke SendMessage, hWinEdit, EM_SETTEXTMODE, TM_PLAINTEXT, 0 		
	invoke RtlZeroMemory, addr @stCf, sizeof @stCf 					;初始化 
	mov @stCf.cbSize, sizeof @stCf 
	mov @stCf.yHeight, 14*1440/96
	mov @stCf.dwMask, CFM_FACE or CFM_SIZE or CFM_BOLD 
	invoke lstrcpy, addr @stCf.szFaceName, addr szFont 
	invoke SendMessage, hWinEdit, EM_SETCHARFORMAT, 0, addr @stCf 
	invoke SendMessage, hWinEdit, EM_EXLIMITTEXT, 0, -1 
	ret 
_init endp 

;-------------------------------
;错误Handler
;--------------------------------
_Handler proc _lpExceptionRecord, _lpSEH, \
			_lpContext, _lpDispathcerContext 
	pushad 
	mov esi, _lpExceptionRecord 
	mov edi, _lpContext 
	assume esi:ptr EXCEPTION_RECORD, edi:ptr CONTEXT 
	mov eax, _lpSEH 
	push [eax+0ch]
	pop [edi].regEbp 
	push [eax+8]
	pop [edi].regEip 
	push eax 
	pop [edi].regEsp 
	assume esi:nothing, edi:nothing 
	popad 
	mov eax, ExceptionContinueExecution
	ret 
_Handler endp 

;---------------------
; 往文本框中追加文本
;---------------------
_appendInfo proc _lpsz 
	local @stCR:CHARRANGE ;字符范围结构体,两个字段LONG cpMin和LONG cpMain。
	
	pushad 
	invoke GetWindowTextLength, hWinEdit 
	mov @stCR.cpMin, eax 	;将插入点移动到最后
	mov @stCR.cpMax, eax 
	invoke SendMessage, hWinEdit, EM_EXSETSEL, 0, addr @stCR 
	invoke SendMessage, hWinEdit, EM_REPLACESEL, FALSE, _lpsz 
	popad 
	ret 
_appendInfo endp 

;--------------------
; 打开PE文件并处理
;--------------------
_openFile proc 
	local @stOF:OPENFILENAME 
	local @hFile, @hMapFile 
	local @bufTemp1					;十六进制字节码
	local @bufTemp2	 				;第一列
	local @dwCount 					;计数,逢16则重新计
	local @dwCount1 				;地址顺号
	local @dwBlanks 				;最后一行空格数
	
	invoke RtlZeroMemory, addr @stOF, sizeof @stOF 
	mov @stOF.lStructSize, sizeof @stOF 
	push hWinMain 
	pop @stOF.hwndOwner 
	mov @stOF.lpstrFilter, offset szExtPe 
	mov @stOF.lpstrFile, offset szFileName 
	mov @stOF.nMaxFile, MAX_PATH 
	mov @stOF.Flags, OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST 
	invoke GetOpenFileName, addr @stOF 		;让用户选择打开的文件
	.if !eax 
		jmp @F 
	.endif 
	invoke CreateFile, addr szFileName, GENERIC_READ, \
			FILE_SHARE_READ or FILE_SHARE_WRITE, NULL, \
			OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL 
	.if eax != INVALID_HANDLE_VALUE 
		mov @hFile, eax 
		invoke GetFileSize, eax, NULL 		;获取文件大小
		mov totalSize, eax 
		
		.if eax 
			invoke CreateFileMapping, @hFile, \		;内存映射文件
					NULL, PAGE_READONLY, 0, 0, NULL 
			.if eax 
				mov @hMapFile, eax 
				invoke MapViewOfFile, eax, \
						FILE_MAP_READ, 0, 0, 0
				.if eax 
					mov lpMemory, eax 				;获得文件在内存的映象起始位置
					assume fs:nothing 
					push ebp 
					push offset _ErrFormat 
					push offset _Handler 
					push fs:[0]
					mov fs:[0], esp 
					;开始处理文件

					;缓冲区初始化
					invoke RtlZeroMemory, addr @bufTemp1, 10 
					invoke RtlZeroMemory, addr @bufTemp2, 20 
					invoke RtlZeroMemory, addr lpServicesBuffer, 100 
					invoke RtlZeroMemory, addr bufDisplay, 50 
					
					mov @dwCount, 1
					mov esi, lpMemory 
					mov edi, offset bufDisplay 
					
					;将第一列写入lpServicesBuffer
					mov @dwCount1, 0 
					invoke wsprintf, addr @bufTemp2, addr lpszFilterFmt4, @dwCount1 
					invoke lstrcat, addr lpServicesBuffer, addr @bufTemp2 
					
					;求最后一行的空格数(16-长度%16)*3
					xor edx, edx 
					mov eax, totalSize 
					mov ecx, 16
					div ecx 
					mov eax, 16 
					sub eax, edx 
					xor edx, edx 
					mov ecx, 3 
					mul ecx 
					mov @dwBlanks, eax 
					
					;invoke wsprintf,addr szBuffer,addr lpszOut1,totalSize
					;invoke MessageBox,NULL,addr szBuffer,NULL,MB_OK
					
					.while TRUE 
						.if totalSize == 0 			;最后一行
							;填充空格
							.while TRUE 
								.break .if @dwBlanks == 0 
								invoke lstrcat, addr lpServicesBuffer, addr lpszBlank 
								dec @dwBlanks 
							.endw 
							;第二列与第三列中间的空格
							invoke lstrcat, addr lpServicesBuffer, addr lpszManyBlanks 
							;第三列内容
							invoke lstrcat, addr lpServicesBuffer, addr bufDisplay 
							;回车换行符号
							invoke lstrcat, addr lpServicesBuffer, addr lpszReturn 
							.break
						.endif 
						;将al翻译成可以显示的ascii码字符,注意不能破坏al的值
						mov al, byte ptr[esi]
						.if al > 20h && al < 7eh 
							mov ah, al 
						.else   			;如果不是ASCII码值,则显示“.”
							mov ah, 2Eh
						.endif 
						;写入第三列的值
						mov byte ptr [edi],ah 
						
						;win2k不支持al字节级别,经常导致程序无故结束,
						;因此用以下方法替代
						;invoke wsprintf,addr @bufTemp1,addr lpszFilterFmt3,al
						
						mov bl,al 
						xor edx, edx 
						xor eax, eax 
						mov al, bl 
						mov cx, 16 
						div cx 			;结果高位在al中,余数在dl中
						
						;组合字节的十六进制字符串到@bufTemp1中,类似于:“7F \0”
						push edi 
						xor bx, bx 
						mov bl, al 
						movzx edi, bx 
						mov bl, byte ptr lpszHexArr[edi]
						mov byte ptr @bufTemp1[0], bl 
						
						xor bx, bx 
						mov bl, dl 
						movzx edi, bx 
						mov bl, byte ptr lpszHexArr[edi]
						mov byte ptr @bufTemp1[1], bl 
						mov bl, 20h 
						mov byte ptr @bufTemp1[2], bl 
						mov bl, 0 
						mov byte ptr @bufTemp1[3], bl 
						pop edi 
						
						;将第二列写入lpServicesBuffer
						invoke lstrcat, addr lpServicesBuffer, addr @bufTemp1 
						
						.if @dwCount == 16 			;已到16个字节,
							;第二列与第三列中间的空格
							invoke lstrcat, addr lpServicesBuffer, addr lpszManyBlanks 
							;显示第三列字符
							invoke lstrcat, addr lpServicesBuffer, addr bufDisplay 
							;回车换行
							invoke lstrcat, addr lpServicesBuffer, addr lpszReturn 
							
							;写入内容
							invoke _appendInfo, addr lpServicesBuffer 
							invoke RtlZeroMemory, addr lpServicesBuffer, 100 
							
							.break .if dwStop == 1 
							
							;显示下一行的地址
							inc @dwCount1 
							invoke wsprintf, addr @bufTemp2, addr lpszFilterFmt4, \
																		@dwCount1 
							invoke lstrcat, addr lpServicesBuffer, addr @bufTemp2 
							dec @dwCount1 
							
							mov @dwCount, 0 
							invoke RtlZeroMemory, addr bufDisplay, 50 
							mov edi, offset bufDisplay 
							;为了能和后面的inc edi配合使edi正确定位到bufDisplay处
							dec edi 
						.endif 
						
						dec totalSize 
						inc @dwCount 
						inc esi 
						inc edi 
						inc @dwCount1 
					.endw 
					
					;添加最后一行
					invoke _appendInfo, addr lpServicesBuffer 
					
					;处理文件结束
					jmp _ErrorExit 
	_ErrFormat:
					invoke MessageBox, hWinMain, offset szErrFormat, NULL, MB_OK 
	_ErrorExit:	
					pop fs:[0]
					add esp, 0ch 
					invoke UnmapViewOfFile, lpMemory 
				.endif 
				invoke CloseHandle, @hMapFile 
			.endif 
			invoke CloseHandle, @hFile 
		.endif 
	.endif 
@@:
	ret 
_openFile endp 
				
;-------------------------------
;窗口程序
;--------------------------------
_ProcDlgMain proc uses ebx edi esi hWnd, wMsg, wParam, lParam 
	local @sClient 
	
	mov eax, wMsg 
	.if eax == WM_CLOSE 
		invoke EndDialog, hWnd, NULL 
	.elseif eax == WM_INITDIALOG				;初始化 	
		push hWnd 
		pop hWinMain 
		call _init 
	.elseif eax == WM_COMMAND 					;菜单
		mov eax, wParam 
		.if eax == IDM_EXIT						;退出
			invoke EndDialog, hWnd, NULL 
		.elseif eax == IDM_OPEN 				;打开文件 
			mov dwStop, 0
			invoke CreateThread, NULL, 0, addr _openFile, addr @sClient, 0, NULL 
			;invoke _openFile 
		.elseif eax == IDM_1 
			mov dwStop, 1
		.elseif eax == IDM_2 
		.elseif eax == IDM_3 
		.endif 
	.else
		mov eax, FALSE 
		ret 
	.endif 
	mov eax, TRUE 
	ret 
_ProcDlgMain endp 

start:
	invoke LoadLibrary, offset szDllEdit 
	mov hRichEdit, eax 
	invoke GetModuleHandle, NULL 
	mov hInstance, eax 
	invoke DialogBoxParam, hInstance, \
			DLG_MAIN, NULL, offset _ProcDlgMain, NULL 
	invoke FreeLibrary, hRichEdit 
	invoke ExitProcess, NULL 
end start 
	

Makefile文件:

NAME = pedump
OBJS = $(NAME).obj
RES  = $(NAME).res

LINK_FLAG = /subsystem:windows
ML_FLAG = /c /coff

$(NAME).exe: $(OBJS) $(RES)
	Link $(LINK_FLAG) $(OBJS) $(RES)

.asm.obj:
	ml $(ML_FLAG) $<
.rc.res:
	rc $<

clean:
	del *.obj
	del *.res

编译: nmake

运行:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值