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

AttributeAssessment
Malware typeElectron-wrapped dropper → obfuscated Java infostealer / RAT
Deliverybelovav3.pages.dev, fake game download → BelovaV3.rar → NSIS installer
MasqueradeLegitimate-looking Electron game client with a Swing decoy UI
Payloaddev.jar (13,511,030 bytes), run by a bundled Temurin 17.0.19 JRE
C2sk.bumblemovies.com, kn.devmicro7.workers.dev (WebSocket, TLS validation disabled)
TargetWindows x64; Chromium browsers (DPAPI + App-Bound Encryption), Firefox (NSS)
CapabilityBrowser credential/cookie theft, keylogging, file grabbing, process injection, named-pipe IPC
EvasionBytenode V8 bytecode, JS string-array obfuscation, homoglyph names, invokedynamic dispatch, opaque predicates, PBKDF2+AES strings, sub-second environment guard, working-directory self-deletion
PersistenceNone observed — no Run keys, no Startup folder, no scheduled tasks
AttributionUnconfirmed. Mutex zarox_run.lock and a license-20260831213428-63aa marker suggest a commercial builder
ConfidenceHigh 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:

LibraryWhat 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.WebSocketC2 transport
com.google.gsonJSON C2 protocol
okhttp3HTTP exfiltration (addFormDataPart — multipart upload)
javax.swing / DarklafThe 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 queryCountPattern
sk.bumblemovies.com45bursts of 5, repeating every ~10–11 s — primary
kn.devmicro7.workers.dev5single 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 PIDEventsLifetimeNetwork ops
45082,5770.91 s0
73842,1950.84 s0
44842,3010.76 s0

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

  1. javaw.exe spawned by an Electron application. The parent/child pair *\Programs\<random>\*.exe → resources\java\bin\javaw.exe -jar is unusual for legitimate desktop software and is the strongest single signal here.
  2. A -jar target 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.
  3. Local State copied out of a browser profile. Any process reading …\User Data\Local State and writing it elsewhere is staging for DPAPI/ABE decryption.
  4. sqlitejdbc.dll dropped to %TEMP% by a non-developer process, paired with reads of Login Data / Cookies.
  5. Mutex zarox_run.lock in %TEMP%.
  6. NSS symbol use by a non-browser process — NSS_Init / PK11SDR_Decrypt against a Firefox profile.
  7. 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

TacticTechniqueDetail
Initial AccessT1189 Drive-by CompromiseFake game download site
ExecutionT1204.002 User Execution: Malicious FileVictim runs the NSIS installer
ExecutionT1059.007 Command and Scripting: JavaScriptBytenode-compiled Electron main
Defense EvasionT1027 Obfuscated Files or InformationBytenode, string-array JS, PBKDF2+AES strings, opaque predicates
Defense EvasionT1027.009 Embedded Payloadsdev.jar inside the Electron bundle
Defense EvasionT1497 Virtualization/Sandbox EvasionEnvironment guard, sub-second System.exit(0)
Defense EvasionT1070.004 Indicator Removal: File DeletionSelf-deletes its working directory
Defense EvasionT1055 Process InjectionVirtualAllocEx / WriteProcessMemory / CreateRemoteThread
Privilege EscalationT1134 Access Token ManipulationRtlAdjustPrivilege (SeDebugPrivilege)
Credential AccessT1555.003 Credentials from Web BrowsersChromium DPAPI + App-Bound Encryption, Firefox NSS
Credential AccessT1056.001 Input Capture: KeyloggingGetAsyncKeyState / ToUnicodeEx with window titles
DiscoveryT1057 Process DiscoveryCreateToolhelp32Snapshot
CollectionT1005 Data from Local SystemRecursive SimpleFileVisitor grabbers with regex filters
Command and ControlT1071.001 Application Layer Protocol: WebWebSocket over TLS, validation disabled
Command and ControlT1090 ProxyCloudflare Workers fallback endpoint
ExfiltrationT1041 Exfiltration Over C2 ChannelOkHttp 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.