本章教程中,使用的工具是上次制作的PE结构解析器,如果还不会使用请先看前一篇文章中对该工具的介绍,本章节内容主要复习导入表结构的基础知识点,并通过前面编写的一些小案例,实现对内存的转储与导入表的脱壳修复等。
关于Dump内存原理,我们可以使用调试API启动调试事件,然后再程序的OEP位置写入CC断点让其暂停在OEP位置,此时程序已经在内存解码,同时也可以获取到程序的OEP位置,转储就是将程序原封不动的读取出来并放入临时空间中,然后对空间中的节表和OEP以及内存对齐进行修正,最后将此文件在内存保存出来即可。
脱壳修复:输入表一般分为IAT与INT,由于加壳后程序可能会加密或者破坏IAT结构,导致脱壳后IAT不一致了,脱壳修复就是使用未脱壳的源程序的输入表覆盖到新程序中,就这麽简单。
解析 IMAGE_IMPORT_DESCRIPTOR
数据目录表第二个成员指向输入表,该指针在PE开头位置向下偏移80H处,PE开始位置就是B0H , B0H+80H= 130H处。
此处存放着一个指针,00002040
即输入表在内存中的偏移量为 2040
,使用前面制作的工具可以快速定位到此处。
2040是一个RVA,需要将其转换为磁盘文件FOA偏移才能定位到输入表在文件中的位置,使用工具快速完成计算任务,转换为文件偏移为 00000640
也可以这样来找到640的位置,首先2040位于rdata,rdata的虚拟偏移是2000h,而实际偏移是600h 使用 2000h - 600h = 1a00h
将相对偏移地址2040转为文件偏移,使用2040-1a00
同样可得出640h
用winhex打开后跳转过去看看。
下面将重点解析一下这几个结构的含义。
如上就是导入表中的IID数组,每个IID结构包含一个装入DLL的描述信息,现在有两个DLL,第三个是一个全部填充为0的结构,标志着IID数组的结束。
结构定义如下。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
以第一个字段为例:
0000 208C => OrignalFirstThunk => 指向输入名称表INT的RVA 0000 0000 => TimeDateStamp => 指向一个32位时间戳,默认此处为0 0000 0000 => ForwardChain => 转向API索引,默认为0 0000 2174 => Name => 指向DLL名字的指针 0000 2010 => FirstThunk => 指向输入地址表IAT的RVA
每个IID结构的第四个字段指向的是DLL名称的地址,以第一个为例,其RVA是0000 2174
将其减去1a00
得到文件偏移774
,跳转过去看看,调用的是USER32.dll
库。
使用工具同样可以快速转换出来。
上方的两个字段OrignalFirstThunk
和FirstThunk
都可以指向导入结构,在实际装入中,当程序中的OrignalFirstThunk值为0时,则就要看FirstThunk里面的数据了,FirstThunk常被叫做IAT他是在程序初始化时被动态填充的,而OrignalFirstThunk常被叫做INT,他是不可改变的,之所以会保留两份是因为,有些时候会存在反查的需求,保留两份是为了更方便的实现。
<br>
解析 IMAGE_THUNK_DATA32
如上,我们找到了User32.dll的OrignalFirstThunk
,其地址为208C
,使用该值减去1A00h
得到 68Ch
,在偏移为68Ch
处保存的就是一个IMAGE_THUNK_DATA32
数组,他存储的内容就是指向 IMAGE_IMPORT_BY_NAME
结构的地址,最后一个元素以一串0000 0000
作为结束标志,先来看一下IMAGE_THUNK_DATA32
的定义规范。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
直接使用WinHex定位到68Ch处,此处就是OrignalFirstThunk中保存的INT的内容,如下图中,出去最后一个00000000以外,一共有11个四字节,则说明User32.dll中导入了11个API函数。
再来看一下FirstThunk也就是IAT中的内容,由于User32的FirstThunk字段默认值是2010h,使用该值减去1a00h即可得到610h,此处就是IAT的内容,定位过去看看,完全一致的。
我们以第一个导入RVA地址
00002110h
,用该值减去1a00h
得到710h
,定位过去正好是LoadIconA
的字符串。 接着来看第二个导入RVA地址0000211ch
,用该值减去1a00h
得到71c0
定位过去正好是PostQuitMessage
的字符串。
如上图,以第二个为例,前两个字节表示的是Hint值,后面的蓝色部分则是PostQuitMessage
字符串,最后的0标志结束标志。
当程序被运行前,它的FirstThunk值与OrignalFirstThunk字段都指向同一片INT中,如下使用上次编写的MyDump工具对其内存进行dump转储,观察内存变化。
观察发现 OrignalFirstThunk字段并不会发生变化,但是FirstThunk值的指向已经改变,系统在装入内存时会自动将FirstThunk指向的偏移转化为一个个真正的函数地址,并回写到原始空间中,定位到输入表RVA地址处2040h查看。
可以发现,黄色的INT并没有变化,但是绿色的IAT则相应的发生了变化,以第一个0x766bd680
则是载入内存后LoadIconA
的内存地址,我们使用X64DBG跟过去看看,没错吧!
当系统装入内存后,其实只会用到IAT中的地址解析,输入表中的INT啥的就已经不需要了,此地址每个系统之间都会不同,该地址是操作系统动态计算后填入的,这也是为什么会存在导入表这个东西的原因,就是为了解决不同系统间的互通问题的。
有时我们在拖壳时,由于IAT发生了变化,所以程序会无法被正常启动,我们Dump出来的文件可能收入表已经被破坏了,导入表不一致,我们可以使用原始的未脱壳的导入表地址对脱壳后的导入表地址进行覆盖,来修复文件,使用修复工具修复即可。
例如dump前导入表是这样的。
dump 后变成了这样。
由于导入表错误导致dump文件无法正常运行,这是需要使用修复工具来对导入表进行修正。
修正后文件就可以正常被打开了,我们来看一下dump后的文件导入表。
是不是很清晰了,就是将原来的导入函数的RVA拷贝过来,就这麽简单。
<br>
工具学习篇
lyshark.exe 是一个加过UPX壳的程序,现在演示如何流程化脱壳处理。
先查节表,发现UPX
定位到数据目录表中第二个字段,也就是输入表的存储位置,直接使用工具计算出foa地址。
加过壳就是这样 442cc
将内存文件转储出来,保存到dump.exe
跳过去看看,空的
尝试打开文件,出现错误。
使用buid工具修正即可。脱壳后文件体积变大了
不过我自己捣鼓的这些脱壳工具只是用来学习的,很多壳还得借助专业的脱壳工具进行修复,我这个修不了。
既然到这份上了,来演示一下专业脱壳,先查壳,upx
压缩壳,使用ESP定律脱掉。F8一次,ESP右击内存窗口转到
断点设置硬件访问断点,四字节,选择,让程序跑起。
然后运行到jmp 即可到达OEP
获取OEP删除无效函数,直接dump转储文件。
文件转储打不开
使用工具修复buitIAT即可。
脱壳完成,程序可运行起来。