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内部有一套“结构化控制流分析”算法。它能从字节码的跳转指令中还原出ifforwhile等高级结构。而jad更多是靠模式匹配,遇到复杂控制流就容易翻车。

实战建议:遇到混淆过的代码,先用CFR反编译一遍,看看能不能还原出可读的源码。如果CFR也搞不定,再回到javap看字节码,手动分析控制流。我一般会同时开两个窗口——一个跑CFR,一个跑javap -verbose,对照着看。

五、知识体系总览

下面这张图是我自己梳理的Java逆向入门知识结构,你可以对照着看看自己卡在哪一环:

Java逆向入门 JVM字节码基础 操作数栈 局部变量表 常量池 指令集(iload/iadd等) javap 字节码查看 -c 反汇编指令 -verbose 常量池 -p 私有成员 JDK自带,无需安装 反编译器 jad(老牌,已停更) CFR(现代首选) 支持lambda/record 结构化控制流分析 先javap看字节码,再CFR反编译,jad作为备选

说白了,Java逆向的第一步就是“看懂字节码”。你不需要背下所有指令,但至少要能认出aloadinvokevirtualgetfield这些高频指令。然后配合javap和CFR,大部分未混淆的Java代码都能还原个七七八八。

我的工作流:拿到一个class文件,先跑javap -c -p -verbose看全貌,重点关注常量池里的字符串和方法签名。然后跑CFR反编译,如果CFR输出有语法错误或者逻辑明显不对,再回头对照字节码手动修正。这样双管齐下,效率最高。

嗯,今天就先聊到这里。工具只是手段,关键是你得理解JVM在背后做了什么。下一讲我们会深入字节码指令的细节,到时候再细聊。


公众号:蓝海资料掘金营,微信deep3321