天天看點

Android 反調試技巧之Self-Debuging/proc 檔案系統檢測、調試斷點探測

<a href="http://jaq.alibaba.com/community/art/show?articleid=849">android 反調試技巧之self-debuging/proc 檔案系統檢測、調試斷點探測</a>

Android 反調試技巧之Self-Debuging/proc 檔案系統檢測、調試斷點探測

首先,我們來看看bluebox security(一家移動資料保護的公司)所描述的反調試方法。gdvm是一個類型為dvmglobals的全局變量,用來收集目前程序所有虛拟機相關的資訊,其中,它的成員變量vmlist指向的就是目前程序中的dalvik虛拟機執行個體,即一個javavmext對象。以後每當需要通路目前程序中的dalvik虛拟機執行個體時,就可以通過全局變量gdvm的成員變量vmlist來獲得,避免了在函數之間傳遞該dalvik虛拟機執行個體。

gdvm的存在使得直接通路jdwp相關資料變得很容易。例如,gdvm。jdwpstate指向包含全局調試資料和函數指針的結構。操作資料會導緻jdwp線程發生故障或崩潰。下圖就是bluebox security的具體方法:

jniexport jboolean jnicall java_com_example_disable(jnienv* env, jobject dontuse ){

// gdvm==struct dvmglobals

gdvm.jdwpstate = null;

return jni_true;

}

不過,libart.so會将jdwp相關類的一些vtables導出為全局符号。但是到目前,我們還搞不清楚其中的原因,以及這是否正常,但是這個方法卻給了我們修改jdwp線程提供了一些很好的提示。這其中就包括jdwpsocketstate和jdwpadbstate這兩個分别通過網絡套接字和adb端處理的jdwp連接配接:剛開始,反調試實作起來并不容易,因為沒有指向重要資料結構的全局符号。雖然我們有一個指向主結構體的jdwpstate,但是 gjdwpstate隻是一個本地符号,是以連結器不會解決不了這個問題。

Android 反調試技巧之Self-Debuging/proc 檔案系統檢測、調試斷點探測

我們可以以各種方式覆寫這個指針,簡單的歸零它們不是一個好主意,因為這會讓整個過程崩潰。于是,我們找到的一個使用“jdwpadbstate :: shutdown()”的位址來覆寫“jdwpadbstate :: processincoming()”位址的好方法,這個方法看起來如下:

#include &lt;jni.h&gt;

#include &lt;string&gt;

#include &lt;android/log.h&gt;

#include &lt;dlfcn.h&gt;

#include &lt;sys/mman.h&gt;

#include &lt;jdwp/jdwp.h&gt;#define log(fmt, ...) __android_log_print(android_log_verbose, "jdwpfun", fmt, ##__va_args__)// vtable structure. just to make messing around with it more intuitivestruct vt_jdwpadbstate {

unsigned long x;

unsigned long y;

void * jdwpsocketstate_destructor;

void * _jdwpsocketstate_destructor;

void * accept;

void * showmanyc;

void * shutdown;

void * processincoming;

};extern "c"jniexport void jnicall java_sg_vantagepoint_jdwptest_mainactivity_jdwpfun(

jnienv *env,

jobject /* this */) { void* lib = dlopen("libart.so", rtld_now); if (lib == null) {

log("error loading libart.so");

dlerror();

}else{ struct vt_jdwpadbstate *vtable = ( struct vt_jdwpadbstate *)dlsym(lib, "_ztvn3art4jdwp12jdwpadbstatee"); if (vtable == 0) {

log("couldn't resolve symbol '_ztvn3art4jdwp12jdwpadbstatee'.n");

}else { log("vtable for jdwpadbstate at: %08xn", vtable); // let the fun begin! unsigned long pagesize = sysconf(_sc_page_size);

unsigned long page = (unsigned long)vtable &amp; ~(pagesize-1); mprotect((void *)page, pagesize, prot_read | prot_write); vtable-&gt;processincoming = vtable-&gt;shutdown; // reset permissions &amp; flush cache mprotect((void *)page, pagesize, prot_read); }

一旦此功能運作,任何連接配接java的調試器都将斷開連接配接,任何進一步的連接配接嘗試都将失敗。令人驚訝的是,目前使用這個方法可以進行反調試了,并沒有在日志中進行任何解釋:

pyramidal neuron:~ berndt$ adb jdwp2926pyramidal neuron:~ berndt$ adb forward tcp:7777 jdwp:2926pyramidal neuron:~ berndt$ jdb -attach localhost:7777java.io.ioexception: handshake failed - connection prematurally closed at com.sun.tools.jdi.sockettransportservice.handshake(sockettransportservice.java:136) at com.sun.tools.jdi.sockettransportservice.attach(sockettransportservice.java:232) at com.sun.tools.jdi.genericattachingconnector.attach(genericattachingconnector.java:116) at com.sun.tools.jdi.socketattachingconnector.attach(socketattachingconnector.java:90) at com.sun.tools.example.debug.tty.vmconnection.attachtarget(vmconnection.java:519) at com.sun.tools.example.debug.tty.vmconnection.open(vmconnection.java:328) at com.sun.tools.example.debug.tty.env.init(env.java:63) at com.sun.tools.example.debug.tty.tty.main(tty.java:1066)

這個方法相當隐秘的,即通過欺騙和隐藏實作,不過我們隻嘗試了進行adb端口連接配接,大家在使用這個方法時可能需要修補jdwpsocketstate,以防止java調試。

但是有一個問題:android是建立在linux上的,是以繼承了ptrace系統調用。

ptrace 系統調從名字上看是用于程序跟蹤的,它提供了父程序可以觀察和控制其子程序執行的能力,并允許父程序檢查和替換子程序的核心鏡像(包括寄存器)的值。其基 本原理是: 當使用了ptrace跟蹤後,所有發送給被跟蹤的子程序的信号(除了sigkill),都會被轉發給父程序,而子程序則會被阻塞,這時子程序的狀态就會被 系統标注為task_traced。而父程序收到信号後,就可以對停止下來的子程序進行檢查和修改,然後讓子程序繼續運作。

其原型為:

#include &lt;sys/ptrace.h&gt;

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)

ptrace有四個參數:

1.enum __ptrace_request request:訓示了ptrace要執行的指令。

2.pid_t pid: 訓示ptrace要跟蹤的程序。

3.void *addr: 訓示要監控的記憶體位址。

4.void *data: 存放讀取出的或者要寫入的資料。

ptrace是如此的強大,以至于有很多大家所常用的工具都基于ptrace來實作,如gdb,strace,jtrace和frida。其中一些工具甚至提供了虛拟機自省技術,為與java虛拟機進行互動提供了便利的後門。

許多過去的linux反調試技巧,例如監控proc檔案系統和檢測記憶體中的斷點,一直都是android上常用的的方法。通常在惡意軟體使用的另一種技術是自我調試。該方法利用了一個事實,即隻有一個調試器可以随時附加到程序。我們來看一下這個工作原理。

在linux上,ptrace()系統調用用于觀察和控制另一個程序(“tracee”)的執行,并檢查和更改跟蹤的記憶體和寄存器。它是實作斷點調試和系統調用跟蹤的主要手段,使用ptrace系統調用進行反調試的最明顯的方法是對記憶體的配置設定和回收,然後調用ptrace(parent_pid)附加到父程序:

void anti_debug() {

child_pid = fork();

if (child_pid == 0)

{

int ppid = getppid();

int status;

if (ptrace(ptrace_attach, ppid, null, null) == 0)

waitpid(ppid, &amp;status, 0);

ptrace(ptrace_cont, ppid, null, null);

while (waitpid(ppid, &amp;status, 0)) {

if (wifstopped(status)) {

} else {

// process has exited for some reason

_exit(0);

如果按照上述方式來實作,子程序将繼續跟蹤父程序,直到父程序退出,進而導緻将調試器附加到父程序時失敗。我們可以通過将代碼編譯成jni函數并将其打包到我們在裝置上運作的應用程式來驗證。

假設一切順利,我們現在就要嘗試調試應用程式的逆向工程了。如下圖所示,ps不是傳回一個程序,而是傳回相同指令行的兩個程序:

root@android:/ # ps | grep -i anti

u0_a151 18190 201 1535844 54908 ffffffff b6e0f124 s sg.vantagepoint.antidebug

u0_a151 18224 18190 1495180 35824 c019a3ac b6e0ee5c s sg.vantagepoint.antidebug

google 這麼多年來,已經把 android 做成了本質上無法分支(fork)的軟體,開源隻是名義上的,讓我們來嘗試使用gdbserver附加到父程序來進行驗證:

root@android:/ # ./gdbserver --attach localhost:12345 18190

warning: process 18190 is already traced by process 18224

cannot attach to lwp 18190: operation not permitted (1)

exiting

如上圖所示,仍然需要進行反向工程:

root@android:/ # kill -9 18224

現在讓我們再試一次嘗試附加gdbserver:

root@android:/ # ./gdbserver --attach localhost:12345 18190 attached; pid = 18190

listening on port 12345

ptrace調用通常常見的方法包括:

分别跟蹤彼此的多個程序

跟蹤運作過程,監視子程序

監視/ proc檔案系統中的值,例如/ proc / pid / status中的tracerpid。

大家來看一下我們對上述方法的簡單改進,在最初的fork()之後,我們在父程序中啟動一個額外的線程來連續監視子程序的狀态。根據應用程式是否已内置在調試或釋出模式(根據manifest中的android:可調試标志),子程序将以下列方式之一運作:

1.在釋放模式下,調用ptrace失敗,子程序立即退出分段錯誤(退出代碼11)。

2.在調試模式下,調用ptrace工作,子程序預計會無限運作下去。是以,對waitpid(child_pid)的調用不應該傳回,如果有的話,會阻礙了整個程序組的運作。

完整的jni實作如下,通過添加jniexport(…)_ antidebug()作為本機方法,可以在自己的項目中自由使用它。

#include &lt;unistd.h&gt;

#include &lt;sys/wait.h&gt;

static int child_pid;

void *monitor_pid(void *) {

waitpid(child_pid, &amp;status, 0);

/* child status should never change. */

_exit(0); // commit seppuku

// process has exited

pthread_t t;

/* start the monitoring thread */

pthread_create(&amp;t, null, monitor_pid, (void *)null);

extern "c"

jniexport void jnicall

java_sg_vantagepoint_antidebug_mainactivity_antidebug(

jobject /* this */) {

anti_debug();

另外,我們将其打包成一個android應用程式,看看它是否有效。就像上文一樣,運作應用程式的調試版本時會顯示兩個程序:

root@android:/ # ps | grep -i anti-debug

u0_a152 20267 201 1552508 56796 ffffffff b6e0f124 s sg.vantagepoint.anti-debug

u0_a152 20301 20267 1495192 33980 c019a3ac b6e0ee5c s sg.vantagepoint.anti-debug

但是,如果我們現在終止子程序,父程序也退出:

root@android:/ # kill -9 20301

root@android:/ # ./gdbserver --attach localhost:12345 20267

gdbserver: unable to open /proc file '/proc/20267/status'

cannot attach to lwp 20267: no such file or directory (2)

為了繞過這個程序,我們有必要稍微修改一下應用程式的程序,最簡單的方法是使用nop将調用修改為_exit,或者在libc.so中hook函數_exit。

預防這種反調試其實有很多種方法,比如我們可以修補應用程式漏洞,防止vtable被篡改。如果你不能及時修複,那以後還會再次受到這種攻擊。另外就是在其他情況下,使用xposed或frida修改核心子產品可能更合适,我們給大家介紹2種方法:

1.修補反調試功能。通過簡單地用nop指令覆寫來禁用不需要的行為。請注意,如果反調試機制已經經過深加工了,則可能需要更複雜的修補程式。

2.使用frida或xposed hook本地api,如ptrace()和fork(),或使用核心子產品hook相關的系統調用。

本文來自合作夥伴“阿裡聚安全”,發表于2017年04月14日 10:07.

阿裡聚安全

阿裡聚安全(http://jaq.alibaba.com)由阿裡巴巴安全部出品,面向企業和開發者提供網際網路業務安全解決方案,全面覆寫移動安全、資料風控、内容安全等次元,并在業界率先提出“以業務為中心的安全”,賦能生态,與行業共享阿裡巴巴集團多年沉澱的專業安全能力。