When to use / when not to use
- Use: restore Java logic from jar/war/class samples; locate registration codes/keys/network logic; handle ProGuard/Allatori obfuscation and Java hardening
- Use: static restoration of malicious Java samples ([[re-malware]] → [[re-managed]] path)
- Do not use: direct analysis of Android DEX ([[re-apk]]; after converting to jar you can return to this skill)
- Do not use: non-Java (.NET goes to [[re-dotnet]], scripts go to [[re-script-deob]], native goes to [[re-binary-core]])
- Note: dynamic steps default to sandbox ([[re-analyze/platform-tips]] highest principle); back up unpacking artifacts first
Tool preparation
See [[re-analyze/platform-tips]]—decompilation/unpacking are static steps, no sandbox needed; dynamic verification goes into the sandbox per the highest principle.
JDK (javap / jar / java runtime)
- Debian/Ubuntu:
apt install openjdk-17-jdk - Fedora:
dnf install java-17-openjdk;Arch:pacman -S jdk17-openjdk - macOS:
brew install openjdk(orbrew install --cask temurin) - Windows:
choco install temurinor the official installer - Verify:
java -version && javap -version
unzip (jar = zip container)
- Linux:
apt install unzip/dnf install unzip/pacman -S unzip;macOS:brew install unzip - Verify:
unzip -v
CFR (jar CLI decompiler, zero dependencies)
- Download (no dependencies, only needs JRE):
curl -L -o cfr.jar https://www.benf.org/other/cfr/cfr-0.152.jar - Verify:
java -jar cfr.jar --help
JD-GUI (GUI decompiler)
- GitHub
java-decompiler/jd-guirelease zip → unzip, Linux/macOS:java -jar jd-gui-1.6.6.jar(the zip contains executables for each platform) - Verify: the GUI starts and can
File > Openopen the jar
procyon (alternative decompiler)
- Maven Central:
curl -L -o procyon-decompiler.jar https://repo1.maven.org/maven2/org/bitbucket/mstrobel/procyon-decompiler/0.6.0/procyon-decompiler-0.6.0.jar - Verify:
java -jar procyon-decompiler.jar --help
Procedure
Execute in order, recording evidence for each step (path + sha256, see [[re-triage]]).
-
jar/war unpacking:
jar tf app.jar # 先看清单(JDK 自带 jar 工具) unzip -o app.jar -d unpacked/ unzip -p app.jar META-INF/MANIFEST.MF # Main-Class / 加固标记- war: classes are in
WEB-INF/classes/, dependencies are inWEB-INF/lib/ - fat jar (Spring Boot): classes are in
BOOT-INF/classes/, nested dependenciesBOOT-INF/lib/*.jarneed to be unpacked one by one - aar (Android): contains
classes.jar, after unpacking process it according to this skill - Multi-Release jar: multiple versions of classes at the same path are under
META-INF/versions/N/(N=9/11/17…) override—at runtime the version is selected and loaded according to the JDK, for reverse engineering look at the corresponding layer for the target JDK, do not just look at the top level
- war: classes are in
-
Class structure identification:
javap -c -p unpacked/com/example/Main.class # 字节码(-c)+ 私有成员(-p) javap -v unpacked/com/example/Main.class # 常量池/元数据- Find the entry point: MANIFEST.MF's
Main-Class→javap -c -p <入口类>to see the main logic - For obfuscated assemblies, first check whether class names are readable (a/b/c → step 4)
- class file header quick reference:
xxd -l 8 <类>.class→CA FE BA BE+ minor(2) + major(2); Java version = major − 44 (52=Java 8, 55=11, 61=17, 65=21, measured JDK 21 artifact major=65);javap -vthe first line directly prints minor/major - Constant pool: the count is after major (u16, index 0 is a placeholder),
javap -vlists entry types (String/Class/NameAndType/Methodref/Utf8 etc.)—string literals, class names, and signatures are all in the constant pool, for obfuscated samples look for the plaintext strings here first
- Find the entry point: MANIFEST.MF's
-
Logic restoration (CFR / JD-GUI):
java -jar cfr.jar app.jar --outputdir cfr_out/ # 整包还原为 Java 源码 java -jar cfr.jar unpacked/com/example/Main.class # 单类还原 java -jar procyon-decompiler.jar -jar app.jar -o procyon_out/ # 备选- JD-GUI:
File > Open→ browse the tree on the left →File > Save All Sourcesexport - Cross-validate key classes/methods with two decompilers (CFR restores lambda/modern bytecode better, JD-GUI is smoother for old code)
- JD-GUI:
-
Obfuscation identification and string decryption:
- ProGuard: class/method names all become
a/b/c、javap -lNo LineNumberTable/LocalVariableTable (debug info stripped) - Allatori: decompiled output shows
com.allatori.*watermark classes,StringEncryptorcalls (string encryption) - ZKM(Zelix KlassMaster):
com.zelix.*class signature - Detection:
grep -rn 'StringEncryptor\|decrypt(' cfr_out/ | head - String encryption handling: locate the decryption class and algorithm (key/transformation method) → replicate in python3 for batch recovery; or dynamically obtain plaintext (step 5, inside sandbox) — static recovery first, fall back to dynamic if static stalls
- ProGuard: class/method names all become
-
Dynamic (optional; execute in sandbox [[re-sandbox]]):
- JDB (desktop JVM):
jdb -classpath app.jar com.example.Main > stop in com.example.Main.checkKey # 下断点 > print key # 取变量 > eval new com.example.Util().decrypt("密文") # 直接调用解密方法取明文 - Frida (Android Java apps, switch to [[re-mobile]]/[[re-apk]] domain):
Java.perform(function () { var c = Java.use("com.example.Main"); c.checkKey.implementation = function (k) { console.log("key = " + k); return this.checkKey(k); }; }); - Desktop JVM has no Frida Java API → use JDB / a self-written Java agent
- JDB (desktop JVM):
Cross-domain combination
- [[re-managed]]: gateway workflow step ② (decompile) ③ (deobfuscate) always invokes this skill
- [[re-malware]]: Java malware sample path (re-malware → re-managed → this skill's static recovery)
- Android side: [[re-apk]] / [[re-mobile]] (after DEX-to-jar conversion, can return to this skill); dynamic [[re-frida]] (Android)
- Foundation [[re-binary-core]]: initial survey ([[re-triage]]); native/JNI parts
Common pitfalls and traps
- After ProGuard renaming, rely on string cross-references: symptom — decompiled output is all
a/b/cclasses,a(...)methods, target logic cannot be located; cause — ProGuard shrink+obfuscate renaming erases semantics; countermeasure — start from strings: grep plaintext URLs/prompts → in the decompiled output find the class that references it (grep -rn "提示语" cfr_out/) → recover semantics along the call chain - Allatori/string encryption must be decrypted first: symptom — decompiled output only shows
StringEncryptor.decrypt("...")calls, no plaintext visible at all; cause — strings are only decrypted at runtime; countermeasure — statically locate the decryption algorithm and key → replicate in python3 for batch recovery; or dynamically obtain plaintext after the decryption call (JDBeval/ Frida), execute inside sandbox - Decompilation is not entirely correct: symptom — CFR/JD-GUI output has syntax errors, chaotic goto/label, bizarre try-catch structures, lambda recovery failures; cause — lossless recovery from bytecode to Java does not exist; countermeasure — cross-check against
javap -c -pbytecode and fix by hand, cross-validate with multiple decompilers - Java hardening (e.g. Virbox) is like a packer and must be unpacked first: symptom — JD-GUI opens with errors/blank,
javapoutput corrupted, non-standard file header; cause — the hardener encrypts class bytecode, decrypted only at runtime (essentially a packer); countermeasure — unpack the hardening first: dump classes at runtime (attach agent / dedicated unpacking tools preferred;-Xbootclasspathonly available on JDK 8, removed in JDK 9+) → then decompile the dumped standard classes; the approach is the same as [[re-anti-analysis]]'s "unpack before analysis" - javap reports unsupported class file version: symptom —
javapreportsmajor version 65or similar unsupported; cause — the class file version is higher than the local JDK tool version; countermeasure — convert via major−44 to the target Java version, install the corresponding or newer JDK; to just read the version and constant pool you can first usexxdto manually inspect the header fields, no need to wait for tooling - Multi-Release jar analyzed at the wrong layer: symptom —
unzip -oAfter decompiling the resolved top-level class, the logic does not match the runtime behavior (the version branch disappears); reason — multi-version classes are inMETA-INF/versions/N/, and different JDKs load different layers; countermeasure — after unpacking, checkMETA-INF/versions/, and select the layer for analysis according to the target JDK; the difference between the top level and the versions layer is the version-conditional logic - Lambda decompilation produces synthetic methods: symptom — CFR/JD-GUI outputs
lambda$xxx$nmethods orinvokedynamiccalls; reason — the lambda body is compiled into a synthetic private method + invokedynamic bootstrap; countermeasure — use the BootstrapMethods table ofjavap -v -pto locate the lambda body method (captured variables are in the synthetic method parameters), and analyze it as an ordinary method