少女祈祷中...

1.字节码概述

  • class文件:字节码(bytecode)文件
    • class文件是Java“一次编译,到处运行”的基础
    • class文件具备平台无关性,由JVM执行
    • 每个class文件包含了一个类或接口或模块的定义
    • class文件是一个二进制文件,由JVM定义class文件的规范
    • 任何满足这种规范的class文件都会被JVM加载运行
    • class文件可以由其他语言编译生成,甚至不用程序语言直接生成
    • JDK版本不同,所编译出.class文件略有不同

2.字节码文件构成

class文件构成

  • 采用类似于C语言结构体的结构来表示数据
  • 包括两种数据类型
    • 定长数据:无符号数,u1, u2, u4 (分别代表1个字节、2个字节、4个字节的无符号数)
    • 不定长数据:由多个无符号数组成,通常在数据的前面给出其长度

魔数

  • 前4个字节为魔数,十六进制表示为0xCAFEBABE,标识该文件为class文件

版本号

  • 第5、6字节表示次版本号
  • 7、8字节表示主版本号
  • 主版本号与JDK版本有映射关系

常量池

  • 常量池主要存放两大类常量
  • 字面量
    • 如文本字符串、final的常量值等
  • 符号引用
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

访问标志

  • 常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被public、abstract、final等修饰符修饰

类索引、父类索引与接口索引集合

  • 类索引
    • 访问标志后的两个字节,描述的是当前类的全限定名
    • 这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名
  • 父类索引
    -当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值
  • 接口索引集合
    • 父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量
    • 紧接着的n个字节是所有接口名称的字符串常量的索引值

字段表

  • 字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量
  • 字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info
  • 方法部分属性
    • Code,源代码对应的JVM指令操作码
    • LineNumberTable,行号表,将Code区的操作码和源代码中的行号对应

方法表

  • 字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数
  • 第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性

附加属性

  • 字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息
  • 属性信息相对灵活,编译器可自由写入属性信息,JVM会忽略不认识的属性信息

3.字节码指令分类

字节码分类

  • 加载和存储指令
    • 用于将数据在栈帧中的局部变量表和操作数栈之间来回传输
    • 将一个局部变量加载到操作栈:iload、lload、fload、dload、aload等
    • 将一个数值从操作数栈存储到局部变量表:istore、lstore、fstore、dstore、astore等
    • 将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1等
  • 运算指令
    • iadd、isub、imul、idiv等
  • 类型转换指令
    • i2b、i2l、i2s等
  • 对象/数组创建与访问指令
    • new、newarray、getfield等
  • 操作数栈管理指令
    • pop、dup等
  • 控制转移指令
    • Ifeq、goto等
  • 方法调用和返回指令
    • invokevirtual、ireturn等
  • 异常处理指令
    • athrow
  • 同步控制指令
    • monitorenter、monitorexit

字节码指令简介

  • JVM指令由操作码和零至多个操作数组成
    • 操作码(OpCode,代表着某种特定操作含义的数字)
    • 操作数(Operand,操作所需参数)
  • JVM的指令集是基于栈而不是寄存器
    • 字节码指令控制的是JVM操作数栈

4.字节码操作ASM

ASM API

  • Core API
    • 类比解析XML文件中的SAX方式
    • 不需要读取类的整个结构,使用流式的方法来处理字节码文件
    • 非常节约内存,但是编程难度较大
    • 出于性能考虑,一般情况下编程都使用Core API
  • Tree API
    • 类比解析XML文件中的DOM方式,把整个类的结构读取到内存中
    • 消耗内存多,但是编程比较简单
    • 通过各种Node类来映射字节码的各个区域

ASM API 核心类

  • 核心类
    • ClassReader 用于读取已经编译好的.class文件
    • ClassWriter 用于重新构建编译后的类
      • 如修改类名、属性以及方法,也可以生成新的类的字节码文件
  • Visitor类
    • CoreAPI根据字节码从上到下依次处理
    • 对于字节码文件中不同的区域有不同的Visitor
      • MethodVisitor 用于访问类方法
      • FieldVisitor 访问类变量
      • AnnotationVisitor 用于访问注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//修改字节码文件
public class MyClassVisitor extends ClassVisitor implements Opcodes {
private static String methodName;

public MyClassVisitor(ClassVisitor cv) {
super(ASM5, cv);
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access, name, signature, superName, interfaces);
}

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
// 忽略构造方法
if (!name.equals("<init>") && mv != null) {
methodName = name;
mv = new MyMethodVisitor(mv);
}
return mv;
}

class MyMethodVisitor extends MethodVisitor implements Opcodes {
public MyMethodVisitor(MethodVisitor mv) {
super(Opcodes.ASM5, mv);
}

public void visitCode() {
super.visitCode();
//方法进入时打印信息
//拿到java.lang.System类的.out static字段,并放入栈顶
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 将待输出信息放入栈顶
mv.visitLdcInsn("method " + methodName + " is starting");
//调用println方法
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}

public void visitInsn(int opcode) {
// 判断return或抛出异常
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("method " + methodName + " is ending");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
mv.visitInsn(opcode);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//生成字节码文件
public class Generator {

public static void main(String[] args) throws Exception {
modifyGreeting();
createAsmGreeting();
}

public static void modifyGreeting() throws IOException {
// 读取
ClassReader classReader = new ClassReader("com/test/Greeting");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
// 处理
ClassVisitor classVisitor = new MyClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
// 输出
File f = new File("target/classes/com/test/Greeting.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(data);
fout.close();
System.out.println("Modify Greeting Class success!!!!!");
}

public static void createAsmGreeting() throws Exception {

ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;
//主版本号设为49,JDK5之后都可以运行
cw.visit(49, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, "com/test/AsmGreeting", null, "java/lang/Object", null);

cw.visitSource("AsmGreeting.java", null);

// 无参构造方法
mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitVarInsn(Opcodes.ALOAD, 0); // 将this放入栈顶

//调用super()
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();

// hello方法,无参无返回值
mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "hello", "()V", null, null);
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello, this class is genrated by ASM");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(Opcodes.RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();

cw.visitEnd();


File f = new File("target/classes/com/test/AsmGreeting.class");
FileOutputStream fout = new FileOutputStream(f);
fout.write(cw.toByteArray());
fout.close();
System.out.println("Create AsmGreeting success!!!!!");
}
}

5.字节码增强

  • 字节码操作:通常在字节码使用之前完成
    • 源码、编译、(字节码操作)、运行
  • 字节码增强:运行时对字节码进行修改/调换
    • Java ClassLoader类加载器
    • Java Instrument

Java Instrument

  • JDK 5 引入,java.lang.instrument包
  • 对程序的替换,都是通过代理程序(javaagent)进行
  • premain:支持在main函数之前,对类的字节码进行修改/替换
  • agentmain:支持在程序运行过程中,对字节码进行替换

Java 运行前代理

  • 在main函数运行之前,修改/替换某类的字节码
  • 启动Java程序时,给java.exe增加一个参数javaagent:someone.jar
  • someone.jar的清单文件(manifest)指定了Premain-Class:SomeAgent
  • SomeAgent类中,有一个premain方法,此方法先于main运行
  • premain方法有一个Instrumentation的形参,可以调用addTransformer方法,增加一个ClassTransformer转换类
  • 自定义一个ClassTransformer类 ,重写tranform方法,修改/替换字节码

Java 运行时代理

  • 在main函数运行时,修改某类的字节码
    • Test调用Greeting类工作
  • 编写AttachToTest类,对Test进程附加一个agent(jar)
  • 在jar中,利用Instrument对Greeting类进行retransformClasses,重新加载
  • 对进程附加agent,是JVMTI的技术
    • JVM Tool Interface

类替换的注意事项

  • 可以修改方法体和常量池
  • 不可以增加、修改成员变量/方法定义
  • 不可以修改继承关系
  • 未来版本还会增加限制条件

6.字节码混淆

Java字节码的弱点

Java字节码的保护

  • 字节码保护
    • 字节码加密
      • 对字节码进行加密,不再遵循JVM制定的规范
      • JVM加载之前,对字节码解密后,再加载
  • 字节码混淆
    • 被混淆的代码依然遵循JVM制定的规范
    • 变量命名和程序流程上进行等效替换,使得程序的可读性变差
    • 代码难以被理解和重用,达到保护代码的效果

ProGuard

  • 最著名的Java字节码混淆器
  • https://www.guardsquare.com/en/products/proguard
  • 除了混淆,也具有代码压缩、优化、预检等功能
  • 可以命令行运行,也可以集成到Eclipse等IDE中使用
  • 不仅可以处理Java代码,也可以处理Android的代码
  • ProGuard核心配置文件
    • ignorewarnings 跳过警告
    • -verbose 显示所有日志
    • -injars 需要转化的对象地址
    • -outjars 输出的地址
    • -libraryjars <java.home>/lib/rt.jar 用到的支持库
    • -printmapping proguard.map 指定自定义的混淆用的映射文件
    • -dontshrink 不压缩,保持项目原有类和方法,仅对命名及内容进行混淆
    • -keepclassmembers public class * {void set*(*** );*** name;}
      • 不混淆指定匹配的内容,匹配到的内容所属类名依旧会被混淆
    • -keepclasseswithmembers
      • 与上面对应,这一项会让匹配到的内容所属类名也不被混淆
    • -keep public class Proguard { void main(java.lang.String[]);}
      • 不混淆指定类及指定其中的成员及方法

ProGuard注意事项

  • 反射调用类或者方法,可能失败
  • 对外接口的类和方法,不要混淆,否则调用失败
  • 嵌套类混淆,导致调用失败
  • native的方法不要混淆
  • 枚举类不要混淆