逆向工程核心原理——PE映像切换

PE映像

PE映像就是PE文件在内存中的映射形态,这在之前PE文件学习中也讲过,32位下OS为进程开辟4GB虚拟空间,低2G为用户空间,高2G是内核空间。

PE文件映射到内存空间中是一个拉伸的过程

PE映像切换

PE映像切换就是先以挂起的方式运行某个进程(A.exe),然后将完全不同的一个PE文件(B.exe)的PE映像映射到A.exe的进程内存空间中,并在A.exe的内存空间中运行。

此时虽然运行的进程名称是A.exe,但实际映射在内存中的PE映射却是B.exe,所以最终的行为动作与A.exe原来的行为动作完全不同。

示例程序:Fake.exe、Real.exe、DebugMe3.exe

Fake.exe

Real.exe

DebugMe3.exe

DebugMe3.exe是以fake.exe为外壳,real.exe为内核程序

要注意在控制台窗口输入运行命令时,命令的第一个参数为充当外壳进程的可执行文件路径,第二个参数为充当内核进程的可执行文件路径

调试

Open-输入运行参数

以参数的方式打开DebugMe3.exe

这里就是EP代码

main()函数分析

代码流程图

CreateProcess()和ResumeThread()的行为动作明确,所以我们主要分析main调用的三个子函数

FUNC_1_401150

该函数是将real.exe文件读取到内存,将内存地址记为MEM_FILE_REAL_EXE

CreateProcess(“fake.exe”,CREATE_SUSPENDED)

调用CreateProcess创建fake.exe进程,此时的进程处于挂起状态,同时也是暂停状态,此时可以自由操作其对应的内存。

FUNC_2_4011D0

这个函数里面实现了PE映像切换技术,我们逐步分析

获取fake.exe进程的实际映射地址

这是获取fake.exe的主线程上下文,然后我们可以通过它来获取进程的PEB,由PEB.ImageBase得到进程的实际映射地址

[EBP-2D0]是ConText结构体,[EBP-22C]是ConText偏移A4的成员,也就是Ebx

因为当前的fake.exe是以挂起模式创建的,处于暂停状态,进程被创建出来时,PE装载器就会把PEB结构体的地址设置给Ebx寄存器,所以当我们获取到Ebx寄存器的值就必须先获取进程主线程的上下文

此时的ECX就是PEB首地址,偏移为8的地方就是ImageBase,所以先执行add ecx+8

  • PS:对于Win XP的EXE文件来说,PE文件头中的ImageBase就是进程的实际映射地址
  • PS:Win Vista(内核6以后),引入了ASLR技术,ImageBase与实际映射地址不在相同

获取real.exe文件的ImageBase

EDI寄存器的值为MEM_FILE_REAL_EXE地址,该地址是在FUNC1中分配得到的内存起始地址,而real.exe被原封不动的保存在其中。也就是说EDI指向real.exe文件的PE头,所以EDI+3C就是IMAGE_DOS_HEADER结构体中的e_lfanew

[EDI+EAX]=IMAGE_NT_HEADER的起始地址

[EDI+EAX+34]指向IMAGE_OPTION_HEADER的ImageBase成员

执行上面指令后,real.exe的ImageBase就被存入了ECX寄存器中

比较ImageBase of fake.exe和ImageBase of real.exe

两值相同时

如果相同就继续往下执行,如果不相同就跳到004012EA

当fake.exe的PE镜像已经映射到400000地址处,地址400000也是real.exe的PE映像要映射的地方,若强行映射到该地址处,就会发生冲突,必须先卸载(Unmapping)fake.exe的PE映像的映射。由于fake.exe进程处于挂起状态,所以卸载PE映像时进程中也不会发生错误。

NTSYSAPI NTSTATUS ZwUnmapViewOfSection(
  [in]           HANDLE ProcessHandle,
  [in, optional] PVOID  BaseAddress
);
两值不同时

这时候不一定要卸载fake.exe的PE映像,可以先在fake.exe进程的虚拟内存空间中为real.exe的PE映像分配所需要的空间,然后将real.exe映射进去就行了。接下来还得告诉PE装载器,fake.exe进程的PE映像是400000地址处的real.exe,而不是 1000000地址处的fake.exe,简单修改PEB的ImageBase成员就行了

通过调用WriteProcessMemory()API成功将fake.exe进程的PEB.ImageBase改为real.exe文件的ImageBase值,此时fake.exe进程的PE映就被卸载了。

FUNC_3_401320

现在需要把real.exe的文件映射到fake.exe进程中,main函数的4010D1地址调用了FUNC_3_401320,进入FUNC3分析

为real.exe的PE映像分配内存

这个就是为real.exe的PE映像分配内存

第二个参数是想要分配的内存起始地址,其值为real.exe的ImageBase(0x4000000);函数的第三个参数是要分配内存大小,其值为real.exe的SizeofImage(0xA000);

映射PE文件头

为real.exe的PE映像分配好内存空间后,接下来就是要将real.exe映射到fake.exe进程中。

调用WriteProcessMemory()API将real.exePE文件头写入到刚刚分配的内存区域,0x262CF8是由FUNC1函数读入到debug3.exe内存中的

BytesToWrite参数为real.exe的sizeofHeader(0x4000)

映射节区

启动循环,根据节区的数量反复调用WriteProcessMemory,映射PE节区。该过程需要读取的信息是读取PE文件头的IMAGE_SECTION_HEADER。循环结束后,real.exe文件被完全映射到fake.exe进程的40000地址处,但是此时real.exe代码还无法正常运行,需要修改进程的EP.

修改EP

现在fake.exe处于挂起状态,将其恢复运行后,进程的主线程会运行何处的代码呢?这时候我们需要查看主线程CONTEXT结构体中的Eip成员

CONTEXT中有两个成员需要注意,一个就是EAX与EIP,分别位于结构体偏移B0和B8处,EAX=0x00401041,这个值很可能位于fake.exe进程中,我们用PE工具查看一下

如图所示,EAX值是fake.exe进程的EP地址,接下来看CONTEXT.EIP,其值为0x77C67098,在调试器中查看一下该地址

可以发现这个地址是ntdll.RtlUserThreadStart()API开始的地址,我们已知EIP寄存器存储的是要执行的指令代码的地址,而EAX存放返回地址,查看CONTEXT的EAX和EIP后,我们可以整理一下上面的代码执行过程:处于挂起状态的fake.exe进程恢复运行,首先会调用ntdll.RtlUserThreadStart(),跳转到EP地址。前面已经将fake.exe的进程PE映像替换为了real.exe的PE映像,所以我们只需要将CONTEXT.EAX修改为real.exe的EP地址(0x401060)

0x4014A2-0x4014B3就是将real.exe的EP地址设置到CONTEXT.EAX中

ResumeThread()

最后调用ResumeThread(),恢复运行fake.exe进程。

通过上面的调试练习,我们学会了PE映像切换技术的工作原理。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 767778848@qq.com