通过cff , depends灯等软件可以看到dll,导出函数的信息,因为dll中本身就存了这些信息,存了dll中有哪些导出函数,导出函数的序号是什么,名字是什么,以及他们的地址是什么,这些东西都存在导出表里面
导出表发展历程
最开始的时候只有序号导出,没有名称导出,使用的时候都是通过序号
序号 |
地址 |
1 |
1005 |
2 |
2005 |
3 |
3005 |
4 |
4005 |
5 |
5005 |
6 |
6005 |
7 |
7005 |
8 |
8005 |
但是上面的时间复杂度比较高是线性阶,但是对数阶占的体积比较大,所以只能折中,用常量阶.把地址作为数组,序号作为索引,只存地址,这样不仅速度更加快了,而且体积更小了
但是所以是从0开始的,因此可以数组首项加一个空

这样可以通过序号,直接去找对应数组对应的索引的值
序号不从1开始
但是上面有一个缺陷,序号不一定从0开始,例如 从 1001开始,这样数组不能前面加1000项0,这样不仅浪费空间,而且还用不上,解决办法是加一个 基址 (最小序号), 后面序号就根据基址来取偏移

例如上面,要拿到 序号1006 的函数地址, 可以 获取 1006对 1001 的偏移 5 ,再去数组取 索引为5的地址
序号不连续
解决了上面基本不从0开始的情况,还有一个问题,就是 序号不连续 ,例如 1001 , 1002 , 1020,1021,1050
这种情况下,数组中间对应的序号就需要填充0,因此会使体积变大,微软并没有解决这个问题,因为这是写dll的人自己的问题
例如
- 不指定序号(这样序号连续)


可以看出大小是38kb
- 部分指定序号使序号不连续


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

序号跟成名2个表是拆开的,并不在一起,因此 基数数 ,导出地址表 ,导出名称表 ,导出序号表 构成了导出表
导出表结构
导出表结构
名称和序号是一一对应的,因此导出名称个数 和导出序号个数 只需要保存一个就可以了
IMAGE_EXPORT_DIRECTORY
// 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来决定的(**但是它是导出函数是否转发的判断条件之一**)。



导出地址表


导出名称表



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

解析
正常情况

在通过cff去看

- 像上面如果我们找到处序号为4的函数信息
- 拿到下标索引 序号 - base = 4 - 1 = 3
- 根据索引取导出地址表拿到地址 11195
- 用OD验证


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

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



此时情况是


导出表序号不连续的情况





- 例如要找序号为110的函数地址
- 获取在导出地址指表的索引 110 -100 = 10
- 根据索引在导出地址指表数组取值 110cd
- OD验证


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

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





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








可以看出转发的话,导出地址表存的是一个字符串的地址,而系统需要解析这个字符串拿出dll名和函数名,继续去查
而且该地址和其他地址不一样, 该地址只需要位于 导出表地址 和 导出表地址+导出表大小 中间 那么他就是一个转出函数,如果没有位于这中间,那么他就不是一个转发函数
如何判断转发函数,即地址指向应为字符串而不是实现?
- 如果是指向的字符串(表示为转发函数),是与导出函数名放在一起的,所以应该先借助正常遍历获取到地址,然后借助导出表的地址和大小判断地址的范围是否在导出表的范围之内:
-
- 不是则为正常函数,直接返回地址节课;
- 是则为转发函数,进行转发函数的处理。
遇到转发函数应该如何处理?
- 1.字符串为dll.函数名,所以要先分割解析,分别拿到dll名和函数名;
- 2.调用LoadLibrary;
- 3.再GetProcAddress递归遍历拿到地址。
模拟GetProcAddress
.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_szFunc db "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, [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 < [esi].NumberOfNames
;获取名称地址
mov eax, [ebx+ecx*4]
add eax, hMod
;字符串比较
push ecx
invoke crt_strcmp, lpProcName, eax
pop ecx
.if eax == 0
;找到了, 从导出序号表取出函数地址下标
mov edi, @pOrdTbl
movzx eax, word ptr [edi+ecx*2]
;从导入地址表,下标寻址,获取导出函数地址
mov ebx, @pAddrTbl
mov eax, [ebx+eax*4]
;判断转发 。。。。解析函数名,递归判断
;返回地址
.if eax != NULL
add eax, hMod
ret
.endif
.endif
inc ecx
.endw
.else
;序号
mov eax, lpProcName
sub eax, [esi].nBase ;获取索引值
;从导入地址表,下标寻址,获取导出函数地址
mov ebx, @pAddrTbl
mov eax, [ebx+eax*4]
.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工具添加解析导出表的功能