找回密码
 立即注册
首页 业界区 安全 WindowsPE文件格式入门08.导出表

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

姬宜欣 2025-6-11 15:35:45
https://bpsend.net/thread-377-1-1.html
通过cff , depends灯等软件可以看到dll,导出函数的信息,因为dll中本身就存了这些信息,存了dll中有哪些导出函数,导出函数的序号是什么,名字是什么,以及他们的地址是什么,这些东西都存在导出表里面
 

导出表发展历程

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

序号地址1100522005330054400555005660057700588005但是上面的时间复杂度比较高是线性阶,但是对数阶占的体积比较大,所以只能折中,用常量阶.把地址作为数组,序号作为索引,只存地址,这样不仅速度更加快了,而且体积更小了
但是所以是从0开始的,因此可以数组首项加一个空
1.png

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

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

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

解决了上面基本不从0开始的情况,还有一个问题,就是 序号不连续 ,例如 1001 , 1002 , 1020,1021,1050
这种情况下,数组中间对应的序号就需要填充0,因此会使体积变大,微软并没有解决这个问题,因为这是写dll的人自己的问题
例如

  • 不指定序号(这样序号连续)
3.png

4.png

可以看出大小是38kb

  • 部分指定序号使序号不连续
5.png

6.png

可以看到此时dll大小变成了 346KB
名称导出

7.png

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

导出表结构

名称和序号是一一对应的,因此导出名称个数 和导出序号个数 只需要保存一个就可以了
IMAGE_EXPORT_DIRECTORY
  1. // IMAGE_EXPORT_DIRECTORY 导出表结构体,40B
  2. typedef struct _IMAGE_EXPORT_DIRECTORY {
  3.     DWORD   Characteristics;        // 无用
  4.     DWORD   TimeDateStamp;          // 时间戳
  5.     WORD    MajorVersion;           // 无用
  6.     WORD    MinorVersion;           // 无用
  7.     DWORD   Name;                   // 描述性字段:'模块的名字'
  8.     //上面20字节没用,说明性的
  9.     DWORD   Base;                   // * 序号base基数,序号导出函数的索引值值从Base开始递增
  10.     DWORD   NumberOfFunctions;      // * 导出地址的个数
  11.     DWORD   NumberOfNames;          // * 导出名称的个数
  12.     DWORD   AddressOfFunctions;     // * 导出地址表的地址RVA
  13.     DWORD   AddressOfNames;         // * 导出名称表的地址RVA,按照ASCII码固定排序
  14.     DWORD   AddressOfNameOrdinals;  // * 导出序号表的地址RVA
  15. } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
复制代码
定位导出表:

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

9.png

10.png

导出地址表

11.png

12.png

导出名称表

13.png

14.png

15.png

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

16.png

解析

正常情况

17.png

在通过cff去看
18.png


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


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

20.png


  • 拿到函数 foo 的地址


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

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

22.png

23.png

24.png

此时情况是
25.png
26.png

导出表序号不连续的情况

27.png

28.png

29.png

30.png

31.png


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


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

33.png


  • 寻找test的函数地址


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


  • 找序号108 的 函数地址


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

35.png

36.png

37.png

38.png

39.png

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

40.png

41.png

42.png

43.png

44.png

45.png

46.png

47.png

可以看出转发的话,导出地址表存的是一个字符串的地址,而系统需要解析这个字符串拿出dll名和函数名,继续去查
而且该地址和其他地址不一样, 该地址只需要位于 导出表地址 和 导出表地址+导出表大小 中间 那么他就是一个转出函数,如果没有位于这中间,那么他就不是一个转发函数
如何判断转发函数,即地址指向应为字符串而不是实现?


  • 如果是指向的字符串(表示为转发函数),是与导出函数名放在一起的,所以应该先借助正常遍历获取到地址,然后借助导出表的地址和大小判断地址的范围是否在导出表的范围之内:

    • 不是则为正常函数,直接返回地址节课;
    • 是则为转发函数,进行转发函数的处理。

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


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

[code].586.model flat,stdcalloption casemap:none   include windows.inc   include user32.inc   include kernel32.inc   include msvcrt.inc   includelib user32.lib   includelib kernel32.lib   includelib msvcrt.libWinMain proto WORD,WORD,WORD,WORD.data   g_szDll db "user32.dll",0   g_szFunc  db "MessageBoxA",0.code;参数:   句柄   导出函数名MyGetProcAddress proc hMod:HMODULE, lpProcNamePCSTR       LOCAL @pDosHdr:ptr IMAGE_DOS_HEADER          ;dos头    LOCAL @pNTHdr:ptr IMAGE_NT_HEADERS           ;Nt头    LOCAL @pExpDir:ptr IMAGE_EXPORT_DIRECTORY    ;导出表    LOCAL @pAddrTblWORD     ;导出地址表地址    LOCAL @pNameTblWORD     ;导出名称表地址    LOCAL @pOrdTblWORD      ;导出序号表地址    ;解析    ;dos 头    mov eax, hMod    mov @pDosHdr, eax    ;nt头    mov esi, @pDosHdr    assume esi:ptr IMAGE_DOS_HEADER    mov eax, hMod    add eax, [esi].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, [esi].OptionalHeader.DataDirectory[0].VirtualAddress    add eax, hMod    mov @pExpDir, eax    mov esi, @pExpDir    assume esi:ptr IMAGE_EXPORT_DIRECTORY    ;导出函数地址表    mov eax, [esi].AddressOfFunctions    add eax, hMod    mov @pAddrTbl, eax    ;导出函数名称表    mov eax, [esi].AddressOfNames    add eax, hMod    mov @pNameTbl, eax    ;导入序号表    mov eax, [esi].AddressOfNameOrdinals    add eax, hMod    mov @pOrdTbl, eax    ;判断是序号还是名称  (序号是一个 word,对于 dword来说高位都是0)    .if lpProcName & ffff0000h           ;名称        mov ebx, @pNameTbl        xor ecx, ecx        .while ecx 
您需要登录后才可以回帖 登录 | 立即注册