Anti-debugging Skills in APK

烏雲知識庫發表於2018-03-08

unLimit安全小組 · 2016/06/20 14:38

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)
Arm0x01, 0x00, 0x9f, 0xef
Thumb0x01, 0xde
Thumb20xf0, 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"//+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


相關文章