大理寺少卿 发表于 2025-3-3 22:44:36

WindowsPE文件格式入门08.导出表

通过cff , depends灯等软件可以看到dll,导出函数的信息,因为dll中本身就存了这些信息,存了dll中有哪些导出函数,导出函数的序号是什么,名字是什么,以及他们的地址是什么,这些东西都存在导出表里面

###

### 导出表发展历程

##### 最开始的时候只有序号导出,没有名称导出,使用的时候都是通过序号


| 序号 | 地址 |
| ------ | ------ |
| 1    | 1005 |
| 2    | 2005 |
| 3    | 3005 |
| 4    | 4005 |
| 5    | 5005 |
| 6    | 6005 |
| 7    | 7005 |
| 8    | 8005 |

但是上面的时间复杂度比较高是线性阶,但是对数阶占的体积比较大,所以只能折中,用常量阶.把地址作为数组,序号作为索引,只存地址,这样不仅速度更加快了,而且体积更小了

但是所以是从0开始的,因此可以数组首项加一个空

!(./mdimg/pe/1656342080279-f1125070-4aff-4b60-894d-b8be2790ad85.png)

这样可以通过序号,直接去找对应数组对应的索引的值

##### 序号不从1开始

但是上面有一个缺陷,序号不一定从0开始,例如 从 1001开始,这样数组不能前面加1000项0,这样不仅浪费空间,而且还用不上,解决办法是加一个基址 (最小序号), 后面序号就根据基址来取偏移

!(./mdimg/pe/1656342382781-1fc803e9-85b1-4f50-beb3-ef581882e6fd.png)

例如上面,要拿到序号1006 的函数地址, 可以 获取 1006对 1001 的偏移 5 ,再去数组取 索引为5的地址

##### 序号不连续

解决了上面基本不从0开始的情况,还有一个问题,就是 序号不连续 ,例如1001, 1002 , 1020,1021,1050

这种情况下,数组中间对应的序号就需要填充0,因此会使体积变大,微软并没有解决这个问题,因为这是写dll的人自己的问题

例如

1. 不指定序号(这样序号连续)

!(./mdimg/pe/1656342947240-11d69b7f-4a5e-460d-8a6e-fdc1249eb7a6.png)

!(./mdimg/pe/1656343062185-fb39b423-bdc4-4578-863c-bfd021c1852b.png)

可以看出大小是38kb

1. 部分指定序号使序号不连续

!(./mdimg/pe/1656343177031-173d84d3-9bc9-4952-bbed-1f02da6471d7.png)

!(./mdimg/pe/1656343206503-573d5d92-66f4-4320-b851-e5c51fc9be1a.png)

可以看到此时dll大小变成了   346KB

##### 名称导出

!(./mdimg/pe/1656343576669-e2a5f72d-6f90-4578-8d36-0764102ad864.png)

序号跟成名2个表是拆开的,并不在一起,因此基数数,导出地址表 ,导出名称表,导出序号表 构成了导出表

### 导出表结构

#### 导出表结构

名称和序号是一一对应的,因此导出名称个数 和导出序号个数 只需要保存一个就可以了

IMAGE_EXPORT_DIRECTORY

```c++
// IMAGE_EXPORT_DIRECTORY 导出表结构体,40B
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;                // 无用
    DWORD   TimeDateStamp;                        // 时间戳
    WORD    MajorVersion;                        // 无用
    WORD    MinorVersion;                        // 无用
    DWORD   Name;                                        // 描述性字段:'模块的名字'
    //上面20字节没用,说明性的

    DWORD   Base;                                        // * 序号base基数,序号导出函数的索引值值从Base开始递增
    DWORD   NumberOfFunctions;                // * 导出地址的个数
    DWORD   NumberOfNames;                        // * 导出名称的个数


    DWORD   AddressOfFunctions;         // * 导出地址表的地址RVA
    DWORD   AddressOfNames;                        // * 导出名称表的地址RVA,按照ASCII码固定排序
    DWORD   AddressOfNameOrdinals;        // * 导出序号表的地址RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
```

#### 定位导出表:

```
```

**数据目录第一项指针指向导出表。(第二项是导入表),该项只能说明导出表所在地址,导出表有多大并不是该Size来决定的(****但是它是导出函数是否转发的判断条件之一****)。**

!(./mdimg/pe/1656344460583-f221d0f6-e6b3-46be-b4f8-ae8bddfd5f35.png)

!(./mdimg/pe/1656344500528-64b2e0df-e8df-432c-b0b4-a9b16c435540.png)

!(./mdimg/pe/1656344756751-32942950-9c91-403d-ad5a-cffb53bd2f84.png)

#### 导出地址表

!(./mdimg/pe/1656344975346-c1f7d21b-0740-491a-a9c4-40c165f74f58.png)

!(./mdimg/pe/1656345200202-25808d99-d650-4b31-ae58-b50fdc2f69ed.png)

#### 导出名称表

!(./mdimg/pe/1656345286660-f52bc14e-4c4c-434e-af5c-235c9d95e666.png)

!(./mdimg/pe/1656345539236-52247eb2-7a84-405f-8723-a69e927422ae.png)

!(./mdimg/pe/1656345588731-0d5a8266-60e7-4b62-8fe2-85b3cb023c7a.png)

可以看出跟我们导出的顺序是不一样的,他进行了 函数名ascii 码排序

#### 导出序号表

!(./mdimg/pe/1656345342834-b23129c4-21c0-4970-ac3a-3bdd0edf2063.png)

#### 解析

##### 正常情况

!(./mdimg/pe/1656346076499-cd837642-8429-469c-b828-8824ff9b0396.png)

在通过cff去看

!(./mdimg/pe/1656345795745-f620077f-a6d3-48bf-9c7e-094dd22c6f50.png)

1. 像上面如果我们找到处序号为4的函数信息

- 拿到下标索引    序号 - base=   4 - 1 = 3
- 根据索引取导出地址表拿到地址    11195
- 用OD验证

!(./mdimg/pe/1656346264564-3e1cab4f-d7ac-40c8-9b44-7660293798b7.png)

!(./mdimg/pe/1656346378040-9b420eea-eae9-4252-b0f8-32a3f0942b2b.png)

1. 拿到函数 foo 的地址

- 在导出名称表遍历,根据该函数名在导出名称表的索引    2
- 在根据上面索引在导出序号表拿到函数导出序号    2
- 在根据序号在 导出地址表拿到导出函数地址    110cd

!(./mdimg/pe/1656346728162-bcb0d4b7-110d-4739-86cb-91a0d7b1cdad.png)

##### 导出表序号不从0开始的情况

!(./mdimg/pe/1656346963217-b4fe04ec-70c5-4adc-b180-052afa0bec80.png)

!(./mdimg/pe/1656347035742-5b2c3365-2fc2-40f4-aa30-b6825842f651.png)

!(./mdimg/pe/1656347390683-9fc7c7ba-c548-4ee5-8c87-e21630d592e4.png)

此时情况是

!(./mdimg/pe/1656347464178-766ae198-f3b2-4c4c-b07e-e49e3ee3eabc.png)!(./mdimg/pe/1656347539115-b8dd2baa-5b8c-4fc7-99df-6070e0c0d19d.png)

##### 导出表序号不连续的情况

!(./mdimg/pe/1656347711124-79c415df-fd0c-4df0-882c-94c308268a64.png)

!(./mdimg/pe/1656347703824-ce080ba0-29c9-4405-8fd4-e908c4490811.png)

!(./mdimg/pe/1656348009558-0fcaa7e3-e71d-4dca-822c-f094db3b7e01.png)

!(./mdimg/pe/1656348109988-b61e960a-feae-4346-8977-226833aa07ea.png)

!(./mdimg/pe/1656348181288-c5a1671f-1d47-4bdb-a395-8edc67d38c68.png)

1. 例如要找序号为110的函数地址

- 获取在导出地址指表的索引   110 -100=10
- 根据索引在导出地址指表数组取值110cd
- OD验证

!(./mdimg/pe/1656348328834-7e834257-9360-4b98-883d-30796566256f.png)

!(./mdimg/pe/1656348396153-a5d1a813-3192-4745-abdd-028c1b83720a.png)

1. 寻找test的函数地址

- 先到导出名称表找到 test在该数组索引   5
- 再到 导出序号表找到对应索引的导出地址的索引
- 再根据导出地址表的索引取导出地址表 获取 地址 1132f

!(./mdimg/pe/1656348640667-1fed26b0-9f52-443b-9cad-235c2be1a61c.png)

1. 找序号108 的 函数地址

- 获取序号108对应的导出地址的索引108-100 = 8
- 再根据导出地址表的索引取导出地址表 获取 地址 0000 ,说明没有这个序号的导出函数

##### 导出函数没有名称只有序号导出

!(./mdimg/pe/1656348895229-dacc761a-ff94-4e64-adc1-9252cef692e4.png)

!(./mdimg/pe/1656348959108-f24a085c-1684-4ccd-b19d-d625af62e525.png)

!(./mdimg/pe/1656348981907-7082e990-f281-46b4-ba76-58c7b16f701c.png)

!(./mdimg/pe/1656349202197-a783b9d1-188e-4977-bcc2-296ecebacf10.png)

!(./mdimg/pe/1656349396872-cf698532-a075-4463-a54d-58363be255cc.png)

这种情况OD可以看到 函数名是通过pdb文件知道的,删掉之后就不知道了

##### 函数转发

!(./mdimg/pe/1656350425946-3d41e9bf-b6f7-4432-b587-f8d718bc3899.png)

!(./mdimg/pe/1656350551629-0eedbdc2-294e-45e0-80d8-b357308a14da.png)

!(./mdimg/pe/1656350620676-1d6f4240-de80-4619-a2d8-cd7e5d359846.png)

!(./mdimg/pe/1656351054287-c5a85e36-eb89-42fb-bfa5-20e3d0a3c65a.png)

!(./mdimg/pe/1656351420421-32fb9eaa-cb6d-47c0-a6cd-ca1e70c49905.png)

!(./mdimg/pe/1656351226026-3ab5dcf4-0cf8-4b1c-b343-e5d18ee470d6.png)

!(./mdimg/pe/1656351443033-adbc1398-6a65-41eb-98a6-ac51e50085f4.png)

!(./mdimg/pe/1656351474418-416269a0-aaad-4004-9a2f-f1292be02f35.png)

可以看出转发的话,导出地址表存的是一个字符串的地址,而系统需要解析这个字符串拿出dll名和函数名,继续去查

而且该地址和其他地址不一样,该地址只需要位于导出表地址   和导出表地址+导出表大小中间 那么他就是一个转出函数,如果没有位于这中间,那么他就不是一个转发函数

###### 如何判断转发函数,即地址指向应为字符串而不是实现?

- 如果是指向的字符串(表示为转发函数),是与导出函数名放在一起的,所以应该先借助正常遍历获取到地址,然后借助导出表的地址和大小判断地址的范围是否在导出表的范围之内:
- - 不是则为正常函数,直接返回地址节课;
- 是则为转发函数,进行转发函数的处理。

###### 遇到转发函数应该如何处理?

- 1.字符串为dll.函数名,所以要先分割解析,分别拿到dll名和函数名;
- 2.调用LoadLibrary;
- 3.再GetProcAddress递归遍历拿到地址。

### 模拟GetProcAddress

```assembly
.586
.model flat,stdcall
option casemap:none

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


WinMain proto :DWORD,:DWORD,:DWORD,:DWORD


.data
   g_szDll db "user32.dll",0
   g_szFuncdb "MessageBoxA",0

.code

;参数:   句柄   导出函数名
MyGetProcAddress proc hMod:HMODULE, lpProcName:LPCSTR   
   
    LOCAL @pDosHdr:ptr IMAGE_DOS_HEADER          ;dos头
    LOCAL @pNTHdr:ptr IMAGE_NT_HEADERS         ;Nt头
    LOCAL @pExpDir:ptr IMAGE_EXPORT_DIRECTORY    ;导出表

    LOCAL @pAddrTbl:DWORD   ;导出地址表地址
    LOCAL @pNameTbl:DWORD   ;导出名称表地址
    LOCAL @pOrdTbl:DWORD      ;导出序号表地址

    ;解析
    ;dos 头
    mov eax, hMod
    mov @pDosHdr, eax

    ;nt头
    mov esi, @pDosHdr
    assume esi:ptr IMAGE_DOS_HEADER
    mov eax, hMod
    add eax, .e_lfanew
    mov @pNTHdr, eax


    mov esi, @pNTHdr
    assume esi:ptr IMAGE_NT_HEADERS


    ;获取导出表
    mov esi, @pNTHdr
    assume esi:ptr IMAGE_NT_HEADERS

    mov eax, .OptionalHeader.DataDirectory.VirtualAddress
    add eax, hMod
    mov @pExpDir, eax

    mov esi, @pExpDir
    assume esi:ptr IMAGE_EXPORT_DIRECTORY

    ;导出函数地址表
    mov eax, .AddressOfFunctions
    add eax, hMod
    mov @pAddrTbl, eax

    ;导出函数名称表
    mov eax, .AddressOfNames
    add eax, hMod
    mov @pNameTbl, eax


    ;导入序号表
    mov eax, .AddressOfNameOrdinals
    add eax, hMod
    mov @pOrdTbl, eax


    ;判断是序号还是名称(序号是一个 word,对于 dword来说高位都是0)
    .if lpProcName & ffff0000h   
      ;名称
      mov ebx, @pNameTbl
      xor ecx, ecx
      .while ecx < .NumberOfNames
            ;获取名称地址
            mov eax,
            add eax, hMod
         
            ;字符串比较
            push ecx
            invoke crt_strcmp, lpProcName, eax
            pop ecx
            .if eax == 0
                ;找到了, 从导出序号表取出函数地址下标
                mov edi, @pOrdTbl
                movzx eax, word ptr
            
            
                ;从导入地址表,下标寻址,获取导出函数地址
                mov ebx, @pAddrTbl
                mov eax,
            
                ;判断转发 。。。。解析函数名,递归判断
            
            
                ;返回地址
                .if eax != NULL
                  add eax, hMod
                  ret
                .endif
            
            .endif
         
            inc ecx
      .endw
      
    .else
      ;序号
      mov eax, lpProcName
      sub eax, .nBase ;获取索引值
      
      ;从导入地址表,下标寻址,获取导出函数地址
      mov ebx, @pAddrTbl
      mov eax,
      
      .if eax != NULL
            add eax, hMod
            ret
      .endif

    .endif
      
    xor eax, eax
    ret

MyGetProcAddress endp

start:
    invoke LoadLibrary, offset g_szDll
    invoke MyGetProcAddress,eax, offset g_szFunc

    push MB_OK
    push offset g_szDll
    push offset g_szFunc
    push NULL
    call eax

        invoke ExitProcess,0
end start

```

### 作业

#### 1. 实现GetProcAddress,可以处理函数转发

#### 2. PE工具添加解析导出表的功能

AO1199 发表于 2025-3-5 09:16:31

努力学习,支持一下!
页: [1]
查看完整版本: WindowsPE文件格式入门08.导出表