62、Java逆向(一):JVM字节码基础、javap/jad/CFR使用
说实话,Java逆向这个系列,我一直想好好写写。为什么?因为太多人一上来就盯着混淆代码硬啃,结果把自己绕晕了。我个人的习惯是——先搞懂字节码,再谈反编译。这就好比你要修车,总得先知道发动机长什么样吧?
今天这一讲,咱们就从最基础的JVM字节码开始,然后聊聊javap、jad、CFR这三把刀怎么用。嗯,都是实战中我反复摸过的工具。
一、JVM字节码长什么样?
Java源码编译后生成的是.class文件,里面存的就是字节码。字节码不是给人读的,但我们可以通过工具把它“翻译”成一种可读的中间表示。
举个例子,你写了个简单的加法:
public class AddDemo {
public int add(int a, int b) {
return a + b;
}
}
编译后用javap -c一看,你会看到类似这样的东西:
public int add(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn
每一行左边是偏移量(字节码地址),右边是指令。iload_1就是把局部变量表第1个位置的int值压到操作数栈上,iadd弹出两个数相加,结果再压回去,最后ireturn返回栈顶的int值。
核心要点:JVM是基于栈的虚拟机。几乎所有操作都围绕“操作数栈”和“局部变量表”展开。你只要记住这两样东西,字节码就懂了一半。
我在项目中遇到过一个问题:一个看似简单的循环,反编译出来的代码逻辑完全不对。后来我直接看字节码,才发现是编译器做了循环展开优化。嗯,有时候源码级别的“直觉”在字节码层面并不成立。
二、javap:JDK自带的瑞士军刀
javap是JDK自带的字节码查看器。你不需要装任何第三方工具,只要配好Java环境就能用。
常用参数我列一下:
| 参数 | 作用 |
|---|---|
-c |
反汇编出字节码指令 |
-verbose |
输出详细信息(常量池、行号表等) |
-p |
显示私有成员 |
-s |
输出内部类型签名 |
我个人最常用的是javap -c -p,既能看字节码,又能看到私有方法。比如你想分析一个第三方jar里的某个类,但源码被混淆了,这时候javap -c -p就是你的第一道防线。
小技巧:加上-verbose可以看到常量池的完整内容。常量池里藏着字符串、类名、方法名等关键信息。我曾经靠常量池里的一个URL字符串,直接定位到了一个恶意类的回调地址。
三、jad:老牌反编译器,但已停更
jad(Java Decompiler)是很多老逆向工程师的回忆。它能把.class文件还原成可读的Java源码,而且还原度相当高。
用法很简单:
jad AddDemo.class
执行后会在当前目录生成AddDemo.jad文件,里面就是反编译后的Java代码。你还可以用-s java参数直接输出.java后缀的文件。
不过要注意,jad已经很多年没更新了。对于Java 8以下的版本,它表现很好。但如果你遇到Java 11、17甚至更高版本编译的类,jad可能会直接报错或者输出一堆乱码。
避坑指南:我曾经用jad反编译一个Java 11的lambda表达式,结果jad把lambda体完全搞丢了,生成了一个空方法。后来我换成CFR才搞定。所以jad只适合老项目,新项目建议直接上CFR。
四、CFR:现代反编译的首选
CFR(Class File Reader)是目前最活跃的Java反编译器之一。它支持Java 8到21的几乎所有特性,包括lambda、泛型、枚举、record等。
使用方式:
java -jar cfr.jar AddDemo.class
它会直接把反编译结果输出到控制台。你也可以加参数输出到文件:
java -jar cfr.jar AddDemo.class --outputdir ./output
CFR有几个特别实用的参数:
--hideutf false:显示所有字符串常量,默认会隐藏一些--renamedupmembers true:重命名重复的成员变量--caseinsensitivefs true:处理大小写不敏感的文件系统
你想想看,为什么CFR比jad强?因为CFR内部有一套“结构化控制流分析”算法。它能从字节码的跳转指令中还原出if、for、while等高级结构。而jad更多是靠模式匹配,遇到复杂控制流就容易翻车。
实战建议:遇到混淆过的代码,先用CFR反编译一遍,看看能不能还原出可读的源码。如果CFR也搞不定,再回到javap看字节码,手动分析控制流。我一般会同时开两个窗口——一个跑CFR,一个跑javap -verbose,对照着看。
五、知识体系总览
下面这张图是我自己梳理的Java逆向入门知识结构,你可以对照着看看自己卡在哪一环:
说白了,Java逆向的第一步就是“看懂字节码”。你不需要背下所有指令,但至少要能认出aload、invokevirtual、getfield这些高频指令。然后配合javap和CFR,大部分未混淆的Java代码都能还原个七七八八。
我的工作流:拿到一个class文件,先跑javap -c -p -verbose看全貌,重点关注常量池里的字符串和方法签名。然后跑CFR反编译,如果CFR输出有语法错误或者逻辑明显不对,再回头对照字节码手动修正。这样双管齐下,效率最高。
嗯,今天就先聊到这里。工具只是手段,关键是你得理解JVM在背后做了什么。下一讲我们会深入字节码指令的细节,到时候再细聊。