逆向基礎(十三) JAVA (一)
翻譯書籍:Reverse Engineering for Beginners
作者:Dennis Yurichev
翻譯者:糖果
54.1介紹
大家都知道,java有很多的反編譯器(或是產生JVM位元組碼) 原因是JVM位元組碼比其他的X86低階程式碼更容易進行反編譯。
a).多很多相關資料型別的資訊。
b).JVM(java虛擬機器)記憶體模型更嚴格和概括。
c).java編譯器沒有做任何的最佳化工作(JVM JIT不是實時),所以,類檔案中的位元組程式碼的通常更清晰易讀。
JVM位元組碼知識什麼時候有用呢?
a).檔案的快速粗糙的打補丁任務,類檔案不需要重新編譯反編譯的結果。
b).分析混淆程式碼
c).建立你自己的混淆器。
d).建立編譯器程式碼生成器(後端)目標。
我們從一段簡短的程式碼開始,除非特殊宣告,我們用的都是JDK1.7
反編譯類檔案使用的命令,隨處可見:javap -c -verbase.
在這本書中提供的很多的例子,都用到了這個。
54.2 返回一個值
可能最簡單的java函式就是返回一些值,oh,並且我們必須注意,一邊情況下,在java中沒有孤立存在的函式,他們是“方法”(method),每個方法都是被關聯到某些類,所以方法不會被定義在類外面, 但是我還是叫他們“函式” (function),我這麼用。
#!java
public class ret
{
public static int main(String[] args)
{
return 0;
}
}
編譯它。
javac ret.java
使用Java標準工具反編譯。
javap -c -verbose ret.class
會得到結果:
#!java
public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_0
1: ireturn
對於java開發者在程式設計中,0是使用頻率最高的常量。 因為區分短一個短位元組的 iconst_0指令入棧0,iconst_1指令(入棧),iconst_2等等,直到iconst5。也可以有iconst_m1, 推送-1。
就像在MIPS中,分離一個暫存器給0常數:3.5.2 在第三頁。
棧在JVM中用於在函式呼叫時,傳參和傳返回值。因此, iconst_0是將0入棧,ireturn指令,(i就是integer的意思。)是從棧頂返回整數值。
讓我們寫一個簡單的例子, 現在我們返回1234:
#!java
public class ret
{
public static int main(String[] args)
{
return 1234;
}
}
我們得到:
清單:
54.2:jdk1.7(節選)
public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: sipush 1234 3: ireturn
sipush(shot integer)如棧值是1234,slot的名字以為著一個16bytes值將會入棧。 sipush(短整型) 1234數值確認時候16-bit值。
#!java
public class ret
{
public static int main(String[] args)
{
return 12345678;
}
}
更大的值是什麼?
清單 54.3 常量區
...
#2 = Integer 12345678
...
5棧頂
public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATI
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // int 12345678
2: ireturn
操作碼 JVM的指令碼操作碼不可能編碼成32位數,開發者放棄這種可能。因此,32位數字12345678是被儲存在一個叫做常量區的地方。讓我們說(大多數被使用的常數(包括字元,物件等等車)) 對我們而言。
對JVM來說傳遞常量不是唯一的,MIPS ARM和其他的RISC CPUS也不可能把32位操作編碼成32位數字,因此 RISC CPU(包括MIPS和ARM)去構造一個值需要一系列的步驟,或是他們儲存在資料段中: 28。3 在654頁.291 在695頁。
MIPS碼也有一個傳統的常量區,literal pool(原語區) 這個段被叫做"lit4"(對於32位單精度浮點數常數儲存) 和lit8(64位雙精度浮點整數常量區)
布林型
#!java
public class ret
{
public static boolean main(String[] args)
{
return true;
}
}
public static boolean main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_1
這個JVM位元組碼是不同於返回的整數學 ,32位資料,在形參中被當成邏輯值使用。像C/C++,但是不能像使用整型或是viceversa返回布林型,型別資訊被儲存在類檔案中,在執行時檢查。
16位短整型也是一樣。
#!java
public class ret
{
public static short main(String[] args)
{
return 1234;
}
}
public static short main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: sipush 1234
3: ireturn
還有char 字元型?
#!java
public class ret
{
public static char main(String[] args)
{
return 'A';
}
}
public static char main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush 65
2: ireturn
bipush 的意思"push byte"位元組入棧,不必說java的char是16位UTF16字元,和short 短整型相等,單ASCII碼的A字元是65,它可能使用指令傳輸位元組到棧。
讓我們是試一下byte。
#!java
public class retc
{
public static byte main(String[] args)
{
return 123;
}
}
public static byte main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush 123
2: ireturn
909
也許會問,位什麼費事用兩個16位整型當32位用?為什麼char資料型別和短整型型別還使用char.
答案很簡單,為了資料型別的控制和程式碼的可讀性。char也許本質上short相同,但是我們快速的掌握它的佔位符,16位的UTF字元,並且不像其他的integer值符。使用 short,為各位展現一下變數的範圍被限制在16位。在需要的地方使用boolean型也是一個很好的主意。代替C樣式的int也是為了相同的目的。
在java中integer的64位資料型別。
#!java
public class ret3
{
public static long main(String[] args)
{
return 1234567890123456789L;
}
}
清單54.4常量區
...
#2 = Long 1234567890123456789l
...
public static long main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #2 // long ⤦
Ç 1234567890123456789l
3: lreturn
64位數也被在儲存在常量區,ldc2_w 載入它,lreturn返回它。 ldc2_w指令也是從記憶體常量區中載入雙精度浮點數。(同樣佔64位)
#!java
public class ret
{
public static double main(String[] args)
{
return 123.456d;
}
}
清單54.5常量區
...
#2 = Double 123.456d
...
public static double main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #2 // double 123.456⤦
Ç d
3: dreturn
dreturn 代表 "return double"
最後,單精度浮點數:
#!java
public class ret
{
public static float main(String[] args)
{
return 123.456f;
}
}
清單54.6 常量區
...
#2 = Float 123.456f
...
public static float main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // float 123.456f
2: freturn
此處的ldc指令使用和32位整型資料一樣,從常量區中載入。freturn 的意思是"return float"
那麼函式還能返回什麼呢?
#!java
public class ret
{
public static void main(String[] args)
{
return;
}
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
這以為著,使用return控制指令確沒有返回實際的值,知道這一點就非常容易的從最後一條指令中演繹出函式(或是方法)的返回型別。
54.3 簡單的計算函式
讓我們繼續看簡單的計算函式。
#!java
public class calc
{
public static int half(int a)
{
return a/2;
}
}
這種情況使用icont_2會被使用。
public static int half(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: iconst_2
2: idiv
3: ireturn
iload_0 將零給函式做引數,然後將其入棧。iconst_2將2入棧,這兩個指令執行後,棧看上去是這個樣子的。
+---+
TOS ->| 2 |
+---+
| a |
+---+
idiv攜帶兩個值在棧頂, divides 只有一個值,返回結果在棧頂。
+--------+
TOS ->| result |
+--------+
ireturn取得比返回。 讓我們處理雙精度浮點整數。
#!java
public class calc
{
public static double half_double(double a)
{
return a/2.0;
}
}
清單54.7 常量區
...
#2 = Double 2.0d
...
public static double half_double(double);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: dload_0
1: ldc2_w #2 // double 2.0d
4: ddiv
5: dreturn
類似,只是ldc2_w指令是從常量區裝載2.0,另外,所有其他三條指令有d字首,意思是他們工作在double資料型別下。
我們現在使用兩個引數的函式。
#!java
public class calc
{
public static int sum(int a, int b)
{
return a+b;
}
}
#!bash
public static int sum(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn
iload_0載入第一個函式引數(a),iload_2 第二個引數(b)下面兩條指令執行後,棧的情況如下:
+---+
TOS ->| b |
+---+
| a |
+---+
iadds 增加兩個值,返回結果在棧頂。
+--------+ TOS ->| result | +--------+
讓我們把這個例子擴充套件成長整型資料型別。
#!java
public static long lsum(long a, long b)
{
return a+b;
}
我們得到的是:
#!java
public static long lsum(long, long);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=2
0: lload_0
1: lload_2
2: ladd
3: lreturn
第二個(load指令從第二引數槽中,取得第二引數。這是因為64位長整型的值佔用來位,用了另外的話2位引數槽。)
稍微複雜的例子
#!java
public class calc
{
public static int mult_add(int a, int b, int c)
{
return a*b+c;
}
}
public static int mult_add(int, int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=3
0: iload_0
1: iload_1
2: imul
3: iload_2
4: iadd
5: ireturn
第一是相乘,積被儲存在棧頂。
+---------+
TOS ->| product |
+---------+
iload_2載入第三個引數(C)入棧。
+---------+
TOS ->| c |
+---------+
| product |
+---------+
現在iadd指令可以相加兩個值。
54.4 JVM記憶體模型
X86和其他低階環境系統使用棧傳遞引數和儲存本地變數,JVM稍微有些不同。
主要體現在: 本地變數陣列(LVA)被用於儲存到來函式的引數和本地變數。iload_0指令是從其中載入值,istore儲存值在其中,首先,函式引數到達:開始從0 或者1(如果0參被this指標用。),那麼本地區域性變數被分配。
每個槽子的大小都是32位,因此long和double資料型別都佔兩個槽。
運算元棧(或只是"棧"),被用於在其他函式呼叫時,計算和傳遞引數。不像低階X86的環境,它不能去訪問棧,而又不明確的使用pushes和pops指令,進行出入棧操作。
54.5 簡單的函式呼叫
mathrandom()返回一個偽隨機數,函式範圍在「0.0...1.0)之間,但對我們來說,由於一些原因,我們常常需要設計一個函式返回數值範圍在「0.0...0.5)
#!java
public class HalfRandom
{
public static double f()
{
return Math.random()/2;
}
}
常量區
...
#2 = Methodref #18.#19 // java/lang/Math.⤦
Ç random:()D
6(Java) Local Variable Array
#3 = Double 2.0d
...
#12 = Utf8 ()D
...
#18 = Class #22 // java/lang/Math
#19 = NameAndType #23:#12 // random:()D
#22 = Utf8 java/lang/Math
#23 = Utf8 random
public static double f();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: invokestatic #2 // Method java/⤦
Ç lang/Math.random:()D
3: ldc2_w #3 // double 2.0d
6: ddiv
7: dreturn
java本地變數陣列 916 靜態執行呼叫math.random()函式,返回值在棧頂。結果是被0.5初返回的,但函式名是怎麼被編碼的呢? 在常量區使用methodres表示式,進行編碼的,它定義類和方法的名稱。第一個methodref 欄位指向表示式,其次,指向通常文字字元("java/lang/math") 第二個methodref表達指向名字和型別表示式,同時連結兩個字元。第一個方法的名字式字串"random",第二個字串是"()D",來編碼函式型別,它以為這兩個值(因此D是字串)這種方式1JVM可以檢查資料型別的正確性:2)java反編譯器可以從被編譯的類檔案中修改資料型別。
最後,我們試著使用"hello,world!"作為例子。
#!java
public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello, World");
}
}
常量區
917 常量區的ldc行偏移3,指向"hello,world!"字串,並且將其入棧,在java裡它被成為飲用,其實它就是指標,或是地址。
...
#2 = Fieldref #16.#17 // java/lang/System.⤦
Ç out:Ljava/io/PrintStream;
#3 = String #18 // Hello, World
#4 = Methodref #19.#20 // java/io/⤦
Ç PrintStream.println:(Ljava/lang/String;)V
...
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/⤦
Ç PrintStream;
#18 = Utf8 Hello, World
#19 = Class #26 // java/io/⤦
Ç PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/⤦
Ç lang/String;)V
...
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
...
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, ⤦
Ç World
5: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
8: return
常見的invokevirtual指令,從常量區取資訊,然後呼叫pringln()方法,貌似我們知道的println()方法,適用於各種資料型別,我這種println()函式版本,預先給的是字串型別。
但是第一個getstatic指令是幹什麼的?這條指令取得物件資訊的欄位的一個引用或是地址。輸出並將其進棧,這個值實際更像是println放的指標,因此,內部的print method取得兩個引數,輸入1指向物件的this指標,2)"hello,world"字串的地址,確實,println()在被初始化系統的呼叫,物件之外,為了方便,javap使用工具把所有的資訊都寫入到註釋中。
54.6 呼叫beep()函式
這可能是最簡單的,不使用引數的呼叫兩個函式。
#!java
public static void main(String[] args)
{
java.awt.Toolkit.getDefaultToolkit().beep();
};
#!bash
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method java/⤦
Ç awt/Toolkit.getDefaultToolkit:()Ljava/awt/Toolkit;
3: invokevirtual #3 // Method java/⤦
Ç awt/Toolkit.beep:()V
6: return
首先,invokestatic在0行偏移呼叫javaawt.toolkit. getDefaultTookKit()函式,返回toolkit類物件的引用,invokedvirtualIFge指令在3行偏移,呼叫這個類的beep()方法。
相關文章
- 逆向基礎(一)2020-08-19
- Java基礎二十三(interface)2020-11-15Java
- iOS逆向-彙編基礎(一)2018-11-05iOS
- 羽夏逆向——逆向基礎2021-11-19
- 逆向基礎(六)2020-08-19
- 逆向基礎(五)2020-08-19
- 逆向基礎(四)2020-08-19
- 逆向基礎(三)2020-08-19
- 逆向基礎(十一)2020-08-19
- 逆向基礎(十)2020-08-19
- 逆向基礎(十二)2020-08-19
- 逆向基礎(二)2020-08-19
- 逆向基礎(九)2020-08-19
- 逆向基礎(八)2020-08-19
- 逆向基礎(七)2020-08-19
- Java逆向基礎之靜態變數存取2021-09-09Java變數
- iOS逆向之旅(基礎篇) — 彙編(一)— 彙編基礎2018-10-25iOS
- 逆向工程核心原理(1)逆向基礎2023-03-16
- EOS基礎全家桶(十三)智慧合約基礎2020-06-18
- iOS逆向與安全:基礎篇2018-10-31iOS
- JAVA學習--JAVA基礎(一)2020-12-19Java
- (2020)JAVA基礎篇(一)2020-10-20Java
- Java基礎系列之一2021-09-09Java
- JAVA基礎語法(一)2021-04-10Java
- 學習Java基礎知識,打通面試關~十三鎖機制2018-07-09Java面試
- iOS逆向之旅(基礎篇) — Macho檔案2018-10-26iOSMac
- 逆向基礎 Finding important/interesting stuff in the code (二)2020-08-19ImportREST
- 20192204-exp1-逆向與Bof基礎2022-03-17
- [Kotlin基礎] Java 呼叫 Kotlin(一)2018-04-29KotlinJava
- Java 基礎學習系列一 —— Java 主要特性2020-05-04Java
- Java基礎-語法基礎2020-07-27Java
- Java面試題-基礎篇一2019-04-04Java面試題
- java基礎:String — 原始碼分析(一)2018-12-16Java原始碼
- Java面試題基礎篇(一)2018-03-28Java面試題
- Java基礎知識之概述(一)2020-12-01Java
- JAVA 基礎2019-01-07Java
- java基礎2024-07-30Java
- [Java基礎]2024-07-09Java