JAVA安全|字节码篇:字节码文件结构与解读

admin 2023年3月2日11:35:38评论24 views字数 10552阅读35分10秒阅读模式

本文为JAVA安全系列文章第二十三篇,学习掌握.class文件的结构,能根据文档对16进制字节码文件进行解读。

0x01  字节码文件结构

一、字节码文件

java是一门跨平台的语言,我们使用javac将.java文件编译为.class文件后,该.class文件便可以在任意平台(Linux,Windows,Macos等)上运行,即“一次编译,到处运行”。.class文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取,故而.class文件又被称为字节码文件。

java之所以可以“一次编译,到处运行”,主要有两个原因:第一,JVM针对各种操作系统、平台都进行了定制;第二,无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。

在学动态字节码加载时,我们说过只要编译器能够将代码编译成.class文件,都可以在JVM虚拟机中运行。这是由于JVM规范的存在,只要最终可以生成符合规范的字节码就可以在JVM上运行。因此这就给了各种运行在JVM上的语言(如Scala、Groovy、Kotlin)一种契机,可以扩展Java所没有的特性或者实现各种语法糖。由此可以看出字节码对于Java生态的重要性。下面就来详细学习字节码文件结构。

二、字节码结构

先编写一个类Human和接口IA:

package bytecodefile;
/*
* @author   walker1995
* @version v1.0
*/

public class Human {
   private char sex;
   private String name;

   public Human(char sex, String name) {
       this.sex = sex;
       this.name = name;
  }

   public String getName() {
       return name;
  }

   public void setName(String name) {
       this.name = name;
  }
}
package bytecodefile;
/*
* @author   walker1995
* @version v1.0
*/

public interface IA {
   void fly();
}

再写一个类SuperMan继承Human,实现IA接口:

package bytecodefile;
/*
* @author   walker1995
* @version v1.0
*/

public class SuperMan extends Human implements IA {
   private int age;

   public SuperMan(char sex, String name, int age) {
       super(sex, name);
       this.age = age;
  }

   public void fly() {
       System.out.println("I can fly!!!");
  }

   public void hi(){
       System.out.println("hi~");
  }
}

然后编译,使用sublime打开编译好的SuperMan.class文件,看到的是16进制数据:

JAVA安全|字节码篇:字节码文件结构与解读

jvm是如何解读这一长串16进制数据的呢?首先附上一张字节码的结构简图:

JAVA安全|字节码篇:字节码文件结构与解读

下面就来进行详细说明。

0x02  字节码解读

一、魔数和版本号
1.魔数(Magic Number)

位于.class文件的开头,占四个字节,固定为0xCAFEBABE。JVM根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。

值得说明的是,魔数的固定值是Java之父James Gosling制定的,为CafeBabe(咖啡宝贝),而Java的图标为一杯咖啡。

2.版本号(Version)

位于魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。此处我的版本号为“00 00 00 34”,即次版本号转化为十进制为0,主版本号转化为十进制为16 x 3 + 4 = 52,对应的Java版本为1.8.0。

JAVA安全|字节码篇:字节码文件结构与解读

二、常量池(constant pool)

主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。

字面量为代码中声明为Final的常量值

符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。

常量池是一个数组,其中包含了在类中出现的所有数值、字符串和类型常量。这些常量仅在常量池中定义一次,然后可以利用其索引,在类文件中的所有其他各部分进行引用。

常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图所示:

JAVA安全|字节码篇:字节码文件结构与解读

1.常量池计数器(constant_pool_count)

占两个字节,由于常量的数量不固定,所以在主版本号后先放置两个字节来表示常量池容量计数值。此处常量池计数为0x002e,即 16 x 2 + 14 = 46。排除掉下标“0”,实际常量个数为46 - 1 =45。

2.常量池数据区

不定长,数据区是由(constant_pool_count - 1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码中共有14种类型的cp_info,每种类型的结构都是固定的,如下图所示:

JAVA安全|字节码篇:字节码文件结构与解读

从16进制解读所描述的常量的思路是先通过tag(一个字节)确定其常量类型,再根据该类型后面连续几个字节确定其具体的值含义。

以常量池计数器后的一个cp_info为例,先读取tag为0x0a(十进制为10),对应CONSTANT_Methodref_info(方法引用);然后读取两字节表示指向声明方法的类描述符CONSTANT_Class_info的索引项,该值为0x0008(十进制为8);再读取两字节表示指向名称及类型描述符CONSTANT_NameAndType的索引项,该值为0x001c(十进制为28)。

这里有很多个常量,我们可以使用JDK自带的javap -v .class文件路径解析得到完整的常量池表:

Constant pool:
  #1 = Methodref          #8.#28         // bytecodefile/Human."<init>":(CLjava/lang/String;)V
  #2 = Fieldref           #7.#29         // bytecodefile/SuperMan.age:I
  #3 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
  #4 = String             #32            // I can fly!!!
  #5 = Methodref          #33.#34        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #6 = String             #35            // hi~
  #7 = Class              #36            // bytecodefile/SuperMan
  #8 = Class              #37            // bytecodefile/Human
  #9 = Class              #38            // bytecodefile/IA
 #10 = Utf8               age
 #11 = Utf8               I
 #12 = Utf8               <init>
 #13 = Utf8               (CLjava/lang/String;I)V
 #14 = Utf8               Code
 #15 = Utf8               LineNumberTable
 #16 = Utf8               LocalVariableTable
 #17 = Utf8               this
 #18 = Utf8               Lbytecodefile/SuperMan;
 #19 = Utf8               sex
 #20 = Utf8               C
 #21 = Utf8               name
 #22 = Utf8               Ljava/lang/String;
 #23 = Utf8               fly
 #24 = Utf8               ()V
 #25 = Utf8               hi
 #26 = Utf8               SourceFile
 #27 = Utf8               SuperMan.java
 #28 = NameAndType        #12:#39        // "<init>":(CLjava/lang/String;)V
 #29 = NameAndType        #10:#11        // age:I
 #30 = Class              #40            // java/lang/System
 #31 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
 #32 = Utf8               I can fly!!!
 #33 = Class              #43            // java/io/PrintStream
 #34 = NameAndType        #44:#45        // println:(Ljava/lang/String;)V
 #35 = Utf8               hi~
 #36 = Utf8               bytecodefile/SuperMan
 #37 = Utf8               bytecodefile/Human
 #38 = Utf8               bytecodefile/IA
 #39 = Utf8               (CLjava/lang/String;)V
 #40 = Utf8               java/lang/System
 #41 = Utf8               out
 #42 = Utf8               Ljava/io/PrintStream;
 #43 = Utf8               java/io/PrintStream
 #44 = Utf8               println
 #45 = Utf8               (Ljava/lang/String;)V

想要完全看懂常量池表中描述的信息,这里还需要补充下内部名,类型描述符,方法描述符的概念:

3.内部名

在许多情况下,一种类型只能是类或接口类型。例如,一个类的超类、由一个实现接口的类, 或者由一个方法抛出的异常就不能是基元类型或数组类型,必须是类或接口类型。这些类型在已编译类中用内部名字表示。一个类的内部名就是这个类的完全限定名,其中的点号用斜线代替。 例如,String 的内部名为 java/lang/String。

4.类型描述符

内部名只能用于类或接口类型。所有其他 Java 类型,比如字段类型,在已编译类中都是用类型描述符表示的,如下图:

JAVA安全|字节码篇:字节码文件结构与解读

一个类类型的描述符是这个类的内部名, 前面加上字符 L , 后面跟有一个分号。例如, String 的类型描述符为 Ljava/lang/String;。而一个数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符。

5.方法描述符

方法描述符是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。 方法描述符以左括号开头,然后是每个形参的类型描述符,然后是一个右括号,接下来是返回类型的类型描述符,如果该方法返回 void,则是 V(方法描述符中不包含方法的名字或参数名)。示例如下图:

JAVA安全|字节码篇:字节码文件结构与解读

一旦知道了类型描述符如何工作,方法描述符的理解就容易了。例如,(I)I 描述一个方法, 它接受一个 int 类型的参数,返回一个 int。


理解了上面的概念,此时查看完整的常量池表,可知第一个cp_info表示的是Human这个类的Human(char sex, String name)构造器。

由于常量池表比较庞大,此处不一一解读了,各位读者可以多解读几个来练习练习。

三、访问标志、当前类索引、父类索引、接口索引
1.访问标志(access_flag)

常量池结束之后的两个字节,描述该类、接口的访问类型。JVM规范规定了如下访问标志:

JAVA安全|字节码篇:字节码文件结构与解读

需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的。

比如此处的修饰符为Public Super,则对应的访问修饰符的值为ACC_PUBLIC | ACC_SUPER,即0x0001 | 0x0020 = 0x0021:

JAVA安全|字节码篇:字节码文件结构与解读

2.当前类索引(this_class)

访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。

此处为0x0007,即索引为十进制的7,查看常量池表为bytecodefile/SuperMan

3.父类索引(super_class)

当前类名后的两个字节,描述父类的全限定名,保存的也是常量池中的索引值

此处为0x0008,即索引为十进制的8,查看常量池表为bytecodefile/Human

4.接口索引(interfaces)

父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值

此处接口计算器为0x0001,即仅有一个接口;索引为0x0009,即十进制的9,查看常量池表为bytecodefile/IA。

如下图所示:

JAVA安全|字节码篇:字节码文件结构与解读

四、字段表(fields)

字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。

字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:

JAVA安全|字节码篇:字节码文件结构与解读

此处的字段表计数器为0x0001,即仅有一个字段;权限修饰符为0x0002,查看访问标志表为ACC_PRIVATE;字段名索引为0x000A,即十进制的10,查看常量池表为age;描述符索引为0x000B,即十进制的11,查看常量池表为I,即int类型。属性个数为0x0000,即没有,故而属性列表也就没有了。

所以,我们的private int age在字节码中的描述就如下图所示:

JAVA安全|字节码篇:字节码文件结构与解读

五、方法表(methods)

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

JAVA安全|字节码篇:字节码文件结构与解读

1.方法计数器、权限修饰符、方法名索引、描述符索引

此处有3个方法,第一个方法权限修饰符为ACC_PUBLIC的;方法名为<init>表示该类的构造方法;方法描述符为(CLjava/lang/String;I)V,即表示该方法接收char,String,int类型的三个参数,返回类型为void。如下图所示:

JAVA安全|字节码篇:字节码文件结构与解读

2.方法的属性

“方法的属性”这一部分较为复杂,此处属性个数为1,属性列表结构参照https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.3,如下:

Code_attribute {
   u2 attribute_name_index;
   u4 attribute_length;
   u2 max_stack;
   u2 max_locals;
   u4 code_length;
   u1 code[code_length];
   u2 exception_table_length;
  {   u2 start_pc;
       u2 end_pc;
       u2 handler_pc;
       u2 catch_type;
  } exception_table[exception_table_length];
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}

而Code_attribute中的attribute_info的结构又要参照https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.12

LineNumberTable_attribute {
   u2 attribute_name_index;
   u4 attribute_length;
   u2 line_number_table_length;
  {   u2 start_pc;
       u2 line_number;
  } line_number_table[line_number_table_length];
}

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.13

LocalVariableTable_attribute {
   u2 attribute_name_index;
   u4 attribute_length;
   u2 local_variable_table_length;
  {   u2 start_pc;
       u2 length;
       u2 name_index;
       u2 descriptor_index;
       u2 index;
  } local_variable_table[local_variable_table_length];
}

很繁琐,此处不进行人工分析,想更深入了解可以查看文档

这里直接借助javap将其反编译为我们可以读懂的信息进行解读:

{
 public bytecodefile.SuperMan(char, java.lang.String, int);
   descriptor: (CLjava/lang/String;I)V
   flags: ACC_PUBLIC
   Code:
     stack=3, locals=4, args_size=4
        0: aload_0
        1: iload_1
        2: aload_2
        3: invokespecial #1                  // Method bytecodefile/Human."<init>":(CLjava/lang/String;)V
        6: aload_0
        7: iload_3
        8: putfield      #2                  // Field age:I
       11: return
     LineNumberTable:
       line 11: 0
       line 12: 6
       line 13: 11
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0      12     0  this   Lbytecodefile/SuperMan;
           0      12     1   sex   C
           0      12     2  name   Ljava/lang/String;
           0      12     3   age   I

 public void fly();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #4                  // String I can fly!!!
        5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
     LineNumberTable:
       line 16: 0
       line 17: 8
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0       9     0  this   Lbytecodefile/SuperMan;

 public void hi();
   descriptor: ()V
   flags: ACC_PUBLIC
   Code:
     stack=2, locals=1, args_size=1
        0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: ldc           #6                  // String hi~
        5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        8: return
     LineNumberTable:
       line 20: 0
       line 21: 8
     LocalVariableTable:
       Start  Length  Slot  Name   Signature
           0       9     0  this   Lbytecodefile/SuperMan;
}

看下每个attribute_info的描述:

(1)Code区

源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。

(2)LineNumberTable

行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。

(3)LocalVariableTable

本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。


Code区的JVM指令是否感觉一脸懵,有木有种汇编指令的感觉?JVM指令又是一个庞大的知识体系,我们将在下一篇文章中对常见JVM指令进行学习。

六、附加属性表(additional attributes)

字节码的最后一部分,附加的该.class文件的其他信息,完整的attributes包括 ClassFile, field_info, method_info, 和 Code_attribute等,其通用结构为:

attribute_info {
   u2 attribute_name_index;
   u4 attribute_length;
   u1 info[attribute_length];
}

详见https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7

0x03  总结

一、本文通过将自写的代码进行编译,根据网上文章和相关文档来从十六进制数据中尝试解读.class文件,其中的繁琐部分采用了javap来帮助我们进行解读。

二、推荐一个Idea插件:jclasslib(https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer)。可通过file-->setting-->plugins搜索安装,代码编译后在菜单栏”View”中选择”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息:

JAVA安全|字节码篇:字节码文件结构与解读

三、字节码文件我认为需要掌握两个重点:

1.掌握理解.class文件结构和知道如何去解读,以及能够读懂常量池中的描述信息;

2.读懂方法表中Code区的JVM指令,并能理解JVM指令是如何工作的。我们将在下一篇文章中进行详细学习。


参考:

https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html

《ASM4 手册》2.1章节

扩展阅读:https://blog.csdn.net/hosaos/article/details/100990954


Java安全系列文集

第0篇:JAVA安全|即将开启:java安全系列文章

第1篇:JAVA安全|基础篇:认识java反序列化

第2篇:JAVA安全|基础篇:实战java原生反序列化

第3篇:JAVA安全|基础篇:反射机制之快速入门

第4篇:JAVA安全|基础篇:反射机制之Class类

第5篇:JAVA安全|基础篇:反射机制之类加载

第6篇:JAVA安全|基础篇:反射机制之常见ReflectionAPI使用

第7篇:JAVA安全|Gadget篇:URLDNS链

第8篇:JAVA安全|Gadget篇:TransformedMap CC1链

第9篇:JAVA安全|基础篇:反射的应用—动态代理

第10篇:JAVA安全|Gadget篇:LazyMap CC1链

第11篇:JAVA安全|Gadget篇:无JDK版本限制的CC6链

第12篇:JAVA安全|基础篇:动态字节码加载(一)

第13篇:JAVA安全|基础篇:动态字节码加载(二)

第14篇:JAVA安全|Gadget篇:CC3链及其通杀改造

第15篇:JAVA安全|Gadget篇:CC依赖下为shiro反序列化利用而生的CCK1 CC11链

第16篇:JAVA安全|Gadget篇:CC5 CC7链

第17篇:JAVA安全|Gadget篇:CC2  CC4链—Commons-Collections4.0下的特有链

第18篇:JAVA安全|Gadget篇:CC8 CC9链

第19篇:JAVA安全|Gadget篇:Ysoserial CB1链

第20篇:JAVA安全|Gadget篇:shiro无依赖利用与常见shiro利用工具对比浅析

第21篇:JAVA安全|Gadget篇:JDK原生链—JDK7u21

第22篇:JAVA安全|字节码篇:字节码操作库—javassist


如果喜欢小编的文章,记得多多转发,点赞+关注支持一下哦~,您的点赞和支持是我最大的动力~


原文始发于微信公众号(沃克学安全):JAVA安全|字节码篇:字节码文件结构与解读

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月2日11:35:38
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JAVA安全|字节码篇:字节码文件结构与解读https://cn-sec.com/archives/1581067.html

发表评论

匿名网友 填写信息