记下笔记以及一点小思考,让诸位见笑啦
在学习了PE加载器的实现原理后,我发现这个加载PE的过程与PE映像切换技术(Process Hollowing)有点类似,区别在于后者并没有去填充内存中PE文件的IAT表与进行重定位。那同样是把PE文件手动从硬盘里加载到内存中运行,为什么PE映像切换技术就没有去对内存中PE文件的IAT表进行操作呢?
为了叙述的完整,先介绍一下PE映像切换技术与PE加载器的具体原理。
效果就是套一个傀儡进程的壳来执行我们希望执行的其他PE文件,《逆向工程核心原理》中给出的代码实现如下:
1、创建挂起的傀儡进程
2、卸载掉原来的模块
3、写入新的文件
4、恢复现场
在看雪知识库的Windows安全-系统篇-PE格式-PE文件的加载
章节收录了许多PE加载器的实现文章。实现上只要模拟操作系统加载PE文件的方式来做就可以,简单来说分为以下几个步骤:
1.申请一块内存,将PE文件由硬盘加载到内存中,这部分与上文基本相同
2.修复重定位
根据重定位表的内容,把PE文件中的对应位置的硬编码地址进行重定位
例如,假设该PE文件的默认基址为0x00400000
,加载到内存后的基址为0x00100000
就是把RVA为10F5
处的硬编码地址0x00467C28
重定位为0x00167C28
,即减去原基址再加上实际的基址
3.加载导入表
先根据IDT里的dll名称用LoadLibrary()
加载对应的dll
然后到对应dll的导入表项(理论上应该去INT中,但实际上这俩表内容是一样的)中用GetProcAddress()
获取导入的函数地址,并写到导入表的相应位置
4.跳转到PE的入口点处执行
问题的关键就是因为PE映像切换技术使用了CreateProcess()
来挂起创建一个傀儡进程,当恢复线程执行后还会进行一些进程初始化的工作,所以不用我们手工的去填IAT表和进行重定位。
进程初始化有一部分实在新的进程中进行的,就比如IAT表的填充和重定位。当初始线程启动时,首先会执行KiThreadStartup
,把目标线程的IRQL从DPC级降低到APC级。然后调用PspUserThreadStartup
,将用户空间ntdll.dll
中的函数LdrInitializeThunk
作为APC函数挂入APC队列,再企图返回到用户空间,执行LdrInitializeThunk
,正是在这个函数中,进行了IAT表的填充以及重定位。
《逆向工程核心原理》
Process Hollowing and Portable Executable Relocations
常见进程注入的实现及内存dump分析——Process Hollowing(冷注入)
PE加载器的简单实现
一种保护应用程序的方法 模拟Windows PE加载器,从内存资源中加载DLL
《深入解析Windows操作系统》
《漫谈兼容内核》
CreateProcess(NULL, FakeProcesssPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)
CreateProcess(NULL, FakeProcesssPath, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)
/
/
通过PEB获取傀儡进程的映像基址
GetThreadContext(pi
-
>hThread, &ctx)
ReadProcessMemory(
pi
-
>hProcess,
(LPCVOID)(ctx.Ebx
+
8
),
/
/
ctx.Ebx
=
PEB, ctx.Ebx
+
8
=
PEB.ImageBase
&dwFakeProcImageBase,
sizeof(DWORD),
NULL) )
...
/
/
卸载原傀儡进程映像
pFunc
=
GetProcAddress(GetModuleHandle(L
"ntdll.dll"
),
"ZwUnmapViewOfSection"
);
(PFZWUNMAPVIEWOFSECTION)pFunc)(pi
-
>hProcess, (PVOID)dwFakeProcImageBase);
/
/
通过PEB获取傀儡进程的映像基址
GetThreadContext(pi
-
>hThread, &ctx)
ReadProcessMemory(
pi
-
>hProcess,
(LPCVOID)(ctx.Ebx
+
8
),
/
/
ctx.Ebx
=
PEB, ctx.Ebx
+
8
=
PEB.ImageBase
&dwFakeProcImageBase,
sizeof(DWORD),
NULL) )
...
/
/
卸载原傀儡进程映像
pFunc
=
GetProcAddress(GetModuleHandle(L
"ntdll.dll"
),
"ZwUnmapViewOfSection"
);
(PFZWUNMAPVIEWOFSECTION)pFunc)(pi
-
>hProcess, (PVOID)dwFakeProcImageBase);
/
/
从硬盘上读取目标PE文件
hFile
=
CreateFile(RealProcessPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwFileSize
=
GetFileSize(hFile, NULL);
ReadFile(hFile, pRealFileBuf, dwFileSize, &dwBytesRead, NULL);
CloseHandle(hFile);
...
/
/
根据PE文件的偏移量得到各个PE头
/
/
DOS头在PE文件的最开始
PIMAGE_DOS_HEADER pIDH
=
(PIMAGE_DOS_HEADER)pRealFileBuf;
/
/
NT头的偏移量
=
pIDH
-
>e_lfanew, 可选头相对于NT头的偏移量
=
0x18
PIMAGE_OPTIONAL_HEADER pIOH
=
(PIMAGE_OPTIONAL_HEADER)(pRealFileBuf
+
pIDH
-
>e_lfanew
+
0x18
);
/
/
节区头的偏移量
=
NT头的偏移量
+
NT头的大小, 因为节区头位于NT头的后面
PIMAGE_SECTION_HEADER pISH
=
(PIMAGE_SECTION_HEADER)(pRealFileBuf
+
pIDH
-
>e_lfanew
+
sizeof(IMAGE_NT_HEADERS));
/
/
在傀儡进程中,目标PE文件基址的地址处,分配目标PE文件大小的内存
pRealProcImage
=
(LPBYTE)VirtualAllocEx(
pi
-
>hProcess,
(LPVOID)pIOH
-
>ImageBase,
pIOH
-
>SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE)
/
/
写入PE头
WriteProcessMemory(
pi
-
>hProcess,
pRealProcImage,
pRealFileBuf,
pIOH
-
>SizeOfHeaders,
NULL);
/
/
写入各节区
for
(
int
i
=
0
; i < pIFH
-
>NumberOfSections; i
+
+
, pISH
+
+
){
if
( pISH
-
>SizeOfRawData !
=
0
){
/
/
这里注意要将各节区写到对应的 映像基址
+
RVA 处的内存中
if
( !WriteProcessMemory(
ppi
-
>hProcess,
pRealProcImage
+
pISH
-
>VirtualAddress,
pRealFileBuf
+
pISH
-
>PointerToRawData,
pISH
-
>SizeOfRawData,
NULL) ){
printf(
"WriteProcessMemory(%.8X) failed!!! [%d]\n"
,
pRealProcImage
+
pISH
-
>VirtualAddress, GetLastError());
return
FALSE;
}
}
}
/
/
从硬盘上读取目标PE文件
hFile
=
CreateFile(RealProcessPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
dwFileSize
=
GetFileSize(hFile, NULL);
ReadFile(hFile, pRealFileBuf, dwFileSize, &dwBytesRead, NULL);
CloseHandle(hFile);
...
/
/
根据PE文件的偏移量得到各个PE头
/
/
DOS头在PE文件的最开始
PIMAGE_DOS_HEADER pIDH
=
(PIMAGE_DOS_HEADER)pRealFileBuf;
/
/
NT头的偏移量
=
pIDH
-
>e_lfanew, 可选头相对于NT头的偏移量
=
0x18
PIMAGE_OPTIONAL_HEADER pIOH
=
(PIMAGE_OPTIONAL_HEADER)(pRealFileBuf
+
pIDH
-
>e_lfanew
+
0x18
);
/
/
节区头的偏移量
=
NT头的偏移量
+
NT头的大小, 因为节区头位于NT头的后面
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)