PE文件格式分析及修改(续)

2.3 IMAGE_SECTION_HEADER

PE文件头后是节表,在winnt.h下如下定义 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text” //IMAGE_SIZEOF_SHORT_NAME=8 union { DWORD PhysicalAddress;//物理地址 DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个, //一般是节的数据大小 } Misc; DWORD VirtualAddress;//RVA DWORD SizeOfRawData;//物理长度 DWORD PointerToRawData;//节基于文件的偏移量 DWORD PointerToRelocations;//重定位的偏移 DWORD PointerToLinenumbers;//行号表的偏移 WORD NumberOfRelocations;//重定位项数目 WORD NumberOfLinenumbers;//行号表的数目 DWORD Characteristics;//节属性 如可读,可写,可执行等 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

图8 Load PE 读取的 IMAGE_SECTION_HEADER 信息

 

Name:节名称  VOffset:相对于ImageBase的虚拟偏移 VSize:实际大小 ROffset:相对于文件起始处的偏移 RSize所占的文件空间大小 Flags:节属性

图9 Load PE 读取的.text节的属性

重要的节属性定义: #define IMAGE_SCN_CNT_CODE 00000020h // 节中包含代码 #define IMAGE_SCN_CNT_INITIALIZED_DATA 00000040h // 节中包含已初始化数据 #define IMAGE_SCN_CNT_UNINITIALIZED_DATA 00000080h // 节中包含未初始化数据 #define IMAGE_SCN_MEM_DISCARDABLE 02000000h // 是一个可丢弃的节,即节中的数据在进程开始后将被丢弃 #define IMAGE_SCN_MEM_NOT_CACHED 04000000h // 节中数据不经过缓存 #define IMAGE_SCN_MEM_NOT_PAGED 08000000h // 节中数据不被交换出内存 #define IMAGE_SCN_MEM_SHARED 10000000h // 节中数据可共享 #define IMAGE_SCN_MEM_EXECUTE 20000000h // 可执行节 #define IMAGE_SCN_MEM_READ 40000000h // 可读节 #define IMAGE_SCN_MEM_WRITE 80000000h // 可写节

[注]

RVA:虚拟偏移地址。RAV是指的某一处由Loader装入内存后,这一处应该在虚拟内存的什么地方,RAV也称为虚拟偏移地址。

Alignment:对齐因子。与对齐因子相关的值有2个地方,一处是文件对齐因子,另一处是内存对齐因子。对齐因子指示出某一类型的对齐方式,以文件对齐为例,如果Alignment 为200h,说明文件中的内容是以200h为单位的,如果数据大小正好是200h的整数倍,则不存在对齐问题,如果数据大小是非200h的整数倍,则要使用Alignment 对数据所占的空间进行修正,取其上限数值(如310h->400h),使其所占的空间是200h的整数倍。

2.3 Improt Table 和IAT

IMAGE_DATA_DIRECTORY的第2项和第13项,指示导入表和导入函数地址表的位置。这部分对于一个PE文件相当重要,很多系统函数都是由此导入。

Import Table 的VirtualAddress指向了一个RVA,他是一个导入表结构数组,数组以全0作为结束标记,该结构定义如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk;// 指向一个 IMAGE_THUNK_DATA 结构数组的RVA } DWORD TimeDateStamp;// 文件生成的时间 DWORD ForwarderChain;// 这个数据一般为0,可以不关心 DWORD Name1;  // RVA,指向DLL名字的指针,ASCII字符串 DWORD FirstThunk; //指向一个 IMAGE_THUNK_DATA 结构数组的RVA,这个数据与IAT所指向的地址一致 }IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR IMAGE_THUNK_DATA 这是一个DWORD类型的集合。

通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针,其定义如下: IMAGE_THUNK_DATA{ union { PBYTE ForwarderString; PDWORD Function; DWORD Ordinal;//判定当前结构数据是不是以序号为输出的

如果是的话该值为0x800000000,此时PIMAGE_IMPORT_BY_NAME不可做为名称使用 PIMAGE_IMPORT_BY_NAME AddressOfData; }u1; } IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;

typedef struct _IMAGE_IMPORT_BY_NAME{ WORD Hint;// 函数输出序号 BYTE Name1[1];//输出函数名称 } IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME

3.IMP和IAT的关系

静态分析时,OriginalFirstThunk与FirstThunk指向的数据是同一组IMAGE_IMPROT_BY_NAME。

OriginalFirstThunk

 

IMAGE_IMPORT_BY_NAME

 

FirstThunk

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

...

IMAGE_THUNK_DATA

--->

--->

--->

--->

--->

--->

Function 1

Function 2

Function 3

Function 4

...

Function n

<---

<---

<---

<---

<---

<---

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

...

IMAGE_THUNK_DATA

Loader 在装入一个可执行的代码时,会分析该文件的导入表(IMP),然后通过导入表的指引,修改IAT指向的数据,这们在装载完成后,数据会变成如下形式

OriginalFirstThunk

 

IMAGE_IMPORT_BY_NAME

 

FirstThunk

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

IMAGE_THUNK_DATA

...

IMAGE_THUNK_DATA

--->

--->

--->

--->

--->

--->

Function 1

Function 2

Function 3

Function 4

...

Function n

 

Address of Function 1

Address of Function 2

Address of Function 3

Address of Function 4

...

Address of Function n

可以看出,OriginalFirstThunk与FirstThunk所指向的数据分离。FirstThunk指向的不再是IMAGE_IMPORT_BY_NAME,而是指向了函数的真实地址。代码段对于一个外部函数的引用始终使用的是FirstThunk处的RVA。当程序真正执行时,就可以跳到真正的函数入口了

四、修改PE文件

PE文件是由源代码经由编译器编译、链接后形成的可执行文件,由系统加载执行。通过对PE文件的分析,给理论上修改PE文件提供了可能。下面我们分几个步骤修改PE文件。这些步骤不是修改PE的必须步骤,但从中我们可以讨论如何修改PE文件。

1.给PE文件增加一个新节

PE文件节的信息保存在文件头的最后部分,如果预留磁盘空间足够大(大于或等于了个节的结构数据大小),我们就可以为其增加一个新节。

由IMAGE_SECTION_HEADER的结构定义可知,IMAGE_SECTION_HEADER的大小为10个DWORD类型数据的大小,也就是40(28h)个字节,我们观察要修改的目标

图10 PE文件的节部分数据

再由图8的数据我们得知,该PE文件的第一个节的数据的文件偏移地址为400h,而最后一节.rsrc,的末尾偏移是中28fh,400h-28fh=171h>28h,可以增加新的节标志。增加新的节标志数据,首先要修改IMAGE_FILE_HEADER结构的NumberOfSections数值(4->5),然后在290h处开始按IMAGE_SECTION_HEADER结构填入相应的数值。这里我们使用Load PE添加新的数据

图11 添加了新节的信息

由于新的节没有实际数据,所以其VSize和RSize大小为0,新节的属性与.text代码段相同,添加新节后,这里只是添加了节信息,还要补充节数据,补充数据我们使用UE进行复制粘贴就可,这一步涉及了两处Alignment,要注意使用,我们先补充100h字节,但由于文件对齐因子,所在至少在文件末尾处添加200h个空白数据,最后修改节的信息和IMAGE_OPTIONAL_HEADER的SizeOfImage(003f000h ->0040000h),使得我们添加的数据也可以由Loader加载到内存中。添加数据完成后,先执行程序,确定PE头信息的正确。

图12修正后的文件头部信息

图13 修改后的程序正常执行的界面

2. 在新节中添加代码

在新节中添加代码是本文修改PE文件的关键,我们的目的不仅仅是添加数据,而是添加可执行的代码,通过添加代码研究PE文件的可感染性。由于高级编译器都会将数据段和代码段分开来编译,所以我们添加的代码将会因为找不到数据而使程序崩溃,因此我们要将我们需要的数据和代码放在同一个Section 内,方便编程

示例汇编代码:

pushad     ;以下两行为保存当前程序上下文 pushfd mov esi, ImageBase+SectionRVA+messbody   ;提示信息体 mov edi, ImageBase+ SectionRVA +messtitle  ;提示信息标题 push 0     ;MB_OK的类型的消息框 push esi    ;参数入栈 push edi    ;参数入栈 push 0     ;hWND call MessageBoxA   popfd     ;恢复程序上下文 popad mov eax,ImageBase+oldoep;原始的入口地址送到eax jmp eax     ;跳到原始入口处 messbody db    ‘New Section Add This File’,0 messtitle db    ‘I Sucess’,0

在这一段代码中我们的是程序首先执行植入代码,完成特定功能,在执行完毕后,跳到原代码入口处继续执行(要注意保存初始环境),此段代码的主要目的就是给用户一个提示,表示我们成功的感染了这个程序。这段代码中涉及的数据有消息框的标题和内容,我们都要在此段中进行定义。我们可以通过NASM编译这段代码为纯二进制代码。(关于NASM编译器,可以通过网络查找其编译程序和文档)。

为使编译通过,我们首先确定MessageBoxA的地址和oldoep的地址

图14 Load PE 文件的人Import Table

通过查阅MSDN,我们知道MessageBoxA的函数由USER32.dll导出,而应用程序使用的这个信息就在导入表中。通过Load PE 查看文件的Import Table,我们找到USER32.dll 和MessageBox的地址在0002743Ch,要执行0002743Ch处的索引函数,就在在其前面加上ImageBase的值。也就在此代码在0042743Ch处,实际调用参考为 Call dword [0042743ch]。Oldoep由IMAGE_OPTIONAL_HEADER的AddressOfEntryPoint得到
(0000D1B5h+00400000h=0040d1b5h)

图15 通过NASM编译后的代码

将这段代码复制到修改后的.exe 的38c00h处,然后保存。如下图:

图16 修改后的新节的数据

运行程序:

图17 节的程序可以照常执行

程序首先弹出对话框,单击确定后看到了程序的初始界面

3. 其它植入方法

启动OllDbg(Ring3)调试器,调试上面刚处理过的程序,发现调试器会给出一个警告,如下图 

图18 OllDbg的警告信息

通过实验得出,之所以OllDbg 会发出如此的警告,是因为该文件的PE信息中AddressOfEntryPoint超出了Code Section(.text)段所记录的地址(0000000h~00270000h)。将.text判定为代码段,由PE头的BaseOfCode得出。如果将此段代码植入.text 段,那么将不会出现此提示。

PE文件能否正常加载执行,与磁盘文件结构密切相关,但一旦将磁盘文件映射为内存镜像后,就与磁盘文件脱离了关系。所以磁盘仅仅是一个规范的数据结构。

通过实验,我们可以得出这样的结论:对于一个小的代码段(其二进制代码长度小于代码段下一节的RVA-(BaseOfCode+代码段的VirtualSize)),植入是成功的。

那么我们通过分析磁盘文件和内存映射的关系,就可以修改代码段,将代码植入到代码段。在植入代码段后,要对PE文件的磁盘数据和进行一次定位修复,就可以完成代码的整体植入。

代码段一般是PE文件的第一个Section,如果此段变长,就要将其后续段的RVA和磁盘偏移地址都要进行修正。修正完成后,仅仅是保证了PE文件的磁盘格式正确,接下来主要就是修改导入表数据,资源表数据。最后要参照原PE文件将新PE文件对数据段的数据的引用进行修正。

至此,我们完成了一次对PE文件新代码的引入问题的研究。但对于一个复杂的PE文件,修改还远不如此。还要处理输出表、TLS表及其它数据的表的内容。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值