5.4 导出表编程
本节讨论与导出表有关的编程,包括获取导出表中导出函数地址的编程,以及遍历导出表信息的编程两部分。
如前所述,通过导出表可以获取相关函数的地址。函数可以通过索引值定位,也可以通过函数名定位。通过编程查找函数地址有两个不同方法,分别是:
❑ 根据编号查找函数地址
❑ 根据名字查找函数地址
下面分别介绍这两种方法。
5.4.1 根据编号查找函数地址
要通过编号查找函数地址,其步骤如下:
步骤1 定位到PE头。
步骤2 从PE文件头中找到数据目录表,表项的第一个双字值是导出表的起始RVA。
步骤3 从导出表的nBase字段得到起始序号。
步骤4 函数编号减去起始序号得到的是函数在AddressOfFunctions中的索引号。
步骤5 通过查询AddressOfFunctions指定索引位置的值,找到虚拟地址。
步骤6 将虚拟地址加上该动态链接库在被导入到地址空间后的基地址,即为函数的真实入口地址。
不建议使用编号查找函数地址。因为有很多的动态链接库中标识的编号与对应的函数并不一致,通过这种方法找到的函数地址往往是错误的。
5.4.2 根据名字查找函数地址
要根据函数名字从导出表结构中查找函数的地址,步骤如下:
步骤1 定位到PE头。
步骤2 从PE文件头中找到数据目录表,表项的第一个双字值是导出表的起始RVA。
步骤3 从导出表中获取NumberOfNames字段的值,以便构造一个循环,根据此值确定循环的次数。
步骤4 从AddressOfNames字段指向的函数名称数组的第一项开始,与给定的函数名字进行匹配;如果匹配成功,则记录从AddressOfNames开始的索引号。
步骤5 通过索引号再去检索AddressOfNameOrdinals数组,从同样索引的位置找到函数的地址索引。
步骤6 通过查询AddressOfFunctions指定函数地址索引位置的值,找到虚拟地址。
步骤7 将虚拟地址加上该动态链接库在被导入到地址空间的基地址,即为函数的真实入口地址。
其中通过函数名获取函数调用地址的编码见代码清单5-3。
代码清单5-3 获取指定字符串的API函数的调用地址的函数_getApi(chapter5\peinfo.asm)
;-------------------------------
; 获取指定字符串的API函数的调用地址
; 入口参数:_hModule为动态链接库的基址,_lpApi指向函数名
; 出口参数:eax为函数在虚拟地址空间中的真实地址
;-------------------------------
_getApi proc _hModule,_lpApi
local @ret
local @dwLen
pushad
mov @ret,0
;计算API字符串的长度,含最后的0
mov edi,_lpApi
mov ecx,-1
xor al,al
cld
repnz scasb
mov ecx,edi
sub ecx,_lpApi
mov @dwLen,ecx
;从PE文件头的数据目录获取导出表地址
mov esi,_hModule
add esi,[esi+3ch]
assume esi:ptr IMAGE_NT_HEADERS
mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
add esi,_hModule
assume esi:ptr IMAGE_EXPORT_DIRECTORY
;查找符合名称的导出函数名
mov ebx,[esi].AddressOfNames
add ebx,_hModule
xor edx,edx
.repeat
push esi
mov edi,[ebx]
add edi,_hModule
mov esi,_lpApi
mov ecx,@dwLen
repz cmpsb
.if ZERO?
pop esi
jmp @F
.endif
pop esi
add ebx,4
inc edx
.until edx>=[esi].NumberOfNames
jmp _ret
@@:
;通过API名称索引获取序号索引,再获取地址索引
sub ebx,[esi].AddressOfNames
sub ebx,_hModule
shr ebx,1 ;除以2
add ebx,[esi].AddressOfNameOrdinals
add ebx,_hModule
movzx eax,word ptr [ebx]
shl eax,2 ;乘以4
add eax,[esi].AddressOfFunctions
add eax,_hModule
;从地址表得到导出函数的地址
mov eax,[eax]
add eax,_hModule ;加上模块的基地址
mov @ret,eax
_ret:
assume esi:nothing
popad
mov eax,@ret
ret
_getApi endp
23~24行对应步骤当中的步骤1,定位PE头。
34~49行是一个循环,结束条件为找到对应的函数地址或者函数个数已经达到NumberOfNames字段所标识的值。如何判断函数已经找到了呢?方法是通过与AddressOfNames所列的每个函数名进行比对,如果字符串相等,则表示找到,否则继续下一次循环。
如果找到,则跳出循环,转到行50处继续执行。如果函数名匹配成功,表示找到了对应的函数。则记录AddressOfNames的索引,即步骤4,对应代码中的51~54行。
代码中的55~61行实现了步骤5的操作。步骤6、7则对应63~64行。
5.4.3 遍历导出表
遍历导出表的编程是以第4章的PEInfo.asm程序为模板开始的。在函数_openFile中加入以下代码(加黑部分):
;到此为止,该文件的验证已经完成。为PE结构文件
;接下来分析文件映射到内存中的数据,并显示主要参数
invoke _getMainInfo,@lpMemory,esi,@dwFileSize
;显示导入表
invoke _getImportInfo,@lpMemory,esi,@dwFileSize
;显示导出表
invoke _getExportInfo,@lpMemory,esi,@dwFileSize
然后编写函数_getExportInfo,如代码清单5-4所示。
代码清单5-4 遍历导出表的函数_getExportInfo(chapter5\peinfo.asm)
;-------------------------------
; 获取PE文件的导出表
;-------------------------------
_getExportInfo proc _lpFile,_lpPeHead,_dwSize
local @szBuffer[1024]:byte
local @szSectionName[16]:byte
local @lpAddressOfNames,@dwIndex,@lpAddressOfNameOrdinals
pushad
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS
mov eax,[esi].OptionalHeader.DataDirectory[0].VirtualAddress
.if !eax
invoke _appendInfo,addr szErrNoExport
jmp _Ret
.endif
invoke _RVAToOffset,_lpFile,eax
add eax,_lpFile
mov edi,eax ;计算导出表所在文件偏移位置
assume edi:ptr IMAGE_EXPORT_DIRECTORY
invoke _RVAToOffset,_lpFile,[edi].nName
add eax,_lpFile
mov ecx,eax
invoke _getRVASectionName,_lpFile,[edi].nName
invoke wsprintf,addr @szBuffer,addr szMsgExport,\
eax,ecx,[edi].nBase,[edi].NumberOfFunctions,\
[edi].NumberOfNames,[edi].AddressOfFunctions,\
[edi].AddressOfNames,[edi].AddressOfNameOrdinals
invoke _appendInfo,addr @szBuffer
invoke _RVAToOffset,_lpFile,[edi].AddressOfNames
add eax,_lpFile
mov @lpAddressOfNames,eax
invoke _RVAToOffset,_lpFile,[edi].AddressOfNameOrdinals
add eax,_lpFile
mov @lpAddressOfNameOrdinals,eax
invoke _RVAToOffset,_lpFile,[edi].AddressOfFunctions
add eax,_lpFile
mov esi,eax ;函数的地址表
mov ecx,[edi].NumberOfFunctions
mov @dwIndex,0
@@:
pushad
mov eax,@dwIndex
push edi
mov ecx,[edi].NumberOfNames
cld
mov edi,@lpAddressOfNameOrdinals
repnz scasw
.if ZERO? ;找到函数名称
sub edi,@lpAddressOfNameOrdinals
sub edi,2
shl edi,1
add edi,@lpAddressOfNames
invoke _RVAToOffset,_lpFile,dword ptr [edi]
add eax,_lpFile
.else
mov eax,offset szExportByOrd
.endif
pop edi
;序号在ecx中
mov ecx,@dwIndex
add ecx,[edi].nBase
invoke wsprintf,addr @szBuffer,addr szMsg4,\
ecx,dword ptr [esi],eax
invoke _appendInfo,addr @szBuffer
popad
add esi,4
inc @dwIndex
loop @B
_Ret:
assume esi:nothing
assume edi:nothing
popad
ret
_getExportInfo endp
行12~16通过检索数据目录表的第1项(用[esi].OptionalHeader.DataDirectory[0]来表示),获取导出表的VirtualAddress。如果该值为0,意味着该PE文件没有导出表,则显示没有导出表的提示信息并退出,否则继续。
行17~30部分,首先将获取的导出表的VirtualAddress的RVA值转换为FOA,然后显示该地址所处的节的名称,并显示结构IMAGE_EXPORT_DIRECTORY的部分字段的值。
行43~71为一个循环,该循环完成了显示该PE文件中所有导出函数相关信息的功能。这些信息包括导出序号、函数的虚拟地址和导出函数的名称。变量@dwIndex跟随循环次数加1递增,同时该变量也是所有函数的索引值。通过查找AddressOfNameOrdinals数组获得该索引是否在数组中存在,以确定对应索引的函数是否是基于名称访问的。如果是,则执行第52~57行的代码;如果不是,表示该函数是基于索引值访问的,则执行第59行的代码。
以下内容是使用PEInfo小工具分析chapter5\winResult.dll文件输出的与导出表有关的信息:
--------------------------------------------------------------
导出表所处的节:.rdata
--------------------------------------------------------------
原始文件名:winresult.dll
nBase 00000001
NumberOfFunctions 00000004
NuberOfNames 00000004
AddressOfFunctions 00002168
AddressOfNames 00002178
AddressOfNameOrd 00002188
-------------------------------------
导出序号 虚拟地址 导出函数名称
-------------------------------------
00000001 00001183 AnimateClose
00000002 00001022 AnimateOpen
00000003 00001282 FadeInOpen
00000004 00001323 FadeOutClose
显示信息分三部分:导出表所处的节、导出表结构IMAGE_EXPORT_DIRECTORY的主要字段的值和导出函数的相关信息(含导出序号、虚拟地址和导出函数名称)。导出表的结构就分析到这里,下面来看导出表的常见应用。
5.5 导出表的应用
导出表常见的应用主要包括对导出表函数的覆盖,以及对动态链接库内部私有函数的导出等。通过对导出表函数进行覆盖,可以更改代码流程或代码功能,为应用程序实施补丁;通过对动态链接库内部私有函数的导出,可以更充分地利用已有的代码,减轻二次开发的工作量。
5.5.1 导出函数覆盖
导出表编程中常见的技术是,不需要修改用户程序,便能将用户程序中调用的动态链接库函数转向或者实施代码覆盖,实现用户程序的调用转移。这种技术通常用在病毒程序的开发中,因为用户程序没有发生改变,所以杀毒软件在对用户程序的防护过程中,针对这种渗透是无效的。下面介绍两种常见的导出函数覆盖技术:
❑ 修改导出结构中的函数地址
❑ 覆盖函数地址部分的指令代码
1.修改导出结构中的函数地址

以winResult.dll为例,使用FlexHex将AddressOfFunctions索引1和2的地址(分别对应函数AnimateOpen和FadeInOpen)交换位置。更改以后的字节码如下:
原始值:

修改后:

00000960 78 21 00 00 88 21 00 00 83 11 00 00 82 12 00 00 x!..!........
00000970 22 10 00 00 23 13 00 00 9E 21 00 00 AB 21 00 00 "...#...!..!..
00000980 B7 21 00 00 C2 21 00 00 00 00 01 00 02 00 03 00 !..!..........
如上所示,无需修改应用程序FirstWindow.exe,仅通过将函数调用RVA地址0x00001282和0x00001022交换位置,即可实现导出函数的覆盖。直接测试,发现显示窗口的动画效果发生了变化。
需要注意的是,在使用导出函数地址覆盖技术的时候,首先要保证所涉及的两个函数参数入口要一致,否则调用完成后栈不平衡,这会导致应用程序调用失败;其次,要求用户对两个函数的内部实现要有充分的了解,使得地址转向后,能够保证应用程序在功能上可以全面兼容并运行良好。
注意 该部分测试文件在随书文件的目录chapter5\a中,winResult.dll是被修改了AddressOfFunctions地址后的动态链接库。在实际的操作中并不赞成大家使用该技术。
2.覆盖函数地址部分的指令代码
第二种常见的覆盖技术,是将AddressOfFunctions指向的地址空间指令字节码实施覆盖。这种技术又衍生出两种:
❑ 暴力覆盖,即将所有的代码全部替换为新代码。新代码可能含有原来代码的全部功能,也可能不包含原有代码功能。
❑ 完美覆盖,通过构造指令,实施新代码与原代码的共存和无遗漏运行。
因为完美覆盖涉及代码的重定位,相对复杂一些,这里以暴力覆盖为例。相关文件在随书文件的chapter5\b目录中,winResult.dll是被覆盖了函数FadeInOpen后的动态链接库。
打开winResult.dll文件查看字节码,可以发现函数FadeInOpen定义部分(文件中起始偏移0x0682)被修改成如下指令序列:
00000680 55 8B EC 6A 00 6A 00 68 28 30 00 10 6A 00 ..Uj.j.h(0..j.
00000690 E8 08 00 00 00 90 90 90 90 C9 C2 04 00 FF 25 A3 ...... %
000006A0 12 00 10 EA 07 D5 77 ....w
为方便阅读,以上按照指令功能对字节码作了加黑处理。第一部分黑体保存原始栈基地址,第二部分黑体代码是维持栈平衡的返回指令。所有字节码对应的反汇编指令为:
10001282 > 55 PUSH EBP
10001283 8BEC MOV EBP,ESP
10001285 6A 00 PUSH 0
10001287 6A 00 PUSH 0
10001289 68 28300010 PUSH winResul.10003028 ; ASCII "user32.dll"
1000128E 6A 00 PUSH 0
10001290 E8 08000000 CALL winResul.1000129D
; JMP 到 user32.MessageBoxA
10001295 90 NOP
10001296 90 NOP
10001297 90 NOP
10001298 90 NOP
10001299 C9 LEAVE
1000129A C2 0400 RETN 4
1000129D - FF25 A3120010 JMP DWORD PTR DS:[100012A3] ; user32.MessageBoxA
100012A3 EA 07D57700 006>JMP FAR 6800:0077D507 ; 远跳转
因为函数FadeInOpen的代码被全部覆盖,所以运行FirstWindow只会弹出提示对话框,内容显示“user32.dll”。显示的字符串是借用了winResult.asm的数据段中定义的函数SetLayeredWindowAttributes所在动态链接库的名称。
从反汇编指令可以看出,调用函数user32.MessageBoxA时使用了硬编码,即将该函数在虚拟地址空间分配的VA直接写入了代码段,不通过导入表直接跳转到函数代码处执行,上面反汇编代码加黑部分即为地址字节码(在这里OD错误地将它识别成了指令序列)。使用硬编码最大的好处是引入动态链接库的函数时不需要修改导入表。因为FirstWindow引入的动态链接库就这一个,不存在基地址被占用的问题,所以,与重定位有关的信息在此例中不需要进行修改。
5.5.2 导出私有函数
在某些场合下,DLL中的私有函数还是很有用的。也许是出于保密考虑,或者其他原因, DLL的开发者将一些比较重要的函数设置为内部私有函数,并不在导出表中声明。当程序被二次开发时,开发者却需要这些函数,这时候就需要开发者自己将这些被定义为私有的函数添加到导出表中。
在本章的实例中,程序winResult.dll一共导出了4个公有函数;源代码中的TopXY函数被声明为私有函数,并未导出,所以在使用PEInfo分析时看不到该函数。下面就以这个函数为例,介绍一下导出私有函数需要做哪些工作。
首先,将最原始的导出表整体搬迁到一个空闲空间中,这里选择从文件偏移0x0940处搬到0x0a50处。以下是添加私有函数到导出表后的两处地址对应的字节码:
原始winResult.dll数据:

从《WindowsPE权威指南》附书源代码\chapter5\c\winResult.dll代码中导出要修改的数据到dll.zim
选中要导出的数据00000A50~00000AE0==》【编辑】菜单=》【读取/写入 数据】=》【写入到文件】

用FlexHEX打开要修改的winResult.dll,选择要导入的起始地址位置=》【编辑】菜单=》【读取/写入 数据】=》【从文件插入】导入上面导出的dll.zim文件

修改后:

00000940 00 00 00 00 EE 0E 51 4D 00 00 00 00 90 21 00 00 .....QM....!..
00000950 01 00 00 00 04 00 00 00 04 00 00 00 68 21 00 00 ............h!..
00000960 78 21 00 00 88 21 00 00 83 11 00 00 22 10 00 00 x!..!....."...
00000970 82 12 00 00 23 13 00 00 9E 21 00 00 AB 21 00 00 ...#...!..!..
00000980 B7 21 00 00 C2 21 00 00 00 00 01 00 02 00 03 00 !..!..........
00000990 77 69 6E 72 65 73 75 6C 74 2E 64 6C 6C 00 41 6E winresult.dll.An
000009A0 69 6D 61 74 65 43 6C 6F 73 65 00 41 6E 69 6D 61 imateClose.Anima
000009B0 74 65 4F 70 65 6E 00 46 61 64 65 49 6E 4F 70 65 teOpen.FadeInOpe
000009C0 6E 00 46 61 64 65 4F 75 74 43 6C 6F 73 65 00 00 n.FadeOutClose..
000009D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000009E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000009F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000A00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000A10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000A20 00 00 00 00 00 00 00 00 75 73 65 72 33 32 2E 64 ........user32.d
00000A30 6C 6C 00 53 65 74 4C 61 79 65 72 65 64 57 69 6E ll.SetLayeredWin
00000A40 64 6F 77 41 74 74 72 69 62 75 74 65 73 00 00 00 dowAttributes...
00000A50 00 00 00 00 EE 0E 51 4D 00 00 00 00 AA 30 00 00 .....QM....0..
00000A60 01 00 00 00 05 00 00 00 05 00 00 00 78 30 00 00 ............x0..
00000A70 8C 30 00 00 A0 30 00 00 83 11 00 00 22 10 00 00 0..0....."...
00000A80 82 12 00 00 23 13 00 00 0C 10 00 00 B8 30 00 00 ...#.......0..
00000A90 C5 30 00 00 D1 30 00 00 DC 30 00 00 E9 30 00 00 0..0..0..0..
00000AA0 00 00 01 00 02 00 03 00 04 00 77 69 6E 52 65 73 ..........winRes
00000AB0 75 6C 74 2E 64 6C 6C 00 41 6E 69 6D 61 74 65 43 ult.dll.AnimateC
00000AC0 6C 6F 73 65 00 41 6E 69 6D 61 74 65 4F 70 65 6E lose.AnimateOpen
00000AD0 00 46 61 64 65 49 6E 4F 70 65 6E 00 46 61 64 65 .FadeInOpen.Fade
00000AE0 4F 75 74 43 6C 6F 73 65 00 54 6F 70 58 59 00 00 OutClose.TopXY..
现在从几个方面来分析添加了私有函数的导出表与原有导出表的区别:
1)长度从8Fh变成了9Fh。增加的部分包含:
❑ 函数名:‘TopXY\0’共6个字节。
❑ 函数的RVA:0x0000100c,共4个字节。
❑ 函数名称所在地址:0x000032e9,共4个字节。
2)函数个数由原来的4个变成5个(见字节码的下划线部分)。
3)修正其他因搬迁和增加而变动的地址。因为上面已经同时列出了前后的字节码,在这里就不再详细描述,大家可以比照两者不同和导出表结构自己进行分析。
4)数据目录项。因为导出表位置和数据大小发生了变化,所以PE文件头部的数据目录项中需要进行如下修正:
❑ 位置由原来的0x2140变成0x3250。
❑ 大小由原来的8Fh变成9Fh。

修改后:(修改原理3050h - 3000h + a00h = A50h位置)

这样就为导出表增加了函数TopXY的导出信息。使用小工具PEInfo查看导出表,输出信息如下:
使用导出工具查看

-------------------------------------------------------------
导出表所处的节:.data
-------------------------------------------------------------
原始文件名:winResult.dll
nBase 00000001
NumberOfFunctions 00000005
NuberOfNames 00000005
AddressOfFunctions 00003078
AddressOfNames 0000308c
AddressOfNameOrd 000030a0
-------------------------------------
导出序号 虚拟地址 导出函数名称
-----------------------------------------
00000001 00001183 AnimateClose
00000002 00001022 AnimateOpen
00000003 00001282 FadeInOpen
00000004 00001323 FadeOutClose
00000005 0000100c TopXY
如上所示,加黑部分明确提示我们,导出函数已经由最初的4个变成了5个;在导出函数的描述部分也显示了新函数的导出序号、函数所在的RVA,以及新导出函数的名称TopXY,这意味着将私有函数转换为导出函数是成功的。你也可以新建一个测试程序对刚导出的函数进行测试,测试代码在随书文件列表chapter5\c\priFun.asm中。
;priFun.asm 导出表导出私有函数测试
;在XP系统环境下运行,使用ml命令进行编译和链接:
;ml /c /coff priFun.asm
;link /subsystem:windows priFun.obj
.386
.model flat, stdcall
option casemap:none ;区分大小写
;include文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;数据段
.data
szBuffer db 200 dup(0)
szBuffer1 db 200 dup(0)
szOut db '(400:600) eax=%d', 0
winResu db 'winResult.dll', 0
SLWA db 'TopXY', 0
pSLWA dd ?
;代码段
.code
;-------------------------------
; 私有函数模拟
;-------------------------------
NTopXY proc wDim:DWORD, sDim:DWORD
shr sDim, 1
shr wDim, 1
mov eax, wDim
sub sDim, eax
mov eax, sDim
ret
NTopXY endp
start:
invoke NTopXY, 400, 600
invoke wsprintf, addr szBuffer, addr szOut, eax
invoke MessageBox, NULL, offset szBuffer, NULL, MB_OK
;指定的动态链接库(DLL)加载到当前进程的地址空间中。
invoke LoadLibrary, addr winResu
;从指定的动态链接库(DLL)中获取一个导出函数或变量的地址
invoke GetProcAddress, eax, addr SLWA
mov pSLWA, eax
push 600
push 400
call pSLWA
invoke wsprintf, addr szBuffer1, addr szOut, eax
invoke MessageBox, NULL, offset szBuffer1, NULL, MB_OK
invoke ExitProcess, NULL
end start
测试结果:

5.6 小结
本章重点介绍了PE结构中的导出数据部分。通过对导出表的学习,读者能够了解导出表在PE中的作用,并从底层了解Windows加载程序修正导入表IAT的过程;同时,本章还对导出表的导出函数覆盖技术及私有函数导出做了简单的介绍。
本文介绍了libcurl库的安装方法及使用流程,并通过一个简单的单线程爬虫程序展示了如何利用libcurl抓取网页内容和解析URL。

2775

被折叠的 条评论
为什么被折叠?



