rymga ← Back to Lock Master

Anti-dump

Class encryption takes your bytecode off disk. But the JVM has to hold it in cleartext in memory to run it — so the last attack is to run your program under a debugger or agent and capture the classes as they unseal. Anti-dump makes that runtime hostile: it detects that observation and, instead of a check you can patch out, it folds the result into the decryption key — so under a debugger the key is wrong and nothing ever decrypts.

What it does

Anti-dump is a set of independent, individually switchable layers attached to the ClassGuard guardian. Together they:

  • detect a debugger or instrumentation agent — from the JVM launch arguments (pure-Java), and from the operating system (native: TracerPid, injected libraries, OS debug flags);
  • couple those signals into the key derivation, so a positive signal produces a wrong key and the sealed pool fails its integrity check rather than unsealing under observation;
  • harden the process at the OS level so it resists memory dumps and debugger attach (non-dumpable on Linux, deny-attach on macOS, hide-thread on Windows);
  • spread the checks across many independent sites — in the guardian and, optionally, scattered through your own methods — so there is no single place to patch.

It's built for the case where class encryption is already on. On its own it protects nothing — there'd be no sealed pool to defend. It lives under classGuard.antiDump for exactly that reason.

Not the same as antiDebug. The older top-level antiDebug block is a single lightweight entry-point check that refuses to start under a debugger. Anti-dump is the ClassGuard-native, layered version: many sites, native signals, OS hardening, and — the key difference — the checks are load-bearing (folded into the key), not a removable if. Use anti-dump when you seal classes; it's strictly stronger.

Fail-closed, not a flag

The important design choice — and the one you have to plan around. A naive anti-debug check is one branch: if (debuggerPresent) refuse();. An attacker flips that one branch and it's gone. Anti-dump's detection signals instead feed the arithmetic that reconstructs the encryption key. When a signal fires, the key comes out wrong, and the authenticated pool refuses to decrypt with an AEADBadTagException. There is no boolean to flip — patching the check to "return false" doesn't recover the key, and the native library bytes are themselves hashed into the key, so swapping a cracked library breaks decryption too.

This means a debugger or agent in the runtime breaks your build — by design. That includes benign ones: a profiler, an APM/monitoring agent (-javaagent), a remote-debug launch, a jstack/jcmd that attaches. If your production environment runs any of those, anti-dump will make your plugin fail to load there. It's off by default for this reason. Turn it on only for builds that run where no legitimate agent is ever attached, and always test the sealed build on that real environment first.

Clean run vs. observed run

Real output from the same sealed jar. On the left, a normal launch — the pool unseals and the plugin enables. On the right, the identical jar launched under a debugger: the key is perturbed, the pool fails its integrity check, and the classes never come back as bytecode to capture.

Normal launch
[INFO]: [ClassGuard] loading sealed pool (META-INF/guardian/pool.bin)
[INFO]: Loading server plugin YourPlugin v1.0.0
[INFO]: [YourPlugin] Enabling YourPlugin v1.0.0
[INFO]: Done (2.314s)! For help, type "help"

# normal launch — the pool unseals, your classes run
Under a debugger — fail-closed
# same jar, launched under a debugger:  java -agentlib:jdwp=... -jar server.jar
[ERROR]: Error occurred while enabling YourPlugin v1.0.0 (Is it up to date?)
java.lang.ExceptionInInitializerError
  ...
Caused by: java.lang.IllegalStateException: ClassGuard: encrypted class pool
    failed its integrity check (tampered, truncated, or wrong key)
    at META-INF/guardian/pool.bin
Caused by: javax.crypto.AEADBadTagException: Tag mismatch

# the debugger perturbed the key — the pool never decrypts. Fail-closed.

The attacker gets an AEADBadTagException, not your classes. The same happens if they patch a byte of the bundled native library (integrity), or launch with a monitoring -javaagent (args detection) — each independent layer arrives at the same wrong key.

The layers

Each layer is independent and individually switchable. Removing or defeating one leaves the others enforcing — and every one that fires arrives at the same fail-closed result.

Detection — is something watching?

LayerWhat it catches
argsDetectionA launch-time -javaagent / -agentpath or jdwp flag, read from the JVM's own input arguments. Pure-Java, no native library needed.
jdwpPropertyA debugger present at launch, detected from the JVM launch arguments (pure-Java). Named for a legacy JVM property that is unreliable on modern JDKs; it now reads the input arguments, which is the signal that actually works. A dynamic attach that isn't in the launch args is caught by nativeDetection.
nativeDetectionThe strong one: a native debugger (gdb/lldb) via TracerPid/ptrace, an injected libjdwp/libinstrument in the process map, and OS debug flags. Catches both launch-time and dynamically-attached observers. Needs the native library bundled.

Integrity & prevention

LayerWhat it does
nativeIntegrityHashes the bundled native libraries into the key. Patch a byte of the library — e.g. to neuter its detection — and the key changes: the pool won't decrypt. Makes the native layer tamper-evident.
nativePreventionHardens the process at the OS level so it resists memory dumps and attach: PR_SET_DUMPABLE(0) on Linux, PT_DENY_ATTACH on macOS, thread-hide on Windows. Prevention, not detection — it raises the cost of dumping in the first place.

Placement & hygiene

LayerWhat it does
sitesHow many independent guard sites carry the key-folding detection (1–16, default 4). More sites = more places an attacker must find and neutralise before the key comes out clean. Non-overlapping: removing one leaves the rest enforcing.
periodicChecksRe-runs detection on every protected-class resolution, not just once, so a debugger that attaches after startup is still caught as more classes unseal.
scatterChecksSpreads self-contained tripwires into your own methods — so the detection isn't concentrated in the guardian and there's no single class to patch. Each site is inline and fails-closed. Off by default; see performance.
scatterDensityRoughly 1 in N eligible methods gets a scattered tripwire (2–1000, default 20). Lower = more sites.
noRetainPlaintextZeroes each class's decrypted bytes right after the JVM defines it, so the cleartext can't be scraped back out of the loader's cache with reflection. On by default.

Every option

Everything under classGuard.antiDump. Every layer is individually toggleable so you can drop any single one that causes trouble in your environment without losing the rest.

FieldDefaultWhat it does
enabledfalseMaster switch for the whole feature. Off by default (fail-closed — see above).
sites4Independent key-folding guard sites per layer (clamped 1–16).
argsDetectiontrueDetect launch-time -javaagent/-agentpath/jdwp. Pure-Java.
jdwpPropertytruePure-Java debugger detection via the JVM launch arguments.
nativeDetectiontrueNative debugger/agent detection. Bundles the native library.
nativeIntegritytrueHash the native library into the key (tamper-evident). Bundles the native library.
nativePreventiontrueOS-level anti-dump/anti-attach hardening. Bundles the native library.
periodicCheckstrueRe-check on each protected class resolution, not only once.
noRetainPlaintexttrueZero decrypted class bytes after they're defined.
scatterChecksfalseScatter tripwires into your own methods (no single class to patch).
scatterDensity20~1 in N eligible methods gets a scattered tripwire (2–1000).
attachListenerCheckfalseFootgun. Fold attach-listener detection into the key — fails-closed on any agent attach ever, including a benign jstack/jcmd/profiler. A single diagnostic attach permanently breaks the build, and it does not stop a scripted attach either. Leave off unless no attach ever happens.
Native layers bundle a small native library. When any of nativeDetection, nativeIntegrity or nativePrevention is on, the build embeds the native libraries (a few hundred KB) for Linux, macOS and Windows. Turn all three off and the build stays pure-Java — no libraries embedded, and only argsDetection/jdwpProperty provide detection.

Turning it on

Anti-dump attaches to class encryption, so classGuard.enabled with a non-empty encrypt is the prerequisite (typically alongside type decoupling). The minimal form turns every layer on at its default:

{
  "classGuard": {
    "enabled": true,
    "encrypt": ["*"],
    "antiDump": { "enabled": true }
  }
}

Every knob spelled out, so you can dial any layer down or off:

{
  "classGuard": {
    "enabled": true,
    "encrypt": ["*"],
    "antiDump": {
      "enabled": true,
      "sites": 6,
      "argsDetection": true,
      "jdwpProperty": true,
      "nativeDetection": true,
      "nativeIntegrity": true,
      "nativePrevention": true,
      "periodicChecks": true,
      "noRetainPlaintext": true,
      "scatterChecks": true,
      "scatterDensity": 20,
      "attachListenerCheck": false
    }
  }
}
Recommended shape. Leave the defaults on, raise sites to 6–8 for more independent guard points, and turn scatterChecks on if the extra runtime cost is acceptable for your jar. Leave attachListenerCheck off. Then test the sealed build on the exact environment it will run in — anti-dump's whole point is to be hostile to a runtime that looks like it's being observed.

Platforms

The native layers ship prebuilt libraries for Linux, macOS (Intel & Apple Silicon) and Windows, all x86-64/arm64, selected automatically at runtime by OS and architecture. The pure-Java layers (argsDetection, jdwpProperty) run everywhere.

The native layer degrades gracefully: if no matching library is available for the host, the native signals simply read as "nothing detected" and the build still runs — coverage never depends on the native layer loading. On the platforms it does support, it adds the strongest signals (native debuggers, OS flags) that pure Java can't see.

Performance

Almost all of anti-dump's cost is one-time, at class-load — not in your steady-state runtime. The key-folding sites, the native library hash, and the OS hardening all run once while the guardian builds the sealed pool. There is no per-call or continuous overhead from those.

  • periodicChecks is the one that scales — it re-runs detection as each sealed class loads (once per class, at load time, not on hot paths). The native probe result is cached after the first call so repeated checks are cheap. Turn it off if you seal a very large number of classes and startup time matters.
  • scatterChecks is the only layer that can touch steady-state runtime: a tripwire that lands in a hot method runs on every call. Raise scatterDensity for fewer sites, or leave it off, if throughput regresses. Its value is defense-in-depth (no single class to patch), not the ceiling.

The honest ceiling

We won't oversell this. Anti-dump raises the cost of a runtime capture a great deal, and against launch-time debuggers, monitoring agents and native debuggers it fails-closed — the classes never decrypt under observation. What it does not do is defeat an attacker with full, scripted control of the runtime: a program that dynamically attaches its own instrumentation and retransforms classes after they've loaded can still, in principle, reach the cleartext the JVM must hold in memory. That is the ceiling every bytecode obfuscator shares — the code has to run on a machine the attacker controls.

What you get is concrete and worth having: decompilation at rest is defeated by class encryption; casual and automated runtime dumps are made to fail-closed; and the determined manual reverse is pushed from "attach a debugger and read it" to "build and script a custom instrumentation harness that never trips any of a dozen independent, fail-closed sites." Size a plan around that — and if you truly cannot ship the asset, don't: keep the crown-jewel logic server-side, where no client-side obfuscator's ceiling applies.