<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/10384

            Author:[email protected]

            0x00 什么是代碼虛擬化


            虛擬化實際上我認為就是使用一套自定義的字節碼來替換掉程序中原有的native指令,而字節碼在執行的時候又由程序中的解釋器來解釋執行。自定義的字節碼是只有解釋器才能識別的,所以一般的工具是無法識別我們自定義的字節碼,也是因為這一點,基于虛擬機的保護相對其他保護而言要更加難破解。但是解釋器一般都是native代碼,這樣才能使解釋器運行起來解釋執行字節碼。其中的關系就像很多的解釋型語言一樣,不是系統的可執行文件,不能直接在系統中運行,需要相應的解釋器才能運行,如python。

            0x01 為什么研究代碼虛擬化


            目前很多地方都會用到虛擬化技術,比如sandbox、程序保護殼等。很多時候為了防止惡意代碼對我們的系統造成破壞,我們需要一個sandbox,使程序運行在sandbox中,即使惡意代碼破壞系統也只是破壞了sandbox而不會對我們的系統造成影響。還有如vmp,shielden這些加密殼就是內置了一個虛擬機來實現對程序代碼的保護,基于虛擬機的保護相對其他保護而言破解起來會更加困難,因為使用現有的工具也是不能識別虛擬機的字節碼。在見識過這類保護殼的威力之后,也萌生出了自己動手寫一個的沖動,所以才有了本文。

            0x02 基于虛擬機的代碼混淆


            基于虛擬機的代碼保護也可以算是代碼混淆技術的一種。代碼混淆的目的就是防止代碼被逆向分析,但是所有的混淆技術都不是完全不能被分析出來,只是增加了分析的難度或者加長了分析的時間,雖然這些技術對保護代碼很有效果,但是也存在著副作用,比如會或多或少的降低程序效率,這一點在基于虛擬機的保護中格外突出,所以大多基于虛擬機的保護都只是保護了其中比較重要的部分。在基于虛擬機的代碼保護中可以大致分為兩種:

            1. 使用虛擬機解釋執行解殼代碼。這種混淆是為了隱藏原代碼是如何被加密的,又是如何被解殼代碼解密的。這種方式對于靜態分析來說比較有效,但是對于動態調試效果不大。因為動態調試的時候完全可以等到解殼代碼解密初源代碼之后進行脫殼。只有配合其他保護技術才會有比較強的保護效果。

            2. 把需要保護的程序源碼轉換為自定義字節碼,再使用虛擬機解釋執行被轉換后的程序字節碼,而程序的源碼是不會出現在程序中的。這種方式不管靜態還是動態都可以有效的保護。

            可以看出兩種保護的區別就是,第一種只保護解殼代碼,沒有保護源碼。第二種直接保護了所有源碼。所以第一種的強度也小于第二種。本文則是以第二種方式來實現保護,也就是保護所有源碼。

            在基于虛擬機的保護技術中,通常自定義的字節碼與native指令都存在著映射關系,也就是說一條或多條字節碼對應于一條native指令。至于為什么需要多條字節碼對應同一條native指令,其實是為了增加虛擬機保護被破解的難度,這樣在對被保護的代碼進行轉換的時候就可以隨機生成出多套字節碼不同,但執行效果相同的程序,導致逆向分析時的難度增加。

            0x03 需要實現什么?

            0x04 解釋器解釋執行過程


            這里就用demo中的第一條字節碼做演示來說明虛擬機中解釋器解釋執行時的過程,首先可以從上面看到解釋器vm_interp執行時eip會指向target_func + 4,也就是target_func中內聯匯編中定義的第一個字節0xa0,之后會判斷eip指向的字節碼是否為ret指令,ret指令是0xa3,所以不是eip指向的不是ret,進入exec_opcode函數進行字節碼解釋。

            進入exec_opcode后開始在虛擬處理器的op_table中查找eip指向的字節碼,當前就是0xa0,找到之后就調用它的解釋函數。

            字節碼與解釋函數的初始化在init_vm_proc中

            可以看出0xa0就對應著mov指令,所以當解釋器遇到0xa0就會調用vm_mov函數來解釋mov指令。

            在vm_mov函數中首先把eip + 1處的一個字節和eip + 2處4個字節分別保存在dest和src中,dest是寄存器標識,在后面的switch中判斷dest是哪個寄存器,在這個例子中dest是0x10,也就是r1寄存器,在case 0x10分支中就把*src賦值給r1。總體來看,前6個字節就是第一條mov指令,對應著mov r1, xxxx,xxxx就是這6個字節中的后4個,在這個例子中就是0x00000000。

            從這個例子中就可以大致了解一個解釋器在解釋執行字節碼時的過程,其實很簡單,就是通過一個字節碼和解釋函數的關系來調用相應的函數,或者通過一個很長的switch來判斷每個字節碼,并調用相應函數。而解釋函數則通過執行相應的操作來模擬出一個指令。最后,把這些指令串聯在一起就可以執行完一個完整的邏輯。

            0x05 代碼運行效果


            0x06 虛擬機保護效果


            靜態分析

            在靜態分析基于虛擬機保護的代碼時,一般的工具都是沒有效果的,因為字節碼都是我們自己定義的,只有解釋器能夠識別。所以在用ida分析時字節碼只是一段不能識別的數據。

            這就是ida識別到的target_func的代碼,已經做到了對抗靜態分析。不過還是可以靜態分析我們的解釋器,在分析解釋器的時候,解釋器中的控制流會比源程序的控制流復雜很多,這樣也是會增加分析難度。

            動態調試

            在動態調試的時候字節碼依然不能被識別,而且處理器也不會真正的去執行這些不被識別的東西。因為這些字節碼都是被我們的虛擬處理器通過解釋器執行的,而我們的解釋器都是native指令,所以可以靜態分析,也可以動態調試。但是動態調試的時候只是在調試解釋器,在調試過程中只能看到在不斷的調用各個指令的解釋函數。所以想要真正還原出源碼就需要在調試過程中找到所有字節碼對應的native指令的映射關系,最后,通過這個映射關系來把字節碼轉換成native指令,當然也可以修復出一個完全脫殼并且可以執行的native程序,只是過程會比較繁瑣。

            0x07 完整代碼


            以下是demo的完整代碼,已經在linux中測試通過。

            xvm.h

            #!c
            #include <stdio.h>
            #include <stdlib.h>
            #include <string.h>    
            
            #define OPCODE_NUM  7 // opcode number
            #define HEAP_SIZE_MAX   1024    
            
            char *heap_buf; // vm heap    
            
            /*
             * opcode enum
             */
            enum OPCODES
            {
                MOV = 0xa0, // mov 指令字節碼對應 0xa0
                XOR = 0xa1, // xor 指令字節碼對應 0xa1
                CMP = 0xa2, // cmp 指令字節碼對應 0xa2
                RET = 0xa3, // ret 指令字節碼對應 0xa3
                SYS_READ = 0xa4, // read 系統調用字節碼對應 0xa4
                SYS_WRITE = 0xa5, // write 系統調用字節碼對應 0xa5
                JNZ = 0xa6 // jnz 指令字節碼對應 0xa0
            };    
            
            enum REGISTERS
            {
                R1 = 0x10,
                R2 = 0x11,
                R3 = 0x12,
                R4 = 0x13,
                EIP = 0x14,
                FLAG = 0x15
            };    
            
            /*
             * opcode struct
             */
            typedef struct opcode_t
            {
                unsigned char opcode; // 字節碼
                void (*func)(void *); // 與字節碼對應的處理函數
            } vm_opcode;    
            
            
            /*
             * virtual processor
             */
            typedef struct processor_t
            {
                int r1; // 虛擬寄存器r1
                int r2; // 虛擬寄存器r2
                int r3; // 虛擬寄存器r3
                int r4; // 虛擬寄存器r4    
            
                int flag; // 虛擬標志寄存器flag,作用類似于eflags    
            
                unsigned char *eip; // 虛擬機寄存器eip,指向正在解釋的字節碼地址    
            
                vm_opcode op_table[OPCODE_NUM]; // 字節碼列表,存放了所有字節碼與對應的處理函數    
            
            } vm_processor;
            

            xvm.c

            #!c
            #include "xvm.h"    
            
            void target_func()
            {
                __asm__ __volatile__(".byte 0xa0, 0x10, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x12, 0x00, 0x00, 0x00, 0xa4, 0xa0, 0x14, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x29, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x20, 0xa6, 0x5b, 0xa0, 0x14, 0x01, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x21, 0xa6, 0x50, 0xa0, 0x14, 0x02, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x22, 0xa6, 0x45, 0xa0, 0x14, 0x03, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x23, 0xa6, 0x3a, 0xa0, 0x14, 0x04, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x24, 0xa6, 0x2f, 0xa0, 0x14, 0x05, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x25, 0xa6, 0x24, 0xa0, 0x14, 0x06, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x26, 0xa6, 0x19, 0xa0, 0x14, 0x07, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x27, 0xa6, 0x0f, 0xa0, 0x10, 0x30, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x09, 0x00, 0x00, 0x00, 0xa5, 0xa3, 0xa0, 0x10, 0x40, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x07, 0x00, 0x00, 0x00, 0xa5, 0xa3");    
            
                /*
                    mov r1, 0x00000000
                    mov r2, 0x12
                    call vm_read    ; 輸入    
            
                    mov r1, input[0]
                    mov r2, 0x29
                    xor r1, r2      ; 異或
                    cmp r1, flag[0] ; 比較
                    jnz ERROR       ; 如果不相同就跳轉到輸出錯誤的代碼    
            
                    ; 同上
                    mov r1, input[1]
                    xor r1, r2
                    cmp r1, flag[1]
                    jnz ERROR    
            
                    mov r1, input[2]
                    xor r1, r2
                    cmp r1, flag[2]
                    jnz ERROR    
            
                    mov r1, input[3]
                    xor r1, r2
                    cmp r1, flag[3]
                    jnz ERROR    
            
                    mov r1, input[4]
                    xor r1, r2
                    cmp r1, flag[4]
                    jnz ERROR    
            
                    mov r1, input[5]
                    xor r1, r2
                    cmp r1, flag[5]
                    jnz ERROR    
            
                    mov r1, input[6]
                    xor r1, r2
                    cmp r1, flag[6]
                    jnz ERROR    
            
                    mov r1, input[7]
                    xor r1, r2
                    cmp r1, flag[7]
                    jnz ERROR
                */
            }    
            
            /*
             * xor 指令解釋函數
             */
            void vm_xor(vm_processor *proc)
            {
                // 異或的兩個數據分別存放在r1,r2寄存器中
                int arg1 = proc->r1;
                int arg2 = proc->r2;    
            
                // 異或結果存在r1中
                proc->r1 = arg1 ^ arg2;    
            
                // xor指令只占一個字節,所以解釋后,eip向后移動一個字節
                proc->eip += 1;
            }    
            
            /*
             * cmp 指令解釋函數
             */
            void vm_cmp(vm_processor *proc)
            {
                // 比較的兩個數據分別存放在r1和buffer中
                int arg1 = proc->r1;、
                // 字節碼中包含了buffer的偏移
                char *arg2 = *(proc->eip + 1) + heap_buf;    
            
                // 比較并對flag寄存器置位,1為相等,0為不等
                if (arg1 == *arg2) {
                    proc->flag = 1;
                } else {
                    proc->flag = 0;
                }    
            
                // cmp指令占兩個字節,eip向后移動2個字節
                proc->eip += 2;
            }    
            
            /*
             * jnz 指令解釋函數
             */
            void vm_jnz(vm_processor *proc)
            {
                // 獲取字節碼中需要的地址相距eip當前地址的偏移
                unsigned char arg1 = *(proc->eip + 1);    
            
                // 通過比較flag的值來判斷之前指令的結果,如果flag為零說明之前指令不想等,jnz跳轉實現
                if (proc->flag == 0) {
                    // 跳轉可以直接修改eip,偏移就是上面獲取到的偏移
                    proc->eip += arg1;
                } else {
                    proc->flag = 0;
                }    
            
                // jnz 指令占2個字節,所以eip向后移動兩個字節
                proc->eip += 2;
            }    
            
            /*
             * ret 指令解釋函數
             */
            void vm_ret(vm_processor *proc)
            {    
            
            }    
            
            /*
             * read 系統調用解釋函數
             */
            void vm_read(vm_processor *proc)
            {
                // read系統調用有兩個參數,分別存放在r1,r2寄存器中,r1中是保存讀入數據的buf的偏移,r2為希望讀入的長度
                char *arg2 = heap_buf + proc->r1;
                int arg3 = proc->r2;    
            
                // 直接調用read
                read(0, arg2, arg3);    
            
                // read系統調用占1個字節,所以eip向后移動1個字節
                proc->eip += 1;
            }    
            
            /*
             * write 系統調用解釋函數
             */
            void vm_write(vm_processor *proc)
            {
                // 與read系統調用相同,r1中是保存寫出數據的buf的偏移,r2為希望寫出的長度
                char *arg2 = heap_buf + proc->r1;
                int arg3 = proc->r2;    
            
                // 直接調用write
                write(1, arg2, arg3);    
            
                // write系統調用占1個字節,所以eip向后移動1個字節
                proc->eip += 1;
            }    
            
            /*
             * mov 指令解釋函數
             */
            void vm_mov(vm_processor *proc)
            {
                // mov 指令兩個參數都隱含在字節碼中了,指令標識后的第一個字節是寄存器的標識,指令標識后的第二到第五個字節是要mov的立即數,目前只實現了mov一個立即數到一個寄存器中和mov一個buffer中的內容到一個r1寄存器
                unsigned char *dest = proc->eip + 1;
                int *src = (int *) (proc->eip + 2);    
            
                // 前4個case分別對應r1~r4,最后一個case中,*src保存的是buffer的一個偏移,實現了把buffer中的一個字節賦值給r1
                switch (*dest) {
                    case 0x10:
                        proc->r1 = *src;
                        break;    
            
                    case 0x11:
                        proc->r2 = *src;
                        break;    
            
                    case 0x12:
                        proc->r3 = *src;
                        break;    
            
                    case 0x13:
                        proc->r4 = *src;
                        break;    
            
                    case 0x14:
                        proc->r1 = *(heap_buf + *src);
                        break;
                }    
            
                // mov指令占6個字節,所以eip向后移動6個字節
                proc->eip += 6;
            }    
            
            /*
             * 執行字節碼
             */
            void exec_opcode(vm_processor *proc)
            {
                int flag = 0;
                int i = 0;    
            
                // 查找eip指向的正在解釋的字節碼對應的處理函數
                while (!flag && i < OPCODE_NUM) {
                    if (*proc->eip == proc->op_table[i].opcode) {
                        flag = 1;
                        // 查找到之后,調用本條指令的處理函數,由處理函數來解釋
                        proc->op_table[i].func((void *) proc);
                    } else {
                        i++;
                    }
                }    
            
            }    
            
            /* 
             * 虛擬機的解釋器
             */
            void vm_interp(vm_processor *proc)
            {
                /* eip指向被保護代碼的第一個字節
                 * target_func + 4是為了跳過編譯器生成的函數入口的代碼
                 */
                proc->eip = (unsigned char *) target_func + 4;    
            
                // 循環判斷eip指向的字節碼是否為返回指令,如果不是就調用exec_opcode來解釋執行
                while (*proc->eip != RET) {
                    exec_opcode(proc);
                }
            }    
            
            /*
             * 初始化虛擬機處理器
             */
            void init_vm_proc(vm_processor *proc)
            {
                proc->r1 = 0;
                proc->r2 = 0;
                proc->r3 = 0;
                proc->r4 = 0;
                proc->flag = 0;    
            
                // 把指令字節碼與解釋函數關聯起來
                proc->op_table[0].opcode = MOV;
                proc->op_table[0].func = (void (*)(void *)) vm_mov;    
            
                proc->op_table[1].opcode = XOR;
                proc->op_table[1].func = (void (*)(void *)) vm_xor;    
            
                proc->op_table[2].opcode = CMP;
                proc->op_table[2].func = (void (*)(void *)) vm_cmp;    
            
                proc->op_table[3].opcode = SYS_READ;
                proc->op_table[3].func = (void (*)(void *)) vm_read;    
            
                proc->op_table[4].opcode = SYS_WRITE;
                proc->op_table[4].func = (void (*)(void *)) vm_write;    
            
                proc->op_table[5].opcode = RET;
                proc->op_table[5].func = (void (*)(void *)) vm_ret;    
            
                proc->op_table[6].opcode = JNZ;
                proc->op_table[6].func = (void (*)(void *)) vm_jnz;    
            
                // 創建buffer
                heap_buf = (char *) malloc(HEAP_SIZE_MAX);
            
                // 初始化buffer
                memcpy(heap_buf + 0x20, "syclover", 8);
                memcpy(heap_buf + 0x30, "success!\n", 9);
                memcpy(heap_buf + 0x40, "error!\n", 7);
            }    
            
            
            // flag: ZPJEF_L[
            int main()
            {
                vm_processor proc = {0};    
            
                // initial vm processor
                init_vm_proc(&proc);    
            
                // execute target func
                vm_interp(&proc);
                return 0;
            }
            

            0x08 總結


            以上程序為學習代碼虛擬化之后的總結,其中有很多理解不正確的地方希望大牛指正。這只是最簡單的實現,僅用于學習使用,想要深入學習虛擬化技術還是非常復雜,需要積累更多知識才能理解到位,這篇文章就當是拋磚引玉。在學習的過程也有很多問題沒有解決,比如:如果想實現一個基于虛擬機的保護殼,必定需要把源程序中的native指令首先轉換為自定義字節碼,但是不知道用什么方法來轉換比較好。

            在很多國外文章里也看到另一種虛擬機保護,是基于LLVM-IR的虛擬機保護,有興趣也可以繼續深入研究一下。

            0x09 參考


            http://www.cs.rhul.ac.uk/home/kinder/papers/wcre12.pdf

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

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

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

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

                      亚洲欧美在线