skip to content
naft.pink
2021 — 2022archived

Java obfuscator & deobfuscator

An obfuscator for Java programs, built on ASM and tested against my own set of test programs. There was also a deobfuscator side aimed at ProGuard and Allatori jars, but this is old enough — 2021/2022 — that I don't fully trust my own memory of the details.

  • Built on ASM, transforming compiled bytecode directly rather than rewriting source.
  • Tested against a set of my own test programs, and it worked on them.
  • ProGuard-obfuscated jars I ran into used around five different methods to derive or decrypt the key — seemingly random from jar to jar, maybe tied to a version or setting I never pinned down.
  • Allatori jars had one real obstacle: a deliberately malformed class that crashed decompilers like Luyten. Once I got past that, decrypting the actual strings wasn't hard.
  • Memory of the exact mechanics is hazy at this point — this section is written from a rough recollection, not from re-reading the code.

outcome — Never public — a personal tool, not a product. I'm confident the obfuscator worked against my test programs; the deobfuscator details below are from memory and could be off.

  • Java
  • ASM
  • JVM bytecode

An obfuscator for Java programs, built on ASM, operating on compiled bytecode rather than source. I tested it against a handful of my own test programs and it worked on them.

There was also a deobfuscator side, aimed at jars produced by ProGuard and Allatori. This project is old enough — 2021 into 2022 — that I'm rewriting this page from memory rather than from the code, and some of what follows might not match what actually happened. Treat the ASM/bytecode part as solid and the ProGuard/Allatori specifics as "this is what I remember, not a transcript."

bytecode instead of source#

The distinction that matters: this doesn't rewrite Java and recompile it. It reads the .class files the compiler already produced and rewrites those.

That changes what's possible in both directions. A lot of what makes bytecode hard to read has no source-level equivalent to express — the JVM will happily run things javac would never emit and no Java file could describe. Working below the language means the transformations aren't limited to what's sayable in it.

It also means the tool doesn't need the source at all, which is the entire premise of the deobfuscator: the only input you have is the compiled artefact, because if you had the source you wouldn't be here.

ASM is the right library for it. It's a visitor over the class format rather than a framework with opinions, so it gets out of the way and hands you the structure — which is what you want when the job is a pile of specific transformations rather than one general one.

what the deobfuscator was aimed at#

Two real targets: ProGuard and Allatori. This is the part I'd flag as memory rather than fact — it's been a few years and I'm not going to pretend I remember it cleanly.

As best I recall, the ProGuard-obfuscated jars were the more annoying side: I ran into something like five different methods for deriving or decrypting the key, and which one a given jar used didn't seem consistent — possibly tied to a version or a setting I never actually isolated.

Allatori's obstacle was different in kind: one deliberately malformed class, built to crash decompilers like Luyten rather than to resist a targeted tool. Once that was out of the way, decrypting the strings themselves wasn't the hard part. Longer note on that — written with the same caveat, that the details are from memory.

writing both sides against each other#

The reason to build the deobfuscator is that it's the only honest test of the obfuscator.

On its own, an obfuscator always looks like it works. The output is unreadable by construction — that's what it does — and "I can't read this" is not the same claim as "this is hard to reverse." The difference between those two only shows up when someone actually tries, and the fastest way to have someone try is to be them.

So each side kept invalidating the other. Something the obfuscator did would look thorough until the deobfuscator undid it mechanically, at which point it was revealed as noise rather than protection — it made the output ugly without making it hard. What survived was the transformations that were still expensive to reverse after I'd sat down and seriously tried to reverse them.

That's the thing I'd actually defend from this project, and it generalises well past bytecode: you learn much more about a defence by attacking it than by admiring it. There's a longer note about that, since it turned out to be the part worth keeping. It's the same instinct that later shows up as caring about the race conditions in an auction system or a booking slot — the interesting case is the adversarial one, and the way to find it is to go looking on purpose.

where it went#

Nowhere, and it didn't need to. It was never a product and it was never public.

It's the most technically interesting thing from my JVM years, and it came directly out of BeePack, overlapping with it — working inside compiled Java that somebody else wrote is exactly the thing that makes you curious about what the compiler is really emitting.

all projects