0. Preface
belovav3.pages.dev presents itself as a game download site. The download is BelovaV3.rar; unpacking it yields an NSIS installer, BelovaV3.exe. The installer is 118 MB and the thing it installs is a 177 MB Electron application — a size profile that reads as “big game client” to a victim and does a good job of burying a 13 MB payload.
The interesting part of this sample is that almost none of the executable weight is malicious. The Electron binary is a stock, unmodified Electron 30.5.1 runtime — no overlay, no patched sections. The malicious logic sits three layers in: a bytenode-compiled JavaScript blob inside app.asar spawns a bundled Temurin 17 JRE against dev.jar, and dev.jar is a heavily obfuscated Java infostealer with a WebSocket C2.
Two findings are worth the writeup. First, the obfuscation stack: homoglyph class names, invokedynamic-based call hiding through MethodHandle tables, integer opaque predicates, and PBKDF2 + AES string encryption. This defeated Vineflower entirely (zero files emitted) and cost jadx 391 method bodies. Second, and more useful operationally: the payload carries an environment guard that calls System.exit(0) within one second of launch, so on an ordinary analysis VM it is completely silent. The C2 only surfaced after the guard was neutered at runtime.
Static work was done on Linux — jadx, javap, hand-written pcap/pcapng parsers in Python. Dynamic work was a detonation in an isolated FLARE-VM on a VirtualBox internal network (--nic1 intnet, no route to the internet, host-side hypervisor packet trace rather than in-guest Wireshark), reverted to a clean snapshot afterwards. No packet reached the real C2.
C2, up front: sk.bumblemovies.com (primary, retried in bursts of five every ~10 s) and kn.devmicro7.workers.dev (Cloudflare Workers fallback, typosquatting “devmicro”), reached over a java.net.http.WebSocket with a trust-all X509TrustManager.
1. The Threat at a Glance
| Attribute | Assessment |
|---|---|
| Malware type | Electron-wrapped dropper → obfuscated Java infostealer / RAT |
| Delivery | belovav3.pages.dev, fake game download → BelovaV3.rar → NSIS installer |
| Masquerade | Legitimate-looking Electron game client with a Swing decoy UI |
| Payload | dev.jar (13,511,030 bytes), run by a bundled Temurin 17.0.19 JRE |
| C2 | sk.bumblemovies.com, kn.devmicro7.workers.dev (WebSocket, TLS validation disabled) |
| Target | Windows x64; Chromium browsers (DPAPI + App-Bound Encryption), Firefox (NSS) |
| Capability | Browser credential/cookie theft, keylogging, file grabbing, process injection, named-pipe IPC |
| Evasion | Bytenode V8 bytecode, JS string-array obfuscation, homoglyph names, invokedynamic dispatch, opaque predicates, PBKDF2+AES strings, sub-second environment guard, working-directory self-deletion |
| Persistence | None observed — no Run keys, no Startup folder, no scheduled tasks |
| Attribution | Unconfirmed. Mutex zarox_run.lock and a license-20260831213428-63aa marker suggest a commercial builder |
| Confidence | High on capability and C2; medium on the builder identity |
2. Infection Chain Overview
belovav3.pages.dev (fake game download)
└─ BelovaV3.rar
└─ BelovaV3.exe NSIS installer, 118 MB
├─ extracts %TEMP%\nsh*.tmp\7z-out\resources\dev.jar
└─ installs %LOCALAPPDATA%\Programs\l12ozg\
└─ BelovaV3.exe stock Electron 30.5.1, unsigned
├─ resources\app.asar
│ └─ hnk53zcu6w.jsc bytenode V8 bytecode
├─ copies payload to %LOCALAPPDATA%\meth\dev.jar
└─ javaw.exe -jar %LOCALAPPDATA%\meth\dev.jar
└─ obfuscated Java stealer → WebSocket C2
3. Stage 0 - The Electron Binary Is Not the Malware
The first job was to rule the 177 MB PE in or out. diec reports PE64 / Microsoft Visual C/C++ / Chromium WebView / Squirrel, and the section layout (LZMADEC, CPADinfo, malloc_h, prot) is stock Chromium. The version string confirms it:
Chrome/124.0.6367.243 Electron/30.5.1
The decisive check is the overlay. A dropper commonly appends its payload past the last section, so:
last section ends : 177307136 (0xa917e00)
file size : 177307136
overlay bytes : 0
cert dir rva/size : 0x0 0 # unsigned
Zero overlay and no Authenticode signature. The binary is an unmodified Electron runtime — decompiling it would have been 177 MB of Chromium for nothing. The program is the ASAR.
4. Stage 1 - app.asar and the Bytenode Blob
resources/app.asar is only 522 KB and contains seven files:
490400 /hnk53zcu6w.jsc
64 /jptwzu.js
170 /package.json
1068 /node_modules/bytenode/LICENSE
522 /node_modules/bytenode/package.json
5988 /node_modules/bytenode/lib/cli.js
22378 /node_modules/bytenode/lib/index.js
package.json names the app with a random string — the same string that later becomes the install directory:
{
"name": "l12ozg",
"version": "1.0.0",
"description": "BelovaV3",
"author": "BelovaV3",
"main": "jptwzu.js",
"dependencies": { "bytenode": "^1.6.0" }
}
And the entire entry point is three lines:
'use strict';
require('bytenode');
require('./hnk53zcu6w.jsc');
bytenode compiles JavaScript to V8 bytecode, so there is no readable source to recover. The constant pool still leaks intent, though. Alongside spawn, __TextDecoder, __Uint8Array and utf8ArrayToStr, it contains a series of shuffled 92-character alphabets — the signature of a javascript-obfuscator string-array layer applied before bytenode compilation:
|YdQiLATGUnkeXap8B7%+CcRf63x)/zW>`rE2S?Z{(O=Vw~[M#0hFHq.9tJgP1y5:D]o$4"NIv!Kl&,^mbuj*}s@<_;
PZA${)xLrnGcB[kCi<.Yw,qOf+Q=8HgXK(%#dNja?F6"p}~vzDSI]:Jl*Tots^VE_beUm9R1W7M`@45|0&u/>y;32!h
together with evalmachine.<anonymous>, indicating a second stage is eval’d at runtime. So the JS layer is obfuscated twice over and then compiled to bytecode. Rather than fight it, I let Procmon tell me what it does — see §10.
5. Stage 2 - dev.jar
resources/dev.jar is a 13.5 MB fat JAR. The manifest immediately signals the obfuscator:
Manifest-Version: 1.0
Main-Class: lIiiIiIlill.IIillIiliIil.Ililllililil.Iiilliiliiil.liiiIIllIl
Every identifier is drawn from I/l/i — homoglyph naming, which renders class names visually indistinguishable. There are 53 application classes under lIiiIiIlill/, against 3,658 files total; the rest is bundled library code, and that library list is the first real intelligence:
| Library | What it implies |
|---|---|
com.sun.jna + jna-platform (win32) | Direct Win32 API access, DPAPI |
org.sqlite (sqlite-jdbc) | Reading browser Login Data / Cookies databases |
com.neovisionaries.ws.client + java.net.http.WebSocket | C2 transport |
com.google.gson | JSON C2 protocol |
okhttp3 | HTTP exfiltration (addFormDataPart — multipart upload) |
javax.swing / Darklaf | The decoy “loader” GUI the victim sees |
Two plaintext config files sit in the archive at a/b/:
a/b/l.txt : license-20260831213428-63aa
a/b/m.txt : 1549151212446945280
m.txt is a 19-digit value that decodes cleanly as a Discord snowflake:
1549151212446945280 >> 22 + 1420070400000
→ 2026-09-14 20:13:50 UTC
which lands on the same day as the JAR’s own build timestamps (a/b/*.txt dated 2026-09-14, classes 2026-09-16). A per-build identifier created at build time. l.txt reads as a license key with an embedded date — this is a licensed builder, not bespoke tooling.
6. The Obfuscation Stack
Individual class files run 1–2.7 MB each, which is pure obfuscator bloat. Three layers are stacked.
Integer opaque predicates. Vineflower managed exactly one small class before dying; this is its output for a trivial X509TrustManager, and it is representative of the whole JAR:
public class IiIlliIliiIl implements X509TrustManager {
public static void IilliliilIIi() {
Object[] var3 = new Object[3];
int var2 = (1460101906 | -217252251) - (1460101906 & -217252251);
switch (var2 + -217252251 - (var2 & -217252251) * 2) {
case 1460101906:
default:
var3[2] = 62197;
var3[1] = 62198;
iIlIlllllill = new int[(var3[1] | var3[2]) & (~var3[1] | ~var3[2])];
...
Every constant is reconstructed through XOR/AND identities, and control flow is flattened into switch dispatch. Note also what this class is: a trust manager, which turns out to matter in §9.
invokedynamic call hiding. Every real call is routed through a MethodHandle table resolved at runtime, keyed by UUID. jadx recovers this shape:
public class iliIiiliiiii extends Structure {
public /* bridge */ /* synthetic */ List getFieldOrder() {
return (List) lIiiIiIlilli(MethodHandles.lookup(),
"08fcafb1-a90e-4d11-89ff-6a1542faa529",
MethodType.methodType(List.class, Object[].class, String.class, Integer.TYPE, ...))
.dynamicInvoker().invoke(strArr,
"nJKmlFyipKCWWX2rsJ6prg==", IIIllIIliIIl[21], "k6SAnKmj", ...) /* invoke-custom */;
}
}
The JAR contains thousands of those UUIDs as bootstrap keys. Static call-graph reconstruction is effectively dead.
PBKDF2 + AES string encryption. All literals are stored as base64 ciphertext and decrypted through a routine taking multiple blobs plus integer keys:
decrypt("07Gszs2zt78=", "qb/LvXGwurS+eqTGxbu/t5E7RkZPTzs=",
"Z3eVtLzOunezt7uzeqHBwLhCSHc=", -1624294166, 79, "VUhhMWJr")
I initially tried to break this statically. Two ciphertexts of equal length XOR to a uniform 0x01, which looks like a weak keystream, but no constant-key, positional (k±i), or offset scheme produced plaintext across 3,171 candidates. The reason showed up only at runtime: the decryptor resolves javax.crypto.SecretKeyFactory, PBEKeySpec, javax.crypto.Cipher and java.util.Base64. It is PBKDF2-derived AES, so there was never a static shortcut.
Decompiler results, honestly: Vineflower produced zero files (Irreducible statement too complex to be decomposed, then a 16.8 GB heap blowup). jadx emitted all 53 classes but failed 391 method bodies across 47 classes (944 errors). Class signatures and interfaces are complete; obfuscated method bodies largely are not.
7. The Capability Surface
Because JNA resolves native functions by their real exported names, Library interfaces cannot be renamed by the obfuscator. They survive intact and hand over the entire capability set. This is the single highest-yield artifact in the sample.
Firefox credential theft — the textbook NSS path for decrypting logins.json against key4.db:
public interface IilliliilIIi extends Library {
int PK11SDR_Decrypt(iliIiiliiiii data, iliIiiliiiii result, Pointer cx);
void SECITEM_FreeItem(iliIiiliiiii item, int freeit);
int NSS_Init(Pointer configdir);
int NSS_Shutdown();
}
The iliIiiliiiii parameter type is a JNA Structure with fields int p; Pointer q; int r; and a getFieldOrder() — i.e. NSS’s SECItem { type, data, len }.
Chromium App-Bound Encryption bypass — CNG calls used to unwrap the ABE key on Chrome v127+:
public interface IliIlllilIi extends Library {
int NCryptOpenStorageProvider(PointerByReference phProvider, WString pszProviderName, int flags);
int NCryptOpenKey(Pointer hProvider, PointerByReference phKey, WString pszKeyName, int spec, int flags);
int NCryptDecrypt(Pointer hKey, byte[] pbInput, int cbInput, Pointer pPaddingInfo,
byte[] pbOutput, int cbOutput, WinDef.DWORDByReference pcbResult, int flags);
int NCryptFreeObject(Pointer hObject);
}
Keylogging with window context — keystrokes plus the title of the window being typed into:
public interface liiiIIllIl extends Library {
short GetAsyncKeyState(int vKey);
boolean GetKeyboardState(byte[] lpKeyState);
Pointer GetKeyboardLayout(int idThread);
int ToUnicode(int wVirtKey, int wScanCode, byte[] state, char[] buf, int cch, int flags);
int ToUnicodeEx(int wVirtKey, int wScanCode, byte[] state, char[] buf, int cch, int flags, Pointer hkl);
int MapVirtualKeyW(int uCode, int uMapType);
Pointer GetForegroundWindow();
int GetWindowTextW(Pointer hWnd, char[] lpString, int nMaxCount);
int GetWindowThreadProcessId(Pointer hWnd, Pointer lpdwProcessId);
boolean SystemParametersInfoW(int uiAction, int uiParam, WString pvParam, int fWinIni);
}
Process injection and named-pipe IPC — a complete remote-injection chain:
public interface IIIllIIliIIl extends Library {
Pointer VirtualAllocEx(HANDLE hProcess, Pointer addr, SIZE_T size, int allocType, int protect);
boolean WriteProcessMemory(HANDLE hProcess, Pointer addr, Pointer buf, int n, IntByReference written);
boolean VirtualProtectEx(HANDLE hProcess, Pointer addr, SIZE_T size, DWORD newProt, DWORDByReference oldProt);
HANDLE CreateRemoteThread(HANDLE hProcess, Pointer sa, int stack, Pointer start, Pointer param, int flags, Pointer tid);
boolean CreateProcessW(String app, String cmd, Pointer pa, Pointer ta, boolean inherit,
DWORD flags, Pointer env, String cwd, STARTUPINFO si, PROCESS_INFORMATION pi);
HANDLE CreateNamedPipeW(String name, int openMode, int pipeMode, int maxInst, int outBuf, int inBuf, int timeout, Pointer sa);
boolean ConnectNamedPipe(HANDLE hNamedPipe, OVERLAPPED overlapped);
boolean PeekNamedPipe(HANDLE h, byte[] buf, int size, IntByReference read, IntByReference avail, IntByReference left);
boolean TerminateProcess(HANDLE hProcess, int exitCode);
int GetLastError();
}
with SeDebugPrivilege acquisition alongside it:
public interface liiiIIllIl extends Library {
int RtlAdjustPrivilege(int privilege, boolean enable, boolean currentThread, ByteByReference enabled);
}
Runtime type references fill in the rest: Crypt32Util (DPAPI CryptUnprotectData), Tlhelp32 / CreateToolhelp32Snapshot (process enumeration), two SimpleFileVisitor recursive walkers with java.util.regex.Pattern (the file grabber), and javax.swing.JFrame / Darklaf (the decoy UI).
Note that sqlite-jdbc appears nowhere in the app classes’ constant pools — library access goes through a reflective Class.forName(decryptedName) helper, which is why it only shows up as a dropped sqlitejdbc.dll at runtime.
8. The Environment Guard
Detonated as-is under a string-dumping agent, the payload emitted 118 strings and exited. The last symbols it resolved before dying were:
getenv
equalsIgnoreCase
getProperty
toLowerCase
contains
java.nio.file.Paths
java.nio.file.Files
exists
exit
(I)V
An environment check, then System.exit. To locate the call site without fighting the obfuscation, I installed a SecurityManager whose checkExit logs a stack trace and throws instead of exiting, so execution continues past the guard:
System.setSecurityManager(new SecurityManager() {
@Override public void checkExit(int status) {
out.println("=== BLOCKED System.exit(" + status + ") ===");
new Exception("exit call site").printStackTrace(out);
throw new SecurityException("exit blocked by analysis agent");
}
@Override public void checkPermission(java.security.Permission perm) { }
});
Run with -Djava.security.manager=allow, this pinned the guard exactly:
=== BLOCKED System.exit(0) ===
java.lang.Exception: exit call site
at StringDumper$2.checkExit(StringDumper.java:26)
at java.base/java.lang.Runtime.exit(Unknown Source)
at java.base/java.lang.System.exit(Unknown Source)
at lIiiIiIlill.IlIlllIlilIl.IiIlliIliiIl.IIlllIlliIll.Illlllllilll.Iilllilliill.IliIlllilIi.IliIlllilIi(Unknown Source)
at lIiiIiIlill.IIillIiliIil.Ililllililil.Iiilliiliiil.liiiIIllIl.main(Unknown Source)
main calls the guard directly. With the exit blocked, the string dump grew from 118 to 624 entries and the sample proceeded to its network stage.
9. Instrumentation
The string dumper is a ByteBuddy agent that hooks every method in the application package returning a String — this sidesteps having to identify which method is the decryptor:
public static void premain(String args, Instrumentation inst) throws Exception {
out = new PrintStream(new FileOutputStream(path, true), true, "UTF-8");
new AgentBuilder.Default()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.type(ElementMatchers.nameStartsWith("lIiiIiIlill"))
.transform((b, td, cl, m, pd) -> b.visit(
Advice.to(Dump.class).on(
ElementMatchers.returns(String.class)
.and(ElementMatchers.not(ElementMatchers.isAbstract())))))
.installOn(inst);
}
public static class Dump {
@Advice.OnMethodExit(suppress = Throwable.class)
static void exit(@Advice.Origin("#t.#m") String origin, @Advice.Return Object ret) {
if (ret instanceof String) StringDumper.log(origin, (String) ret);
}
}
Two practical notes for anyone reproducing this. Write the log outside the malware’s working directory — on the first run the sample deleted its entire directory, taking the log with it. And prefer hypervisor-level packet capture (VBoxManage modifyvm --nictrace1 on --nictracefile1) over in-guest Wireshark: the sample enumerates processes via CreateToolhelp32Snapshot, and a host-side pcap is also beyond reach of the self-delete.
With the guard bypassed, the dump confirmed the transport — newWebSocketBuilder, buildAsync, java.net.URI, javax.net.ssl.SSLContext, com.google.gson.JsonObject, plus okhttp3 addFormDataPart and zip-entry handling for staging stolen data.
10. Detonation - C2 and Host Artifacts
Detonated on an isolated internal network with a host-side trace, the beacons appeared:
| DNS query | Count | Pattern |
|---|---|---|
sk.bumblemovies.com | 45 | bursts of 5, repeating every ~10–11 s — primary |
kn.devmicro7.workers.dev | 5 | single burst — Cloudflare Workers fallback |
No TCP SYN or TLS SNI was captured, because nothing on the isolated network answers DNS and resolution fails before a socket opens. The hostnames are the recoverable IOC; the WebSocket protocol body was not observed.
Worth flagging for anyone continuing this: the trust-all X509TrustManager from §6 means TLS interception requires no certificate work at all — a DNS responder plus mitmproxy would yield the full C2 protocol in cleartext.
Host artifacts dropped during execution:
%TEMP%\zarox_run.lock 4 bytes - single-instance mutex
%TEMP%\sqlite-3.45.1.0-<guid>-sqlitejdbc.dll SQLite JDBC native, extracted at runtime
%TEMP%\jna-<id>\ JNA native dispatch
%APPDATA%\l12ozg\Local State Chromium master key, stolen
That last one is the clearest single piece of evidence. The sample copied Chromium’s Local State into its own directory:
{"os_crypt":{"audit_enabled":true,"encrypted_key":"RFBBUEkBAAAA0Iyd3wEV0RGMegDAT8KX6wEAAAAUEI6Z9TeOQ7n9ml4pJx/qEAAAABIAAABDAGgAcgBvAG0AaQB1AG0AAAAQZgAAAAEAACAAAACcUTsuNKPIFihzVeL9YCFTuuxCQCgaVmDljFfc6QNowAAAAA...
RFBBUEkB base64-decodes to DPAPI\x01 — the DPAPI blob header. That is the browser master key staged for offline decryption.
The name zarox_run.lock is the only internal string that looks like a family label; nothing else in the sample corroborates it, so treat it as a lead rather than an attribution.
11. Installation Telemetry
A separate Procmon capture (1,190,918 events) and pcapng of the installation phase pinned the dropper behaviour precisely.
The NSIS installer extracts the payload and the Electron app launches it:
9:57:47 explorer.exe → C:\Users\<user>\Desktop\BelovaV3.exe
9:57:50 BelovaV3.exe → writes %TEMP%\nsh6FEC.tmp\7z-out\resources\dev.jar (EndOfFile: 13,511,030)
9:58:00 explorer.exe → %LOCALAPPDATA%\Programs\l12ozg\BelovaV3.exe
9:58:01 BelovaV3.exe → creates %LOCALAPPDATA%\meth\ (+ a second full JRE under meth\java\bin\)
9:58:02 BelovaV3.exe → javaw.exe -jar C:\Users\<user>\AppData\Local\meth\dev.jar
The extracted dev.jar is byte-identical in size to the analysed sample. Two details stand out: the payload is relocated to a separate directory away from the install path, and it is launched with javaw.exe, not java.exe — windowless, so no console ever appears.
The most useful result here is a negative one. The installation pcapng contains no C2 traffic whatsoever — only Microsoft and Google update endpoints. Procmon explains exactly why:
javaw.exe PID | Events | Lifetime | Network ops |
|---|---|---|---|
| 4508 | 2,577 | 0.91 s | 0 |
| 7384 | 2,195 | 0.84 s | 0 |
| 4484 | 2,301 | 0.76 s | 0 |
Three launches, each dead in under a second, zero network operations. That is the §8 guard firing. On an unmodified analysis machine this sample never contacts its C2 — which is precisely why the guard bypass was necessary, and why a naive sandbox run would report it as benign.
Searching the full 1.19 M-event log for persistence found nothing: no Run/RunOnce writes, no Startup folder entries, no scheduled tasks. The sample relies on the victim relaunching the “game” — consistent with the three separate explorer.exe-initiated launches in the capture.
12. Indicators of Compromise
Network
sk.bumblemovies.com primary C2 (WebSocket)
kn.devmicro7.workers.dev fallback C2 (Cloudflare Workers)
belovav3.pages.dev distribution site
Files
BelovaV3.rar distributed archive
BelovaV3.exe 0b03649bb73745afab576b32799c9999e34ae50c3beba3ea2cc54c54bfac2cb0 (NSIS installer, 118,836,987 b)
BelovaV3.exe 776d4168bd190aa08c45537d6edd09ce4503fd0c57204935501e7e89f062235c (Electron runtime, 177,307,136 b)
resources\app.asar dc2fa0df6ed545d86a2fc771da06df4f1c906cde1ceffc8e4665a7255431e2df
hnk53zcu6w.jsc 59a9346129dbfd8fb3cc83206b7a35cb9f1bfa0992d527c41e0b4dd5443662f9
resources\dev.jar bc3da0956cb3b2ad5ba5bab94d51f0c5a6ced2d3d15bcf0f4fe951955c321a11 (13,511,030 b)
Paths
%LOCALAPPDATA%\Programs\l12ozg\ install directory
%LOCALAPPDATA%\meth\dev.jar relocated payload
%LOCALAPPDATA%\meth\java\ second bundled JRE
%APPDATA%\l12ozg\ Electron user-data; holds stolen "Local State"
%TEMP%\nsh*.tmp\7z-out\ NSIS staging
%TEMP%\zarox_run.lock single-instance mutex
Execution
javaw.exe -jar %LOCALAPPDATA%\meth\dev.jar
Build markers
license-20260831213428-63aa a/b/l.txt - builder licence, dated 2026-08-31
1549151212446945280 a/b/m.txt - Discord snowflake, created 2026-09-14 20:13:50 UTC
13. Detection Opportunities
javaw.exespawned by an Electron application. The parent/child pair*\Programs\<random>\*.exe→resources\java\bin\javaw.exe -jaris unusual for legitimate desktop software and is the strongest single signal here.- A
-jartarget outside the application directory. The payload runs from%LOCALAPPDATA%\meth\while the app lives in%LOCALAPPDATA%\Programs\l12ozg\. Legitimate bundled-JRE apps run their own JAR. Local Statecopied out of a browser profile. Any process reading…\User Data\Local Stateand writing it elsewhere is staging for DPAPI/ABE decryption.sqlitejdbc.dlldropped to%TEMP%by a non-developer process, paired with reads ofLogin Data/Cookies.- Mutex
zarox_run.lockin%TEMP%. - NSS symbol use by a non-browser process —
NSS_Init/PK11SDR_Decryptagainst a Firefox profile. - Two bundled JREs on disk under one user profile is itself anomalous.
Note that network-based detection alone will miss this sample in a sandbox, for the reason in §11.
14. MITRE ATT&CK Mapping
| Tactic | Technique | Detail |
|---|---|---|
| Initial Access | T1189 Drive-by Compromise | Fake game download site |
| Execution | T1204.002 User Execution: Malicious File | Victim runs the NSIS installer |
| Execution | T1059.007 Command and Scripting: JavaScript | Bytenode-compiled Electron main |
| Defense Evasion | T1027 Obfuscated Files or Information | Bytenode, string-array JS, PBKDF2+AES strings, opaque predicates |
| Defense Evasion | T1027.009 Embedded Payloads | dev.jar inside the Electron bundle |
| Defense Evasion | T1497 Virtualization/Sandbox Evasion | Environment guard, sub-second System.exit(0) |
| Defense Evasion | T1070.004 Indicator Removal: File Deletion | Self-deletes its working directory |
| Defense Evasion | T1055 Process Injection | VirtualAllocEx / WriteProcessMemory / CreateRemoteThread |
| Privilege Escalation | T1134 Access Token Manipulation | RtlAdjustPrivilege (SeDebugPrivilege) |
| Credential Access | T1555.003 Credentials from Web Browsers | Chromium DPAPI + App-Bound Encryption, Firefox NSS |
| Credential Access | T1056.001 Input Capture: Keylogging | GetAsyncKeyState / ToUnicodeEx with window titles |
| Discovery | T1057 Process Discovery | CreateToolhelp32Snapshot |
| Collection | T1005 Data from Local System | Recursive SimpleFileVisitor grabbers with regex filters |
| Command and Control | T1071.001 Application Layer Protocol: Web | WebSocket over TLS, validation disabled |
| Command and Control | T1090 Proxy | Cloudflare Workers fallback endpoint |
| Exfiltration | T1041 Exfiltration Over C2 Channel | OkHttp multipart upload |
15. Closing Notes
The engineering effort in this sample is concentrated almost entirely in not being analysed: four obfuscation layers, a runtime that hides every call behind invokedynamic, and a guard that makes the sample inert on the machines most likely to be watching it. The capability itself is conventional infostealer work.
The practical lesson is that the JNA Library interfaces gave up the entire capability set for free. The obfuscator could rename all 53 classes and encrypt every string, but it could not rename PK11SDR_Decrypt or NCryptOpenStorageProvider without breaking the native binding. When a Java sample is too obfuscated to decompile, the native boundary is where to look first.
Open items. The WebSocket protocol body was never observed — a DNS responder plus mitmproxy against the trust-all X509TrustManager would close that. The PBKDF2+AES string decryptor was not reimplemented; hooking it at runtime was sufficient here, but a static decryptor would make the remaining 391 undecompiled method bodies far more tractable.