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.
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.
c:\tmp\windbg_script.js
) on your host..scriptload
.g
).---- invokescript ----
printed in your debugger output.cmd.exe
and start whatever process you want.cmd.exe
the script will print some information and stop the debugger.[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