<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/tips/6924

            翻譯書籍: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()方法。

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线