标题:重建 PE 文件的输入表
原著:TiTi/BLiZZARD
翻译:Sun Bird [CCG]
日期:2000年5月24日
1. 前言
=======
大家好 :) 我之所以写这篇短文,是由于我在 Dump 时发现,很多加压、加密软件都使
得输入表(Import Table)不可用,所以 Dump 出的可执行文件必须要重建输入表。而在普
通的讲授 Win32 汇编的站点上我没有找到这样的介绍,所以如果你对此感兴趣,那么这篇
短文对你会有些帮助。
例如,为了让从内存中 Dump 出的经 PETite v2.1 压缩过的可执行文件正常运行,必
须重建输入表。(对于 ASPack、PEPack、PESentry……也同样)这就是所有 Dump 软件都
具备重建输入表功能的原因(例如 G-RoM/UCF 制作的 Phoenix Engine(ProcDump 内含),
或者由 Virogen/PC 和我制作的 PE Rebuilder)。
鉴于这个问题十分特殊,而且比较复杂,所以我假定你已经了解了 PE 文件结构。(你
需要阅读有关 PE 文件的文档)
2. 预备知识
===========
首先是一些关于输入表和 RVA/VA 的简介。
输入表的相对虚拟地址(RVA)储存在 PE 文件头部的相应目录入口(它的偏移量为
[ PE 文件头偏移量+80h ])。由于是虚拟偏移量,所以它和文件输入表中的偏移量(VA)
是不匹配的(除非文件纯粹是刚刚从内存中 Dump 出来的)。于是我们首先要做的事情是,
找到 PE 文件的输入表,将 RVA 转换为相应的 VA。为此,我们可以采用不同的办法:你可
以自行编制软件来分析块(Sections)目录并计算 VA,但最简单的办法是使用专门为此设
计的应用程序接口(API)。这个 API 包括在 IMAGEHLP.DLL(Win9X 和 NT 系统都使用的
一个库)中,名为 ImageRvaToVa。下面是对它的描述(完整的内容详见 MSDN 库):
# LPVOID ImageRvaToVa(
# IN PIMAGE_NT_HEADERS NtHeaders,
# IN LPVOID Base,
# IN DWORD Rva,
# IN OUT PIMAGE_SECTION_HEADER *LastRvaSection
#);
#
# 参数:
#
# NtHeaders
#
# 指示一个 IMAGE_NT_HEADERS 结构。通过调用 ImageNtHeader 函数可以获得这个结构。
#
# Base
#
# 指定通过调用 MapViewOfFile 函数映射入内存的一个映象的基址(Base Address)。
#
# Rva
#
# 指定相对虚拟地址的位置。
#
# LastRvaSection
#
# 指向一个指定的最终 RVA 块的 IMAGE_SECTION_HEADER 结构。这是一个可选参数。当被
#指定时,它指向一个变量,该变量包含指定映象的最后块值,以便将 RVA 转换为 VA。
就这么简单。你只需要将 PE 文件映射入内存,然后调用这个函数就能够得到输入表的正
确 VA。
注意,下面我会忽略所有的 RVA/VA 注释,但是,当你对重建的 PE 文件进行读出或写入
RVAs 操作时,不要忘记它们之间的转换。
3. 完整说明
===========
这里是一个完整改变输入表的例子(这个 PE 文件的输入表已经被 PETite v2.1 压缩过,
并且是直接从内存中 Dump 出来的):
我们用“`”表示 00,用“-”表示非字符串
0000C1E8h : 00 00 00 00 00 00 00 00 00 00 00 00 BA C2 00 00 ````````````----
0000C1F8h : 38 C2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ----````````````
0000C208h : C5 C2 00 00 44 C2 00 00 00 00 00 00 00 00 00 00 --------````````
0000C218h : 00 00 00 00 D2 C2 00 00 54 C2 00 00 00 00 00 00 ````--------````
0000C228h : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ````````````````
0000C238h : 7F 89 E7 77 4C BC E8 77 00 00 00 00 E6 9F F1 77 --------````----
0000C248h : 1A 38 F1 77 10 40 F1 77 00 00 00 00 4F 1E D8 77 --------````----
0000C258h : 00 00 00 00 00 00 4D 65 73 73 61 67 65 42 6F 78 ``````MessageBox
0000C268h : 41 00 00 00 77 73 70 72 69 6E 74 66 41 00 00 00 A```wsprintfA```
0000C278h : 45 78 69 74 50 72 6F 63 65 73 73 00 00 00 4C 6F ExitProcess```Lo
0000C288h : 61 64 4C 69 62 72 61 72 79 41 00 00 00 00 47 65 adLibraryA````Ge
0000C298h : 74 50 72 6F 63 41 64 64 72 65 73 73 00 00 00 00 tProcAddress````
0000C2A8h : 47 65 74 4F 70 65 6E 46 69 6C 65 4E 61 6D 65 41 GetOpenFileNameA
0000C2B8h : 00 00 55 53 45 52 33 32 2E 64 6C 6C 00 4B 45 52 ``USER32.dll`KER
0000C2C8h : 4E 45 4C 33 32 2E 64 6C 6C 00 63 6F 6D 64 6C 67 NEL32.dll`comdlg
0000C2D8h : 33 32 2E 64 6C 6C 00 00 00 00 00 00 00 00 00 00 32.dll``````````
正如你看到的,这个输入表被分成三个主要部分:
- C1E8h - C237h:IMAGE_IMPORT_DESCRIPTOR 结构部分,对应着每一个需要输入的动态
链接库(DLL)。这部分以关键字 00 结束。
IMAGE_IMPORT_DESCRIPTOR struct
OriginalFirstThunk dd 0 ;原拆分 IAT 的 RVA
TimeDateStamp dd 0 ;没有使用
ForwarderChain dd 0 ;没有使用
Name dd 0 ;DLL 名字符串的 RVA
FirstThunk dd 0 ;IAT 部分的 RVA
IMAGE_IMPORT_DESCRIPTOR ends
- C238h - C25Bh:这部分双字(DWord) 称作“IAT”,由 IMAGE_IMPORT_DESCRIPTOR
结构中的 FirstThunk 部分指明。这部分每一个 DWord 对应一个输入函数。
- C25Ch - C2DDh : 这里是输入函数和 DLL 文件的名称。问题是,这些是没有规定顺序
的:有时候 DLL 文件在函数前面,有时候正好相反,另外一些时候它们混在一起。
输入表的简介
------------
OriginalFirstThunk 是 IAT 的一部分,它是 PE 文件引导时首先要搜索的。如果存在,PE
文件的引导部分将使用它来纠正在 FirstThunk IAT 部分的问题。当调入内存后,FirstThunk
的每一个 Dword (包含有函数名字符串的 RVA),将被 RVA 替换为函数的真实地址(当调用这
些函数时,它们调入内存的位置将被执行)。所以,只要 OriginalFirstThunk 没有被改变,基
本上这里不存在输入表的问题。
下面来看我们的问题
------------------
好了,经过简单描述后,下面来看我们的问题。如果你试图运行包含上面显示的输入表的可
执行文件,它不会被调入,Windows 会显示一个错误信息。为什么?很简单,因为
OriginalFirstThunk 被删除了。事实上,你应该注意到,在这个输入表的每一个IMAGE_IMPORT_DESCRIPTOR 结构,OriginalFirstThunk 的内容都是 00000000h。嗯,所以我们
可以推测出,当我们运行这个可执行程序时,PE 文件的引导部分试图从 FirstThunk 部分获得
输入函数的名字。但是,正象你注意到的,这部分根本没有包含函数名字符串的 RVA,但是函数
地址的 RVA 在内存中。
我们需要怎么做
--------------
现在,为了让这个可执行文件运行,我们需要重建 FirstThunk 部分的内容,让它们指向我
们在输入表第三部分看到的函数名字符串。这不是一项很困难的任务,但是,我们需要知道哪个
IAT 对应哪个函数,而函数字符串和 FirstThunk 内容并不采用同样的存储方法。所以,对于每
一个 IAT,我们需要验证它对应的是哪个函数名(事实上,根据 IMAGE_IMPORT_
DESCRIPTOR.Name DWord 我们已经有了 DLL 名称,这些并没有被改变)。
如何验证每一个函数
------------------
正向我们上面所见到的,在内存中,每一个被破坏的 IAT 都有一个函数地址的 RVA。这些
地址并没有被破坏,所以,我们只要重新找回指向错误 IAT 的函数地址,把它们指向函数名字
符串。
为此,在 Kernel32.dll 中有一个非常有用的 API:GetProcAddress。它允许你得到给定函
数的地址。这里是它的描述:
GetProcAddress(
HMODULE hModule, // DLL 模块的句柄
LPCSTR lpProcName // 函数名
);
所以,对于每一个被破坏的 IAT,在 GetProcAddress 返回我们寻找的函数地址之前,只需
要分析包含在输入表第三部分的所有函数名。
- hModule 参数是 DLL 模块的句柄(也就是说,模块映象在内存中的基址),我们可以通
过 GetModuleHandleA API 得到:
HMODULE GetModuleHandle(
LPCTSTR lpModuleName // 返回模块名地址句柄
);
(lpModuleName 只需要指向我们从 IMAGE_IMPORT_DESCRIPTOR.Name 部分得到的 DLL 文件
名字符串)
- lpProcName 仅指向函数名字符串。
注意,有时候函数是按序号输入的。这些序号是在每个 [ 函数名偏移量-2 ] 处的单字(WORDS)。
所以,你在分析程序时需要检查函数是按名称还是按序号输入的。
使用上面输入表的实例
--------------------
针对上面输入表的例子,我将说明如何修复第一个输入 DLL 的第一个输入函数。
1. 我们来看第一个 IMAGE_IMPORT_DESCRIPTOR 结构部分(C1E8h),.Name 部分(C1E4h,指向
C1BAh)指出了 DLL 名。我们看到,那是 USER32.dll。
2. 我们来看 .FirstThunk 部分,它们指向 IAT 部分;每个对应一个这个 DLL(user32.dll)的
输入函数。在这里是 C1F8h,指向 C238h。所以,在 C238h,我们可以修复被破坏的 IATs。(你
会注意到,这个 IAT 部分包含二个 DWords,所以,这个 DLL 有二个函数输入)
3. 我们得到了第一个被破坏的 IAT。它的值是 77E7897Fh。这是函数在内存中的地址。
4. 对每一个输入表第三部分中的函数,我们调用 GetProcAddress API。当该 API 返回 7E7897Fh
时就意味着,我们到达了正确的函数。所以我们让被破坏的 IAT 指向正确函数名(在本例中为 'wsprintfA')。
5. 现在我们只需要将 IAT 指向:偏移量(函数名字符串)-2。为什么是 -2 ?因为有时候使用了
函数序列。
所以在本例中,我们改变地址 C238h,让它指向 C26Ah(以代替 77E7897Fh)。
6. 就这样,这个函数被修复了,下面你只需要对所有的 IATs 重复这个过程就可以了。
后记
----
我描述的是一般的操作过程。当然只有在 DLLs 被正常调入内存后才能够这样做。对于其他情
况,你需要将它们调入,或者你需要仔细研究它们的输出表才能找到正确的函数地址。
3、IceDump和NticeDump使用
IceDump和NticeDump是一款配合SoftICE扩展其内存操作的工具,IceDump支持Windows 9x、Windows Millennium系统,NticeDump支持Windows NT/2000。它们的出现,使SoftICE如虎添翼,TRW2000的许多特色功能在SoftICE里也可实现了。
1.Icedump操作简介
运行IceDump前,首先要确定SoftICE版本号,按Ctrl+D切换到SoftICE下命令:VER,查看版本号。然后在相应SoftICE版本号目录下运行icedump.exe文件,它会调用自身的VXD文件,装载成功出现图7.16所示画面。如果发现SoftICE没运行或版本不符,就拒绝运行。 如果想从内存中卸载它,可以在DOS下键入"icedump u"。
1)/DUMP <起始地址> [<长度> <文件名>]
抓取内存中的数据到文件里,类似TRW2000中的W命令。<文件名>参数可以指定盘符和路径,当在Ring-0下还原时最好清除还原区域内的全部断点,否则会给SoftICE带来不必要麻烦。(这一点在所有的IceDump命令里都应该值得注意)
在Win32系统下读者可能会想到用/BHRAMA或/PEDUMP从内存内中重建一个可用的PE镜像。请看下面关于/OPTION命令的说明。
注意: IceDump 6.015以前类似的命令是PAGEIN D <address> [<length> <filename>]
2)/LOAD <地址> <长度> <文件名>
把<文件>指定长度的字节内容调入到内存中的<地址>处。与/DUMP的作用相反,同样需要注意的是不要设置断点。
3)/BHRAMA <Bhrama dumper server 窗口名>
用Procdump的Bhrama(由G-Rom出品的著名脱壳工具)来初始化dumping。用户必须提供窗口的名称,可以从标题条找到它。为了使工作简单化,可以在winice.dat里设置F3键:
F3="/BHRAMA ProcDump32 - Dumper Server;"
4) /TRACEX <low EIP> [<high EIP>]
控制跟踪器并退出SoftICE。注意该命令只能用于跟踪当前线程,如果要跟踪其它线程,请使用/TRACE命令。
/TRACEX <low EIP>: 跟踪当前线程。注意,如果跟踪当前线程弹出SoftICE窗口后想继续跟踪,必须使用/TRACEX命令,否则跟踪器会失去对当前线程的控制。
当线程的EIP到达<low EIP>时,跟踪停止并弹出SoftICE窗口。这也要求EIP真正可以到达,否则SoftICE不会弹出。
/TRACEX <low EIP> <high EIP> 跟踪当前线程,注意事项同上。
当线程的EIP到达<low EIP>与<high EIP>之间的区域内时停止并弹出SoftICE窗口。注意这里没有作<low EIP>和<high EIP>的边界检查,所以错误的参数地址会使SoftICE不能中断。
5) /SCREENDUMP [<文件名>]
把SoftICE屏幕内容保存到一个文件中。注意该功能只支持通用显示驱动模式。这个命令的用法类似于/DUMP,如果没有指定<文件名>,IceDump将在模式0、1、2、3和4中切换。
模式1:默认模式,将以ASCII格式输出。
模式0:字节属性也将被抓取。
模式2:可以把屏幕内容保存成一个HTML文件。
模式3:会把屏幕内容保存成LaTeX格式的文件。
模式4 :把屏幕内容保存为EPS (encapsulated Postscript)格式。
2.NticeDump操作简介
Nticedump远不如IceDump功能强大,并且Nticedump装载方式不同于IceDump,它是通过给SoftICE打补丁来实现0特权级控制权的,这是因为在Windows 2000上,要切换到0特权级不象Windows9x那么容易了。
要打补丁的文件是\WINNT\SYSTEM32\DRIVERS\Ntice.sys,在Nticedump目录里有一补丁工具ntid.exe,把安装目录下相应SoftICE版本的Icedump文件与ntid.exe一同复制到\WINNT\SYSTEM32\DRIVERS\目录下,然后运行ntid.exe程序就能正确补丁Ntice.sys。这样Nticedump和SoftICE就完全结合了。
1) 抓取内存数据:PAGEIN D 基地址 长度 文件名
例: PAGEIN D 400000 512 \??\C:\memory.dmp
注意: 在NT输入输出管理系统中,象"C:\memory.dmp"不是合法路径。"\??\C:\filename.dmp"是在C盘根目录下创建"filename.dmp"文件。
2) 抓取进程: PAGEIN B <Bhrama窗口名>
例: PAGEIN B ProcDump32 - Dumper Server
3) 导入文件: PAGEIN L 基地址 长度 文件名
Example: PAGEIN L 400000 512 \??\C:\memory.dmp
4) 帮助: PAGEIN 例: PAGEIN
4.1、Import REConstructor使用
Import REConstructor可以从杂乱的IAT中重建一个新的Import表(例如加壳软件等),它可以重建Import表的描述符、IAT和所有的ASCII函数名。用它配合手动脱壳,可以脱UPX、CDilla1、PECompact、PKLite32、Shrinker、ASPack, ASProtect等壳。该工具位于:光盘\tools\PE tools\Rebuilders\Import REConstructor。
在运行Import REConstructor之前,必须满足如下条件:
1) 目标文件己完全被Dump到另一文件;
2) 目标文件必须正在运行中;
3) 事先要找到真正的入口点(OEP);
4) 最好加载IceDump,这样建立的输入表较少存在跨平台的问题。
步骤如下:
(1)找被脱壳的入口点(OEP);
(2)完全Dump目标文件;
(3)运行Import REConstructor和需要脱壳的应用程序;
(4)在Import REConstructor下拉列表框中选择应用程序进程;
(5)在左下角填上应用程序的真正入口点偏移(OEP);
(6)按"IAT AutoSearch"按钮,让其自动检测IAT位置, 出现"Found address which may be in the Original IAT.Try 'Get Import'"对话框,这表示输入的OEP发挥作用了。
(7)按"Get Import"按钮,让其分析IAT结构得到基本信息;
(8)如发现某个DLL显示"valid :NO" ,按"Show Invalids"按钮将分析所有的无效信息,在Imported Function Found栏中点击鼠标右键,选择"Trace Level1 (Disasm)",再按"Show Invalids"按钮。如果成功,可以看到所有的DLL都为"valid:YES"字样;
(9)再次刷新"Show Invalids"按钮查看结果,如仍有无效的地址,继续手动用右键的Level 2或3修复;
(10)如还是出错,可以利用"Invalidate function(s)"、"Delete thunk(s)"、编辑Import表(双击函数)等功能手动修复。
(11)开始修复已脱壳的程序。选择Add new section (缺省是选上的) 来为Dump出来的文件加一个Section(虽然文件比较大,但避免了许多不必要的麻烦) 。
(12)按"Fix Dump"按钮,并选择刚在(2)步Dump出来的文件,在此不必要备份。如修复的文件名是"Dump.exe",它将创建一个"Dump_.exe",此外OEP也被修正。
(13)生成的文件可以跨平台运行。
4.2、ReVirgin 使用指南 (作者:blowfish)
ReVirgin(简称RV)
功能: 重建Import Table;寻找OEP
下载地址: http://www.woodmann.com/fravia/index.htm
具体地址是:http://www.woodmann.com/fravia/exe/revirgin.zip
一、安装。
1、自动安装:
直接双击*.msi就会自动激活Windows Installer进行安装,这是最省事的办法。如果你的系统太老,可能需要到微软主页上免费下载Windows Installer的包。
2、手动安装:
可以用WinRAR将*.msi解开到某个目录中。然后把tracer.dll、thread.dll拷贝到%SystemRoot%目录下;对于NT/2K/XP系统,把rvtracer.sys拷贝到%SystemRoot%\system32\drivers目录中。
二、重建IT
1、首先选择被加壳的程序所对应的进程。如果找不到则点击refresh按钮。
2、再找被加壳了的程序的OEP。找OEP的方法主要有:用IceDump的/tracex;用冲击波;手动用debugger跟;利用各种编译器生成的可执行程序的startup code的机器码的pattern来找;利用RV自带的tracer来找。
3、找到OEP之后将其填入RV的相应位置(注意要填VA而不是RVA),然后点击“Fetch IAT”,RV会自动分析出IAT的起始RVA和Length。如果你觉得RV找到的不对,你也可以手动找到这个RVA和Length并填入该位置。
4、然后点击“IAT Resolver”按钮,RV会自动分析,可能要等好几分钟。这时CPU占用率很高,最好不要切换到其它程序。
5、分析完之后会看到一些API函数标记为“redirected/emulated”,此时点击“Resolve again”,大部分函数都可以resolve出来。
6、然后在下拉列表中选择“Show unresolved”,因为此时只关心尚未分析出来的API函数。
在函数列表框中选中一个或多个函数(按住shift可以选多个),然后打开右键菜单。对于每个未分析出来的API函数,你可以试试右键菜单中的“tracer”或“API Emulator”,如果RV能够分析出来,则相应的行会变成“traced”或“emulated”状态,并且Address这一列会指向DLL的地址范围。
一旦变成“traced”或“emulated”状态,则你可以再次点击“resolve again”按钮,这些API函数将会被分析出来。
注:
a、未分析出来的函数很多时,最好不要用右键菜单中的“Trace all”,后果你试试就知道了;
b、右键菜单中的edit是个开关,表示列表中的每列是否可以编辑,此时可以手动输入API函数的相关信息(但这里似乎有bug,因为即使你手动把未分析出来的函数都手动填入了,RV仍说没有全部分析完)。
c、右键菜单中的Tracer功能可能会导致被跟踪的程序出现非法操作,所以最好是随时使用“Save resolved”按钮的功能把阶段性结果存为文本文件,一旦出现非法操作还可以用“Load resolved”功能把以前的结果加载进来继续干活。
7、对所有的API函数重复5、6两步,大部分都可以分析出来。剩下的那些基本上要用debugger来手动分析了。
8、所有的API函数都分析出来之后,就可以生成IT信息,并粘贴到脱壳后的文件中。首先你得确定把生成的IT放在程序的什么位置,一般是放在末尾(此时要添加一个section),但实际上可以放在任何合理的位置。
把存放IT的位置的RVA填入,并点击“generate”按钮。如果选中了“Auto fix sections + IT Paste”,则RV会问你脱壳后的exe文件名并自动把IT粘贴进去(但目前的版本没有帮你修改程序的OEP,你得手动修改粘贴好的程序的OEP),并生成一个BIN文件,这个BIN文件是由IT、IAT、DLL名和函数名组成的,供你手动粘贴用(如果你喜欢手动粘贴的话)。
至此重建IT完毕。
三、其它说明:
1、RV的mangled scheme选项是用来对付那种将多个API重定向到同一个函数的壳的,一般用不上。
2、RV的底部的“Tracer”按钮是用来跟踪程序找OEP的,功能也很强。 只需要指定OEP可能存在于什么范围(给出最小值、最大值),当被跟踪的程序的EIP落在此区间时RV就会停下来。这和利用IceDump的/tracex来找OEP有些相似。
3、API函数列表中的Refs这一列是该函数的引用计数。
4、最好是在OEP处将被加壳的进程suspend,然后用RV。因为某些被ASProtect加壳的程序在进入OEP之后会修改IAT的某些项。