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.
If you have written Livehunt rules on top of sandbox behavior, you have probably used 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.
Our sandboxes have always recorded the full process tree. The problem was getting it into YARA, which has no recursive types. As of this month, every file with sandbox reports that reaches Livehunt carries 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.
This post covers what is in each entry, what to keep in mind when writing rules against it, and five cases we worked through while preparing it, each with the rules we wrote and tested. Every case comes with at least one hash, so you can paste the rule in the Livehunt editor and test it yourself.
If you haven't read it yet, we recommend our earlier post It's all about the structure! Creating YARA rules by clicking. It shows how to open the Structure view of any file in the Livehunt editor and turn fields into rule conditions with a click, which is handy for everything that follows.
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.
The fields that change how you write rules are 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".
The simplest possible rule looks like this:
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
Names are not normalized. Each sandbox writes process names its own way. CAPE gives the quoted command line, Zenbox prepends the image path to the command line, Tencent HABO gives only the executable name, and C2AE and Microsoft Sysinternals often leave environment variables unexpanded (%ComSpec% /c ...). Use icontains and match on the executable name, not on a full path with == or startswith.
Link processes by name, not by PID. A file is usually run by several sandboxes, and Livehunt puts all their processes in the same list. PID 1040 can be 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.
A practical workflow is to search first and write the rule second. Intelligence search already supports 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
Sigma's Suspicious MSHTA Child Process (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.
The Livehunt version maps 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"
)
)
}
One thing does not carry over. Sigma also checks OriginalFileName to catch renamed binaries, and sandbox process names have no equivalent. A renamed mshta.exe will get past this rule.
We tested it against 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:
Excel is not the parent of 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"
)
}
The first entry of 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.
The same rule matches other spreadsheets from the same wave, for example 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
On a normal Windows system, 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"
)
}
The 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.
Our first version also covered 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.
The rule matches 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.
Where did the fake 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:
You can combine both fields. This rule looks for a copy named after a system process, and then for a process in the list that runs from exactly that path and has a parent:
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 != ""
)
)
}
The copy proves the file was planted, and the process entry proves it ran. We tested the rule against 30 recent files with a 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"
)
}
A plain search shows why a rule on the command line alone gets noisy. 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.
On the malicious side, 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.
Notice that the rule checks for --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
Spotting 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
)
)
)
}
This is the first rule in the post that uses 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.
Back to 77da8a02d71078564409863c358a544e981b9643c8f50c1daabc5d5d176e3c75. In the CAPE report, software.exe writes rhpmx63snl.exe and xd24mgasgb4.exe to %APPDATA%\MicrosoftWorker_e08ec640d\, and then runs:
When the task fires, the scheduler starts 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.
Zenbox saw the same task fire, but reported 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.
One more format issue we ran into. 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
Loaders often inject the final payload into a signed .NET Framework binary such as 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")
)
)
}
All three conditions apply to the same process: what it is, what is above it, and what it wrote. That is the difference from the flat fields. behaviour_files:"Login Data" together with behavior_processes:"MSBuild.exe" can match a file where those two things happened in unrelated processes.
The rule matches 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.
Checking the ancestors instead of only the parent matters in 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
Everything in this post was tested two ways. First, against the hashes above and against a set of clean files, using the Livehunt rule tester. The tester rebuilds the 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
A command line tells you what ran. Its parent and ancestors tell you how it got there, and the files it wrote tell you what it left behind. Rules that join the two are harder for a sample to sidestep, because the operator would need to change the chain, not just the strings. Open the vt module view of any file you are investigating, find the processes_list entry that bothers you, and write the rule from there.
VirusTotal has incorporated a powerful new tool to fight against
malware: JA4 client fingerprinting. This feature allows
security researchers to track and identify malicious files based
on the unique characteristics of their TLS client communications.
JA4: A More Robust Successor to JA3
JA4,
developed by
FoxIO, represents a significant
advancement over the older JA3 fingerprinting method. JA3's
effectiveness had been hampered by the increasing use of TLS
extension randomization in https clients, which made
fingerprints
less consistent. JA4 was specifically designed to be
resilient to this randomization, resulting in more stable and
reliable fingerprints.
Unveiling the Secrets of the Client
Hello
JA4 fingerprinting focuses on
analyzing the
TLS Client Hello packet, which is sent unencrypted from
the client to the server at the start of a TLS connection.
This packet contains a treasure trove of information that can
uniquely identify the client application or its underlying
TLS library. Some of the key elements extracted by JA4
include:
TLS
Version: The version of TLS supported by the
client.
Cipher
Suites: The list of cryptographic algorithms the client can
use.
TLS
Extensions: Additional features and capabilities supported
by the client.
ALPN
(Application-Layer Protocol Negotiation): The
application-level protocol, such as HTTP/2 or HTTP/3, that
the client wants to use after the TLS
handshake.
JA4 in Action: Pivoting and Hunting on
VirusTotal
VirusTotal has integrated JA4
fingerprinting into its platform through the behavior_networkfile
search modifier. This allows analysts to quickly
discover relationships between files based on their JA4
fingerprints.
To find the JA4 value, navigate to the "behavior" section of
the desired sample and locate the TLS subsection. In addition
to JA4, you might also find JA3 or JA3S there.
Example Search: Let's say you've encountered a suspicious
file that exhibits the JA4 fingerprint
"t10d070600_c50f5591e341_1a3805c3aa63" during VirusTotal's
behavioral analysis.
You can click on this JA4 to pivot using the
search query
behavior_network:t10d070600_c50f5591e341_1a3805c3aa63
finding other files with the same fingerprint This search
will pivot you to additional samples that share the same JA4
fingerprint, suggesting they might be related. This could
indicate that these files are part of the same malware family
or share a common developer or simply share a common TLS
library.
Returns files that match the
JA4_A and JA4_C components of the JA4 hash while allowing
for variations in the middle section, which often corresponds
to the cipher suite. This technique is useful for identifying
files that might use different ciphers but share other JA4
characteristics.
YARA hunting rules using the
"vt" module can be written to
automatically detect files based on their JA4 fingerprints.
Here's an example of a YARA rule that targets a specific JA4
fingerprint:
This rules will flag any file submitted to VirusTotal that
exhibits the matching JA4 fingerprint. The first example only
matches "t12d190800_d83cc789557e_7af1ed941c26" during
behavioral analysis. The second rule will match a regular
expression /t10d070600_.*_1a3805c3aa63/, only matching JA4_A
and JA4_C components, excluding the JA4_B cipher suite. These
fingerprints could be linked to known malware, a suspicious
application, or any TLS client behavior that is considered
risky by security analysts.
VirusTotal's adoption
of JA4 client fingerprinting will provide users with an
invaluable tool for dissecting and tracking TLS client
behaviors, leading to enhanced threat hunting, pivoting, and
more robust malware identification.
Earlier last week, I ran into a sample that turned out to be PureCrypter, a loader and obfuscator for all different kinds of malware such as Agent Tesla and RedLine.
Upon further investigation, I developed Yara rules for the various stages, which can be found here (excluding the final payload):
With that out of the way, all of this reminded me of the fact that we can also write Yara rules for unique identifiers specific to malware written in .NET, or any other .NET assemblies for that matter.
A bit of history
This isn’t my first encounter with analysing .NET malware at scale: several years ago, I co-authored a presentation with Santiago on hunting SteamStealer malware, which was surging exponentially at the time (the malware intended to steal your Steam inventory items and/or your account). A huge thanks goes to Brian Wallace who had developed a tool at the time called GetNetGUIDs which made it trivial to extract all the GUID types and start clustering to identify patterns: basically, which of the malware samples are likely authored by the same person or belong to the same attack campaign.
.NET assemblies or binaries often contain all sorts of metadata, such as the internal assembly name and GUIDs, specifically; the MVID and TYPELIB.
GUID: Also known as the TYPELIB ID, generated when creating a new project.
MVID: Module Version ID, a unique identifier for a .NET module, generated at build time.
TYPELIB: the TYBELIB version – or number of the type library (think major & minor version).
These specific identifiers can be parsed with the strings command and a simple regular expression (regex): [a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}
Taking a sample of PureLogStealer posted by James_in_the_box, you could then write a Yara rule based on the MVID or Typelib detected.
The MVID is stored as a binary value rather than a string, whereas the Typelib GUID is effectively stored as a string and since we only have the MVID here, the sample above will not be detected with this rule.
It is important to note that VirusTotal does not seem to report the Typelib.
It is cumbersome to “do it the manual way” with strings and regex, especially on larger data sets – and it’s prone to issues such as:
false positives:
if you run "strings" on the sample and then use the following CyberChef recipe – we get plenty of GUIDs, but only 1 is the actual Typelib;
false negatives:
we miss out on unique identifiers, which means we might miss detection of samples, campaigns or actors.
Note that with tools such as IlSpy or dnSpy(Ex), you can also view the Typelib GUID and MVID, however, not all tools display all data, for example:
Figure 2 - dnSpy detects the Typelib GUID of the sample
And if we go the "oldschool" route using ildasm:
Figure 3 - ildasm displays the MVID or Module Version ID
For all the above reasons, let’s go beyond and do more: both with Yara, and with a new Python tool I’ve created.
The now and the tooling
Before we dive into the tooling, some final history to say that Yara has evolved and thanks to that, we can now hunt and detect more effectively due to the following modules added:
Let’s now leverage the power of Yara and its dotnet and console modules to write a new Yara rule that displays useful data of any given .NET sample that can be leveraged to create meaningful rules, for example: assembly name, typelib and MVID.
Figure 5 - Yara rule to display .NET information to the console
We first verify if the binary is a .NET compiled file, if so, log certain Portable Executable (PE) or binary information to the console as well, and then display all relevant .NET information.
And the output will be, again for the same sample:
Figure 6 - Yara rule output: sample metadata!
Meaning we can now write a rule as follows:
import "dotnet"
rule PureLogStealer_GUID
{
condition:
dotnet.guids[0]=="9066ee39-87f9-4468-9d70-b57c25f29a67" or
But what if we want to run this on a large set of samples and produce statistics, which we can then use to hunt or classify malware families, or cluster campaigns?
A newly developed Python tool will help you do exactly just that. It supports both a single file as well as a whole folder of your samples or malware repository. It will skip over any non-.NET binary and simply report the typelib, MVID and typelib ID (if present, which is seldom the case and rarely useful).
If we run it on our single sample like before:
Figure 7 - New tool output on single sample
The tool (or script) has the following capabilities:
Figure 8 - Run the tool with -h to display usage or help
You need Python 3, pythonnet and a compiled dnlib.dll in order for it to work.
You are of course not limited to just using the MVID or Typelib for .NET malware hunting: you can also use the assembly name and other features that could be unique, using either the Yara rule or the Python tool to extract the data you’d like.
I highly recommend to use the tool rather than the Yara rule, as it detects .NET metadata more reliably. Both Yara rule and Python tool can be adapted to display less or more information according to your needs.
Clustering
Tracking attacker’s campaigns is always an exercise, and can be both fun and exhausting, depending on how many rabbit holes you (want to) go through. An example of clustering campaigns as well as malware developers was done in the work I did with Santiago as mentioned earlier, which resulted in the following graphics:
Figure 9 - Statistics from 2016 research (bonus obfuscation stats)
This was a pretty large dataset (1.300 samples!) and specific to SteamStealers at the time.
For our analysis purposes, I took 4 of the most current popular malware (that are .NET based or have at least a .NET variant) according to Any.run’s Malware Trends: https://any.run/malware-trends/. These are:
RedLine
Agent Tesla
Quasar
Pure*: basically anything related to PureCrypter, PureLogs, …
Downloading the latest available samples per family from MalwareBazaar, then running my DotNetMetadata Python script, and playing around with pandasand matplot, we can create the following graphs per family:
RedLine – 56 samples
Figure 10 - RedLine Typelib GUID frequency
Figure 11 -RedLine MVID frequency
Agent Tesla – 140 samples
Figure 12 - Agent Tesla Typelib GUID frequency
Figure 13 -Agent Tesla MVID frequency
Quasar – 141 samples
Figure 14 - Quasar Typelib GUID frequency
Figure 15 -Quasar MVID frequency
Pure* family - 194 samples
Figure 16 - Pure* Typelib GUID frequency
Figure 17 -Pure* MVID frequency
While these piecharts are certainly hypnotic and display the frequency - or occurrence of the same typelib or MVID, we can also leverage these and create meaningful Yara rules for clustering samples per family, especially in the case of Quasar, the MVID with GUID "60f5dce2-4de4-4c86-aa69-383ebe2f504c" appears like a good candidate.
You might think that while these charts look visually appealing (depending on your art preferences), they may not be particularly useful because they don't scale well with larger datasets. You’re exactly right! By limiting the amount of results displayed, we can indeed produce even better results. In our sample dataset for the 4 malware families above, so a total of 531 samples, let’s run our visualisations again and now we will:
Run it on the whole sample set
Extract the assembly name
List only the top 10 of assembly names
Use a bar chart instead of a pie
And the result:
Figure 18 - Assembly name frequency - looking better right?
The top 3 is then:
“Client”: Quasar family
“Product Design 1”: Pure family
“Sample Design 1”: Pure family
Client is likely the default assembly name when compiling the Quasar malware (project), and Product Design and Sample Design are likely default assembly names from the PureCrypter builder.
If we then want to write a Yara rule for Quasar based on the default assembly name:
import "dotnet"
rule Quasar_AssemblyName
{
condition:
dotnet.assembly.name == "Client"
}
But why stop there? We can build a Yara rule to classify our malware dataset or repository:
import "dotnet"
import "console"
rule DotNet_Malware_Classifier
{
condition:
(dotnet.assembly.name == "Client" and console.log(“Likely Quasar, assembly name: ", dotnet.assembly.name)) or
(dotnet.assembly.name == "Product Design 1" and console.log("Likely Pure family, assembly name: ", dotnet.assembly.name)) or
(dotnet.assembly.name == "Sample Design 1" and console.log("Likely Pure family, assembly name: ", dotnet.assembly.name))
}
And we run this new Yara rule on the combined samples of the Pure family and Quasar:
Figure 19 - Simple "malware classifier"
We can combine sets of Yara rules bases on assembly name, Typelib, MVID and so on to create rules with a higher confidence, and we can use this in further hunting, classification and... much more.
Bonus
If you’ve made it this far, it only makes sense to add in an additional extra use-case for all of this: finding new crypters or obfuscators!
When I ran the script on the +500 samples, there was 1 assembly / binary that stood out:
4 matches in 12 weeks: it appears this crypteris not popular (yet): 2 Async RAT samples and 2 PovertyStealer samples have used it so far.
Bonus on Bonus
Let’s go with a final bonus round: improving the previous “classification” rule by also reviewing results for Async RAT. Seeing the previous crypter was used on at least 2 Async RAT samples, I wanted to see some statistics for this malware as well, for just the assembly name. This results in the following, based on 86 samples:
Figure 22 - Another pie chart: AsyncRat top used assembly names
Jumping out are the following assembly names:
AsyncClient
Client --> Also seen in Quasar!
XClient
Output
Loader
Stub
AsyncClient is likely the default name when building the Async RAT project. But we are interested in widening the net: from the previous rule DotNet_Malware_Classifier, let’s update it with these new “generic” or default assembly names:
import "dotnet"
import "console"
rule DotNet_Malware_Classifier
{
condition:
(dotnet.assembly.name == "Client" and console.log("Suspicious assembly name: ", dotnet.assembly.name)) or
(dotnet.assembly.name == "Output" and console.log("Suspicious assembly name: ", dotnet.assembly.name)) or
(dotnet.assembly.name == "Loader" and console.log("Suspicious assembly name: ", dotnet.assembly.name)) or
(dotnet.assembly.name == "Stub" and console.log("Suspicious assembly name: ", dotnet.assembly.name))
}
Figure 23 - Classifier Yara rule results
Conclusion
In this blog post, two new tools were presented to extract metadata from .NET malware samples. Specifically, we can now reliably extract 2 unique GUIDs: the Typelib and the MVID.
The Python script is capable of extracting the desired data from a large set of .NET assemblies, whereas the Yara rule is tailored for use with one particular sample. Of course, either of them can be used interchangeably: you can still fine-tune the Yara rule for a large set and work this way if you don’t want to rely on an external script. Similarly, the script can be extended to extract more data to be used.
Based on the output of these tools, you can then create Yara hunting rules, combine it with your existing rule sets, or use them in an attempt to classify malware families or specific attack campaigns.
Some closing remarks:
GUIDs could be spoofed or even removed.
No method is 100% reliable.
However, this method can enhance already existing rulesets, especially those where .NET obfuscators (e.g. SmartAssembly) obfuscate (user) strings, modules and more, making it harder to write Yara rules for a malware family.
Detecting based on GUID however, can work regardless of obfuscation method.
That said, obfuscating or deobfuscating may also alter the GUIDs.
Keep this in mind when creating your detection rules based on an original or unpacked/deobfuscated sample.
If you encounter a GUID comprised entirely of zeros, such as 00000000-0000-0000-0000-000000000000, avoid using it for hunting since it's an empty GUID.
This indicates the value may not be set or has been altered.
This would make for a poor hunting rule as it can be a default value for any .NET project.
You can also use this methodology and tooling for .NET assemblies that are not malicious:
extract developer information and other metadata per your use case or purpose.
The Python tool in addition, just as the Yara rule, allows for analysing, classifying and hunting on much more .NET (meta)data.
FARA, or Faux YARA, is a simple repository that contains a set of purposefully erroneous Yara rules. It is meant as a training vehicle for new security analysts, those that are new to Yara and even Yara veterans that want to keep their rule writing (and debugging) sharp.
There's two workflows running on that Github repository:
YARA-CI: runs automatically to detect signature errors, as well as false positives and negatives.
Package Yara rules:
allows download of a complete rules file (all Yara rules from this repo
in one file) for convenience from the Actions tab > Artifacts (see
image below).
The Yara rules are divided into:
APT
Crimeware
Generic
Hacktools
Ransomware
Furthermore, the rules can work natively with AssemblyLine due to the CCCS Yara rule standard adoption.
Kyle Avery // Introduction This post compliments a presentation I gave at DEF CON 30 – “Avoiding Memory Scanners: Customizing Malware to Evade YARA, PE-sieve, and More,” which included the […]