Process trees in Livehunt: YARA rules that know who launched what

Note:
YARA-X v1.21.0 adds a parent_name field to the items in vt.behaviour.processes_list, and v1.22.0 adds ancestor_names. VirusTotal now fills that list for every sandbox report that includes a process tree, by flattening it. The result is one entry per process, and each entry knows the name of the process that created it and of the processes above it. Read the YARA-X blog.vt.behaviour.processes_created. It is a flat list of command lines. It tells you that mshta.exe ran and that powershell.exe ran, but not that one started the other. For a lot of detections that is the whole point: PowerShell on its own is noise, PowerShell launched by mshta.exe is not.vt.behaviour.processes_list: one entry per process, from every sandbox that ran the file, and each entry knows the name of its parent, the names of its ancestors, and the files it touched.What each process entry carries
| Field | Type | What it holds |
|---|---|---|
name |
string | The process, as the sandbox reports it. Usually the image path plus the command line. |
parent_name |
string | The name of the process that created this one. Empty for root processes and when the parent is unknown. |
ancestor_names |
array of strings | The names of its ancestors, nearest first: ancestor_names[0] is the parent (the same as parent_name), ancestor_names[1] the grandparent, and so on. Empty for root processes and when the parent is unknown. |
process_id / parent_process_id |
string | PIDs, as reported by the sandbox. |
start_time / termination_time |
integer | When the process started and ended, if known. |
files_written |
array of strings | Files this process created or modified. |
files_opened |
array of strings | Files this process opened. |
files_deleted |
array of strings | Files this process deleted. |
files_copied[x].source / .destination |
string | Files this process copied, when the sandbox attributes copies to a process. |
parent_name, ancestor_names and the per-process file lists. With them, “PowerShell wrote a .dll” becomes “the PowerShell that cmd.exe started wrote a .dll“.import "vt"
rule cmd_spawns_schtasks_create {
condition:
for any p in vt.behaviour.processes_list : (
p.name icontains "schtasks" and
p.name icontains "/create" and
p.parent_name icontains "cmd.exe"
)
}
Things to know before you write rules
%ComSpec% /c ...). Use icontains and match on the executable name, not on a full path with == or startswith.cmd.exe in one report and explorer.exe in another. To go more than one level up, use ancestor_names: it is built from each sandbox report on its own, so a chain never mixes processes from different sandboxes. For long chains, you can also climb by name: look for the entry whose name is equal to the parent_name you already have. Both strings come from the same sandbox report, so an exact == is safe there.behavior_processes:"<command line>", which matches anywhere in the list with no hierarchy. It is a quick way to collect candidates. Then you write the rule with the parent condition, and test it against those hashes in the Livehunt editor.Case 1: porting a Sigma rule, and what it misses
03cc0c25-389f-4bf8-b48d-11878079f1ca) is a good example of a detection that needs the parent. Stripped down, it says: the parent image ends with \mshta.exe and the child is a shell or a scripting host.detection:
selection_parent:
ParentImage|endswith: '\mshta.exe'
selection_child:
- Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\pwsh.exe'
- '\wscript.exe'
- '\cscript.exe'
- '\sh.exe'
- '\bash.exe'
- '\reg.exe'
- '\regsvr32.exe'
- '\bitsadmin.exe'
condition: all of selection*
ParentImage to parent_name and Image to name:import "vt"
rule mshta_spawns_shell {
meta:
description = "Port of Sigma 03cc0c25 (Suspicious MSHTA Child Process)"
condition:
for any p in vt.behaviour.processes_list : (
p.parent_name icontains "mshta.exe" and
(
p.name icontains "cmd.exe" or
p.name icontains "powershell" or
p.name icontains "pwsh" or
p.name icontains "wscript" or
p.name icontains "cscript" or
p.name icontains "bash.exe" or
p.name icontains "reg.exe" or
p.name icontains "regsvr32" or
p.name icontains "bitsadmin"
)
)
}
OriginalFileName to catch renamed binaries, and sandbox process names have no equivalent. A renamed mshta.exe will get past this rule.d30a0a6c09f664446943d82982f0630e85c447cbe84be8a74b6db6d66effbe80, an Excel file named NOVI USD 44570.00 952PLA3262852510.xls (18/76, tagged cve-2017-0199). It matches. What is more interesting is the CAPE process tree behind the match:| Process | Parent |
|---|---|
EXCEL.EXE "...\attachment.xls" /dde |
Explorer.EXE |
mshta.exe -Embedding |
svchost.exe -k DcomLaunch -p |
cmd.exe "/c POwERSHeLl.Exe ... -EX BypaSS -nOp -w 1 -ec ..." |
mshta.exe -Embedding |
POwERSHeLl.Exe ... -ec ... |
cmd.exe |
WScript.exe "...\AppData\Roaming\westingcostringforme.vbe" |
POwERSHeLl.Exe |
powershell.exe -nop -ep bypass -file "C:\Temp\XBRYINEZ.ps1" |
WScript.exe |
aspnet_compiler.exe |
powershell.exe -file ...XBRYINEZ.ps1 |
mshta.exe. The exploit has Office request an object through DCOM, so mshta.exe is started by the DCOM launcher service, and it shows up with -Embedding. A rule that looks for “Office spawns mshta” would never fire on this document. What we can do is walk one level up from the shell and check the grandparent:import "vt"
rule dcom_mshta_spawns_shell {
condition:
for any p in vt.behaviour.processes_list : (
(p.name icontains "cmd.exe" or p.name icontains "powershell") and
p.parent_name icontains "mshta.exe" and
p.ancestor_names[1] icontains "svchost.exe" and
p.ancestor_names[1] icontains "dcomlaunch"
)
}
ancestor_names is always the parent, so ancestor_names[1] is the grandparent: the process that started mshta.exe. Without it, you need a second loop over the list to find the entry whose name is the shell’s parent_name, and one more loop for every extra level.8d249972358268b7a00e182e3f1ecf93ec8bbbee65d22ef4d8a3c382783c1cfa (PO#3302-20260520002.xls), 2d199776cc50b9ad548132105e95af86b29ec8ebca03895a7ddecde58587306b (Request Quotation.xls) and 7c8a3c8366b3106db56359e3defc654d365ce4de21d0a7b1fbad02fbb2aeae52. The rest of that chain shows up again in cases 4 and 5.Case 2: a services.exe that is not, and the copies behind it
services.exe is started by wininit.exe and by nothing else. A services.exe with any other parent deserves a look:import "vt"
rule fake_services_wrong_parent {
condition:
for any p in vt.behaviour.processes_list : (
p.name iendswith "\\services.exe" and
p.parent_name != "" and
not p.parent_name icontains "wininit.exe"
)
}
parent_name != "" check matters. In the reports we looked at, the real services.exe shows up as a root process with no parent, and we don’t want those to match.lsass.exe. We tested it against 43 recent, clean, signed files, and it matched 19 of them. In those reports the sandbox records lsass.exe with services.exe as its parent. Whatever the reason, that is what the data says, so we dropped lsass.exe from the rule. The lesson is a general one: before encoding what the parent “should” be, check what the sandboxes actually report for clean files.fe8357febe314becf31510a7a2ecb0369e312ad58a448292f5f8a09075d7694c, a .NET executable (45/76) submitted as Cd5KK.exe. In the Jujubox report, Cd5KK.exe starts C:\Windows\System32\NlsLexicons0c1a\services.exe. That process writes two .vbs files to %TEMP% and runs each one with WScript.exe, and one of the scripts starts the fake services.exe again. The sample also launches one PowerShell per path to add Defender exclusions, and in the CAPE report it creates ONLOGON scheduled tasks through WMI, with wmiprvse.exe as the parent of schtasks.exe.services.exe come from? Jujubox does not attribute file copies to a process, but it does report them at the summary level, in vt.behaviour.files_copied. Cd5KK.exe copied itself to eight places, each one with the name of a Windows binary:C:\Windows\System32\NlsLexicons0c1a\services.exe C:\System Volume Information\services.exe C:\Windows\system\smss.exe C:\Windows\System32\mfc42\taskeng.exe C:\Users\Default User\WmiPrvSE.exe C:\Program Files\Common Files\System\ado\en-US\WmiPrvSE.exe C:\PerfLogs\Admin\sysmon.exe C:\Users\<USER>\Downloads\patchregistry\event-stream.exe
import "vt"
rule copy_with_system_name_is_executed {
condition:
for any c in vt.behaviour.files_copied : (
(
c.destination iendswith "\\services.exe" or
c.destination iendswith "\\smss.exe" or
c.destination iendswith "\\lsass.exe" or
c.destination iendswith "\\csrss.exe" or
c.destination iendswith "\\svchost.exe" or
c.destination iendswith "\\wmiprvse.exe" or
c.destination iendswith "\\taskhostw.exe" or
c.destination iendswith "\\conhost.exe"
) and
for any p in vt.behaviour.processes_list : (
p.name icontains c.destination and
p.parent_name != ""
)
)
}
services.exe path in their Jujubox file activity: it matched 9 of them, among them 4930cc4e7e3e258bf2815793f0e5698bdd45adfae8e052bb1dc4c05c6689ade6, which shares the trojan.msil/basic label with Cd5KK.exe and also matches the services.exe parent rule above. It did not match any of the 43 clean files.Case 3: headless consoles, renamed or not
conhost.exe --headless runs a console program without showing a window. Sigma has a rule that looks for --headless and powershell in the conhost.exe command line. With the parent available, we can ask a slightly different question: did a PowerShell process actually start with a headless console as its parent?import "vt"
rule headless_console_runs_powershell {
condition:
for any p in vt.behaviour.processes_list : (
(p.name icontains "powershell" or p.name icontains "pwsh") and
p.parent_name icontains "--headless"
)
}
behavior_processes:"conhost.exe --headless" fs:2026-09-01+ returned 947 files when we ran it. Part of that is legitimate software. Terminal emulators use pseudo-consoles, which start conhost.exe --headless --inheritcursor ... --server .... herdr.exe (c18555a6bb74cd021198b57d16b93b40e74e7d0f8f0c28a45f0a4cc22dea0f8b, 0/76) does exactly that, and the rule above does not match it, because that conhost.exe never starts a shell.5c2ceaec797dfa79e81cde6d765898ee8102357e280fa322db6e5a4b0ce3fc4a is a Windows shortcut with a Korean-language lure about penalty guidelines for e-commerce law violations, named with a double .hwp.lnk extension (34/76). CAPE and Jujubox both show the same chain: opening the shortcut starts conhost.exe --headless powershell.exe -NoP -exec BYpass -w 1 -C $rukp = '...', and that console starts the PowerShell that runs the Base64 blob. The decoded script opens with a function called Invoke-EnvironmentCheck.--headless in the parent, not for conhost.exe. That is deliberate. 77da8a02d71078564409863c358a544e981b9643c8f50c1daabc5d5d176e3c75 (42/76) drops a file called rhpmx63snl.exe into %APPDATA%\MicrosoftWorker_e08ec640d\. That file is 51bb5aeef2e48dd14fae764f80dc67f516df9914ea9c350329e229f6cea919a2, an unmodified Microsoft conhost.exe (original name CONHOST.EXE, 0/77) with a random name. It later runs as rhpmx63snl.exe --headless powershell.exe -w h -ep bypass -c & "...\xd24mgasgb4.exe". A rule that required conhost.exe in the parent would miss it. If you only want the renamed case, add not p.parent_name icontains "conhost".Case 4: persistence that actually ran
schtasks /create is easy. Knowing whether the task did anything during the analysis is harder, and the process list can answer it. When a scheduled task fires, the new process has the Task Scheduler service as its parent (svchost.exe -k netsvcs -p -s Schedule, or taskeng.exe on older systems). If that process runs a file the sample wrote earlier, the persistence worked.import "vt"
rule task_scheduler_runs_dropped_file {
condition:
for any run in vt.behaviour.processes_list : (
(
run.parent_name icontains "-s schedule" or
run.parent_name icontains "taskeng.exe"
) and
for any dropper in vt.behaviour.processes_list : (
for any f in dropper.files_written : (
(f iendswith ".exe" or f iendswith ".dll" or f iendswith ".vbs" or
f iendswith ".js" or f iendswith ".ps1" or f iendswith ".bat" or
f iendswith ".cmd") and
run.name icontains f
)
)
)
}
files_written, and it uses it to link two processes that are not parent and child: the one that wrote the file, and the one the scheduler started from it.77da8a02d71078564409863c358a544e981b9643c8f50c1daabc5d5d176e3c75. In the CAPE report, software.exe writes rhpmx63snl.exe and xd24mgasgb4.exe to %APPDATA%\MicrosoftWorker_e08ec640d\, and then runs:schtasks.exe /create /sc minute /mo 1 /tn "AdobeSecurityManager" /tr "C:\Users\<USER>\AppData\Roaming\MicrosoftWorker_e08ec640d\rhpmx63snl.exe --headless powershell.exe -w h -ep bypass -c & '...\xd24mgasgb4.exe'"
rhpmx63snl.exe (the renamed conhost.exe from case 3), which starts PowerShell, which starts xd24mgasgb4.exe. Every link in that sentence is a parent_name, and the rule matches. The whole chain is also in the ancestor_names of xd24mgasgb4.exe: the PowerShell, rhpmx63snl.exe, the Schedule service and services.exe.rhpmx63snl.exe as a root process with no parent. That is fine: CAPE has the link, and the rule only needs one sandbox to see it. It also shows why you shouldn’t write rules that need every sandbox to agree.a549a6c8c01da1d03e8da663eec70997a5bd5207e665d8e23f6fa75804dafc06, a VBS file we use in the next case, also creates a task that fires during the VMRay analysis, with taskeng.exe as the parent. The rule misses it, because VMRay logged that command line with doubled backslashes (C:\\Users\\Public\\Downloads\\PoItWjNFjv.vbs), so the path in files_written is not a substring of the process name. When a rule should match and does not, check the raw strings first. Here ancestor_names gives a check that does not depend on how paths are written: the task runs wscript.exe with the dropped script, the script starts PowerShell, and taskeng.exe is among that PowerShell’s ancestors. A PowerShell process with for any a in p.ancestor_names : (a icontains "taskeng.exe") catches it.Case 5: .NET binaries reading browser credentials
MSBuild.exe, RegAsm.exe, RegSvcs.exe, InstallUtil.exe or aspnet_compiler.exe. On their own, these processes are normal on a developer machine. Started by PowerShell or a scripting host, and writing temporary copies of browser credential databases, they are not.import "vt"
rule dotnet_lolbin_touches_browser_logins {
condition:
for any p in vt.behaviour.processes_list : (
(
p.name icontains "aspnet_compiler.exe" or
p.name icontains "regasm.exe" or
p.name icontains "regsvcs.exe" or
p.name icontains "msbuild.exe" or
p.name icontains "installutil.exe" or
p.name icontains "addinprocess"
) and
for any a in p.ancestor_names : (
a icontains "powershell" or
a icontains "wscript" or
a icontains "cscript" or
a icontains "mshta"
) and
for any f in p.files_written : (
f icontains "\\User Data\\" and
(f icontains "\\Login Data" or f icontains "\\Web Data" or f icontains "\\Cookies")
)
)
}
behaviour_files:"Login Data" together with behavior_processes:"MSBuild.exe" can match a file where those two things happened in unrelated processes.a549a6c8c01da1d03e8da663eec70997a5bd5207e665d8e23f6fa75804dafc06, a VBS file named RFQ_638O-0194RFQ26_202610070614034439RFQ.vbs (14/76). In the CAPE report, a hidden PowerShell that rebuilds its script from environment variables is started through WMI (its parent is wmiprvse.exe), and that PowerShell starts MSBuild.exe, which writes ...\Google\Chrome\User Data\Default\Login Data.tmp and the matching -wal and -shm files. It also matches the Excel file from case 1, d30a0a6c09f664446943d82982f0630e85c447cbe84be8a74b6db6d66effbe80, where the last process in the chain, aspnet_compiler.exe, writes the same files. In that file, the ancestor_names of aspnet_compiler.exe go all the way back to the exploit: the PowerShell running XBRYINEZ.ps1, WScript.exe, the obfuscated PowerShell, cmd.exe and mshta.exe -Embedding.2d199776cc50b9ad548132105e95af86b29ec8ebca03895a7ddecde58587306b, the Request Quotation.xls from case 1. There, the process that writes the Chrome and Edge Login Data and Web Data files is a second aspnet_compiler.exe, started with /stext by the first one. Its parent is aspnet_compiler.exe, so a rule that only looks at parent_name misses it, while the PowerShell and WScript.exe above it are its second and third ancestors. On the 420 malicious and 324 clean files we used to measure ancestor_names, this version matched 5 malicious files that the parent-only version missed, and none of the clean ones.Testing the rules
vt module for the hash you give it, so it works for files analyzed before these fields existed. Second, in a live Livehunt ruleset with all the rules. Within a few minutes of enabling it we were already getting notifications for mshta_spawns_shell, dcom_mshta_spawns_shell, task_scheduler_runs_dropped_file and others. The ancestor_names versions of the rules were also run with YARA-X 1.22.0 against the same hashes and 324 clean files.Wrapping up
vt module view of any file you are investigating, find the processes_list entry that bothers you, and write the rule from there.Continue reading Process trees in Livehunt: YARA rules that know who launched what