<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/mobile/16969

            Author:超六、曲和

            0x00 時間相關反調試


            通過計算某部分代碼的執行時間差來判斷是否被調試,在Linux內核下可以通過time、gettimeofday,或者直接通過sys call來獲取當前時間。另外,還可以通過自定義SIGALRM信號來判斷程序運行是否超時。

            0x01 檢測關鍵文件


            (1)/proc/pid/status、/proc/pid/task/pid/status

            在調試狀態下,Linux內核會向某些文件寫入一些進程狀態的信息,比如向/proc/pid/status或/proc/pid/task/pid/status文件的TracerPid字段寫入調試進程的pid,在該文件的statue字段中寫入t(tracing stop):

            pic1

            (2)/proc/pid/stat、/proc/pid/task/pid/stat

            調試狀態下/proc/pid/stat、/proc/pid/task/pid/stat文件中第二個字段是t(T):

            pic2

            (3)/proc/pid/wchan、/proc/pid/task/pid/wchan

            若進程被調試,也會往/proc/pid/wchan、/proc/pid/task/pid/wchan文件中寫入ptrace_stop。

            0x02 檢測端口號


            使用IDA動態調試APK時,android_server默認監聽23946端口,所以通過檢測端口號可以起到一定的反調試作用。具體而言,可以通過檢測/proc/net/tcp文件,或者直接system執行命令netstat -apn等。

            0x03 檢測android_server、gdb、gdbserver


            在對APK進行動態調試時,可能會打開android_server、gdb、gdbserver等調試相關進程,一般情況下,這幾個打開的進程名和文件名相同,所以可以通過運行狀態下的進程名來檢測這些調試相關進程。具體而言,可以通過打開/proc/pid/cmdline、/proc/pid/statue等文件來獲取進程名。當然,這種檢測方法非常容易繞過――直接修改android_server、gdb、gdbserver的名字即可。

            0x04 signal


            信號機制在apk調試攻防中有著非常重要的作用,大部分主流加固廠商都會通過信號機制來增加殼的強度。在反調試中最常見的要數SIGTRAP信號了,SIGTRAP原本是調試器設置斷點時發出的信號,為了能更好的理解SIGTRAP信號反調試,先讓我們看看一下調試器設置斷點的原理:

            和x86架構類似,arm架構下調試器設置斷點先要完成兩件事:

            1. 保存目標地址上的數據
            2. 將目標地址上頭幾個字節替換成arm/thumb下的breakpoint指令

            Arm架構下各類指令集breakpoint機器碼如下:

            指令集 Breakpoint機器碼(little endian)
            Arm 0x01, 0x00, 0x9f, 0xef
            Thumb 0x01, 0xde
            Thumb2 0xf0, 0xf7, 0x00, 0xa0

            調試器設置完斷點之后程序繼續運行,直至命中斷點,觸發breakpoint,這時程序向操作系統發送SIGTRAP信號。調試器收到SIGTRAP信號后,會繼續完成以下幾件事:

            1. 在目標地址上用原來的指令替換之前的breakpoint指令
            2. 回退被跟蹤進程的當前pc值

            當控制權回到原進程時,pc就恰好指向了斷點所在位置,這就是調試器設置斷點的基本原理。在知道上述原理之后,再讓我們繼續分析SIGTRAP反調試的細節,如果我們在程序中間插入一條breakpoint指令,而不做其他處理的話,操作系統會用原來的指令替換breakpoint指令,然而這個breakpoint是我們自定義插入的,該地址上并不存在原指令,所以操作系統就跳過這個步驟,進入下一步回退pc值,即breakpoint的前一條指令。這時就出現問題了,下一條指令還是breakpoint指令,這也就造成了無限循環。

            為了能繼續正常執行,就需要模擬調試器的操作――替換breakpoint指令,而完成這個步驟的最佳時機就是在自定義signal的handle中。Talk is cheap,show me the code,下面給出此原理的簡單實例:

            #!cpp
            char dynamic_ccode[] = {0x1f,0xb4, //push {r0-r4}
                                    0x01,0xde, //breakpoint
                                    0x1f,0xbc, //pop {r0-r4}
                                    0xf7,0x46};//mov pc,lr
            
            char *g_addr = 0;
            
            void my_sigtrap(int sig){
            
                char change_bkp[] = {0x00,0x46}; //mov r0,r0
                memcpy(g_addr+2,change_bkp,2);
                __clear_cache((void*)g_addr,(void*)(g_addr+8)); // need to clear cache
                LOGI("chang bpk to nop\n");
            
            }
            
            void anti4(){//SIGTRAP
            
                int ret,size;
                char *addr,*tmpaddr;
            
                signal(SIGTRAP,my_sigtrap);
            
                addr = (char*)malloc(PAGESIZE*2);
            
                memset(addr,0,PAGESIZE*2);
                g_addr = (char *)(((int) addr + PAGESIZE-1) & ~(PAGESIZE-1));
            
                LOGI("addr: %p ,g_addr : %p\n",addr,g_addr);
            
                ret = mprotect(g_addr,PAGESIZE,PROT_READ|PROT_WRITE|PROT_EXEC);
                if(ret!=0)
                {
                    LOGI("mprotect error\n");
                    return ;
                }
            
                size = 8;
                memcpy(g_addr,dynamic_ccode,size);
            
                __clear_cache((void*)g_addr,(void*)(g_addr+size)); // need to clear cache
            
                __asm__("push {r0-r4,lr}\n\t"
                        "mov r0,pc\n\t"  //此時pc指向后兩條指令
                        "add r0,r0,#4\n\t"http://+4 是的lr 地址為 pop{r0-r5}
                        "mov lr,r0\n\t"
                        "mov pc,%0\n\t"
                        "pop {r0-r5}\n\t"
                        "mov lr,r5\n\t" //恢復lr
                :
                :"r"(g_addr)
                :);
            
                LOGI("hi, i'm here\n");
                free(addr);
            
            }
            

            在代碼中主動觸發breakpoint指令,然后在自定義SIGTRAP handle中將breakpoint替換成nop指令,于是程序可以正常執行完畢。

            其中可使用r_debug-r_brk來觸發異常,其原理即是用到了linker中一些調試特性。Linker中有一個和調試相關的結構體r_debug,其定義如下:

            #!cpp
            struct r_debug {
                int32_t r_version;
                link_map_t* r_map;
                void (*r_brk)(void);
                int32_t r_state;
                uintptr_t r_ldbase;
            };  
            

            r_debug是以靜態變量的形式存在于linker中,其初始化代碼如下:

            #!cpp
            static r_debug _r_debug = {1, NULL, &rtld_db_dlactivity, RT_CONSISTENT, 0};
            

            在初始化時,r_debug中的r_brk函數指針被初始化成了rtld_db_dlactivity函數,該函數只是一個空的樁函數:

            #!cpp
            /*
             * This function is an empty stub where GDB locates a breakpoint to get notified
             * about linker activity.  It can?t be inlined away, can't be hidden.
             */
            extern "C" void __attribute__((noinline)) __attribute__((visibility("default"))) rtld_db_dlactivity() {
            }
            

            沒調試下,該函數即為空函數,而在調試狀態下會將該函數的內容改寫為相應指令集的breakpoint指令。所以先注冊自己的signal函數處理breakpoint異常(SIGTRAP),然后在運行時調用該函數,即可觸發自定義SIGTRAP的接管函數。而動態調試時,SIGTRAP會先被調試器接收,這樣不僅能迷惑調試器,還能在自定義接管函數中做一些tricky的事。

            0x05 檢測軟件斷點


            上一節說了使用SIGTRAP反調試的原理,由此可以衍生出另一種很常見的反調試方法――檢測軟件斷點。軟件斷點通過改寫目標地址的頭幾字節為breakpoint指令,只需要遍歷so中可執行segment,查找是否出現breakpoint指令即可。實現大致如下:

            #!cpp
            unsigned long GetLibAddr() {
                unsigned long ret = 0;
                char name[] = "libanti_debug.so";
                char buf[4096], *temp;
                int pid;
                FILE *fp;
                pid = getpid();
                sprintf(buf, "/proc/%d/maps", pid);
                fp = fopen(buf, "r");
                if (fp == NULL) {
                    puts("open failed");
                    goto _error;
                }
                while (fgets(buf, sizeof(buf), fp)) {
                    if (strstr(buf, name)) {
                        temp = strtok(buf, "-");
                        ret = strtoul(temp, NULL, 16);
                        break;
                    }
                }
                _error: fclose(fp);
                return ret;
            }
            
            
            
            void anti5(){
            
                Elf32_Ehdr *elfhdr;
                Elf32_Phdr *pht;
                unsigned int size, base, offset,phtable;
                int n, i,j;
                char *p;
            
                //從maps中讀取elf文件在內存中的起始地址
                base = GetLibAddr();
                if(base == 0){
                    LOGI("find base error\n");
                    return;
                }
            
                elfhdr = (Elf32_Ehdr *) base;
            
                phtable = elfhdr->e_phoff + base;
            
                for(i=0;i<elfhdr->e_phnum;i++){
            
                    pht = (Elf32_Phdr*)(phtable+i*sizeof(Elf32_Phdr));
            
                    if(pht->p_flags&1){
                        offset = pht->p_vaddr + base + sizeof(Elf32_Ehdr) + sizeof(Elf32_Phdr)*elfhdr->e_phnum;
                        LOGI("offset:%X ,len:%X",offset,pht->p_memsz);
            
                        p = (char*)offset;
                        size = pht->p_memsz;
            
                        for(j=0,n=0;j<size;++j,++p){
            
                            if(*p == 0x10 && *(p+1) == 0xde){
                                n++;
                                LOGI("### find thumb bpt %X \n",p);
                            }else if(*p == 0xf0 && *(p+1) == 0xf7 && *(p+2) == 0x00 && *(p+3) == 0xa0){
                                n++;
                                LOGI("### find thumb2 bpt %X \n",p);
                            }else if(*p == 0x01 && *(p+1) == 0x00 && *(p+2) == 0x9f && *(p+3) == 0xef){
                                n++;
                                LOGI("### find arm bpt %X \n",p);
                            }
            
                        }
                        LOGI("### find breakpoint num: %d\n",n);
            
                    }
                }
            
            }
            

            大家在使用IDA調試的時候,也許會注意到IDA的代碼窗口和hex view窗口在設置斷點的時候,目標地址的內容并沒有發生改變,其實這是IDA故意將其隱藏了,設置完斷點之后直接用dd dump內存就能看見設置斷點的地址頭幾字節發生了改變。

            0x06 進程間通信


            大部分加固會新建進程或者新建線程,在這些新建的線程和進程中完成反調試操作,然而如果這些進程、線程相對獨立的話,很容易通過掛起、殺死的方式直接使得反調試失效。為了保證反調試線程、進程的存活,就需要一種通信方式,定期確認反調試線程、進程依然存活,所以進程間通信是高級反調試不可或缺的方式。在Linux下有很多進程間通信的方式,比如管道、信號、共享內存、套接字(socket)等,下面提供一個通過管道將反調試進程和主進程聯系起來的簡單例子:

            #!cpp
            int pipefd[2];
            int childpid;
            
            void *anti3_thread(void *){
            
                int statue=-1,alive=1,count=0;
            
                close(pipefd[1]);
            
                while(read(pipefd[0],&statue,4)>0)
                    break;
                sleep(1);
            
                //這里改為非阻塞
                fcntl(pipefd[0], F_SETFL, O_NONBLOCK); //enable fd的O_NONBLOCK
            
                LOGI("pip-->read = %d", statue);
            
                while(true) {
            
                    LOGI("pip--> statue = %d", statue);
                    read(pipefd[0], &statue, 4);
                    sleep(1);
            
                    LOGI("pip--> statue2 = %d", statue);
                    if (statue != 0) {
                        kill(childpid,SIGKILL);
                        kill(getpid(), SIGKILL);
                        return NULL;
                    }
                    statue = -1;
                }
            }
            
            void anti3(){
                int pid,p;
                FILE *fd;
                char filename[MAX];
                char line[MAX];
            
                pid = getpid();
                sprintf(filename,"/proc/%d/status",pid);// 讀取proc/pid/status中的TracerPid
                p = fork();
                if(p==0) //child
                {
                    close(pipefd[0]); //關閉子進程的讀管道
                    int pt,alive=0;
                    pt = ptrace(PTRACE_TRACEME, 0, 0, 0); //子進程反調試
                    while(true)
                    {
                        fd = fopen(filename,"r");
                        while(fgets(line,MAX,fd))
                        {
                            if(strstr(line,"TracerPid") != NULL)
                            {
                                int statue = atoi(&line[10]);
                                LOGI("########## tracer pid:%d", statue);
                                write(pipefd[1],&statue,4);//子進程向父進程寫 statue值
            
                                fclose(fd);
            
                                if(statue != 0)
                                {
                                    return ;
                                }
            
                                break;
                            }
                        }
                        sleep(1);
            
                    }
                }else{
                    childpid = p;
                }
            }
            pipe(pipefd);
            pthread_create(&id_0,NULL,anti3_thread,(void*)NULL);
            anti3();
            

            傳統檢測TracerPid的方法是直接在子進程中循環檢測,一旦發現則主動殺死進程。本實例將循環檢測TracerPid和進程間通信結合,一旦反調試子進程被掛起或被殺死,父進程也會馬上終止,原理大致如下圖:

            pic3

            父進程的守護線程在從pipe中read到statue值之前,默認statue值為-1,收到子進程往pipe中寫的statue值之后,重置statue值,如果未被調試,statue值為0,反之則為被調試狀態。該做法的優勢在于,一旦反調試進程被終止或被掛起,守護線程也能馬上發現。

            當然,如果通過hook或者修改kernel同樣可以輕易的繞過這種反調試。這種做法只是為了演示而寫的簡單例子,真實的進程間通信反調試可以寫的復雜的多,大家可以盡情發揮想象。

            0x07 dalvik 虛擬機內部相關字段


            在dalvik虛擬機中自帶了檢測調試器的代碼,其本質是檢測DvmGlobals結構體中的相關字段:

            #!cpp
            struct DvmGlobals {
                …
                bool   debuggerConnected;    /* debugger or DDMS is connected */
                bool   debuggerActive;        /* debugger is making requests */
                …
            }
            

            檢測調試器的函數:

            #!cpp
            /*
             * static boolean isDebuggerConnected()
             *
             * Returns "true" if a debugger is attached.
             */
            static void Dalvik_dalvik_system_VMDebug_isDebuggerConnected(const u4* args, JValue* pResult)
            {
                UNUSED_PARAMETER(args);
                RETURN_BOOLEAN(dvmDbgIsDebuggerConnected());
            }
            

            本質是檢測該dalvik虛擬機中DvmGlobals結構體中的調試器狀態字段:

            #!cpp
            bool dvmDbgIsDebuggerConnected()
            {
                return gDvm.debuggerActive;
            }
            

            知道原理之后可以更進一步,不通過這些Dalvik虛擬機的自定義函數,而是直接獲取這些字段值,這樣可以更好的隱藏反調試信息。

            0x08 IDA arm、thumb指令識別缺陷


            眾所周知,IDA采用遞歸下降算法來反匯編指令,而該算法最大的缺點在于它無法處理間接代碼路徑,無法識別動態算出來的跳轉。而arm架構下由于存在arm和thumb指令集,就涉及到指令集切換,IDA在某些情況下無法智能識別arm和thumb指令,比如下圖所示代碼:

            pic4

            bx r3指令會切換指令集,而參數r3是動態計算出來的,IDA無法失敗r3的值,而默認將bx r3后面的指令當成跳轉地址,將后面地址的指令識別成了arm指令,而實際上其仍為thumb指令。

            在IDA動態調試時,仍然存在該問題,若在指令識別錯誤的地點寫入斷點,有可能使得調試器崩潰。

            0x09 Ptrace


            Ptrace是gdb等調試器實現的核心,通過ptrace可以監控、控制被調試進程的狀態、信號、執行等。而每個進程在同一時刻最多只能被一個調試進程ptrace,根據這個原理,可以主動ptrace自己的關鍵子進程,這樣可以在一定程度上防止子進程被調試。

            為了防止fork出來的反調試子進程被直接掛起或殺死,可以通過Ptrace的PTRACE_PEEKTEXT、PTRACE_PEEKDATA、PTRACE_POKETEXT等參數來完成父子進程之間的通信,比如子進程中使用的解密密鑰先存于父進程空間,父進程往ptrace的子進程中寫入密鑰后,再解密出關鍵數據。

            總之,通過ptrace增加父子進程之間的聯系,是十分有效并且廣泛存在于各類加固的反調試方法。

            0x0A Inotify 監控文件


            在Linux下,inotify可以實現監控文件系統事件(打開、讀寫、刪除等),加固方案可以通過inotify監控apk自身的某些文件,某些內存dump技術通過/proc/pid/maps、/proc/pid/mem來實現內存dump,所以監控對這些文件的讀寫也能起到一定的反調試效果。

            0x0B 總結


            本文總結了主流加固廠商大部分反調試技巧,APK下的反調試技巧和win、linux下的大同小異,核心原理都是類似的。說到底,反調試只能盡可能的增加逆向難度,APK的安全防護絕不能僅僅依靠反調試,APK安全需要從整體架構上入手,在關鍵代碼上加入強混淆,甚至通過vmp來增大關鍵代碼的逆向難度。

            0x0C Reference


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

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

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

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

                      亚洲欧美在线