任务:
给程序Reversed.exe添加一个输出表,将调用MessageBoxA的那个过程改造成一个输出函数,然后在ReverseMe.exe中装载修改后的Reversed.exe文件,调用该函数,让ReverseMe.exe和Reversed.exe有相同的功能。
工具:U-Edit、OllyDBG
需要知识:PE头结构、输入表结构、少许汇编知识
导出表的起始位置有且只有一个IMAGE_EXPORT_DIRECTORY结构(《Window环境下32位汇编语言程序设计(第一版)》P700,我发现这本书中的PE文件结构很好懂):
IMAGE_EXPORT_DIRECTORY STRUCT
Characteristics DWORD ?;未使用,总是为0
TimeDateStamp DWORD ?;文件产生的时刻
MajorVersion WORD ?;未使用,总是为0
MinorVersion WORD ?;未使用,总是为0
nName DWORD ?;指向文件名的RVA
nBase DWORD ?;导出函数的起始序号
NumberOfFunctions DWORD ?;导出函数的总数
NumberOfNames DWORD ?;以名称导出的函数总数
AddressOfFunctions DWORD ?;指向导出函数地址表的RVA
AddressOfNames DWORD ?;指向函数名地址表的RVA
AddressOfNameOrdinals DWORD ?;指向函数名序号表RVA
IMAGE_EXPORT_DIRECTORY ENDS
部分字段的说明:
nName
这个文件名存的不是文件当前的名字,而是他被编译的时候的名字。
nBase
将AddressOfFunctions字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出序号,举例来说,若nBase为x,则入口表指定的第一个导出函数的序号是x,第二个导出函数的序号是x+1,总之,一个导出函数的导出序号等于nBase字段的值加上其在入口地址表中的位置索引值。
NumberOfNames
既可以通过序号导出又可以通过名称导出的函数的数目,NumberOfFunctions-NumberOfNames是只能通过序号导出的函数的数目。不一定所有的函数都是通过函数名导出的。
AddressOfFunctions
这是一个地址数组,每一项都是一个函数的入口地址,项的数目为NumberOfFunctions
AddressOfNames和AddressOfNameOrdinals
AddressOfNames是一个字符串数组,每一项都是一个函数名,项的数目为NumberOfNames。
前面说过AddressOfNames和NumberOfFunctions不一定相等,就是说函数名和函数地址不一定是一一对应的关系。那么如何知道这些函数名究竟对应地址表中的哪个函数呢?AddressOfNameOrdinals字段就派上用场了。
AddressOfNameOrdinals是一个word类型的数组,实际上是一个索引数组,每一项存一个函数的索引,项的数目为NumberOfNames。
这样函数名数组AddressOfNames就通过索引数组AddressOfNameOrdinals与地址数组AddressOfFunctions对应起来了。
举例来说,假如函数名称字符串地址表的第n项指向一个字符串“MyFunction”,那么可以去查找AddressOfNameOrdinals字段指向的数组的第n项,若该项的值为a,则AddressOfFunctions的第a项函数入口地址就是名为“MyFunction”的函数的入口地址。
可见,AddressOfNameOrdinals指向的数组起了一个桥梁的作用。
============
一.给程序Reversed.exe建立输出表
1.将调用MessageBoxA的那个过程改造成一个函数
将00404080处的代码改成这个样子:
00404080 > 68 00104000 push 00401000
00404085 55 push ebp
00404086 8BEC mov ebp, esp
00404088 6A 00 push 0
0040408A 6A 00 push 0
0040408C 68 A0404000 push 004040A0 ; ASCII "I am a LaMe rEvErSeR!:p"
00404091 6A 00 push 0
00404093 FF15 44404000 call dword ptr [<&USER32.MessageBoxA>>; USER32.MessageBoxA
00404099 5D pop ebp
0040409A C3 retn
当然,函数是从00404085处开始的。
2.建立输出表并将新函数加入输出表
新建一个块:
Name: .edata
VirtualSize: 0x0100
VirtualAddress: 0x5000
SizeOfRawData: 0x0200
PointerToRawData: 0x0C00
PointerToRelocations: 0
PointerToLinenumbers: 0
NumberOfRelocations: 0
NumberOfLinenumbers: 0
Characteristics: 0xC0000040
我们要建立的输出表只包含一个名为"MyMessageBox"的输出函数,文件名为"Reversed_.exe"。
根据上面的信息建立一个输出表:
Characteristics 0x00
TimeDateStamp 0x00
MajorVersion 0x00
MinorVersion 0x00
nName 0x05040 "Reversed_.exe"
nBase 0x01
NumberOfFunctions 0x01
NumberOfNames 0x01
AddressOfFunctions 0x05050
AddressOfNames 0x05030“MyMessageBox”
AddressOfNameOrdinals 0x05060;数组只有一个元素0
AddressOfFunctions数组中的值:004085
然后修改PE头中输出表的指针指向新块。
============
2.将Reversed_.exe中的新函数MyMessageBox添加到ReverseMe.exe的输入表中。
============
3.在程序中添加调用新函数的代码:
00401010 FF15 1D404000 call dword ptr [<&Reversed_.MyMessage>; Reversed.MyMessageBox
00401016 ^ EB E8 jmp short <模块入口点>
============
4.调整入口点到401010处
============
到这里似乎所有的工作都做完了,把两个文件放到一起运行一下试试,得到了个错误。
怎么产生这个错误的?我们漏掉了什么?请看二楼=.=
===================
为什么会产生这个错误?跟踪一下ReverseMe_.exe的运行情况就会发现:对MyMessageBox的调用没错,程序正确的找到了这个函数的入口地址,该函数在我这里表现是这个样子的:
00374085 > 55 push ebp
00374086 8BEC mov ebp, esp
00374088 6A 00 push 0
0037408A 6A 00 push 0
0037408C 68 A0404000 push 4040A0《一
00374091 6A 00 push 0
00374093 FF15 44404000 call dword ptr [404044] ; ReverseM.00400000《二
为了便于理解,这里使Reversed_.exe中的同一段代码:
00404085 > 55 push ebp
00404086 8BEC mov ebp, esp
00404088 6A 00 push 0
0040408A 6A 00 push 0
0040408C 68 A0404000 push 004040A0 ; ASCII "I am a LaMe rEvErSeR!:p"《一
00404091 6A 00 push 0
00404093 FF15 44404000 call dword ptr [<&USER32.MessageBoxA>>; USER32.MessageBoxA《二
应该注意的到各个命令除了地址不同,其他都是一样的(真是废话,他们就是同一段代码)。
对照一下就能发现造成错误的原因是地址:代码的地址被移动到了另一个位置,而代码中使用的数据地址、函数地址却没有做调整。
怎么能让程序自己对地址做调整?这就是重定位表的作用了。
观察一下出问题的两条语句(标记一、二)会发现命令中用到的地址与正确的地址相差的距离是一样的,如果能把这个距离求出来,让程序自己把地址调整一下,命令不就都是对的了么?这个距离是模块实际装入地址(00370000 )与模块建议装入地址(00400000)之差。可见,重定位需要3个数据:模块实际装入地址、模块建议装入地址、需要修正的机器码地址。其中,前两个数据分别是由PE头和WINDOWS装载器确定的,所以需要我们提供的数据仅仅是需要修改的机器码的地址一个。
每个重定位块以一个IMAGE_BASE_RELOCATION结构开头,后面跟着本页面使用的所有重定位项,每个重定位项占用16位的地址(WORD):
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ?;重定位内存页的起始RVA
SizeOfBlock dd ?;重定位块的长度
IMAGE_BASE_RELOCATION ENDS
一般来说,重定位项里放的应该是地址,而存放一个地址需要32位(DWORD),为何重定位表中的重定位项是16位的?
每个需要重定位的块都需要单独的重定位块(我猜是因为不同的块被装入的地址不一样,重定位信息不一样,自然不同的块的重定位信息要分开放)。一个块中的地址的高位总是相同的而且在一个页面中寻址需要的指针位数是12,如果把高位地址统一表示,就可以省略一部分空间。
每个重定位项的低12位就是要重定位的数据在页面中的地址,高4位用来描述当前重定位项的类型,能见到的只有两种:
0 这个重定位项无意义,仅仅用来作为对齐用
3 重定位地址指向的双字的32位都需要被修正
最后所有的重定位块以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构作为结束。
另外,重定位块中的重定位项数=(SizeOfBlock-8)/2
============
一.给程序Reversed_.exe建立重定位表
建立重定位表
新建一个块:
Name: .reloc
VirtualSize: 0x0100
VirtualAddress: 0x6000
SizeOfRawData: 0x0200
PointerToRawData: 0x0E00
PointerToRelocations: 0
PointerToLinenumbers: 0
NumberOfRelocations: 0
NumberOfLinenumbers: 0
Characteristics: 0x42000040
我们需要重定位的有两处:
0040408C 68 A0404000 push 004040A0 ; ASCII "I am a LaMe rEvErSeR!:p"《一
00404093 FF15 44404000 call dword ptr [<&USER32.MessageBoxA>>; USER32.MessageBoxA《二
所以需要修改的机器码的地址为408D和4094(记得吧?命令中用到的地址),建立这样一个重定位块:
VirtualAddress dd 0x4000
SizeOfBlock dd 0xC(重定位块的长度,而不是需要修改的机器码的地址的个数)
0x308D(加上类型说明)
0x3095
VirtualAddress dd 0x0000(表示重定位块结束)
SizeOfBlock dd 0x0
============
2.将Reversed_.exe的重定位表指向新块。
============
3.运行一下,发现还是不行。跟踪程序的运行情况会发现重定位的两处地址并没有改变。
重新看一遍PE结构,发现IMAGE_FILE_HEADER结构里有个Characteristics项,这一项决定了文件的装入方式。
我把第0位置0(说明文件中存在重定位信息)时情况依然。
我更干脆的把文件类型改成DLL文件(第13位置1)时两个程序都运行不了了。
看来我选错对手了,这条路我走不下去了 =.=
============
4.不用重定位的解决方法。
之所以会有重定位问题,是因为文件被装载到的位置不是我们预期的位置,会造成这种情况的原因是建议装入地址已经有别的文件占用了,所以我的解决方法就是改变Reversed_.exe的基址和一些代码的地址参数:
因为这个方法并不好,没有通用性,就不细说了,有兴趣的自己看看程序
============
问题:
1.重定位表是DLL文件专用的?就没办法将EXE当DLL一样用么?
2.我把Reversed_.exe的第13位置1的时候,运行ReverseMe_.exe会遇到一个初始化错误。这是怎么回事?