最近由于項目要求,需要對 Java Class 文件進行更改。因此必須先了解 Java Class 文件的結構。下面是對?JVMS(Java Virtual Machine Specification)?和一些博客內容的總結。
每個 class 文件包括了一個類或者接口的定義。盡管并不是每個類或者接口都要在一個文件中有外部表示(例如通過類加載器生成的類),我們一般認為 class 文件格式是一個類或接口的有效表示。
一個 class 文件由 8位字節流構成。所有16位、32位以及64位的屬性都通過讀取2個、4個或者8個連續的8位字節構造出來,并以此類推。多字節字段用大端法存儲,也就是說高位優先。在 Java SE 平臺中,這種格式由 接口 java.io.DataInput 和 java.io.DataOutput 以及 java.io.DataInputStream 和 java.io.DataOutputStream 等類支持。
Java Class 文件結構
一個 Java Class 文件包括 10 個基本組成部分:
魔數: 0xCAFEBABE Class 文件格式版本號:class 文件的主次版本號(the minor and major versions) 常量池(Constant Pool):包含 class 中的所有常量 訪問標記(Access Flags):例如該 class 是否為抽象類、靜態類,等等。 該類(This Class):當前類的名稱 父類(Super Class):父類的名稱 接口(Interfaces):該類的所有接口 字段(Fields):該類的所有字段 方法(Methods):該類的所有方法 屬性(Attributes):該類的所有屬性(例如源文件名稱,等等)
下面是一個示意圖。
ClassFile {u4 magic;u2 minor_version;u2 major_version;u2 constant_pool_count;cp_info constant_pool[constant_pool_count-1];u2 access_flags;u2 this_class;u2 super_class;u2 interfaces_count;u2 interfaces[interfaces_count];u2 fields_count;field_info fields[fields_count];u2 methods_count;method_info methods[methods_count];u2 attributes_count;attribute_info attributes[attributes_count];
}
下圖是使用?Java Bytecode Editor?打開 HelloWorld.class 文件(該文件由后面的 HelloWorld.java 編譯得到)后顯示的該文件的一些信息:(后面詳細介紹到每個部分的時候可以再看看這個圖)
這里有一些可變長度部分,例如常量池、方法、以及屬性,因此在加載之前無法知道 Java Class 文件的長度。在這些部分的前面都有長度信息。這樣 JVM 在真正加載這些部分之前就可以知道可變長度部分的大小。
Class 文件中的數據都是按照單字節對齊并且緊密壓縮。這使得 Class 文件能盡可能小。
Java Class 文件中不同部分的順序是嚴格定義的,因此 JVM 知道 Class 文件中每個部分分別是什么、要按照什么順序加載。
下面來詳細看看一個 Class 文件中的每個部分。
魔數(Magic number)
魔數(Magic number) 用來唯一確定格式并和其它格式區別開來。 Class 文件的頭四個字節是0xCAFEBABE 。
Class 文件版本號
Class 文件接下來的 4 個字節表示主次版本號 。這個數字使得 JVM 可以識別和驗證 class 文件。如果數字比 JVM 能夠加載的還要大,就會拒接加載該 class 文件并拋出?java.lang.UnsupportedClassVersionError ?異常。
你可以使用?javap ?命令行工具查看任意 Java Class 文件的版本號。例如:
假設我們有如下一個 Java 類:
1 public?class?HelloWorld {
3 ??public?HelloWorld(String msg) {
7 ????this.msg =?"Default message";
9 ??public?String getMsg() {
12 ??public?void?setMsg(String msg) {
15 ??public?void?printMsg() {
16 ????System.out.println(msg);
18 ??public?static?void?main(String args[]) {
19 ????HelloWorld hw =?new?HelloWorld("Hello world from Java");
我們用命令?javac HelloWorld.java ?編譯創建 class 文件。然后執行?javap -verbose HelloWorld 命令查看 class 文件的版本號:
下面是一個主版本號(Major version)和 class 文件對應 JDK 版本號的列表。
Major VersionHexJDK version 51 0x33 J2SE 7 50 0x32 J2SE 6.0 49 0x31 J2SE 5.0 48 0x30 JDK 1.4 47 0x2F JDK 1.3 46 0x2E JDK 1.2 45 0x2D JDK 1.1
常量池(Constant Pool)
所有和類或者接口相關的常量都保存在常量池里。這些常量包括類名、變量名、接口名稱、方法名稱、簽名和字符串常量等。
常量在常量池中以一個可變長數組的元素形式保存。常量數組前面有一個數組大小,因此 JVM 知道加載 class 文件的時候需要加載多少個常量。
對于每一個數組元素,第一個字節是一個標記(tag),表示該位置常量的類型。JVM 通過讀取這個字節確定常量的類型。如果單字節標記表示是一個字符串字面值,就會讀取后兩個字節,表示字符串字面值的長度,根據長度再從后面讀取對應長度的字符串的實際值。
你可以使用?javap ?命令分析任何 class 文件的常量池。如果對上面的 HelloWorld.class 文件執行 javap 命令,我們可以獲得下面的符號表。
常量池總共有 42 個元素。注意:constant_pool_count 的值是常量池的數目再加上1,例如這里是 43。一個常量池索引只有大于0且小于 constant_pool_count 時才認為有效。
下面是單字節標記對應的值及其解釋,對于每個類型對應的結構體,可以參考?JVMS The Constant Pool。
常量類型 值 CONSTANT_Class 7 CONSTANT_Fieldref 9 CONSTANT_Methodref 10 CONSTANT_InterfaceMethodref 11 CONSTANT_String 8 CONSTANT_Integer 3 CONSTANT_Float 4 CONSTANT_Long 5 CONSTANT_Double 6 CONSTANT_NameAndType 12 CONSTANT_Utf8 1 CONSTANT_MethodHandle 15 CONSTANT_MethodType 16 CONSTANT_InvokeDynamic 18
訪問標記(Access flags)
常量池后面的就是訪問標記。它由兩個字節組成,表示該文件定義的是類還是接口、如果是個類,是 public、abstract還是 final 等。下面是訪問標記列表及其對應的解釋:
標記名稱值解釋 ACC_PUBLIC 0x0001 表示public/strong>;包外的類也可以訪問。 ACC_FINAL 0x0010 表示?final ;不允許有任何子類。 ACC_SUPER 0x0020 通過 invokespecial 指令調用時調用父類的方法。 ACC_INTERFACE 0x0200 是一個接口 而不是類 ACC_ABSTRACT 0x0400 表示?抽象類 ,不能被實例化。
this Class
This class 是一個兩個字節的條目,它的值是一個常量池索引。例如對于 HelloWorld.class 文件,該處的值是0x0006 。在常量池中這個索引指向的條目包括兩個部分,第一個部分是單字節標記,表示這是一個類或是接口,第二部分又是一個兩個字節的常量池索引,指向表示該類或接口的字符串字面值。例如在這個例子中,0x0006 ?索引所在的條目是一個Class_info ,它指向索引值為?0x0021 ,也就是 33 的?Utf8_info ,這個 utf8_info 的值為 HelloWorld,也就是實際的類名。可以查看上面 Java Class 文件常量池示意圖對應 #6 和 #33部分。
super Class
接下來的 2 個字節是該類的父類(Super Class)。和 this class 類似,兩個字節的值是常量池的一個索引,該索引處的常量值是該類的父類。
接口(Interfaces)
該類(或接口)定義的所有接口都在 class 文件的這個部分。起始的兩個字節表示接口的數目,接下來是一個數組,每個數據包括兩個字節,這兩個字節的值又是一個常量池索引,指向具體的接口名稱。
字段(Fields)
一個字段是類或者接口在實例或類層面的變量(屬性)。字段(Fields)部分只包括 class 文件中類或接口定義的字段,而不包括從父類或父接口中繼承而來的字段。
Fileds 部分的前兩個字節也是一個計數,表示字段的數目。接下來是一個表示每個字段的一個數組。每個數組元素是一個可變長度的結構體。該字段的一些信息保存在這個結構體中,也有一些信息保存在常量池中。
方法(Methods)
Methods 部分包括了該類顯式定義的方法,不包括從父類或父接口中繼承來的方法。
頭兩個字節表示方法的數目。剩下的又是一個可變長度數組,其中保存了每個方法的信息。方法結構體保存了方法的多個信息,例如參數列表、返回值、保存局部變量和操作數需要的堆棧數目、異常表、字節碼系列等。
屬性(Attributes)
屬性部分包括了 class 文件的多個屬性信息,例如其中之一是源碼屬性(source code attribute),表示這個 class 文件是從哪個源文件編譯得到的。
屬性部分的前兩個字節表示屬性的數目,接下來的是屬性具體內容。JVM 會忽視任何它無法識別的屬性。
前面介紹的可以說是背景知識,下面的就是是實際的動手實踐
Hacking Into Java Class File
假如我們手里只有一個 HelloWorld.class 文件,我們想在沒有源文件的情況下修改類名,例如我想把類改為 CppWorld。該怎么辦呢?一般有兩種方法:反編譯或者修改直接修改 class 文件。
下面是我在?Decompilers online?用 CFR 方法反編譯 HelloWorld.class 文件得到的結果:
2 ?* Decompiled with CFR 0_110.
4 import?java.io.PrintStream;
6 public?class?HelloWorld {
9 ????public?HelloWorld(String string) {
10 ????????this.msg = string;
13 ????public?HelloWorld() {
14 ????????this.msg =?"Default message";
17 ????public?String getMsg() {
18 ????????return?this.msg;
21 ????public?void?setMsg(String string) {
22 ????????this.msg = string;
25 ????public?void?printMsg() {
26 ????????System.out.println(this.msg);
29 ????public?static?void?main(String[] arrstring) {
30 ????????HelloWorld helloWorld =?new?HelloWorld("Hello world from Java");
31 ????????helloWorld.printMsg();
看起來和上面的 HelloWorld.java 完全一樣,這時候我們再修改 .java 文件,更改類名,然后再編譯得到新的類。這對于一個 Java 新手來說都是輕而易舉。但問題是,對于一個復雜的類或者有很多 .class 文件的 jar 包,反編譯的結果仍然正確嗎?
答案顯然是否定的,我嘗試了Decompilers online?上面的所有方法去反編譯一個 JDBC Jar 包,得到的結果存在一大堆錯誤,從顯而易見的到人肉眼都難以發現的錯誤都有。如果這時候再去一一修正,顯然比較困難。一方面反編譯出來的源碼比較晦澀難懂,例如它里面使用了非常多的 switch case 語句,或者對于無法簡單判斷出來的類型,反編譯器使用了 Object 類代替;另一方面,反編譯出來的源碼是沒有注釋的,一個有上千個文件但卻沒有一行注釋的源碼,單只是想想就令人恐懼。
下面我們就嘗試第二種方法,直接修改 class文件。顯而易見的是我們可以嘗試把 class文件中的所有 “HelloWorld” 字符串替換為 “CppWorld” 字符串。這只需要一個支持16進制編輯的文本編輯器就可以實現。例如我使用 UltraEdit 完成這個字符串替換操作,然后把文件名 HelloWorld.class 修改為 CppWorld.class。然后運行,結果如下:
是什么原因呢?這里我們只替換了字符串,但沒有替換字符串前面的長度。那么如果替換前后字符串長度相同是不是就可以了呢?例如我想替換為 MycppWorld。再來嘗試一次,結果在上面的截圖中。可以看出,對于相同長度的字符串,簡單地進行字符串替換是可以達到 Hack Class File 目的的。同樣,對于字符串長度不一樣的情況,我們只需要同時修改字符串前面的長度即可。通過閱讀?JVMS?中的?Class File Format?章節,發現其實只需要修改?Constant Pool?部分、其余保持不變即可。例如說下面這個簡單的事例程序,它實現了 Class 文件 Constant Pool 部分的字符串替換:
1 import?java.io.BufferedReader;
3 import?java.io.FileInputStream;
4 import?java.io.FileOutputStream;
5 import?java.io.IOException;
6 import?java.io.InputStreamReader;
7 import?java.nio.ByteBuffer;
8 import?java.nio.ByteOrder;
11 ?* String replace in Java .class file.
12 ?* Reference: Java Virtual Machine Specification CLASS file format
13 ?*?https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
19 public?class?Localization {
21 ??public?static?void?localize(String path) {
22 ????FileInputStream fis =?null;
23 ????FileOutputStream fos =?null;
24 ????long?totalsize =?0;
25 ????int?aval_buf =?100;
26 ????byte[] bs =?new?byte[aval_buf];
28 ??????// Output replaced content to file path.out
29 ??????fis =new?FileInputStream(
30 ??????????new?File(path));
31 ??????fos =?new?FileOutputStream(
32 ??????????new?File(path +?".out"));
33 ??????System.out.println("Processing: "?+ path);
35 ??????// Skip magic, max and minus version, 8 bytes
36 ??????fis.read(bs,?0,?8);
37 ??????fos.write(bs,?0,?8);
40 ??????// Get number of constant pool entries, 2 bytes
41 ??????fis.read(bs,?0,?2);
42 ??????fos.write(bs,?0,?2);
44 ??????short?cp_number = bytes2short(bs,?0,?2);
45 ??????System.out.println("Constant pool number: "?+ cp_number);
47 ??????// Handle each constant pool entry
48 ??????String str =?null;
49 ??????for?(short?i =?1; i < cp_number; i++) {
50 ????????// Read flag, 1 byte
51 ????????fis.read(bs,?0,?1);
52 ????????fos.write(bs,?0,?1);
53 ????????totalsize +=?1;
54 ????????// Unless tag value is 1(means utf-8_info where replacement
55 ????????// to be done), just skip specific bytes.
56 ????????short?tag = bytes2short(bs,?0,?1);
61 ??????????fis.read(bs,?0?,2);
62 ??????????fos.write(bs,?0,?2);
63 ??????????totalsize +=?2;
66 ??????????fis.read(bs,?0,?3);
67 ??????????fos.write(bs,?0,?3);
68 ??????????totalsize +=?3;
77 ??????????fis.read(bs,?0,?4);
78 ??????????fos.write(bs,?0,?4);
79 ??????????totalsize +=?4;
83 ??????????fis.read(bs,?0,?8);
84 ??????????fos.write(bs,?0,?8);
85 ??????????//? Next cp index must be valid but is considered unusable
87 ??????????totalsize +=?8;
91 ??????????fis.read(bs,?0,?2);
92 ??????????totalsize +=?2;
93 ??????????short?str_len = bytes2short(bs,?0?,2);
94 ??????????while?(str_len > aval_buf) {
95 ????????????System.out.println("Constant pool number: "?+ i);
96 ????????????System.out.println("Buffer overflow, double it from "+
97 ????????????????aval_buf +?" to "?+ aval_buf *?2);
98 ????????????aval_buf *=?2;
99 ????????????bs =?new?byte[aval_buf];
101 ??????????fis.read(bs,?0, str_len);
102 ??????????totalsize += str_len;
103 ??????????// There may be '\0' in bytes array, but UTF-8 can't
104 ??????????// handle it, so using 'ISO-8859-1' to encode string.
105 ??????????str =?new?String(bs,?0, str_len,?"ISO-8859-1");
106 ??????????str = localizeInternal(str);
107 ??????????str_len = (short)str.length();
108 ??????????byte[] new_len = short2bytes(str_len);
109 ??????????// Update string and length
110 ??????????fos.write(new_len,?0,?2);
111 ??????????fos.write(str.getBytes("ISO-8859-1"),?0, str_len);
115 ??????????System.out.println("File: "?+ path);
116 ??????????System.out.println("Unrecognized tag: "?+ tag +?", cp num: "?+ i);
117 ??????????System.out.println("After: "?+ str +?". Byte offset:"?+ totalsize);
118 ??????????System.exit(1);
119 ????????}// end switch
122 ??????byte[] bsrest =?new?byte[fis.available()];
123 ??????fis.read(bsrest);
124 ??????fos.write(bsrest);
125 ????}?catch?(Exception e) {
126 ??????e.printStackTrace();
128 ??????if?(fis !=?null) {
130 ??????????fis.close();
131 ????????}?catch?(IOException e) {
132 ??????????e.printStackTrace();
135 ??????if?(fos !=?null) {
137 ??????????fos.close();
138 ????????}?catch?(IOException e) {
139 ??????????e.printStackTrace();
145 ??private?static?short?bytes2short(byte[] bs,?int?offset,?intlength) {
146 ????if?(length ==?1)?return?(short) (bs[0] &?0xFF);
147 ????ByteBuffer buf = ByteBuffer.wrap(bs, offset, length);
148 ????buf.order(ByteOrder.BIG_ENDIAN);
149 ????return?buf.getShort();
152 ??private?static?byte[] short2bytes(short?val) {
153 ????ByteBuffer buf = ByteBuffer.allocate(2);
154 ????buf.putShort(val);
155 ????return?buf.array();
158 ??private?static?String localizeInternal(String str) {
160 ????// Replace "HelloWorld" whih "CppWorld"
161 ????String new_str = str.replaceFirst("HelloWorld",?"CppWorld");
162 ????while?(!new_str.equals(str)) {
164 ??????new_str = str.replaceFirst("HelloWorld",?"CppWorld");
169 ??public?static?void?main(String args[]) {
170 ????localize("HelloWorld.class");
下面是運行的結果,我們首先編譯這個工具類 Localization.java,然后使用這個工具類修改 HelloWorld.class 文件生成 HelloWorld.class.out 文件,重命名 HelloWorld.class.out 文件為 CppWorld.class 文件,然后運行 java CppWorld。運行成功!
from:?http://www.stay-stupid.com/?p=401?
總結
以上是生活随笔 為你收集整理的Java Class 文件格式及其简单 Hack 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。