<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址: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


    97av