<a href="http://jaq.alibaba.com/community/art/show?articleid=849">android 反調試技巧之self-debuging/proc 檔案系統檢測、調試斷點探測</a>
首先,我們來看看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隻是一個本地符号,是以連結器不會解決不了這個問題。
我們可以以各種方式覆寫這個指針,簡單的歸零它們不是一個好主意,因為這會讓整個過程崩潰。于是,我們找到的一個使用“jdwpadbstate :: shutdown()”的位址來覆寫“jdwpadbstate :: processincoming()”位址的好方法,這個方法看起來如下:
#include <jni.h>
#include <string>
#include <android/log.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <jdwp/jdwp.h>#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 & ~(pagesize-1); mprotect((void *)page, pagesize, prot_read | prot_write); vtable->processincoming = vtable->shutdown; // reset permissions & 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 <sys/ptrace.h>
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, &status, 0);
ptrace(ptrace_cont, ppid, null, null);
while (waitpid(ppid, &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 <unistd.h>
#include <sys/wait.h>
static int child_pid;
void *monitor_pid(void *) {
waitpid(child_pid, &status, 0);
/* child status should never change. */
_exit(0); // commit seppuku
// process has exited
pthread_t t;
/* start the monitoring thread */
pthread_create(&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)由阿裡巴巴安全部出品,面向企業和開發者提供網際網路業務安全解決方案,全面覆寫移動安全、資料風控、内容安全等次元,并在業界率先提出“以業務為中心的安全”,賦能生态,與行業共享阿裡巴巴集團多年沉澱的專業安全能力。