# 高级进程注入总结

前几天看玄武的 “每日安全” 板块,发现了一篇关于进程注入的文章,之前了解过一些相关的技术,不过是零散的。看到文章总结的挺详细的,且各种方法的异同细节容易忘,于是就想着总结一下各种 进程注入 的异同和优缺点,因此有了本文。

  • 关于常见的进程注入方法,请参考这个这个,本文将重点描述值得留意的方法(比如 Module Stomping),和一些思路新颖的方法。
  • 以下描述的方法在进程注入这个仓库都能找到。

# 进程注入简介

进程注入就是将代码注入到另一个进程中,shellcode 注入和 DLL 注入都属于进程注入。

进程注入的作用是隐藏自身或者利用另一个进程的权限做一些不好的事情 ^_^,比如病毒等恶意程序注入到一个正常的进程,以此隐藏自己。

进程注入的方法非常之多,很多与 DLL 注入有关,比如注册表(Image File Execution Options)、DLL 劫持、输入法、COM、LSP 劫持(LayerService Provider,与 winsock 有关)...

除了 DLL 注入,还有 shellcode 注入,因为 shellcode 更小,所以 shellcode 的使用也更加多样。

2017 年,在黑客大会上 Eugene Kogan 和 Tal Liberman 又分享了更加隐蔽和特别的方法,比如 Process Doppelganging。那么接下来就开始进程注入方法的介绍。

# Module Stomping

该方法通过在目标进程中加载一个合法 DLL,然后将 shellcode 或恶意 DLL 覆写到这个合法 DLL 的地址空间里。

伪代码如下(省略错误检查):

HANDLE hTargetHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_ALL_ACCESS | PROCESS_SUSPEND_RESUME, FALSE, targetPid);
//...
// Use OpenProcess + VirtualAllocEx + CreateRemoteThread(the traditional dll injection method)
injectLoadLibrary(hTargetHandle, legitimateDll);
//...
if (has executable section)
    BypassCFG();
// Copy every malware dll's section into legitimate dll's corresponding sections within the target process address space.
for(section : malwareDll){
    WriteToMudole(hTargetHandle, legitimateDllSection, section, section_size...);
}
RebuildImportTable();
RebuildRelocationTable();
callTLSCallback();
CreateRemoteThread(malwareDllEntryPoint);

通过该方法可以将恶意代码隐藏,使得 Defender 扫描目标进程空间时不会发现恶意代码的存在(因为恶意代码潜伏在合法的模块里)。

该方法有两个值得关注的点:

  • 先在目标进程加载一个合法 DLL,以此隐藏恶意代码。

  • 它用了 SetProcessValidCallTargets ,该方法可以 bypass 开启 CFG 的程序。

与 CFG (control flow guard) 相关的知识可参考 MSDN 或者 System Internals 7th part1 security chapter。

不过该方法还是有明显的不足,比如会调用显眼的 VirtualAllocEx、WriteProcessMemory、VirtualProtectEx 和 CreateRemoteThread,通常的 EDR (endpoint detection and response) 都能检测出可能的恶意行为。

该方法的详细细节可参考这篇文章

# Process Hollowing

该方法也比较经典,并被广泛使用,其基本流程如下:

CreateProcess(“svchost.exe” ,, CREATE_SUSPENDED,);
NtUnmapViewOfSection() and VirtualAllocEx();
For each section:
	WriteProcessMemory(..., EVIL_EXE,);
Relocate Image*;
Set new base address in the NT header of EVIL_EXE;
SetThreadContext();
ResumeThread();

该方法和 Module Stomping 很像,都是替换掉了一个模块,不过有以下三个不同:

  • Process Hollowing 替换的模块是目标 EXE 程序
  • 该方法使用 NtUnmapViewOfSection 将 EXE 程序解除映射
  • 用 ResumeThread 来启动恶意程序的入口点

该方法的详细实现可参考这个

# Process Doppelganging

接下来描述的几个进程注入方法使用了不同的注入思路,它们之间只有细微的差别,最后会总结它们的异同点。

该方法是 2017 年在黑客大会介绍的,演讲者首先说明了 Process Hollowing 涉及的敏感操作和一些不足:

  • Unmap 目标进程的 EXE 模块(非常可疑),目前的的 EDR 一般都有 Unmap 检查。

  • 如果不 Unmap,而是直接覆写程序,那么覆写地址的页属性就不是共享的,也很可疑。

    页属性共享涉及到操作系统的共享内存机制,这里简单描述一下:

    为了节省内存,当一个模块加载进内存时,比如 ntdll.dll,操作系统会看该模块是否已被其他进程加载过了(即已经在物理内存中了),如果在,那么操作系统只需要简单地将该物理区域映射到需要加载模块的进程中。因此,这些模块对应的内存是共享的,这些模块内存对应的进程 pte (page table entry) 会指向一个叫 prototypePTE 的结构,该结构会指向这些共享的物理内存。

    不过,如果我们向模块,比如 ntdll.dll,的数据段写入数据,那么这时操作系统就会对 ntdll.dll 的数据段分配一段物理内存,然后当前进程对应数据段的 pte 就不会再指向 prototypePTE,而是指向操作系统分配的物理内存。

    关于内存共享机制的详细描述,读者看参考 Windows Internals 7th,第五章的 Page fault handling 部分。

  • 直接覆写程序不够好,那就 Unmap 后再 Remap:

    • 如果 remap 时的类型不是 IMAGE,通过检查节的类型可判定是否可疑

    • 如果 remap 时的类型是 IMAGE,这时可疑的点就不多了。不过因为 Process Hollowing 用 SetThreadContext 修改了初始线程的执行入口点(ETHREAD.Win32StartAddress),那么我们可以检测其执行入口点是否是 ETHREAD.Win32StartAddress。如果不是,那值得怀疑,并且我们可以检测其执行入口点对应的文件名,这样可进一步判定这段内存是否是可疑 payload。

      检测文件名可通过该字段查看:VAD (ETHREAD.Win32StartAddress).Subsection.ControlArea.FilePointer。

    另外,如果要使用 Remap,那就需要一个 section,打开 section 需要一个文件句柄,也就是说 Remap 需要一个落地的文件,因此采取 process hollowing 时,攻击者很少会使用 Remap 的方式。

综上所述,Process Hollowing 已经不是那么好了,那还有什么其他更隐蔽的方法吗?

于是演讲者介绍了 Process Doppelganging,该方法由 CreateFileTransacted API 开始。由于其内容较多,且不易描述,读者可查看这个 PPT 和这个源码和这篇文章,这些资料讲得非常详细。

这里归纳了简要的流程:

  • 创建一个 transaction(事务)

  • 打开原程序句柄(通过 CreateFileTransacted)

  • 向原程序句柄写入恶意代码,根据此时的文件内容,创建一个 section

  • rollback(回滚)之前的写操作

    虽然回滚了文件的内容,但已生成的 section 映射的内容是修改后的,即内容是 payload,解释可参考 Process Herpaderping 小节。

  • 通过刚刚创建的 section,创建进程(通过 NtCreateProcessEx)

  • 准备参数到目标进程(跨进程)

  • 创建初始线程 (NtCreateThreadEx),之后唤醒线程(NtResumeThread)

以上流程可抽象如下:

transact -> write -> map -> rollback -> execute

该方法的新颖点在于通过 Windows 提供的 事务 API,将恶意代码写入打开的文件,并根据这个文件句柄创建一个 section,之后回滚写入操作,最后再根据创建的 section 创建进程。这样可以隐藏执行的恶意代码,虽然你查看该进程时(比如 procExp),其显示的是原程序的信息,但其真正执行的代码是恶意代码。同时,它比 Process Hollowing 更隐蔽,因为它不用 Unmap,也不需要 Remap,它就像正常启动一个进程一样。

最近,我编译了这份代码,发现 win10、win8.1 和 win7 都失败了,说明 windows 已经 patch 了该方法,以下是 win10 测试的结果:

  • 如果覆盖用的是非 PE 文件,NtCreateSection 返回错误,提示无效的 PE。

  • 如果覆盖用的是 PE 文件,创建进程成功,不过我们在 procExp 中看到的是名叫 System Idle Process ,观察该进程信息,其提示 “请求的操作是在不再活动的事务的上下文中进行的”。

image-20220216102919516

虽然该方法已经失效了,不过它的思路很好。之后介绍的 Process Herpaderping 借鉴了该方法,且目前也是有效的。

可能我实践时,代码的参数没设置好,所以没有复现 demo 里的结果。从现在的恶意攻击看,基于 Process Doppelganging 的攻击很少,不知道是不是因为被 patch 了。不管怎么说,该方法是一个很好的起点。

关于实例 demo 展示,读者可参考这个

关于该方法的描述详情,读者可参考这篇文章

# Transacted Hollowing

该方法借鉴了 Process Doppelganging事务 特性和 Process Hollowing 启动进程的便捷性。通过 事务 特性,可以更好的隐藏恶意代码;通过便捷性,可以免去 Process Doppelganging 创建进程、准备进程参数的复杂过程。

该方法的大致流程是:

  • 采用 Process Doppelganging 的前半段,transact -> write -> map -> rollback
  • Remap 恶意代码的 section 到目标进程
  • 采用 process hollowing 的技巧,通过 SetThreadContext 和 ResumeThread 的执行恶意代码

Process Doppelganging 小节,我们讲到 process hollowing 如果要 Remap,需要有一个落地的文件。通过事务的回滚,可以免去这个落地的文件。因此,我们可以把 Transacted Hollowing 当做是增强版的 process hollowing

  • 关于该方法的代码,可参考这个仓库

  • 关于该方法的描述细节,可参考这篇文章

# Process Ghosting

讲该方法之前,我们先说一下背景知识。

在 windows 操作系统里,如果我们映射了可执行程序,那么可执行程序就不应该被修改,如果尝试修改,则返回错误。但这也是一个限制,即只针对映射过的可执行程序不能被修改,也就是说我们可以打开一个文件、对其设置删除标志、写 payload 到文件,之后映射文件,最后删除这个文件。

注:如果我们以 GENERIC_READ 和 GENERIC_WRITE 打开文件,那么映射可执行程序之后,我们还是可以修改文件,这在 Process Herpaderping 将会体现。

该方法与 Process Doppelganging 相似,它们的目的都是做的比 process hollowing 更隐蔽,该方法与

Process Doppelganging 从实现上几乎一样,唯一的区别就是处理不落地文件的方式:

  • Process Doppelganging :通过 事务 API 打开文件,修改文件(写入 payload),创建 section,再回滚修改的内容。

  • Process Ghosting :打开文件,设置删除标志,修改文件(写入 payload),创建 section,删除文件。这样进程运行时,反病毒软件打不开文件,因此无法做检测。

    调用 GetProcessImageFileName 会返回空字符串。

  • 代码可参考这个仓库

  • 关于该方法的描述细节,可参考这篇文章

# Ghostly Hollowing

了解完 Process Ghosting ,我们来看 Ghostly Hollowing 。与 Transacted Hollowing 类似,该方法也是为了免去创建进程和准备进程参数的复杂过程。于是可以得到以下结论:

image-20220219142624707

  • 关于该方法的实现,可参考这个仓库

# Process Herpaderping

这是本文描述的最后一个方法,因此我们先来一个方法小总结:

TypeTechnique
Hollowing(including Module Stomping )map or VirtualAlloc -> SetThreadContext -> ResumeThread
Doppelgangingtransact -> write -> map -> rollback -> execute
Herpaderpingwrite -> map -> modify -> execute -> close
GhostingsetDeleteFlag -> map -> delete -> execute
Transacted Hollowingtransact -> write -> map -> rollback + Hollowing
Ghostly HollowingsetDeleteFlag -> map -> delete + Hollowing

该方法的原理、实现都和 GhostingDoppelganging 类似,读者可以把 GhostingHerpaderping 都理解为 Doppelganging 的变体。 Ghosting 是删除文件, Doppelganging 是替换文件的内容(不替换文件), Herpaderping 是替换文件和文件内容,其结果是反病毒软件检测执行的进程时,其打开的程序文件内容是我们设定的(比如 lsass.exe,包括文件签名)。

Herpaderping 的流程如下:

  • 打开一个可读可写的文件
  • 向文件写入 payload(calc.exe),创建 section
  • 创建进程 A(和 Doppelganging 一样,使用 NtCreateProcessEx)
  • 向同一个文件写入伪装的程序,比如 lsass.exe
  • 关闭并保存文件为 output.exe(文件保存至磁盘,磁盘的内容是 lsass.exe)
  • 准备进程参数,创建线程(这时 payload 开始执行)

一个有趣的现象是如果我们不关闭执行 payload(计算器)的进程 A,那么双击 output.exe 时会启动另一个进程 B,弹出另一个计算器。其原因是进程 A 有一个 section,这个 section 指向的文件路径是 output.exe,当我们启动进程 B 时,操作系统发现路径一样,于是使用了进程 A 的 section 对应的 SectionObjectPointer,以此实现文件的共享,也就是使用已经映射到内存的 output.exe 来启动另一个计算器。但如果我们打开 output.exe 文件,会发现内容又是 lsass.exe 的。因为文件映射到内存包括 data 和 image 类别,而读文件是 data 类,所以 data 类对应的内存和 image 类对应的内存是分开的,也就是说操作系统的内存有两份 output.exe 文件的数据。下面贴一张关于进程 A 的 section 对应的 SectionObject 示意图。

image-20220219143458739

这里我们通过 windbg 讲述刚刚的解释。首先拿到 herpaderping 的 demo 源码,用 visual studio 编译完成后,我们启动一个 windbg,启动命令如下:

"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe" ProcessHerpaderping.exe "E:\my_knowledge\Reverse\Tools\CFF_Explorer\CFF Explorer.exe" E:\tmp\cpp_test_ano.exe

其中 CFF Explorer.exe 是要执行的 payload(实际利用场景下,一般没有这样的落地文件,payload 是恶意进程解密出来的),cpp_test_ano.exe 是一个合法的可读可写文件。

启动命令后,我们在 herpaderp.cpp 的 142 行下断点:

wil::unique_handle sectionHandle;
auto status = NtCreateSection(&sectionHandle,
                              SECTION_ALL_ACCESS,
                              nullptr,
                              nullptr,
                              PAGE_READONLY,
                              SEC_IMAGE,
                              targetHandle.get());
//windbg 命令如下:
    bp `ProcessHerpaderping!herpaderp.cpp:142`

142 行是创建 section,类型是 SEC_IMAGE。在创建 section 之前,我们观察一下目标文件(targetHandle)的句柄:

image-20220219123625433

打开管理员权限的 windbg,启动本地内核调试,查看这个句柄,如下:

lkd> !process 0 1 ProcessHerpaderping.exe
PROCESS ffffac0f0ede7080
...
lkd> .process /p ffffac0f0ede7080
Implicit process is now ffffac0f`0ede7080
lkd> !handle ac
...
00ac: Object: ffffac0f2281a300  GrantedAccess: 0012019f (Protected) (Audit) Entry: ffff818476fff2b0
Object: ffffac0f2281a300  Type: (ffffac0efc2d4d20) File
    ObjectHeader: ffffac0f2281a2d0 (new version)
        HandleCount: 1  PointerCount: 32655
        Directory Object: 00000000  Name: \tmp\cpp_test_ano.exe {HarddiskVolume2}
lkd> dt nt!_FILE_OBJECT ffffac0f2281a300
...
   +0x028 SectionObjectPointer : 0xffffac0f`066a6358 _SECTION_OBJECT_POINTERS
...
lkd> dx -id 0,0,ffffac0f0ede7080 -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)
((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)                 : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *]
    [+0x000] DataSectionObject : 0xffffac0f0727b1d0 [Type: void *]
    [+0x008] SharedCacheMap   : 0xffffac0f07911dc0 [Type: void *]
    [+0x010] ImageSectionObject : 0x0 [Type: void *]

因为我们打开并写了目标文件,所以 SectionObjectPointer 的 DataSectionObject 不为空,即文件内容映射到了内存。

我们单步步过 142 行,再观察一下 SectionObjectPointer:

lkd> dx -id 0,0,ffffac0f0ede7080 -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)
((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)                 : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *]
    [+0x000] DataSectionObject : 0xffffac0f0727b1d0 [Type: void *]
    [+0x008] SharedCacheMap   : 0xffffac0f07911dc0 [Type: void *]
    [+0x010] ImageSectionObject : 0xffffac0f0ebd3720 [Type: void *]

现在目标文件以 Image(可执行程序)这一类别加载进了内存,因此内存中现在有两份目标文件,一份是 data 类的,一份是 image 类的。注意这两类现在对应的内容是一样的,之后 ProcessHerpaderping 会向目标文件再写入数据,即修改 data 类所在的内存,然后关闭目标文件的句柄。此时 image 类和 data 类的内容就不同了,但在 windows 的设计里这是不应该出现的,详情可参考下面推荐的书。

执行 ProcessHerpaderping 的剩下部分,它会等待创建的 cpp_test_ano.exe 进程退出(实际执行的是 CFF Explorer.exe)。这时,如果我们用二进制编辑器打开 cpp_test_ano.exe,会发现全是明文数据,不是可执行代码:

image-20220219133338725

如果我们双击 cpp_test_ano.exe,会发现又弹出了一个 CFF Explorer.exe 进程,这时观察我们刚刚创建的进程:

lkd> !process 0 1 cpp_test_ano.exe
PROCESS ffffac0f2b92f080
...
PROCESS ffffac0f1ccb6080(第二个是刚刚创建的进程)
...
lkd> dt nt!_EPROCESS sectionobject imagefilepointer ffffac0f1ccb6080
   +0x3c0 SectionObject    : 0xffff8184`7b06ecf0 Void
   +0x448 ImageFilePointer : 0xffffac0f`159a2180 _FILE_OBJECT
lkd> dx -id 0,0,ffffac0f0ede7080 -r1 ((ntkrnlmp!_FILE_OBJECT *)0xffffac0f159a2180)
((ntkrnlmp!_FILE_OBJECT *)0xffffac0f159a2180)                 : 0xffffac0f159a2180 [Type: _FILE_OBJECT *]
    [+0x028] SectionObjectPointer : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *]
lkd> dx -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)
((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xffffac0f066a6358)                 : 0xffffac0f066a6358 [Type: _SECTION_OBJECT_POINTERS *]
    [+0x000] DataSectionObject : 0xffffac0f0727b1d0 [Type: void *]
    [+0x008] SharedCacheMap   : 0x0 [Type: void *]
    [+0x010] ImageSectionObject : 0xffffac0f0ebd3720 [Type: void *]

刚创建的进程对应的 ImageSectionObject 和之前在 ProcessHerpaderping 进程看到的结果一样,代表刚创建的进程和 ProcessHerpaderping 启动的 cpp_test_ano.exe 进程共享了 image 类对应的内存,共享了对应的 CFF Explorer 程序代码。

关于上面的解读,这涉及到 windows 操作系统对 section 的管理,比如文件的映射细节和文件的缓存管理,有兴趣的读者可以参考 Windows Internals 7th part1 第五章内存管理的 section 小节和 Windows Internals 7th part2 第十一章缓存管理器部分。

  • 该方法的源码可参考这个仓库
  • 该方法的作者用 windbg 进一步分析了共享的更多细节,可参考这个文档

# Conclusion

虽然进程注入在不断更新,不过安全厂商也在与时俱进,目前很多安全厂商都有这些方法的监控了(比如上面提到的分析文章,大部分是安全厂商写的)。从 Process Doppelganging 开始,我们能看到新的方法都是源于操作系统的不足,并慢慢衍生,可能以后的进程注入会越来越底层,越来越复杂。

本文涉及的细节很多,不能面面俱到,推荐读者看本文各个小节的推荐文章,最后再来看本文,相信读者会有更多的收获。

更新于 阅读次数