❌

Normal view

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

27 October 2026 at 14:12
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.
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*
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:
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
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:
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
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:
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'"
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.
Happy hunting!

Unveiling Hidden Connections: JA4 Client Fingerprinting on VirusTotal

18 October 2024 at 11:48
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_network file 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.

Wildcard Searches

To broaden your search, you can use wildcards within the JA4 hash. For instance, the search: behaviour_network:t13d190900_*_97f8aa674fd9

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: Automating JA4-Based Detection

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.



JA4: Elevating Threat Hunting on VirusTotal

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.

Happy Hunting.

Analyse, hunt and classify malware using .NET metadata

By: Bart
25 March 2024 at 20:13

Introduction

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.

As shown on VirusTotal for this sample:

A screen shot of a computer

Description automatically generated
Figure 1 - Sample with MVID 9066ee39-87f9-4468-9d70-b57c25f29a67

And the resulting (simple) Yara rule, could then be as follows:

rule PureLogStealer_GUID

{

strings:

$mvid = "9066ee39-87f9-4468-9d70-b57c25f29a67" ascii wide fullword

condition:

$mvid

}

There are however some issues with this: 

  • 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:

A screenshot of a computer program

Description automatically generated
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:

  • 2017: introduction of .NET module (link)

  • 2022: introduction of console module (link)

This means that using the .NET module, we can now write a Yara rule like so instead:

import "dotnet"

rule PureLogStealer_GUID

{

condition:

dotnet.guids[0]== "9066ee39-87f9-4468-9d70-b57c25f29a67"

}

And indeed:

Figure 4 - Yara now detects the sample

Yara rule

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. 

A screenshot of a computer code

Description automatically generated
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:

A computer screen shot of a computer program

Description automatically generated
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

dotnet.typelib=="856e9a70-148f-4705-9549-d69a57e669b0"

}

Python tool

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:

A computer code with white text

Description automatically generated
Figure 7 - New tool output on single sample


The tool (or script) has the following capabilities:

A screen shot of a computer program

Description automatically generated
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.
Both the Yara rule and the Python tool are published on the following GitHub page: https://github.com/bartblaze/DotNet-MetaData 

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:

A screenshot of a graph

Description automatically generated
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 pandas and matplot, we can create the following graphs per family:



RedLine – 56 samples

A pie chart with colorful circles

Description automatically generated
Figure 10 - RedLine Typelib GUID frequency


A colorful circular chart with numbers and numbers

Description automatically generated
Figure 11 -RedLine MVID frequency


Agent Tesla – 140 samples

A pie chart with numbers and a number

Description automatically generated
Figure 12 - Agent Tesla Typelib GUID frequency



A circular pattern with different colors

Description automatically generated with medium confidence
Figure 13 -Agent Tesla MVID frequency





Quasar – 141 samples


A pie chart with colorful circles

Description automatically generated
Figure 14 - Quasar Typelib GUID frequency



A pie chart with different colored circles

Description automatically generated
Figure 15 -Quasar MVID frequency




Pure* family - 194 samples 


A diagram of a pie chart

Description automatically generated
Figure 16 - Pure* Typelib GUID frequency



A circular pattern with different colors

Description automatically generated with medium confidence
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:

A bar chart with blue squares

Description automatically generated
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:

A screenshot of a computer

Description automatically generated
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:

A cartoon of a bathtub

Description automatically generated
Figure 20 - Potential new crypter "Cronos"

Making a simple Yara rule again:

import "dotnet"

rule cronos_crypter

{

strings:

$cronos = "Cronos-Crypter" ascii wide nocase

condition:

dotnet.is_dotnet and $cronos

}


Running this on the Unpac.me dataset yields:

A screenshot of a computer

Description automatically generated
Figure 21 - Unpac.me Yara hunt results


4 matches in 12 weeks: it appears this crypter is 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:

A pie chart with different colored circles

Description automatically generated
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))

}




A screenshot of a computer

Description automatically generated
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.

     

Happy .NET hunting! You can find the tools and some of the example Yara rules in the repository: https://github.com/bartblaze/DotNet-MetaData 

As always, feedback is welcomed.


Fara: Faux YARA

By: Bart
4 December 2023 at 20:09

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.


Example "faux" rule


Find it over on Github:

https://github.com/bartblaze/FARA 


Yara rules collection

By: Bart
10 December 2022 at 16:20

Quite a while ago, I've published some of my private Yara rules online, on Github.

They can be found here:

https://github.com/bartblaze/Yara-rules

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).

image

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.

PR's are welcome where you see fit. 

Avoiding Memory Scanners

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 […]

The post Avoiding Memory Scanners appeared first on Black Hills Information Security, Inc..

❌