在 PE文件头的 IMAGE_OPTIONAL_HEADER 结构中的 DataDirectory(数据目录表)
的第二个成员就是指向输入表的。每个被链接进来的 DLL文件都分别对应一个
IMAGE_IMPORT_DESCRIPTOR (简称IID) 数组结构。

【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 
没有任何烦人的参数,很方便,不是么? 😛 
我们先来看一下这个程序的结构。第一句是format指示字,它指定了程序的类型,PE表示我 
们编写的是一个PE文件,后面的GUI指示编译器我们将使用Windows图形界面。如果要编写一 
个控制台应用程序则可以指定为CONSOLE。如果要写一个内核驱动,可以指定为NATIVE,表示 
不需要子系统支持。最后的4.0指定了子系统的版本号(还记得前面的MajorSubsystemVersion 
和MinorSubsystemVersion么?)。 
下面一行指定了程序的入口为__start。 
section指示字表示我们要开始一个新节。我们的程序只有一个节,即代码节,我们将其命名 
为.text,并指定节属性为只读(readable)和可执行(executable)。 
之后就是我们的代码了,我们仅仅用一条ret指令返回系统,这时堆栈里的返回地址为Exit- 
Thread,所以程序直接退出。 
下面运行它,程序只是简单地退出了,我们成功地用FASM编写了一个程序!我们已经迈出了 
第一步,下面要让我们的程序可以做点什么。我们想要调用一个API,我们要怎么做呢?让 
我们再来充充电吧 😀 

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real datetime stamp
                                            // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

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程序 
是多么的漂亮 😛 
format  PE GUI 4.0 
entry   __start 

在这个
IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0)
的 IID 作为结束的标志。


; 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] 

OriginalFirstThunk

它指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function
name 的地址。


; 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文件有更理性的认识。不过每个人的口味不同,嘿嘿,也许上面的理由还不够说服各位读 
者,没关系,选择一款适合你的编译器吧,它们都同样出色 😛 

Name

它表示DLL
名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称。如:KERNEL32.DLL)。

5.2、导出表 
———– 
通过导入表的学习,我想各位读者已经对PE文件的学习过程有了自己认识和方法,所以下面 
关于导出表的一节我将加快一些速度。“朋友们注意啦!!! @#$%$%&#^”  😀 
在导出表的起始位置是一个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 

发表评论

电子邮件地址不会被公开。 必填项已用*标注