Search code examples
windbgchild-process

Monitor creation of child process from CMD parent in WinDbg


Not sure about the procedure on how to perform monitorization of child processes in WindDBG. I need to know how I can monitor the creation of child processes from CMD.exe parent using conditional breakpoints in WinDBG.

In this case I need to use bp nt!NtCreateUserProcess and the idea is to use command j (if-else). Basically, I need to print a message in case there is a creation of CMD.Exe process (child process) and if not, continue the debugging (g). I only need to monitor CMD.exe child processes and I need to ask for it in a if-else loop if possible.

I want to monitor the creation of processes in Windbg which have CMD.exe as the parent process considering kernel debugging. I know the function to be used in Windbg is !NtCreateUserProcess but I'm not sure which field needs to be used for that purpose considering the attributes of the function.

There is a PsAttributeValue(PsAttributeParentProcess) in the attributes structure based on the following source: https://captmeelo.com//redteam/maldev/2022/05/10/ntcreateuserprocess.html but not sure if this is an ID or the process name. Could it be used in this case?

Basically, considering command j (if-else) it must be something like:

bp nt!NtCreateUserProcess "j (XXXXXXXXXXXXXXXXXXXXXXXXXXXX) '.printf "New CMD process child!!\n ---->%mu\n" ; ' .printf "No New CMD process child!!\n ---->%mu\n"; g' "

Can you please provide me some clues?

Thanks. Regards.


Solution

  • I have a deep grudge against Windbg's OG scripting language, so here's a JS script for Windbg.

    Tested on a fully up to date Win10 22H2 machine. I guess it may work on other windows versions / updates (at least Win 10 and Win 11), but nothing is guaranteed.

    You'll need a rather recent version of Windbg or Windbg preview.

    • This is a script which can only be used for kernel debugging.
    • Have your kernel debugger attached to the target machine.
    • Copy / paste the script in its own file (e.g. c:\tmp\windbg_script.js) on your host.
    • Break in the debugger.
    • Load & run the script using .scriptload.
    • Once the script is loaded, run the target (g).
    • You should see ---- invokescript ---- printed in your debugger output.
    • Now the target machine is running, spawn a cmd.exe and start whatever process you want.
    • If a process is spawned by cmd.exe the script will print some information and stop the debugger.
    • If a process is started but not by cmd.exe it just prints [not CMD] and continue execution (without breaking into the debugger).
    "use strict";
    
    function initializeScript()
    {
        return [new host.apiVersionSupport(1, 7)];
    }
    
    let logln = function (e) {
        host.diagnostics.debugLog(e + '\n');
    }
    
    function read_u64(addr) {
        return host.memory.readMemoryValues(addr, 1, 8)[0];
    }
    
    function handle_bp() {
        let retVal = false; // continue exec by default.
    
        if(host.currentProcess.Name === "cmd.exe")
        {
            logln("Process spawned by CMD!");
    
            // read RSP
            let cpuRegs = host.currentThread.Registers.User;
            let rsp = cpuRegs.rsp;
            logln("RSP: 0x" + rsp.toString(16));
    
            // RSP + 0x48 is "ProcessParameters" location on the stack.
            let procParamAddr = rsp.add(0x48);
            logln("ProcessParamAddr (on stack): 0x" + procParamAddr.toString(16));
            let procParam = read_u64(procParamAddr);
            logln("procParam: 0x" + procParam.toString(16));
    
            // procParam + 0x68 hold a string to the full path of the process that will be started.
            let procPathAddr = procParam.add(0x68);
            logln("procPathAddr: 0x" + procPathAddr.toString(16));
            let procPath = read_u64(procPathAddr);
            logln("procPath: 0x" + procPath.toString(16));
    
            // now read the string.
            let procPathString = host.memory.readWideString(procPath);
            logln("CMD Spawned the following process: " + procPathString);
            retVal = true; // stop exec
        }
        else 
        {
            logln("[not CMD]");
            //host.namespace.Debugger.Utility.Control.ExecuteCommand("g");
        }
    
        return retVal;    
    }
    
    function invokeScript()
    {
        // startup
        logln("---- invokescript ----");
        let Control = host.namespace.Debugger.Utility.Control;
        let CurrentProcess = host.currentProcess;
        let NtCreateUserProcess = host.getModuleSymbolAddress('nt', 'NtCreateUserProcess');
        let BreakpointAlreadySet = CurrentProcess.Debug.Breakpoints.Any(
            c => c.Address == NtCreateUserProcess
        );
        if(BreakpointAlreadySet == false) {
            logln('NtCreateUserProcess: 0x' + NtCreateUserProcess.toString(16));
            Control.ExecuteCommand('bp /w "@$scriptContents.handle_bp()" ' + NtCreateUserProcess.toString(16));
        } else {
            logln('Breakpoint already set.');
        }
        logln('Press "g" to run the target.');
    
    }
    

    so the main stuff happens in handle_bp().

    First we just check we're in cmd.exe context, otherwise we bail out (this is the same things as checking the ImageFileName from the _EPROCESS structure):

    if(host.currentProcess.Name === "cmd.exe")
    

    After that it gets a bit complicated, so here's the breakdown:

    Once we're sure we're in the cmd.exe context and with our BP on nt!NtCreateUserProcess:

    NtCreateUserProcess(
        _Out_ PHANDLE ProcessHandle,  // rcx
        _Out_ PHANDLE ThreadHandle,  // rdx
        _In_ ACCESS_MASK ProcessDesiredAccess,  // r8
        _In_ ACCESS_MASK ThreadDesiredAccess,   // r9
        _In_opt_ POBJECT_ATTRIBUTES ProcessObjectAttributes,
        _In_opt_ POBJECT_ATTRIBUTES ThreadObjectAttributes,
        _In_ ULONG ProcessFlags,
        _In_ ULONG ThreadFlags,
        _In_ PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
        _Inout_ PPS_CREATE_INFO CreateInfo,
        _In_ PPS_ATTRIBUTE_LIST AttributeList
    );
    

    (side note: in the example below, I started ping 127.0.0.1 from cmd.exe)

    The 1st 4 params by registers (nothing really interesting), the reminder on the stack. Here's an example of the state of the stack at BP:

    2: kd> dqs @rsp
    fffffd82`7e537448  fffff802`5c80d8f5 nt!KiSystemServiceCopyEnd+0x25 +0x00
    fffffd82`7e537450  00000000`00000002  // (shadow space)             +0x08
    fffffd82`7e537458  00000000`00000001                                +0x10
    fffffd82`7e537460  00000000`00000000                                +0x18
    fffffd82`7e537468  0000014f`f4df28e0                                +0x20
    fffffd82`7e537470  00000000`00000000  // ProcessObjectAttributes    +0x28
    fffffd82`7e537478  00000000`00000000  // ThreadObjectAttributes     +0x30
    fffffd82`7e537480  0000000c`00000204  // ProcessFlags               +0x38
    fffffd82`7e537488  00000000`00000001  // ThreadFlags                +0x40
    fffffd82`7e537490  0000014f`f4df28e0  // ProcessParameters          +0x48
    fffffd82`7e537498  0000000c`9856de70  // CreateInfo                 +0x50
    fffffd82`7e5374a0  0000000c`9856e660  // AttributeList              +0x58
    fffffd82`7e5374a8  00000000`00000001
    fffffd82`7e5374b0  00000000`00000000
    

    Pointer at AttributeList+0x18 gives you the full path of the program that is started:

    2: kd> dq c`9856e660
    0000000c`9856e660  00000000`00000088 00000000`00020005
    0000000c`9856e670  00000000`00000040 0000014f`f4deb740
    0000000c`9856e680  00000000`00000000 00000000`00010003
    0000000c`9856e690  00000000`00000010 0000000c`9856df20
    0000000c`9856e6a0  00000000`00000000 00000000`00000006
    0000000c`9856e6b0  00000000`00000040 0000000c`9856e060
    0000000c`9856e6c0  00000000`00000000 00000000`0006001a
    0000000c`9856e6d0  00000000`00000001 00000000`00000001
    
    2: kd> db 0000014f`f4deb740
    0000014f`f4deb740  5c 00 3f 00 3f 00 5c 00-43 00 3a 00 5c 00 57 00  \.?.?.\.C.:.\.W.
    0000014f`f4deb750  69 00 6e 00 64 00 6f 00-77 00 73 00 5c 00 73 00  i.n.d.o.w.s.\.s.
    0000014f`f4deb760  79 00 73 00 74 00 65 00-6d 00 33 00 32 00 5c 00  y.s.t.e.m.3.2.\.
    0000014f`f4deb770  50 00 49 00 4e 00 47 00-2e 00 45 00 58 00 45 00  P.I.N.G...E.X.E.
    

    Below is ProcessParameters:

    2: kd> dq 14f`f4df28e0
    0000014f`f4df28e0  000006f0`000006f0 00000000`00000001
    0000014f`f4df28f0  00000000`00000048 00000000`00000000
    0000014f`f4df2900  00000000`00000050 00000000`00000054
    0000014f`f4df2910  00000000`00000058 00000000`02080020
    0000014f`f4df2920  0000014f`f4df2d20 00000000`00000000
    0000014f`f4df2930  00000000`00000000 00000000`00000000
    0000014f`f4df2940  00000000`003a0038 0000014f`f4df2f28
    0000014f`f4df2950  00000000`0020001e 0000014f`f4df2f68
    

    ProcessParameters+0x68 gives you the same info:

    2: kd> db 14f`f4df2f28
    0000014f`f4df2f28  43 00 3a 00 5c 00 57 00-69 00 6e 00 64 00 6f 00  C.:.\.W.i.n.d.o.
    0000014f`f4df2f38  77 00 73 00 5c 00 73 00-79 00 73 00 74 00 65 00  w.s.\.s.y.s.t.e.
    0000014f`f4df2f48  6d 00 33 00 32 00 5c 00-50 00 49 00 4e 00 47 00  m.3.2.\.P.I.N.G.
    0000014f`f4df2f58  2e 00 45 00 58 00 45 00-00 00 00 00 00 00 00 00  ..E.X.E.........
    

    Pointer at ProcessParameters+0x78 gives you the entire command line of the process that is started (as given in cmd.exe console window):

    0000014f`f4df2f68  70 00 69 00 6e 00 67 00-20 00 20 00 31 00 32 00  p.i.n.g. . .1.2.
    0000014f`f4df2f78  37 00 2e 00 30 00 2e 00-30 00 2e 00 31 00 00 00  7...0...0...1...
    

    In the script I decided to simply go for ProcessParameters +0x68. You 'll need to change the offsets or parameter if you want the full command line.

    The script is very chaty, but this is helpful to understand what's going on (also execution stops if the parent process is cmd.exe, but you can change that).

    Output sample after loading the script:

    ---- invokescript ----
    NtCreateUserProcess: 0xfffff80248a145a0
    Press "g" to run the target.
    0: kd> g
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    [not CMD]
    ... // yeah, a lot of process are started on a Windows machine...
        // now starting ping on the machine (in cmd.exe)
    Process spawned by CMD!
    RSP: 0xffffa384c47d8448
    ProcessParamAddr (on stack): 0xffffa384c47d8490
    procParam: 0x17bf1d00e90
    procPathAddr: 0x17bf1d00ef8
    procPath: 0x17bf1d014d8
    CMD Spawned the following process: C:\Windows\system32\PING.EXE
    Breakpoint 0 hit
    // ...
    // starting calc from cmd.exe
    Process spawned by CMD!
    RSP: 0xffffa384c47d8448
    ProcessParamAddr (on stack): 0xffffa384c47d8490
    procParam: 0x17bf1d00e90
    procPathAddr: 0x17bf1d00ef8
    procPath: 0x17bf1d014d8
    CMD Spawned the following process: C:\Windows\system32\calc.exe
    Breakpoint 0 hit