第2章 三个小工具的编写(1)
俗话说:“工欲善其事,必先利其器。”本章将完成与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*20mov @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 .elsemov 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).resLINK_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 *.objdel *.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 4003ICO_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,"宋体"
BEGINCONTROL "",IDC_INFO,"RichEdit20A",196 | ES_WANTRETURN | WS_CHILD | ES_READONLY| WS_VISIBLE |WS_BORDER | WS_VSCROLL | WS_TABSTOP,0,0,540,396
ENDIDM_MAIN menu discardable
BEGINPOPUP "文件(&F)"BEGINmenuitem "打开文件(&O)...",IDM_OPENmenuitem separatormenuitem "退出(&x)",IDM_EXITENDPOPUP "编辑(&E)"BEGINmenuitem separatorENDPOPUP "格式(&O)"BEGINmenuitem separatorENDPOPUP "查看(&V)"BEGINmenuitem "停止Dump...",IDM_1menuitem "窗口透明度",IDM_2menuitem separatormenuitem "大小",IDM_3menuitem "宽度",IDM_4ENDPOPUP "帮助(&H)"BEGINmenuitem separatorENDEND
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',0byte '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/96mov @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, ExceptionContinueExecutionret
_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, 1mov esi, lpMemory mov edi, offset bufDisplay ;将第一列写入lpServicesBuffermov @dwCount1, 0 invoke wsprintf, addr @bufTemp2, addr lpszFilterFmt4, @dwCount1 invoke lstrcat, addr lpServicesBuffer, addr @bufTemp2 ;求最后一行的空格数(16-长度%16)*3xor edx, edx mov eax, totalSize mov ecx, 16div 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,almov 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 ;将第二列写入lpServicesBufferinvoke 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, 0invoke 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 .elsemov 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).resLINK_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 *.objdel *.res
编译: nmake
运行: