APP加固反調試(Anti-debugging)技術點彙總

0x00 時間相關反調試

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

0x01 檢測關鍵文件

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

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

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

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

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

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

0x02 檢測端口號

使用IDA動態調試APK時,android_server默認監聽23946端口,因此經過檢測端口號能夠起到必定的反調試做用。具體而言,能夠經過檢測/proc/net/tcp文件,或者直接system執行命令 netstat -apn 等。tcp

0x03 檢測android_server、gdb、gdbserver

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

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"//+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和進程間通訊結合,一旦反調試子進程被掛起或被殺死,父進程也會立刻終止,原理大體以下圖:

父進程的守護線程在從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指令,好比下圖所示代碼:

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

相關文章
相關標籤/搜索