PE
PE简介
可执行文件(executable file)指的是可以由操作系统进行加载执行的文件。
大致有两种可执行文件的格式:
- PE 文件格式(Windows 平台);
- ELF 文件格式(Linux 平台)。
其中常见的PE文件格式的可执行文件有:exe, sys, dll 等。
PE文件格式与win32汇编的关系
由于EXE文件被执行、传播的可能性最大,因此Win32 病毒感染文件时,基本上都是将EXE 文件作为目标。
通常而言,Win32 病毒的运行流程为:
- 用户点击或系统自动运行 HOST 程序;
- 装载HOST 程序到内存;
- 通过 PE 文件中的 AddressOfEntryPoint 和 ImageBase 之和来定位第一条语句的位置;
- 从第一条语句开始执行(
病毒代码可能在此时或HOST代码运行过程中获得控制权
); - 病毒主体代码执行完毕,将控制权交还给 HOST 程序;
- HOST 程序继续运行。
相关概念
-
虚拟内存地址(VA)PE 文件中的指令被装入内存后的地址。
-
相对虚拟内存地址(RVA)相对于可执行文件映射到内存的基地址的偏移量。
-
装载基址(ImageBase)PE 装入内存时的基地址。默认情况下,EXE 文件在内存中的基地址为 0x0040 0000, DLL 文件为 0x1000 0000.
$$VA=ImageBase+RVA$$
文件结构
-
MZ 文件头: 前两个字节 4D5A 表示是PE文件格式;
-
DOS 插桩程序: 向下兼容部分,C代码结构体如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header 指出PE头的偏移地址 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
-
NT 头
该部分结构对应winnt.h中的_IMAGE_NT_HEADERS结构体
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64,*PIMAGE_NT_HEADERS64; typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32; typedef struct _IMAGE_ROM_HEADERS { IMAGE_FILE_HEADER FileHeader; IMAGE_ROM_OPTIONAL_HEADER OptionalHeader; } IMAGE_ROM_HEADERS,*PIMAGE_ROM_HEADERS;
-
Signature: 即
PE\0\0
,它标志着NT映像头的开始,也是PE文件中与Windows有关内容的开始. -
Fileheader: 映像文件头即影响头的主要部分,它包含PE文件最基本的信息,它的结构定义如下:
typedef struct _IMAGE_FILE_HEADER { 2 WORD Machine; 机器类型 2 WORD NumberOfSections; 文件中节的个数 4 DWORD TimeDateStamp; 生成文件的时间 4 DWORD PointerToSymbolTable; COFF符号表的偏移 4 DWORD NumberOfSymbols; 符号数目 2 WORD SizeOfOptionalHeader; 可选头的大小 2 WORD Characteristics; 标记 } IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;
-
OptionalHeader: 可选的结构,C语言结构体定义为:
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;
Datadirectory[16]:数据目录表,指向输入表、输出表、资源块等数据,他的成员如下表所示。
PE文件在定位输入表、输出表和资源等重要数据时就是从
IMAGE_DATA_DIRECTORY
结构开始的。若表示该项的8个字节均为0,则表示不存在该表。
-
-
节表(section table): 结构数组,其中每个结构包含一个节的具体信息,每个结构占用
28H
字节节表是PE文件后续节的描述,Windows根据节表的描述加载每个节。PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中IMAGE_SECTION_HEADER结构数量等于节的数量加一。
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
块表主要用来表示当前文件一共分为几个部分,和后面的块相对应.
块表决定了后面的块,每一块从哪里开始,里面存储的数据是什么等等.
域 名 含义 VirtualSize 该节的实际字节数,这是区块的数据在没有进行对齐处理前的实际大小。 VirtualAddress 该区块装载到内存中的RVA地址。这个地址是按照内存页来对齐的,因此它的数值总是SectionAlignment的值的整数倍。 SizeOfRawData 该区块在磁盘中所占的大小,这个数值等于VirtualSize字段的值按照FileAlignment的值对齐以后的大小。 PointerToRawData 该节在磁盘文件中的偏移,程序经编译或汇编后生成原始数据,这个字段用于给出原始数据在文件中的偏移。 PointerToRelocations PE 文件在调入内存后该节的存放位置 。(在EXE文件中无意义) Characteristics 节的属性,该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。这个字段的值是将多个标志值求或。 代码节的属性一般为
60000020H
,也就是可执行、可读;数据节的属性一般为
C0000040H
,即可读、可写; -
节
每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义。
通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。例如有一个区块叫.rdata,表明他是一个只读区块。注意:区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。
另外,使用区块名字只是人们为了认识和编程的方便,而对操作系统来说这些是无关紧要的。微软给这些区块取了个有特色的名字,但这不是必须的。当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位。
这里给出常见的区块:
可以使用 010editor中的模板来查看PE格式的文件
区块的对齐
区块的大小是需要对齐的,有两种对齐值:一种用于磁盘文件内,另一种用于内存中。这两个值通常是不同的,在这个时候就需要进行地址间的转换。
下面这一段描述摘自《加密与解密(第四版)》
在PE文件头里,FileAlignment定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始,而区块的实际代码或数据的大小不一定刚好是这么多,所以在不足的地方一般以00来填充,这就是区块的间隙。例如,在PE文件中,一个典型的对齐值是200h,这样每个区块从200h的倍数的文件偏移位置开始。假设区块的第1个节在400h处,长度为90h,那么400h~490h为这一区块的内容,而文件对齐值是200h,为了使这一节的长度为FileAlignment的整数倍,490h~600h会被0填充,这段空间称为区块间隙,下一个区块的开始地址为600。
在PE文件头里,SectionAlignment定义了内存中区块的对齐值。当PE文件被映射到内存中时,区块总是至少从一个页边界处开始。也就是说,当一个PE文件被映射到内存中时,每个区块的第1个字节对应于某个内存页。在x86系列CPU中,内存页是按4KB(1000h)排列的;在x64中,内存页是按8KB(2000h)排列的。所以,在x86系统中,PE文件区块的内存对齐值一般为1000h,每个区块从1000h的倍数的内存偏移位置开始。
当磁盘对齐值和内存对齐值不同时,需要进行转换,方式如下:
在上图中可以观察到,文件被映射到内存中时,MS-DOS头部、PE文件头和块表的偏移与大小均没有发生变化,而之后的区块的偏移位置则发生了变化。
$FileOffset=RVA-(add2-add1)$
add2:该节的RVA;add1:该节在文件中的偏移。
实际操作中推荐使用 Stud_PE 进行转换:
DLL文件
DLL 文件为动态链接文件,又被称为“应用程序拓展”,时软件文件类型。在Windows中,许多应用程序并不是一个完整的可执行文件,他们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。一个应用程序可以使用多个DLL文件,一个DLL文件也可能被多个应用程序使用(共享DLL文件)。
这里如果向创建DLL文件可以可以使用DEV创建DLL项目,然后上网搜教程就OK了。
之后把DLL文件拷贝到需要使用的目录,然后进行调用。
// #include <iostream>
#include <windows.h>
/*
Windows头文件中包含可以加载DLL的函数。
*/
typedef void(*ptrSub)();
/*
在调用DLL函数之前,要定义函数指针,用来调用函数。
可以看出,函数指针的类型与DLL中的要一致。
*/
/*
调用LoadLibrary函数加载DLL文件。加载成功,hMod指针不为空。
这里也可以是一个地址加文件名
*/
int main()
{
HMODULE hMod = LoadLibrary("DLLProject.dll");
if (hMod != NULL)
{
/*
如果加载成功,则可通过GetProcAddress函数获取DLL中需要调用的函数的地址。
获取成功,sum指针不为空。
*/
ptrSub hello ;
hello=(ptrSub)GetProcAddress(hMod, "HelloWorld");
if (hello != NULL)
{
hello();
/*获取地址成功后,通过sum调用函数功能。*/
}
FreeLibrary(hMod);
/*在完成调用功能后,不在需要DLL支持,则可以通过FreeLibrary函数释放DLL。*/
}
}
输入表
可执行文件使用来自其他DLL的代码或数据的动作称为输入。当PE文件被载入时,Windows加载器的工作之一就是定位所有被输入的函数或数据,并让正在载入的文件可以使用这些地址。这个过程就是通过PE文件的输入表(导入表)完成的。输入表中保存的时函数名和其驻留的DLL名等动态链接所需要的信息。
下面这一段内容摘自《加密与解密(第四版)》
导入函数的调用
在代码分析或编程中经常会遇到输入函数(Import Functions,或称导人函数)。输人函数就是被程序调用但其执行代码不在程序中的函数,这些函数的代码位于相关的DLL文件中,在调用者程序中只保留相关的函数信息,例如函数名、DLL文件名等。对磁盘上的PE文件来说,它无法得知这些输入函数在内存中的地址。只有当PE文件载人内存后,Windows加载器才将相关DLL载人,并将调用输人函数的指令和函数实际所处的地址联系起来。
当应用程序调用一个DLL的代码和数据时,它正在被隐式地链接到DLL,这个过程完全由Windows加载器完成。另一种链接是运行期的显式链接,这意味着必须确定目标DLL已经被加载,然后寻找API的地址,这几乎总是通过调用LoadLibrary和GetProcAddress完成的。
当隐含地链接一个API时,类似LoadLibrary和GetProcAddress的代码始终在执行,只不过这是由Windows加载器自动完成的。Windows加载器还保证了PE文件所需的任何附加的DLL都已载入。例如,Windows2000/XP上每个由Visual C.++创建的正常程序都要链接KERNEL32.DLL,而它又从NTDLL.DLL中输人函数。同样,如果链接了GDI32.DLL,它又依赖USER32、ADVAPI32、NTDLL和KERNEL32等DLL的函数,那么都要由Windows加载器来保证载入并解决输入问题。
这也就是即使只有几行代码,但是生成的EXE文件依然不小的原因。
在PE文件内有一组数据结构,它们分别对应于被输人的DLL。每一个这样的结构都给出了被输入的DLL的名称并指向一组函数指针。这组函数指针称为输入地址表(Import Address Table,IAT)。每一个被引入的API在IAT里都有保留的位置,在那里它将被Windows加载器写人输人函数的地址。最后一点特别重要:一旦模块被载入,IAT中将包含所要调用输入函数的地址。
为了区分输入函数调用和普通函数调用,CALL 有两种形式
CALL DWORD PTR XXXXXXX => 普通函数调用
CALL XXXXXXX;DWORD PTR YYYYYY; => 输入函数调用
输入表的结构
输入表以一个IMAGE_IMPORT_DESCRIPTOR数组开始,它的结构如下:
- OriginalFirstThunk(Characteristics):包含指向输入名称表(INT)的RVA。INT是一个IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构都指向IMAGE_IMPORT_BY_NAME结构,数组以一个内容为O的IMAGE_THUNK DATA结构结束。
- TimeDateStamp:一个32位的时间标志,可以忽略。
- ForwarderChain:这是第1个被转向的API的索引,一般为0,在程序引用一个DLL中的API,而这个API又在引用其他DLL的API时使用(但这样的情况很少出现)。
- Name:DLL名字的指针。它是一个以“O0”结尾的ASCII字符的RVA地址,该字符串包含输人的DLL名,例如“KERNEL32.DLL”“USER32.DLL”。
- FirstThunk:包含指向输人地址表(IAT)的RVA。IAT是一个IMAGE_THUNK_DATA结构
的数组。
每个IMAGE_THUNK_DATA元素对应于一个从可执行文件输人的函数。两个数组的结束都是由一个值为O的IMAGE_THUNK_DATA元素表示的。IMAGE_THUNK_DATA结构实际上是一个双字,该结构在不同时刻有不同的含义,具体如下。
当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时低31位(或者一个64位可执行文件的低63位)被看成一个函数序号。当双字的最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构。
为什么会有两个并行的指针数组指向IMAGE_IMPORT_BY_NAME结构呢?第1个数组(由OriginalFirstThunk所指向)是单独的一项,不可改写,称为INT,有时也称为提示名表(Hint-nameTable)。第2个数组(由FirstThunk所指向)是由PE装载器重写的。PE装载器先搜索OriginalFirstThunk,如果找到,加载程序就迭代搜索数组中的每个指针,找出每个IMAGE_IMPORT_BY_NAME结构所指向的输人函数的地址。然后,加载器用函数真正的入口地址来替代由FirstThunk指向的IMAGE_THUNK_.DATA数组里元素的值。“Jmp dword ptr[XXXXXXXX]”语句中的“[XXXXXXXx]”是指FirstThunk数组中的一个人口,因此称为输入地址表(Import Address Table,IAT)。所以,当PE文件装载内存后准备执行时,图11.13已转换成如图11.14所示的状态,所有函数入口地址排列在一起。此时,输入表中的其他部分就不重要了,程序依靠IAT提供的函数地址就可以正常运行。
输出表的结构
引出函数节一般名为 .edata,这是本文件向其他程序提供调用函数的列表所在的“索引”及具体代码实现(引出目录表)。
我们可以通过其索引部分来找到对应函数的具体地址。
输出表是数据目录项的第一个成员,指向 IMAGE_EXPORT_DIRECTORY结构,结构如下图所示:
其各个字段的含义分别为:
通过函数名定位函数的导出地址,这里以ExitProcess 为例:
- 从AddressOfNames 指向的指针数组中找到ExitProcess ,并记下该数组的序号x;
- 从AddressofNameOrdinals 指向的数组中,定位第x项成员,得到序号y;
- 从AddressOfFunction 指向的数组中定位第y项获得该函数的RVA。
资源节
Windows程序的各种界面称为资源,包括加速键、位图、光标、对话框、图标、菜单、串表、工具栏和版本信息等。在PE文件的所有结构中,资源部分是最复杂的。
资源结构
资源用类似磁盘目录结构的方式保存,目录通常包含三层。目录结构如下图所示:
资源目录结构中的每一个节点都是由IMAGE_RESOURCE_DIRECTORY 结构和紧随其后的数个 IMAGE_RESOURCE_DIRECTORY_ENTRY 结构组成的,这两种结构组成一个目录块。
IMAGE_RESOURCE_DIRECTORY 结构:
NumberOfNamedEntries+NumberOfIdEntries = 该目录项IMAGE_RESOURCE_DIRECTORY_ENTRY 结构的数量
IMAGE_RESOURCE_DIRECTORY_ENTRY 结构
- name 字段定义目录项的名称或ID。
- offsetToData: 是一个指针。当最高位为1时,低位数据指向下一层目录块的起始地址;当最高位是0时,指针指向IMAGE_RESOURCE_DATA_ENTRY 结构。
重定位表
当链接器生成一个PE文件时,会假设这个文件在执行时会被装载到默认的基地址处,并将code和data的相关地址都写入PE文件。如果载入时将默认的值作为基地址载入,则不需要重定位。但是,如果PE文件被载入到虚拟内存的另外一个地址中国,链接器登记的那个地址就是错误的,这时候就需要重定位表来调整(.reloc)。
详细介绍可以看这个链接,写的挺详细的
实战
向空白区添加代码
通常而言,PE文件在文件中向200H对其,在内存中向1000H对其,因此在PE文件中会出现较多的00字节,我们就可以利用这些区域来写入代码,这里利用的是代码节。
本次实验目的是在目标程序执行前执行MessageBoxA(0, 0, 0, 0)
。
基本思路:
准备MessageBoxA所需要的参数,call MessageBoxA ,调用完之后返回到正常执行的代码部分。
-
获取MessageBoxA的地址
使用
dp MessageBoxA
即可在函数入口位置加入断点,进而可以获取函数入口地址 77D707EA。 -
计算E8 E9
E8 是call 指令的硬编码,他们之间的对应形式为
aaa: E8 xxx : call yyy
bbb: next insruction
其中$$yyy=xxx+bbb$$,即跳转地址为下一条指令的地址加上偏移值。
E9 是 jmp 指令的硬编码,使用形式和call一致。
并且FOA与RVA的值不同,要进行转换:FOA = n.PointerToRawData + 偏移(即在RVA中所属节+偏移,因为在内存中节空隙之间会被拉伸,但节中的数据并不会被拉伸)
E8后跟随的硬编码 = 要跳转的地址- E8下条指令所在的地址
E9后跟随的硬编码 = 要跳转的地址- E9下条指令所在的地址
假设我们尝试在这个位置写入代码,即0x0450,对应的虚拟地址为 400000 + 0050(相对400的偏移)+ 1000 = 401050 :
那么 E8 后面的数值为 77D507EA - 40105D = 7794F78D
程序的入口点为 401000h,那么E9 后面的值为 401000h - 401062h = A1
-
将结果按小端序写入
-
修改OEP使其指向我们加入的代码
-
结果展示
-
心得体会
当文件页面大小与内存页面大小不一致时,需要进行FOA与RVA的转换 => 使用偏移地址和基地址。
DLL 注入:静态修改PE文件
处理并加载输入表中的DLL模块是进程创建阶段一项非常重要的工作。当一个进程被创建后,不会直接到EXE本身的入口处执行,首先被执行的是ntdll.dl中的LdrInitializeThunk函数(ntdll是Windows操作系统中一个非常重要的基础模块,它在进程创建阶段就已经被映射到新进程中了)。LdrInitializeThunk会调用LdrpInitializeProcess对进程的一些必要内容进行初始化,LdrpInitializeProcess会继续调用LdrpWalkImportDescriptor对输人表进行处理,即加载输人表中的模块,并填充应用程序的IAT。所以,只要在输人表被处理之前进行干预,为输入表增加一个项目,使其指向要加载的目标DLL,或者替换原输入表中的DLL并对调用进行转发,那么新进程的主线程在输入表初始化阶段就会主动加载目标DLL。
准备工作:一个自行编写的msg.dll
DLL 的文件内容如下:
#include <windows.h>
#include <stdio.h>
DWORD WINAPI ThreadShow(LPVOID lpParameter);
BOOL APIENTRY DllMain( HMODULE hModule, //指向自身的句柄
DWORD ul_reason_for_call, //调用原因
LPVOID lpReserved //静态加载为非NULL,动态加载为NULL
)
{
if(ul_reason_for_call == DLL_PROCESS_ATTACH) {
CreateThread(NULL,0,ThreadShow,NULL,0,NULL);
}
return TRUE;
}
DWORD WINAPI ThreadShow(LPVOID lpParameter) {
char szPATH[MAX_PATH] ={0};
char szBUF[1024]={0};
// 获取宿主进程全路径
GetModuleFileName(NULL,szPATH,MAX_PATH);
sprintf(szBUF,"DLL 已注入到进程 %s [pid = %lu]\n",szPATH,GetCurrentProcessId());
MessageBox(NULL,szBUF,"DLL Inject",MB_OK);
printf("%s",szBUF);
OutputDebugString(szBUF);
return 0;
}
__declspec(dllexport) void Msg()
{
return;
}
Msg()函数是msg.dll文件项外部提供服务的导出函数,它没有任何功能,为了保持形式上的完整性,是msg.dll能够顺利添加到hello.exe文件的导入表。
在PE文件中导入某个DLL,实质就是在文件伪造内调用该DLL提供的导出函数,PE文件头记录着DLL名称、函数名称等信息,因此,msg.dll至少要提供1个以上的导出函数才能保持形式上的完整性。
执行结果为:
修改对象:hello.exe
(这个应该是软件安全实验最常用的?)
修改目标:让hello.exe启动时能够加载 msg.dll
修改流程
回顾输入表的结构如下:
该结构简称IID,一个输入表就是一个IID数组,最后一项为全0,作为结束标志。要添加一个IID,就需要修改紧邻当前IID数组的一块内存,这块内存一般是与IID相关连的结构 OriginalFirstThunk和FirstThunk,不能被覆盖。也可以参考下面的关系图(注意指向都是靠的地址,只有在最后一部分才存的内容):
因此,必须要整体移动IID数组到一个新的位置,此时就必须要考虑内存占用的问题。由于只添加了一个IDD,它所关联的OriginalFirstThunk和FirstThunk 不会占用太多的空间,直接使用原来IID数组所在的内存即可。
-
寻找放置新IID数组的位置
查看输入表的位置和大小:
整理以下得到:
ImportAddress RVA RowoOffset Size newIIDSize 0x2014 0x614 0x60 0x60 + 0x14 = 0x74 这里在引入函数节的部分有很多填充区域可以使用,就可以直接在这部分写入,否则就需要新增节或拓展节。
这里选择写入的地址为:
文件偏移(Raw Offset):0x06c0
内存偏移(RVA):0x06c0 - 0x0600 + 0x2000 = 0x20c0
-
备份原IID数组结构
原输入表部分:
将上面的内容复制到 0x06c0开始的空白区域,如下图:
这里选中的部分就是后面需要新填入的IDD。
-
在原IID区域构造新IID的OriginalFirstThunk、FirstThunk和Name等
OriginalFirstThunk和FirstThunk 都是偏移地址表,比较容易对齐,而Name的长度可能不确定,所以把两个Thunk放在前面,并留一个全0向作为结束标记(Thunk数组的规定),然后填写DLL的名称,最后填写一个Name结构。
结果如下:
区域 Raw Offset RVA DLL Name 0x0624 0x0624-0x0600+0x2000=0x2024 IMPORT BY NAME 0x0630 0x2030 OriginalFirstThunk 0x0614 0x2014 FirstThunk 0x061c 0x201c 在手动修改数据时,一定要注意字节序的问题
-
填充新输入表项的IID结构
根据刚才填充的两个结构和Name的偏移,填写新的IID项,这里填充的都是内存偏移(RVA)。
-
修正PE文件头的信息
由于修改了输入表的位置和大小,PE文件头中输入表指向的位置也需要修正,修正后的结果为:
因为这是在引入函数节添加的,所以不用修改属性。
提取最大的图标
本次实验分析的程序为 zoomlt.exe 文件,可以使用PEview直接查看资源文件
可以看到ICON 0005 0409 的尺寸最大,需要将其提取出来,可以跳转到他的文件偏移地址然后从该位置提取EA8大小的内容即:
然后将其复制到另一个文件,需要注意的是这里不能直接保存成ico文件,因为还没有文件头!
自己去网上找一个ico文件,然后仿照格式添加即可:
重定位修复
当将可执行文件的ImageBase从0x400000修改为0x600000时,需要进行重定位(relocation)操作来修复程序中涉及到ImageBase的地址引用。
重定位的过程如下:
-
获取PE文件的基地址(原ImageBase)和新的ImageBase。在这种情况下,原ImageBase为0x400000,新的ImageBase为0x600000。
-
遍历PE文件的重定位表(relocation table)。重定位表位于可选头(optional header)中的数据目录(data directory)中。
-
对于每个重定位项,计算需要修复的地址。重定位项中记录了需要修复的地址的偏移量。
-
根据偏移量,找到需要修复的地址所在的位置。这个位置通常是指令中的立即数、全局变量的地址等。
-
将原地址加上新的ImageBase与原ImageBase的差值,得到修复后的地址。
-
将修复后的地址写回到原地址所在的位置。
参考链接
- https://www.52pojie.cn/thread-1391994-1-1.html
- https://www.52pojie.cn/thread-1765357-1-1.html
- PE在空白区域添加代码
- 通过修改PE加载DLL
- 《加密与解密 (第四版)》