【pker / CVC.GB】
5、关于FASM
-----------
下面我们用FASM来编写我们的第一个程序。我们可以编写如下代码:
format PE GUI 4.0
entry __start
section '.text' code readable executable
__start:
ret
我们把这个文件存为test.asm并编译它:
fasm test.asm test.exe
没有任何烦人的参数,很方便,不是么? :P
我们先来看一下这个程序的结构。第一句是format指示字,它指定了程序的类型,PE表示我
们编写的是一个PE文件,后面的GUI指示编译器我们将使用Windows图形界面。如果要编写一
个控制台应用程序则可以指定为CONSOLE。如果要写一个内核驱动,可以指定为NATIVE,表示
不需要子系统支持。最后的4.0指定了子系统的版本号(还记得前面的MajorSubsystemVersion
和MinorSubsystemVersion么?)。
下面一行指定了程序的入口为__start。
section指示字表示我们要开始一个新节。我们的程序只有一个节,即代码节,我们将其命名
为.text,并指定节属性为只读(readable)和可执行(executable)。
之后就是我们的代码了,我们仅仅用一条ret指令返回系统,这时堆栈里的返回地址为Exit-
Thread,所以程序直接退出。
下面运行它,程序只是简单地退出了,我们成功地用FASM编写了一个程序!我们已经迈出了
第一步,下面要让我们的程序可以做点什么。我们想要调用一个API,我们要怎么做呢?让
我们再来充充电吧 :D
5.1、导入表
-----------
我们编写如下代码并用TASM编译:
;
; tasm32 /ml /m5 test.asm
; tlink32 -Tpe -aa test.obj ,,, import32.lib
;
ideal
p586
model use32 flat
extrn MessageBoxA:near
dataseg
str_hello db 'Hello',0
codeseg
__start:
push 0
push offset str_hello
push offset str_hello
push 0
call MessageBoxA
ret
end __start
下面我们用w32dasm反汇编,得到:
:00401000 6A00 push 00000000
:00401002 6800204000 push 00402000
:00401007 6800204000 push 00402000
:0040100C 6A00 push 00000000
:0040100E E801000000 call 00401014
:00401013 C3 ret
:00401014 FF2530304000 jmp dword ptr [00403030]
可以看到代码中的call MessageBoxA被翻译成了call 00401014,在这个地址处是一个跳转
指令jmp dword ptr [00403030],我们可以确定在地址00403030处存放的是MessageBoxA的
真正地址。
其实这个地址是位于PE文件的导入表中的。下面我们继续我们的PE文件的学习。我们先来看
一下导入表的结构。导入表是由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成的。结构的个
数由文件引用的DLL个数决定,文件引用了多少个DLL就有多少个IMAGE_IMPORT_DESCRIPTOR
结构,最后还有一个全为零的IMAGE_IMPORT_DESCRIPTOR作为结束。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
Name字段是一个RVA,指定了引入的DLL的名字。
OriginalFirstThunk和FirstThunk在一个PE没有加载到内存中的时候是一样的,都是指向一
个IMAGE_THUNK_DATA结构数组。最后以一个内容为0的结构结束。其实这个结构就是一个双
字。这个结构很有意思,因为在不同的时候这个结构代表着不同的含义。当这个双字的最高
位为1时,表示函数是以序号的方式导入的;当最高位为0时,表示函数是以名称方式导入的,
这是这个双字是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构用来指定导入函数
名称。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint字段表示一个序号,不过因为是按名称导入,所以这个序号一般为零。
Name字段是函数的名称。
下面我们用一张图来说明这个复杂的过程。假设一个PE引用了kernel32.dll中的LoadLibraryA
和GetProcAddress,还有一个按序号导入的函数80010002h。
IMAGE_IMPORT_DESCRIPTOR IMAGE_IMPORT_BY_NAME
+--------------------+ +--> +------------------+ +-----------------------+
| OriginalFirstThunk | --+ | IMAGE_THUNK_DATA | --> | 023B | ExitProcess | <--+
+--------------------+ +------------------+ +-----------------------+ |
| TimeDataStamp | | IMAGE_THUNK_DATA | --> | 0191 | GetProcAddress | <--+--+
+--------------------+ +------------------+ +-----------------------+ | |
| ForwarderChain | | 80010002h | | |
+--------------------+ +------------------+ +---> +------------------+ | |
| Name | --+ | 0 | | | IMAGE_THUNK_DATA | ---+ |
+--------------------+ | +------------------+ | +------------------+ |
| FirstThunk |-+ | | | IMAGE_THUNK_DATA | ------+
+--------------------+ | | +------------------+ | +------------------+
| +--> | kernel32.dll | | | 80010002h |
| +------------------+ | +------------------+
| | | 0 |
+------------------------------+ +------------------+
还记得前面我们说过在一个PE没有被加载到内存中的时候IMAGE_IMPORT_DESCRIPTOR中的
OriginalFirstThunk和FirstThunk是相同的,那么为什么Windows要占用两个字段呢?其实
是这样的,在PE文件被PE加载器加载到内存中的时候这个加载器会自动把FirstThunk的值替
换为API函数的真正入口,也就是那个前面jmp的真正地址,而OriginalFirstThunk只不过是
用来反向查找函数名而已。
好了,又讲了这么多是要做什么呢?你马上就会看到。下面我们就来构造我们的导入表。
我们用以下代码来开始我们的引入节:
section '.idata' import data readable
section指示字表示我们要开始一个新节。.idata是这个新节的名称。import data表示这是
一个引入节。readable表示这个节的节属性是只读的。
假设我们的程序只需要引入user32.dll中的MessageBoxA函数,那么我们的引入节只有一个
描述这个dll的IMAGE_IMPORT_DESCRIPTOR和一个全0的结构。考虑如下代码:
dd 0 ; 我们并不需要OriginalFirstThunk
dd 0 ; 我们也不需要管这个时间戳
dd 0 ; 我们也不关心这个链
dd RVA usr_dll ; 指向我们的DLL名称的RVA
dd RVA usr_thunk ; 指向我们的IMAGE_IMPORT_BY_NAME数组的RVA
; 注意这个数组也是以0结尾的
dd 0,0,0,0,0 ; 结束标志
上面用到了一个RVA伪指令,它指定的地址在编译时被自动写为对应的RVA值。下面定义我们
要引入的动态链接库的名字,这是一个以0结尾的字符串:
usr_dll db 'user32.dll',0
还有我们的IMAGE_THUNK_DATA:
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0 ; 结束标志
上面的__imp_MessageBox在编译时由于前面有RVA指示,所以表示是IMAGE_IMPORT_BY_NAME的
RVA。下面我们定义这个结构:
__imp_MessageBox dw 0 ; 我们不按序号导入,所以可以
; 简单地置0
db 'MessageBoxA',0 ; 导入的函数名
好了,我们完成了导入表的建立。下面我们来看一个完整的程序,看看一个完整的FASM程序
是多么的漂亮 :P
format PE GUI 4.0
entry __start
;
; data section...
;
section '.data' data readable
pszText db 'Hello, FASM world!',0
pszCaption db 'Flat Assembler',0
;
; code section...
;
section '.text' code readable executable
__start:
push 0
push pszCaption
push pszText
push 0
call [MessageBox]
push 0
call [ExitProcess]
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,RVA krnl_dll,RVA krnl_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
krnl_dll db 'kernel32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
krnl_thunk:
ExitProcess dd RVA __imp_ExitProcess
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_ExitProcess dw 0
db 'ExitProcess',0
看到这里我相信大家都对FASM这个编译器有了一个初步的认识,也一定有很多读者会说:“
这么麻烦啊,干吗要用这个编译器呢?”。是的,也许上面的代码看起来很复杂,编写起来
也很麻烦,但FASM的一个好处在于我们可以更主动地控制我们生成的PE文件结构,同时能对
PE文件有更理性的认识。不过每个人的口味不同,嘿嘿,也许上面的理由还不够说服各位读
者,没关系,选择一款适合你的编译器吧,它们都同样出色 :P
5.2、导出表
-----------
通过导入表的学习,我想各位读者已经对PE文件的学习过程有了自己认识和方法,所以下面
关于导出表的一节我将加快一些速度。“朋友们注意啦!!! @#$%$%&#^” :D
在导出表的起始位置是一个IMAGE_EXPORT_DIRECTORY结构,但与引入表不同的是在导出表中
只有一个这个结构。下面我们来看一下这个结构的定义:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Characteristics、MajorVersion和MinorVersion不使用,一般为0。
TimeDataStamp是时间戳。
Name字段是一个RVA值,它指向了这个模块的原始名称。这个名称与编译后的文件名无关。
Base字段指定了导出函数序号的起始序号。假如Base的值为n,那么导出函数入口地址表中
的第一个函数的序号就是n,第二个就是n+1...
NumberOfFunctions指定了导出函数的总数。
NumberOfNames指定了按名称导出的函数的总数。按序号导出的函数总数就是这个值与到处
总数NumberOfFunctions的差。
AddressOfFunctions字段是一个RVA值,指向一个RVA数组,数组中的每个RVA均指向一个导
出函数的入口地址。数组的项数等于NumberOfFuntions。
AddressOfNames字段是一个RVA值,同样指向一个RVA数组,数组中的每个双字是一个指向函
数名字符串的RVA。数组的项数等于NumberOfNames。
AddressOfNameOrdinals字段是一个RVA值,它指向一个字数组,注意这里不再是双字了!!
这个数组起着很重要的作用,它的项数等于NumberOfNames,并与AddressOfNames指向的数组
一一对应。其每个项目的值代表了这个函数在入口地址表中索引。现在我们来看一个例子,
假如一个导出函数Foo在导出入口地址表中处于第m个位置,我们查找Ordinal数组的第m项,
假设这个值为x,我们把这个值与导出序号的起始值Base的值n相加得到的值就是函数在入口
地址表中索引。
下图表示了导出表的结构和上述过程:
+-----------------------+ +-----------------+
| Characteristics | +----> | 'dlltest.dll',0 |
+-----------------------+ | +-----------------+
| TimeDataStamp | |
+-----------------------+ | +-> +-----------------+
| MajorVersion | | | 0 | 函数入口地址RVA | ==> 函数Foo,序号n+0 <--+
+-----------------------+ | | +-----------------+ |
| MinorVersion | | | | ... | |
+-----------------------+ | | +-----------------+ |
| Name | -+ | x | 函数入口地址RVA | ==> 按序号导出,序号为n+x |
+-----------------------+ | +-----------------+ |
| Base(假设值为n) | | | ... | |
+-----------------------+ | +-----------------+ |
| NumberOfFunctions | | |
+-----------------------+ | +-> +-----+ +----------+ +-----+ <-+ |
| NumberOfNames | | | | RVA | --> | '_foo',0 | <==> | 0 | --+---+
+-----------------------+ | | +-----+ +----------+ +-----+ |
| AddressOfFunctions | ----+ | | ... | | ... | |
+-----------------------+ | +-----+ +-----+ |
| AddressOfNames | -------+ |
+-----------------------+ |
| AddressOfNameOrdinals | ---------------------------------------------------+
+-----------------------+
好了,下面我们来看构键我们的导出表。假设我们按名称导出一个函数_foo。我们以如下代
码开始:
section '.edata' export data readable
接着是IMAGE_EXPORT_DIRECTORY结构:
dd 0 ; Characteristics
dd 0 ; TimeDataStamp
dw 0 ; MajorVersion
dw 0 ; MinorVersion
dd RVA dll_name ; RVA,指向DLL名称
dd 0 ; 起始序号为0
dd 1 ; 只导出一个函数
dd 1 ; 这个函数是按名称方式导出的
dd RVA addr_tab ; RVA,指向导出函数入口地址表
dd RVA name_tab ; RVA,指向函数名称地址表
dd RVA ordinal_tab ; RVA,指向函数索引表
下面我们定义DLL名称:
dll_name db 'foo.dll',0 ; DLL名称,编译的文件名可以与它不同
接下来是导出函数入口地址表和函数名称地址表,我们要导出一个叫_foo的函数:
addr_tab dd RVA _foo ; 函数入口地址
name_tab dd RVA func_name
func_name db '_foo',0 ; 函数名称
最后是函数索引表:
ordinal_tab dw 0 ; 只有一个按名称导出函数,序号为0
下面我们看一个完整的程序:
format PE GUI 4.0 DLL at 76000000h
entry _dll_entry
;
; data section...
;
section '.data' data readable
pszText db 'Hello, FASM world!',0
pszCaption db 'Flat Assembler',0
;
; code section...
;
section '.text' code readable executable
_foo:
push 0
push pszCaption
push pszText
push 0
call [MessageBox]
ret
_dll_entry:
xor eax,eax
inc eax
ret 0ch
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,RVA krnl_dll,RVA krnl_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
krnl_dll db 'kernel32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
krnl_thunk:
ExitProcess dd RVA __imp_ExitProcess
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_ExitProcess dw 0
db 'ExitProcess',0
;
; export section...
;
section '.edata' export data readable
; image export directory
dd 0,0,0,RVA dll_name,0,1,1
dd RVA addr_tab
dd RVA name_tab
dd RVA ordinal_tab
; dll name
dll_name db 'foo.dll',0
; function address table
addr_tab dd RVA _foo
; function name table
name_tab dd RVA ex_foo
; export name table
ex_foo db '_foo',0
; ordinal table
ordinal_tab dw 0
;
; relocation section...
;
section '.reloc' fixups data discardable
程序的一开始用format指定了PE和GUI,在子系统版本号的后面我们使用了DLL指示字,表示
这是一个DLL文件。最后还有一个at关键字,指示了文件的image base。
程序的最后一个节是重定位节,对于重定位表我不做过多解释,有兴趣的读者可以参考其他
书籍或文章。我们可以把刚才的程序编译成一个DLL:
fasm foo.asm foo.dll
下面我们编写一个测试程序检验程序的正确性:
#include <windows.h>
int __stdcall WinMain (HINSTANCE,HINSTANCE,LPTSTR,int)
{
HMODULE hFoo=LoadLibrary ("foo.dll");
FARPROC _foo=GetProcAddress (hFoo,"_foo");
_foo ();
FreeLibrary (hFoo);
return 0;
}
我们把编译后的exe和刚才的dll放在同一个目录下并运行,看看程序运行是否正确 :P
5.3、强大的宏
-------------
关于FASM,还有一个强大的功能就是宏。大家对宏一定都不陌生,下面我们来看看在FASM中
如何定义宏。假设我们要编写一个复制字符串的宏,其中源、目的串由ESI和EDI指定,我们
可以:
macro @copysz
{
local next_char
next_char:
lodsb
stosb
or al,al
jnz next_char
}
下面我们再来看一个带参数的宏定义:
macro @stosd _dword
{
mov eax,_dword
stosd
}
如果我们要多次存入几个不同的双字我们可以简单地在定义宏时把参数用中括号括起来,比
如:
macro @stosd [_dword]
{
mov eax,_dword
stosd
}
这样当我们调用@stosd 1,2,3的时候,我们的代码被编译成:
mov eax,1
stosd
mov eax,2
stosd
mov eax,3
stosd
对于这种多参数的宏,FASM提供了三个伪指令common、forward和reverse。他们把宏代码分
成块并分别处理。下面我分别来介绍:
forward限定的块表示指令块对参数进行顺序处理,比如上面的宏,如果把上面的代码定义在
forward块中,我们可以得到相同的结果。对于forward块我们可以这样定义
macro @stosd [_dword]
{
forward
mov eax,_dword
stosd
}
reverse和forward正好相反,表示指令块对参数进行反向处理。对于上面的指令块如果用
reverse限定,那么我们的参数将被按照相反的顺序存入内存。
macro @stosd [_dword]
{
reverse
mov eax,_dword
stosd
}
这时当我们调用@stosd 1,2,3的时候,我们的代码被编译成:
mov eax,3
stosd
mov eax,2
stosd
mov eax,1
stosd
common限定的块将仅被处理处理一次。我们现在编写一个调用API的宏@invoke:
macro @invoke _api,[_argv]
{
reverse
push _argv
common
call [_api]
}
现在我们可以使用这个宏来调用API了,比如:
@invoke MessageBox,0,pszText,pszCaption,0
对于宏的使用我们就介绍这些,更多的代码可以参看我的useful.inc(其中有很多29A的宏,
tnx 29a :P)
5、关于FASM
-----------
下面我们用FASM来编写我们的第一个程序。我们可以编写如下代码:
format PE GUI 4.0
entry __start
section '.text' code readable executable
__start:
ret
我们把这个文件存为test.asm并编译它:
fasm test.asm test.exe
没有任何烦人的参数,很方便,不是么? :P
我们先来看一下这个程序的结构。第一句是format指示字,它指定了程序的类型,PE表示我
们编写的是一个PE文件,后面的GUI指示编译器我们将使用Windows图形界面。如果要编写一
个控制台应用程序则可以指定为CONSOLE。如果要写一个内核驱动,可以指定为NATIVE,表示
不需要子系统支持。最后的4.0指定了子系统的版本号(还记得前面的MajorSubsystemVersion
和MinorSubsystemVersion么?)。
下面一行指定了程序的入口为__start。
section指示字表示我们要开始一个新节。我们的程序只有一个节,即代码节,我们将其命名
为.text,并指定节属性为只读(readable)和可执行(executable)。
之后就是我们的代码了,我们仅仅用一条ret指令返回系统,这时堆栈里的返回地址为Exit-
Thread,所以程序直接退出。
下面运行它,程序只是简单地退出了,我们成功地用FASM编写了一个程序!我们已经迈出了
第一步,下面要让我们的程序可以做点什么。我们想要调用一个API,我们要怎么做呢?让
我们再来充充电吧 :D
5.1、导入表
-----------
我们编写如下代码并用TASM编译:
;
; tasm32 /ml /m5 test.asm
; tlink32 -Tpe -aa test.obj ,,, import32.lib
;
ideal
p586
model use32 flat
extrn MessageBoxA:near
dataseg
str_hello db 'Hello',0
codeseg
__start:
push 0
push offset str_hello
push offset str_hello
push 0
call MessageBoxA
ret
end __start
下面我们用w32dasm反汇编,得到:
:00401000 6A00 push 00000000
:00401002 6800204000 push 00402000
:00401007 6800204000 push 00402000
:0040100C 6A00 push 00000000
:0040100E E801000000 call 00401014
:00401013 C3 ret
:00401014 FF2530304000 jmp dword ptr [00403030]
可以看到代码中的call MessageBoxA被翻译成了call 00401014,在这个地址处是一个跳转
指令jmp dword ptr [00403030],我们可以确定在地址00403030处存放的是MessageBoxA的
真正地址。
其实这个地址是位于PE文件的导入表中的。下面我们继续我们的PE文件的学习。我们先来看
一下导入表的结构。导入表是由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成的。结构的个
数由文件引用的DLL个数决定,文件引用了多少个DLL就有多少个IMAGE_IMPORT_DESCRIPTOR
结构,最后还有一个全为零的IMAGE_IMPORT_DESCRIPTOR作为结束。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
Name字段是一个RVA,指定了引入的DLL的名字。
OriginalFirstThunk和FirstThunk在一个PE没有加载到内存中的时候是一样的,都是指向一
个IMAGE_THUNK_DATA结构数组。最后以一个内容为0的结构结束。其实这个结构就是一个双
字。这个结构很有意思,因为在不同的时候这个结构代表着不同的含义。当这个双字的最高
位为1时,表示函数是以序号的方式导入的;当最高位为0时,表示函数是以名称方式导入的,
这是这个双字是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构用来指定导入函数
名称。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint字段表示一个序号,不过因为是按名称导入,所以这个序号一般为零。
Name字段是函数的名称。
下面我们用一张图来说明这个复杂的过程。假设一个PE引用了kernel32.dll中的LoadLibraryA
和GetProcAddress,还有一个按序号导入的函数80010002h。
IMAGE_IMPORT_DESCRIPTOR IMAGE_IMPORT_BY_NAME
+--------------------+ +--> +------------------+ +-----------------------+
| OriginalFirstThunk | --+ | IMAGE_THUNK_DATA | --> | 023B | ExitProcess | <--+
+--------------------+ +------------------+ +-----------------------+ |
| TimeDataStamp | | IMAGE_THUNK_DATA | --> | 0191 | GetProcAddress | <--+--+
+--------------------+ +------------------+ +-----------------------+ | |
| ForwarderChain | | 80010002h | | |
+--------------------+ +------------------+ +---> +------------------+ | |
| Name | --+ | 0 | | | IMAGE_THUNK_DATA | ---+ |
+--------------------+ | +------------------+ | +------------------+ |
| FirstThunk |-+ | | | IMAGE_THUNK_DATA | ------+
+--------------------+ | | +------------------+ | +------------------+
| +--> | kernel32.dll | | | 80010002h |
| +------------------+ | +------------------+
| | | 0 |
+------------------------------+ +------------------+
还记得前面我们说过在一个PE没有被加载到内存中的时候IMAGE_IMPORT_DESCRIPTOR中的
OriginalFirstThunk和FirstThunk是相同的,那么为什么Windows要占用两个字段呢?其实
是这样的,在PE文件被PE加载器加载到内存中的时候这个加载器会自动把FirstThunk的值替
换为API函数的真正入口,也就是那个前面jmp的真正地址,而OriginalFirstThunk只不过是
用来反向查找函数名而已。
好了,又讲了这么多是要做什么呢?你马上就会看到。下面我们就来构造我们的导入表。
我们用以下代码来开始我们的引入节:
section '.idata' import data readable
section指示字表示我们要开始一个新节。.idata是这个新节的名称。import data表示这是
一个引入节。readable表示这个节的节属性是只读的。
假设我们的程序只需要引入user32.dll中的MessageBoxA函数,那么我们的引入节只有一个
描述这个dll的IMAGE_IMPORT_DESCRIPTOR和一个全0的结构。考虑如下代码:
dd 0 ; 我们并不需要OriginalFirstThunk
dd 0 ; 我们也不需要管这个时间戳
dd 0 ; 我们也不关心这个链
dd RVA usr_dll ; 指向我们的DLL名称的RVA
dd RVA usr_thunk ; 指向我们的IMAGE_IMPORT_BY_NAME数组的RVA
; 注意这个数组也是以0结尾的
dd 0,0,0,0,0 ; 结束标志
上面用到了一个RVA伪指令,它指定的地址在编译时被自动写为对应的RVA值。下面定义我们
要引入的动态链接库的名字,这是一个以0结尾的字符串:
usr_dll db 'user32.dll',0
还有我们的IMAGE_THUNK_DATA:
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0 ; 结束标志
上面的__imp_MessageBox在编译时由于前面有RVA指示,所以表示是IMAGE_IMPORT_BY_NAME的
RVA。下面我们定义这个结构:
__imp_MessageBox dw 0 ; 我们不按序号导入,所以可以
; 简单地置0
db 'MessageBoxA',0 ; 导入的函数名
好了,我们完成了导入表的建立。下面我们来看一个完整的程序,看看一个完整的FASM程序
是多么的漂亮 :P
format PE GUI 4.0
entry __start
;
; data section...
;
section '.data' data readable
pszText db 'Hello, FASM world!',0
pszCaption db 'Flat Assembler',0
;
; code section...
;
section '.text' code readable executable
__start:
push 0
push pszCaption
push pszText
push 0
call [MessageBox]
push 0
call [ExitProcess]
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,RVA krnl_dll,RVA krnl_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
krnl_dll db 'kernel32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
krnl_thunk:
ExitProcess dd RVA __imp_ExitProcess
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_ExitProcess dw 0
db 'ExitProcess',0
看到这里我相信大家都对FASM这个编译器有了一个初步的认识,也一定有很多读者会说:“
这么麻烦啊,干吗要用这个编译器呢?”。是的,也许上面的代码看起来很复杂,编写起来
也很麻烦,但FASM的一个好处在于我们可以更主动地控制我们生成的PE文件结构,同时能对
PE文件有更理性的认识。不过每个人的口味不同,嘿嘿,也许上面的理由还不够说服各位读
者,没关系,选择一款适合你的编译器吧,它们都同样出色 :P
5.2、导出表
-----------
通过导入表的学习,我想各位读者已经对PE文件的学习过程有了自己认识和方法,所以下面
关于导出表的一节我将加快一些速度。“朋友们注意啦!!! @#$%$%&#^” :D
在导出表的起始位置是一个IMAGE_EXPORT_DIRECTORY结构,但与引入表不同的是在导出表中
只有一个这个结构。下面我们来看一下这个结构的定义:
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
Characteristics、MajorVersion和MinorVersion不使用,一般为0。
TimeDataStamp是时间戳。
Name字段是一个RVA值,它指向了这个模块的原始名称。这个名称与编译后的文件名无关。
Base字段指定了导出函数序号的起始序号。假如Base的值为n,那么导出函数入口地址表中
的第一个函数的序号就是n,第二个就是n+1...
NumberOfFunctions指定了导出函数的总数。
NumberOfNames指定了按名称导出的函数的总数。按序号导出的函数总数就是这个值与到处
总数NumberOfFunctions的差。
AddressOfFunctions字段是一个RVA值,指向一个RVA数组,数组中的每个RVA均指向一个导
出函数的入口地址。数组的项数等于NumberOfFuntions。
AddressOfNames字段是一个RVA值,同样指向一个RVA数组,数组中的每个双字是一个指向函
数名字符串的RVA。数组的项数等于NumberOfNames。
AddressOfNameOrdinals字段是一个RVA值,它指向一个字数组,注意这里不再是双字了!!
这个数组起着很重要的作用,它的项数等于NumberOfNames,并与AddressOfNames指向的数组
一一对应。其每个项目的值代表了这个函数在入口地址表中索引。现在我们来看一个例子,
假如一个导出函数Foo在导出入口地址表中处于第m个位置,我们查找Ordinal数组的第m项,
假设这个值为x,我们把这个值与导出序号的起始值Base的值n相加得到的值就是函数在入口
地址表中索引。
下图表示了导出表的结构和上述过程:
+-----------------------+ +-----------------+
| Characteristics | +----> | 'dlltest.dll',0 |
+-----------------------+ | +-----------------+
| TimeDataStamp | |
+-----------------------+ | +-> +-----------------+
| MajorVersion | | | 0 | 函数入口地址RVA | ==> 函数Foo,序号n+0 <--+
+-----------------------+ | | +-----------------+ |
| MinorVersion | | | | ... | |
+-----------------------+ | | +-----------------+ |
| Name | -+ | x | 函数入口地址RVA | ==> 按序号导出,序号为n+x |
+-----------------------+ | +-----------------+ |
| Base(假设值为n) | | | ... | |
+-----------------------+ | +-----------------+ |
| NumberOfFunctions | | |
+-----------------------+ | +-> +-----+ +----------+ +-----+ <-+ |
| NumberOfNames | | | | RVA | --> | '_foo',0 | <==> | 0 | --+---+
+-----------------------+ | | +-----+ +----------+ +-----+ |
| AddressOfFunctions | ----+ | | ... | | ... | |
+-----------------------+ | +-----+ +-----+ |
| AddressOfNames | -------+ |
+-----------------------+ |
| AddressOfNameOrdinals | ---------------------------------------------------+
+-----------------------+
好了,下面我们来看构键我们的导出表。假设我们按名称导出一个函数_foo。我们以如下代
码开始:
section '.edata' export data readable
接着是IMAGE_EXPORT_DIRECTORY结构:
dd 0 ; Characteristics
dd 0 ; TimeDataStamp
dw 0 ; MajorVersion
dw 0 ; MinorVersion
dd RVA dll_name ; RVA,指向DLL名称
dd 0 ; 起始序号为0
dd 1 ; 只导出一个函数
dd 1 ; 这个函数是按名称方式导出的
dd RVA addr_tab ; RVA,指向导出函数入口地址表
dd RVA name_tab ; RVA,指向函数名称地址表
dd RVA ordinal_tab ; RVA,指向函数索引表
下面我们定义DLL名称:
dll_name db 'foo.dll',0 ; DLL名称,编译的文件名可以与它不同
接下来是导出函数入口地址表和函数名称地址表,我们要导出一个叫_foo的函数:
addr_tab dd RVA _foo ; 函数入口地址
name_tab dd RVA func_name
func_name db '_foo',0 ; 函数名称
最后是函数索引表:
ordinal_tab dw 0 ; 只有一个按名称导出函数,序号为0
下面我们看一个完整的程序:
format PE GUI 4.0 DLL at 76000000h
entry _dll_entry
;
; data section...
;
section '.data' data readable
pszText db 'Hello, FASM world!',0
pszCaption db 'Flat Assembler',0
;
; code section...
;
section '.text' code readable executable
_foo:
push 0
push pszCaption
push pszText
push 0
call [MessageBox]
ret
_dll_entry:
xor eax,eax
inc eax
ret 0ch
;
; import section...
;
section '.idata' import data readable
; image import descriptor
dd 0,0,0,RVA usr_dll,RVA usr_thunk
dd 0,0,0,RVA krnl_dll,RVA krnl_thunk
dd 0,0,0,0,0
; dll name
usr_dll db 'user32.dll',0
krnl_dll db 'kernel32.dll',0
; image thunk data
usr_thunk:
MessageBox dd RVA __imp_MessageBox
dd 0
krnl_thunk:
ExitProcess dd RVA __imp_ExitProcess
dd 0
; image import by name
__imp_MessageBox dw 0
db 'MessageBoxA',0
__imp_ExitProcess dw 0
db 'ExitProcess',0
;
; export section...
;
section '.edata' export data readable
; image export directory
dd 0,0,0,RVA dll_name,0,1,1
dd RVA addr_tab
dd RVA name_tab
dd RVA ordinal_tab
; dll name
dll_name db 'foo.dll',0
; function address table
addr_tab dd RVA _foo
; function name table
name_tab dd RVA ex_foo
; export name table
ex_foo db '_foo',0
; ordinal table
ordinal_tab dw 0
;
; relocation section...
;
section '.reloc' fixups data discardable
程序的一开始用format指定了PE和GUI,在子系统版本号的后面我们使用了DLL指示字,表示
这是一个DLL文件。最后还有一个at关键字,指示了文件的image base。
程序的最后一个节是重定位节,对于重定位表我不做过多解释,有兴趣的读者可以参考其他
书籍或文章。我们可以把刚才的程序编译成一个DLL:
fasm foo.asm foo.dll
下面我们编写一个测试程序检验程序的正确性:
#include <windows.h>
int __stdcall WinMain (HINSTANCE,HINSTANCE,LPTSTR,int)
{
HMODULE hFoo=LoadLibrary ("foo.dll");
FARPROC _foo=GetProcAddress (hFoo,"_foo");
_foo ();
FreeLibrary (hFoo);
return 0;
}
我们把编译后的exe和刚才的dll放在同一个目录下并运行,看看程序运行是否正确 :P
5.3、强大的宏
-------------
关于FASM,还有一个强大的功能就是宏。大家对宏一定都不陌生,下面我们来看看在FASM中
如何定义宏。假设我们要编写一个复制字符串的宏,其中源、目的串由ESI和EDI指定,我们
可以:
macro @copysz
{
local next_char
next_char:
lodsb
stosb
or al,al
jnz next_char
}
下面我们再来看一个带参数的宏定义:
macro @stosd _dword
{
mov eax,_dword
stosd
}
如果我们要多次存入几个不同的双字我们可以简单地在定义宏时把参数用中括号括起来,比
如:
macro @stosd [_dword]
{
mov eax,_dword
stosd
}
这样当我们调用@stosd 1,2,3的时候,我们的代码被编译成:
mov eax,1
stosd
mov eax,2
stosd
mov eax,3
stosd
对于这种多参数的宏,FASM提供了三个伪指令common、forward和reverse。他们把宏代码分
成块并分别处理。下面我分别来介绍:
forward限定的块表示指令块对参数进行顺序处理,比如上面的宏,如果把上面的代码定义在
forward块中,我们可以得到相同的结果。对于forward块我们可以这样定义
macro @stosd [_dword]
{
forward
mov eax,_dword
stosd
}
reverse和forward正好相反,表示指令块对参数进行反向处理。对于上面的指令块如果用
reverse限定,那么我们的参数将被按照相反的顺序存入内存。
macro @stosd [_dword]
{
reverse
mov eax,_dword
stosd
}
这时当我们调用@stosd 1,2,3的时候,我们的代码被编译成:
mov eax,3
stosd
mov eax,2
stosd
mov eax,1
stosd
common限定的块将仅被处理处理一次。我们现在编写一个调用API的宏@invoke:
macro @invoke _api,[_argv]
{
reverse
push _argv
common
call [_api]
}
现在我们可以使用这个宏来调用API了,比如:
@invoke MessageBox,0,pszText,pszCaption,0
对于宏的使用我们就介绍这些,更多的代码可以参看我的useful.inc(其中有很多29A的宏,
tnx 29a :P)