Skip to content
Go back

Field Notes

CryBound JAR Basic: A Fabric Mod With a Memory-Only Loader

CryBound JAR Basic is a client-side Fabric loader (modid=crycry, Minecraft 1.21.11) distributed as StarBoundRuins. Its roughly 17 MB JAR contains about 110 KB of loader code across 17 classes, with no game assets, data/, or mixins. Vineflower 1.10.1, CFR, javap -v, and crybound_extract.py analysis show Windows fingerprinting, an HMAC-authenticated C2 session, chunked encrypted delivery, and five-layer in-memory class loading. Stage two was not recovered; browser theft remains a hypothesis.

Fabric entrypoint to remote pack

The reported delivery is a 31.6 MB modpack with ten legitimate mods and the CryBound JAR, sent through a Discord DM after roughly a day of rapport. The lure asks the victim to join a Minecraft server. Fabric loads me.cry.cry.Basic.client.cryFabricEntry; onInitializeClient passes the string "Fabric" to cryClient.start.

cryFabricEntry.onInitializeClient()
  -> cryClient.start("Fabric")
     -> ensureJnaTmpDir()       %TEMP%\cry-jna; sets jna.tmpdir
     -> applyConfig()           decrypts _P0; sets crycry.api.* and crycry.runtime.*
     -> cryBoot.fail(...)       startup breadcrumb in %TEMP%\cry-pipe.log
     -> Thread("Render thread")  non-daemon thread created by the mod
        -> cryBoot.clean()      hard-coded return true
        -> sleep(ingameDelayMs) default 500 ms; capped at 5 s
        -> cryBoot.pull()       open session, fetch manifest and chunks
        -> ModBootstrap.boot(wire, sm, nonce, sessionKey, table, salt)

That second thread borrows Minecraft’s Render thread name. cryBoot.clean() does not gate execution in this build; it always returns true. The only environment rejection described in the notes is the weak empty-fingerprint case 0|||. The local/offline path is stubbed: ModBootstrap.initialize() throws SecurityException("remote pack").

Startup breadcrumbs go to %TEMP%\cry-pipe.log; failures also reach <Minecraft directory>\cry-run.log. The loader creates %TEMP%\cry-jna\ and points jna.tmpdir there before making Windows API calls. Decoded string references place TEMP, cry-jna, and jna.tmpdir in cryClient.ensureJnaTmpDir at bytecode PCs 48, 130, and 218.

String and config decoding

cryClientOx.d(int[], seed) and ModCodec._Sx.d use an ANSI C LCG as a keystream. _Sx.b applies it to byte arrays, while _Sx.r decodes the mixVm round table.

// 32-bit Java int overflow supplies the LCG wraparound.
state = state * 1103515245 + 12345;
decoded[i] = encoded[i] ^ ((state >>> 16) & 0xffff);

The config is separate: _P0 contains 348 integers, unmasked bytewise with a Numerical Recipes LCG (state = state * 1664525 + 1013904223 mod 2^32, low byte). The result is a 12-byte IV followed by AES-256-GCM ciphertext. A key derived from build-local salt and constants is confirmed by the GCM tag. The plaintext has seven 0x01-separated fields: sender URL, unused value, licence, opaque build token, build ID, HMAC root, and another unused value.

cryBoot.Cfg.load() rejects unpatched builder fields %SENDERBASE%, %LICENSEKEY%, %ENCBUILD%, %BUILDID%, and %JBIND%, direct evidence of per-customer config stamping. Other constants are XOR pairs; one pair decodes to five pack layers. _jx_<8 hex digits>() methods are dead arithmetic filler used for per-build variation.

Host binding and session open

JNA reads the volume serial with Kernel32.GetVolumeInformation and MachineGuid from HKLM\SOFTWARE\Microsoft\Cryptography. The fingerprint adds the first MAC address and processor identifier (or os.arch). Hostname and username also feed hwid and are sent in the initial JSON. The binding logic is:

hwid = SHA256hex(volumeSerial | MachineGuid | hostname | user.name)
root = HKDF(fingerprint, salt="crybound-machine-secret-v1", info="machine-root")
lb = HMAC-SHA256(root, lenPrefixed("crybound-lbind-v1", hwid, buildId[:32]))

The registry and volume APIs make this path Windows-specific in practice.

The first HTTPS POST goes to /jsharing. Its JSON contains the build token, hardware ID, timestamp, nonce, machine binding and signature, plus user.name and hostname in plaintext:

{
  "op": "open", "enc": "<redacted>", "hwid": "<sha256>",
  "ts": "<milliseconds>", "nonce": "<16-byte hex>",
  "lb": "<machine binding>", "sig": "<HMAC>",
  "un": "<username>", "hn": "<hostname>"
}

The sig field is HMAC(jBind, hwid|nonce|timestamp|lb). This first POST sends account and host names before the loader asks for stage two. The C2 response carries sid, sk, buildId, and cx; the build ID must match the embedded value. For sk, the client derives wrapKey = HKDF-SHA256(jBind, salt="crybound-java-wrap-v1", info=hwid) and authenticates AAD open|hwid|buildId[:32] in the JSK1 envelope. Later requests carry x-jsession, x-jhwid, x-jts, and x-jsig, with the session signature HMAC-bound to sid, hwid, timestamp, and route.

The HTTP client uses HTTPS POST, JSON, and a 25-second timeout, with no custom User-Agent. There is no TrustManager, custom SSLContext, HostnameVerifier, or certificate pinning. CRYCRY_SENDER overrides the configured sender, providing a redirection point for controlled lab analysis without patching the JAR.

CryBound delivery, host registration, and manifest/chunk exchange

Manifest and chunk protocol

The loader uses /jclass for both manifest and chunk requests. A manifest request includes op: "manifest", probe: "ok", and ack = HMAC(sk, "ack|" + cx). The JCM1 envelope uses AAD jclass|sid|hwid|manifest. Its plaintext contains v (1 or 2), n, packHash, nonce, sealed master sm, cx, salt, table[16+], hashes[n], and sig. The HMAC signature covers v|n|packHash|nonce|sm|cx, optionally salt|table, then the pipe-joined chunk hashes. The manifest can carry JSON alone or [u32 length][JSON][inline pack].

If the pack is chunked, the client repeats op: "chunk" with index i. Each JCK1 response uses AAD jclass|sid|hwid|chunk|i. The client checks every chunk SHA-256 against hashes[i], joins the wire pack, and compares the full SHA-256 with packHash.

JSK1 / JCM1 / JCK1 envelope:
magic[4] | version:u32be(1) | IV[12] | GCM tag[16] | ciphertext

The GCM tag is stored before the ciphertext. The encrypted _P0 config uses a different layout: IV followed by ciphertext and tag. Decoded strings in cryBoot.pull identify the manifest, probe and route handling; fetchChunks contains the chunk and i fields. These names are useful static pivots alongside the HTTP headers.

Five-layer pack unwrap

The pack key is session-derived. The input material combines SHA-256 digests of the manifest table, salt, joined wire data, session key, and mixVm output, followed by the server nonce. HKDF with salt crybound-java-pack-v1 and info query/v3/session derives the key that opens sm; the master key derives enc|L, mac|L, sig|L, and str|L keys for each layer.

ikm = SHA256(table) || SHA256(salt) || SHA256(wire)
   || SHA256(sessionKey) || SHA256(mixVm(...)) || nonce
Ksession = HKDF(ikm, "crybound-java-pack-v1", "query/v3/session")
master = AES-GCM-open(sm, Ksession)

for L = 5 down to 1:
    kEnc, kMac, kSig, kStr = HKDF(master, labels || L)
    verify layer signature, MAC, ciphertext hash, and plaintext hash
    decrypt layer L
LayerTransform
5AES-CTR using kStr, then AES-GCM using kEnc
4AES-GCM with IV mixed from the nonce and kStr
3AES-CTR with a 16-byte IV
2ChaCha20-Poly1305
1AES-GCM

Each C5L\x01 header carries the layer number, 16-byte nonce, plaintext and ciphertext hashes, MAC, signature, and ciphertext length. The verifier checks:

mac = HMAC(kMac, le32(L) || nonce16 || ptHash || ctHash || ciphertext)
sig = HMAC(kSig, "CRY-SIG-v1" || le32(L) || mac || ptHash || ctHash)

The 16-round mixVm is an ARX mixer over eight 32-bit words: add, XOR, rotate, multiply by 0x9E3779B9, and XOR with a neighboring word plus nonce bytes. Its decoded round table contains 16 [op, a, b] triples. It diversifies the per-build key; it is not an interpreter for downloaded opcodes.

The result is a CLZ\x01 container with two name-to-byte tables. Table 1 holds bootstrap classes Stub and Stub$CL; table 2 holds the main payload. The custom loader calls defineClass on decrypted bytes, then invokes Stub.r(vault2, "me.cry.cry.Basic.common.cryVoiceBetaClientP0"). ClassVault seals cached classes under an ephemeral AES-GCM key rotated after every take(); ModBootstrap.findClass zeroes each byte array after definition. Keys, plaintext, and chunk buffers are cleared with Arrays.fill.

Session-key derivation, five-layer unwrap, and in-memory class loading

Detection logic

The useful chain is Minecraft’s javaw.exe reading MachineGuid and the volume serial, creating cry-jna or the two log files, then making authenticated JSON POSTs to the C2. Look for op values open, manifest, and chunk, plus x-jhwid and x-jsig. The encrypted stage-two bytes are not written as a file; monitor reflective ClassLoader.defineClass activity and rapid clearing of class buffers in the Minecraft JVM.

The class files are deflated inside the JAR. Raw-file string rules should match ZIP central-directory names; content strings require unzipping or a ZIP-aware scanner.

rule CryBound_JarLoader_Entries {
  strings:
    $e1 = "me/cry/cry/Basic/client/cryFabricEntry"
    $e2 = "me/cry/cry/Basic/client/cryBoot"
    $e3 = "me/cry/cry/Basic/common/ModCodec"
    $e4 = "me/cry/cry/Basic/common/ModBootstrap"
    $e5 = "me/cry/cry/Basic/client/cryClientOx"
    $e6 = "crycry.accesswidener"
    $l1 = "META-INF/jars/sqlite-jdbc-"
    $l2 = "META-INF/jars/jna-platform-"
  condition:
    uint32(0) == 0x04034b50 and 2 of ($e*) and all of ($l*)
}

rule CryBound_Loader_Class {
  strings:
    $a = "crybound-java-wrap-v1"
    $b = "crybound-lbind-v1"
    $c = "crybound-machine-secret-v1"
    $d = "CRYCRY_SENDER"
    $e = "JCM1"
  condition:
    uint32(0) == 0xBEBAFECA and 2 of them
}

The rules were string-checked against the sample and bundled libraries; the YARA executable was unavailable for compilation. A class-level _jx_[0-9a-f]{8} method name is a cheap family pivot, but combine it with crybound- or CRYCRY_ strings. The JAR bundles SQLite JDBC 3.46.1.3 and JNA 5.14.0, which could support browser-database and DPAPI collection; no such stage-two behavior was recovered. /jblob, /jweb, and /jdebug are configured properties unused by this loader. The licence identifies a CryBound customer, not the developer or C2 operator.

ATT&CK mapping: T1195.002, T1204.002, T1566.003, T1027/T1027.013, T1140, T1620, T1082, T1016, T1033, T1012, T1071.001, T1573.001, T1036.004, and T1070. T1555.003/T1539 apply only if the unobserved stage two performs the inferred browser collection.

IOCs

TypeIndicatorContext
Current C2assyg[.]latActive build; HTTPS JSON POST
Session routehxxps://assyg[.]lat/jsharingSession open
Pack routehxxps://assyg[.]lat/jclassManifest and chunk requests
Configured routes/jblob, /jweb, /jdebugStage-two channels inferred from properties; unused by loader
Previous C2api.crycry[.]latPrior build
Reported MaaS panelcrybound[.]pwPanel reference in research notes
Landing pagescry[.]a.pinggy[.]link, cryw[.]a.pinggy[.]linkCryBound product pages
Lure servercastles.bloom[.]hostMinecraft server named in lure; legitimate host/customer infrastructure
Mod IDcrycryFabric mod identifier
Loader namespaceme.cry.cry.BasicJava package family
Environment variableCRYCRY_SENDEROverrides configured C2 base
HTTP headersx-jsession, x-jhwid, x-jts, x-jsigAuthenticated session/request metadata
Sample SHA-256cb0d5f7ce93fd191f1dfa40316d664f16d4fe0480a31a9ccb0b7d4a1a4f8de3cStarBoundRuins-fabric.jar
Prior sample SHA-2563b2510d3b2a24a27845f1b132ffcb1ba1853a3db9441a8887930bae131862175smp-1.0.3-release.jar
Class SHA-256cd27c0fd3591dff0465d89b3bbf2a5274ec5d390692c8b7a5bf7f8cefec5fb26cryClient.class
Class SHA-256ea268ca6d5461f06eca310cfcadc75c71cae794b881933fabe502db9d6c7f5bbcryBoot.class
Class SHA-25623cd02847c9abb150ae97a6b469cdf10ed817fc828dab690927f5b1561999820ModCodec.class
Class SHA-256b6d0193b9110e69d4c253c7f830e04f95d85815149c3df4308fba9a9c5ddfbdcModBootstrap.class
Host artifacts%TEMP%\cry-pipe.log, %TEMP%\cry-jna\, <Minecraft directory>\cry-run.logStartup, JNA temp path, and failure logs
Registry keyHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuidHost fingerprint source