Digital Forensics & Incident Response
NVISO analyzes BRICKSTORM espionage backdoor
By Maxime
April 15, 2025
Backdoor
Report
UNC5221

NVISO recently identified new information related to BRICKSTORM, a backdoor linked to the China-nexus cluster UNC5221. Through its Digital Forensics & Incident Response activities, NVISO observed BRICKSTORM's usage as part of an active espionage campaign targeting European industries since at least 2022.

Over the past years, the number of persistent intrusions attributed to China-nexus actors has drastically increased. Numerous reports of compromises affecting both critical infrastructure as well as industries of strategic interest outline the continuous efforts deployed by the PRC. As opposed to the more common extortion-driven intrusions, PRC-associated intrusions have been observed to employ a high degree of discretion, enabling the actors to remain undetected for extended duration. To achieve this strategic long-term placement, PRC-attributed intrusions increasingly involve the usage of previously unknown vulnerabilities (a.k.a., zero-days) alongside low-noise backdoors such as the hereafter documented BRICKSTORM family. PRC-aligned actors furthermore distinguish themselves through the effective targeting of network appliances and subsequently achieved persistent access.

The PRC’s cyber operations are amongst the most active offensive programs, backed by a diverse network of military, state and state-aligned operators. The PRC’s focus on espionage operations has long been linked to China’s political strategy which considers the strengthening of their economy as a matter of national security. To this end, PRC operations are known to heavily involve the theft of intellectual property (IP) and trade secrets pertaining to China’s long-term strategic goals to further develop the manufacturing sector and establish itself as a technology-intensive leader.

NVISO and its partners provide the detailed BRICKSTORM analysis to highlight employed adversary techniques and infrastructure. NVISO's BRICKSTORM report further provides insights into the operator's capabilities ranging from file theft to full remote access. Extending on Mandiant's previous Linux sighting, NVISO's identification of BRICKSTORM's long-running usage in Windows environments outlines the diversity of targeted environments. The report highlights defensive measures employed by the backdoor to avoid detection, such as the abuse of legitimate cloud services and repeated nested encryption of network communications.

Analyze the backdoor with NVISO's experts

Read the BRICKSTORM report!

As part of its mission to protect, NVISO provides the following detection & hunting rules in an actionable format. More information about these rules can be found in BRICKSTORM's report.

YARA detection rule for BRICKSTORM's Windows backdoor
rule NVISO_BACKDOOR_BRICKSTORM {
  meta:
    description = "Detects the BRICKSTORM backdoor Windows executables"
    author      = "NVISO"
    created     = "2024-11-25"
    md5         = "8af1c3f39b60072d4b68c77001d58109"
    md5         = "c65d7f8accb57a95e3ea8a07fac9550f"
    license     = "Detection Rule License (DRL) 1.1"
    reference   = "https://nviso.eu/blog/nviso-analyzes-brickstorm-espionage-backdoor"

  strings:
    $lib1   = "wsshell/core/task.DoTask" ascii wide
    $lib2   = "wssoft/core/task.DoTask"  ascii wide
    $wss    = "wss://"       ascii wide
    $go     = "/golang.org/" ascii wide
    $doh01  = "https://1.0.0.1/dns-query"  ascii wide
    $doh02  = "https://1.1.1.1/dns-query"  ascii wide
    $doh03  = "https://8.8.4.4/dns-query"  ascii wide
    $doh04  = "https://8.8.8.8/dns-query"  ascii wide
    $doh05  = "https://9.9.9.9/dns-query"  ascii wide
    $doh06  = "https://9.9.9.11/dns-query" ascii wide
    $doh07  = "https://45.90.28.160/dns-query"    ascii wide
    $doh08  = "https://45.90.30.160/dns-query"    ascii wide
    $doh09  = "https://149.112.112.11/dns-query"  ascii wide
    $doh10  = "https://149.112.112.112/dns-query" ascii wide
    $cmd1   = "/get-file" ascii wide
    $cmd2   = "/put-file" ascii wide
    $cmd3   = "/slice-up" ascii wide
    $cmd4   = "/file-md5" ascii wide

  condition:
    uint16be(0) == 0x4D5A 
    and any of ($lib*)
    and any of ($doh*)
    and any of ($cmd*)
    and $wss and $go
}
Suricata detection rule for BRICKSTORM's active Command & Control server

alert tls $HOME_NET any -> $EXTERNAL_NET any (msg:"[NVISO] Observed BRICKSTORM CnC Domain (ms-azure .azdatastore .workers .dev in TLS SNI)"; flow:established,to_server; tls.sni; bsize:32; content:"ms-azure.azdatastore.workers.dev"; fast_pattern; reference:url,nviso.eu/blog/nviso-analyzes-brickstorm-espionage-backdoor; classtype:domain-c2; sid:1; rev:1; metadata:attack_target Server_Endpoint, created_at 2025_03_25, deployment Perimeter, performance_impact Low, confidence High, signature_severity Critical, malware_family BRICKSTORM;)

Kusto (KQL) hunting rule for rare long-running processes with network connections
let Lookback = 30d;                 //Parameters:
let ProcessAge = 10d;               // The minimal age of the running process
let URLThreshold = 2;               // The limit of contacted URLs
let LocalPrevalenceThreshold = 5;   // The limit of internal sightings
let GlobalPrevalenceThreshold = 20; // The limit of world-wide sightings
// Identify long-running processes performing successful public network connections
DeviceNetworkEvents
| where Timestamp > ago(Lookback)
    and isnotempty(InitiatingProcessSHA256)
    and RemoteIPType == "Public"
    and ActionType == "ConnectionSuccess"
    and InitiatingProcessCreationTime < Timestamp-ProcessAge
| summarize
    DeviceCount=dcount(DeviceId),
    DeviceNames=make_set(DeviceName, LocalPrevalenceThreshold),
    IPCount=dcount(RemoteIP),
    URLCount=dcountif(RemoteUrl, isnotempty(RemoteUrl)),
    arg_max(Timestamp, *)
    by InitiatingProcessSHA256
// Where the executables have rarely been seen publicly
| where URLCount <= URLThreshold and DeviceCount <= LocalPrevalenceThreshold
| as IntermediaryResult
| where assert(toscalar(IntermediaryResult | count) <= 1000, "Too many matches for FileProfile")
| invoke FileProfile("InitiatingProcessSHA256", 1000)
| where GlobalPrevalence <= GlobalPrevalenceThreshold
// Order the results by priority
| project-reorder
    Timestamp,
    DeviceNames,
    GlobalPrevalence,
    InitiatingProcessFolderPath,
    InitiatingProcessCommandLine,
    RemoteIP,
    RemoteUrl,
    SHA256,
    IPCount,
    URLCount
| order by GlobalPrevalence asc, URLCount asc, IPCount desc
Kusto (KQL) hunting rule for unsigned system processes interacting with Cloudflare
let Lookback = 30d;
// Define Cloudflare IPs (see https://www.cloudflare.com/ips-v4/#)
let Cloudflare = datatable(Range: string)[
    "173.245.48.0/20" ,   "103.21.244.0/22" ,    "103.22.200.0/22",   "103.31.4.0/22"  ,
    "141.101.64.0/18" ,   "108.162.192.0/18",    "190.93.240.0/20",   "188.114.96.0/20",
    "197.234.240.0/22",   "198.41.128.0/17" ,    "162.158.0.0/15" ,   "104.16.0.0/13"  ,
    "104.24.0.0/14"   ,   "172.64.0.0/13"   ,    "131.0.72.0/22"];
// Identify system processes making Cloudflare connections
DeviceNetworkEvents
| where Timestamp > ago(Lookback) and InitiatingProcessAccountName =~ "System"
| evaluate ipv4_lookup(Cloudflare, RemoteIP, Range)
| summarize Count=count()
    by
    DeviceId,
    DeviceName,
    InitiatingProcessFolderPath,
    InitiatingProcessFileName,
    InitiatingProcessSHA256,
    InitiatingProcessSHA1
// Where the file is not signed (based on Defender telemetry)
| join kind=leftanti (DeviceFileCertificateInfo | where Timestamp > ago(Lookback))
    on $left.InitiatingProcessSHA1 == $right.SHA1
| summarize
    Devices=dcount(DeviceId),
    Count=sum(Count),
    InitiatingProcessFolderPath=make_list(InitiatingProcessFolderPath),
    DeviceName=make_list(DeviceName),
    InitiatingProcessFileName=make_list(InitiatingProcessFileName)
    by InitiatingProcessSHA256, InitiatingProcessSHA1
// Where the file is not signed (based on Microsoft telemetry) and uncommon
| as IntermediaryResult
| where assert(toscalar(IntermediaryResult | count) <= 1000, "Too many matches for FileProfile")
| invoke FileProfile("InitiatingProcessSHA1", 1000)
| where SignatureState == "Unsigned" and GlobalPrevalence < 50000
| mv-expand DeviceName, InitiatingProcessFolderPath, InitiatingProcessFileName
| project-reorder DeviceName, InitiatingProcessFolderPath, SHA256, SHA1, Count
| order by Count desc
By Maxime
April 15, 2025
Backdoor
Report
UNC5221

Get supportinfo@nviso.eu

Belgium
Guimardstraat 8 b6 1040 Brussels +32 2 318 58 31
Germany
Holzgraben 5 60313 Frankfurt am Main Machtlfinger Str. 21 81379 München +49 69 9675 8554
Austria
Gumpendorfer Straße 19-21 1060 Wien+43 1358 0084
Greece
Feidiou 9 10678 Athens+30 211 955 7637