安卓動态調試七種武器之孔雀翎 – ida pro
作者:蒸米@阿裡聚安全
随着移動安全越來越火,各種調試工具也都層出不窮,但因為環境和需求的不同,并沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬于自己的調試武器。是以,筆者将會在這一系列文章中分享一些自己經常用或原創的調試工具以及手段,希望能對國内移動安全的研究起到一些催化劑的作用。
目錄如下:
安卓動态調試七種武器之長生劍 - smali instrumentation
安卓動态調試七種武器之離别鈎 - hooking
安卓動态調試七種武器之碧玉刀- customized dvm
安卓動态調試七種武器之多情環- customized kernel
安卓動态調試七種武器之霸王槍 - anti anti-debugging
安卓動态調試七種武器之拳頭 - tricks & summary
天下的暗器共有三百六十餘種,但其中最成功、最可怕的就是孔雀翎。它使用簡單,卻威力無邊。據說,孔雀翎發動之時,暗器四射,有如孔雀開屏,輝煌燦爛,而就在敵人目眩神迷之際,便已魂飛魄散。這武器的描述與ida是何其的相似啊!是以說安卓動态調試七種武器中的孔雀翎非ida莫屬。因為ida太有名了,相應的教程也是漫天飛,但很多并不是安卓相關的内容,是以筆者決定将一些經典的安卓調試技巧總結歸納一下。因為篇幅原因,筆者并不能保證本文能夠覆寫到ida調試的方方面面,看官如有興趣可以再繼續深入研究學習。
在android調試中,你會經常見到這種類型的函數:

首先是一個指針加上一個數字,比如v3+676。然後将這個位址作為一個方法指針進行方法調用,并且第一個參數就是指針自己,比如(v3+676)(v3…)。這實際上就是我們在jni裡經常用到的jnienv方法。因為ida并不會自動的對這些方法進行識别,是以當我們對so檔案進行調試的時候經常會見到卻搞不清楚這個函數究竟在幹什麼,因為這個函數實在是太抽象了。解決方法非常簡單,隻需要對jnienv指針做一個類型轉換即可。比如說上面提到v3指針,我們選中後按一下”y”鍵,然後将類型聲明為”jnienv*”。
随後ida就會自動查找對應的方法并且顯示出來了:
是不是瞬間清晰了很多?另外有人( 貌似是看雪論壇上的)還總結了所有jnienv方法對應的數字,位址以及方法聲明:
有興趣的同學可以去我的github下載下傳。
我們知道so檔案在被加載的時候會首先執行.init_array中的函數,然後再執行jni_onload()函數。jni_onload()函數因為有符号表是以非常容易找到,但是.init_array裡的函數需要自己去找一下。首先打開view ->open subviews->segments。然後點選.init.array就可以看到.init_array中的函數了。
但一般當我們使用ida進行attach的時候,.init_array和jni_onload()早已經執行完畢了,根本來不急調試。這時候我們可以使用jdb這個工具來解決,這個工具是安裝完jdk以後自帶的,可以在jdk的bin目錄下找到。在這裡我們使用阿裡移動安全挑戰賽2014的第二題作為例子講解一下如何調試jni_onload()。
打開程式後,界面是這樣的:
我們的目标就是擷取到密碼。使用ida反編譯一下so檔案會看到我們輸入後的密碼會和off_628c這個指針指向的字元串進行比較。
于是我們檢視off_628c這個位址對應的指針,發現對應的字元串是”wojiushidaan”。
于是我們把這個密碼輸入一下,發現密碼錯誤。看樣子so檔案在加載的時候對密碼字元串進行了動态修改。既然動态修改了那我們用ida動态調試一下好了,我們打開程式,然後再用ida attach一下,發現程式直接閃退了,ida那邊也沒有任何有用資訊。原來這就是自毀程式的意思啊。既然如此我們動态調試一下jni_onload()來看一下程式究竟做了什麼吧。步驟如下:
1 ddms
一定要打開ddms,否則調試端口是關閉的,就無法在程式剛開始的暫停了。我之前不知道要打開ddms才能用jdb,還以為android系統或者sdk出問題了,重裝好幾次。汗。
2 adb push androidserver /data/local/tmp/
這裡我們把ida的androidserver push到手機上,并以root身份執行。
3 adb forward tcp:23946 tcp:23946
将ida的調試端口進行轉發,這樣pc端的ida才能連接配接手機。
4 adb shell am start -d -n com.yaotong.crackme/.mainactivity
這裡我們以debug模式啟動程式。程式會出現waiting for debugger的調試界面。
5 ida attach target app
這時候我們啟動ida并attach這個app的程序。
6 suspend on libary loading
我們在debugger setup裡勾選 suspend on library load。然後點選繼續。
7 jdb -connect com.sun.jdi.socketattach:hostname=127.0.0.1,port=8700
用jdb将app恢複執行。
8 add breakpoint at jni_onload
随後程式會在加載libcrackme.so這個so檔案的時候停住。這時候ida會出現找不到檔案的提示,不用管他,點取消即可。随後就能在modules中看到libcrackme.so這個so檔案了,我們點進去,然後在jni_onload處下個斷點,然後點選執行,程式就進入了jni_onload()這個函數。
ps:有時候你明明在一個函數中卻無法f5,這時候你需要先按一下”p”鍵,程式會将這段代碼作為函數分析,然後再按一下”f5”,你就能夠看到反彙編的函數了。
因為過程有點繁瑣,我錄制了一個調試jni_onload()的視訊在我的github,有興趣的同學可以去下載下傳觀看。因為涉及到其他的技巧,我們将會在随後的”ida雙開定位”章節中繼續講解如何調試.init_array中的函數。
ida雙開定位的意思是先用ida靜态分析so檔案,然後再開一個ida動态調試so檔案。因為在動态調試中ida并不會對整個動态加載的so檔案進行詳細的分析,是以很多函數并無法識别出來。比如靜态分析中有很多的sub_xxxx函數:
但動态調試中的ida是沒有這些資訊的。
是以我們需要雙開ida,然後通過ida靜态分析的内容來定位ida動态調試的函數。當然很多時候我們也需要動态調試的資訊來幫助了解靜态分析的函數。
在上一節中,我們提到.init.array中有個sub_2378(),但當ida動态加載so後我們并無法在module中找到這個函數。那該咋辦呢?這時候我們就要通過靜态分析的位址和so檔案在記憶體中的基址來定位目标函數。首先我們看到sub_2378()這個函數在靜态分析中的位址為.text:00002378。而在動态加載中這個so在記憶體中的基址為:4004f000。
是以sub_2378()這個函數在記憶體中真正的位址應該為4004f000 + 00002378 =40051378。下面我們在動态調試視窗輸入”g”,跳轉到40051378這個位址。然後發現全是亂碼的節奏:
不要擔心,這是因為ida認為這裡是資料段。這時候我們隻要按”p”或者選中部分資料按”c”,ida就會把這段資料當成彙編代碼進行分析了:
我們随後還可以按”f5”,将彙編代碼反編譯為c語言。
是不是和靜态分析中的sub_2378()長的差不多?
我們随後可以在這個位置加入斷點,再結合上一節提到的調試技巧就可以對init.array中的函數進行動态調試了。
我們接下來繼續分析自毀程式這道題,當我們在對init.array和jni_onload()進行調試的時候,發現程式在執行完dowrd_400552b4()後就挂掉了。
于是我們在這裡按”f7”進入函數看一下:
原來是libc.so的phread_create()函數,估計是app本身開了一個新的線程進行反調試檢測了。
有意思的是在靜态分析中我們并不清楚dword_62b4這個函數是做什麼的,因為這個函數的位址在.bss段還沒有被初始化:
但是當我們動态調試的時候,這個位址的值已經修改為了phread_create()這個函數的位址了。:
是以說自毀程式密碼這個app會用pthread_create()開一個新的線程對app進行反調試檢測。線程會運作sub_16a4()這個函數。于是我們對這個函數進行分析,發現裡面的内容有大量的混淆,看起來十分吃力。這裡我介紹個小trick:常見的反調試方法都會用fopen打開一些檔案來檢測自己的程序是否被attach,比如說status這個檔案中的tracerpid的值是否為0,如果為0說明沒有别的程序在調試這個程序,如果不為0說明有程式在調試。是以我們可以守株待兔,在libc.so中的fopen()處下一個斷點,然後我們在hex view視窗中設定資料與r0的值同步:
這樣的話,當函數在fopen處停住的時候我們就能看到程式打開了哪些檔案。果不其然,程式打開了/proc/[pid]/status這個檔案。我們”f8”繼續執行fopen函數,看看傳回後的位址在哪。然後發現我們程式卡在了某個函數中間,pc上面都是資料,pc下面才是彙編。這該咋辦呢?
解決辦法還是ida雙開,我們知道現在pc的位址為40050420,libcrackme.so檔案的基址為4004f000。是以這段代碼在so中的位置應該是:40050420 - 4004f000 = 1420。是以我們回到ida靜态分析界面,就可以定位到我們其實是在sub_130c()這個函數中。于是我們猜測這個函數就是用來做反調試檢測的。
是以我們可以通過基址來定位sub_130c()這個函數在記憶體中的位址:40050420 + 130c = 4005030c。然後我們在4005030c這個位址處按”p”, ida就可以正确的識别整個函數了。
是以說動态調試的時候可以幫我們了解到很多靜态分析很難擷取到的資訊。這也就是ida雙開的意義所在:靜态幫助動态定位函數位址,動态幫助靜态擷取運作時資訊。
我們繼續分析自毀程式密碼這個app,我們發現該程式會用fopen ()打開/proc/[pid]/status這個檔案,随後會用fgets()和strstr()來擷取,于是我們在strstr()處下個斷點,然後讓hex view的資料與r0同步。每次點選繼續,我們都會看到strstr傳入的參數。當傳入的參數變為tracerpid:xxxx的時候我們停一下。因為在正常情況下,tracerpid的值應該是0。但是當被調試的時候就會變成調試器的pid。
為了防止程式發現我們在調試,在這裡我們需要把值改回0。我們在hex view的2那裡點選右鍵,然後選擇edit。随後我們輸入30和00,再點選”apply changes”。就可以把tracerpid改為0了。然後就可以bypass這一次的反調試的檢測。
但這個程式檢測tracerpid的次數非常頻繁,我們要不斷的修改tracerpid的值才行,這種方法實在有點治标不治本,是以我們會在下一節介紹patch so檔案的方法來解決這個問題。
另外在ida動态調試過程中,除了記憶體中的資料可以修改,寄存器的資料也是可以動态修改的。比如說程式執行到cmp r6, #0。本來r6的值是0,經過比較後,程式會跳轉到4082a3fc這個位址。
但是如果我們在pc執行到4082a1f8這條語句的時候,将r6的值動态修改為0。程式就不會進行跳轉了。
你甚至可以修改pc寄存器的值來控制程式跳轉到任何想要跳轉到的位置,簡直和rop的原理一樣。但記得要注意棧平衡等問題。
在上文中,我們通過分析定位到sub_130c()這個函數有很大可能性是用來做反調試檢測的,并且作者開了一個新的線程,并且用了一個while來不斷執行sub_130c()這個函數,是以說我們每次手動的修改tracerpid實在是不現實。
既然如此我們何不把sub_130c()這個函數給nop掉呢?為了防止nop出錯,我們先在”f5”界面選擇所有代碼,然後用”copy to assembly”功能,就可以把c語言代碼注釋到彙編代碼裡。
在這裡我們看到如果想要注釋掉sub_130c()函數,隻需要注釋掉000016b8這個位置上的代碼即可,如果我們想要注釋掉dword_62b0(3)這個函數,我們則需要注釋掉000016bc-000016c4這三個位置上的代碼。接下來我們選中000016b8這一行,然後再點選hexview。hexview會幫我們自動定位到000016b8這個位置。
因為arm是沒有單獨的nop指令的。于是我們采用movs r0,r0作為nop。對應的機器碼為”00 00 a0 e1”。是以我們把”13 ff ff eb”這段内容修改為”00 00 a0 e1”。
我們再回”f5”界面,就會發現sub_130c()函數已經沒有了。
最後我們點選”edit->plugins->modifyfile”,然後就可以儲存新的so檔案了。我們将這個so檔案覆寫原apk中的so檔案,然後再重新簽名。
這次我們先運作程式,再用ida加載,app并沒有閃退,說明我們patch成功了。于是我們先在”java_com_yaotong_crackme_mainactivity_securitycheck”處下斷點。然後在app随便輸入一個密碼,點選app上的”輸入密碼”按鈕。
程式就會暫停在”java_com_yaotong_crackme_mainactivity_securitycheck”處。我們先按”p”再按”f5”,就可以看到反彙編的c語言了。而這裡的unk_4005228c就是儲存了密碼字元串指針的指針。
因為是指針的指針,是以我們先輕按兩下進入這個位址。
然後在這個位址上按三下”d”,将這裡的資料格式從字元轉化為指針形式。
然後我們再輕按兩下進入這個位址,就可以看到最後的flag了。答案是”aiyou,bucuoo”。
這道題裡我們隻是用到了很簡單的patch so技巧,在實戰中我們不光可以nop,我們還可以改變條件判斷語句,比如将”bne”變為” beq”。我們甚至可以修改跳轉位址,比如直接讓程式b到某個位址去執行,這樣的話就不需要挨個的nop很多語句了。要注意的是,arm中的跳轉指令是根據相對位址計算的,是以你要根據目前指令位址和目标位址來計算出相對跳轉的值。
比如說00001bcc: beq loc_1c28對應的彙編代碼為”15 00 00 0a”。
0x0a代表beq,”15 00 00”代表跳轉的相對位址,因為在arm中pc的值是目前指令的下兩條(下一條的下一條)指令的位址,是以我們需要将0x15再加上2。随後就可以計算出最後跳轉到的位址: (0x15 + 0x2)*4 + 0x1bcc = 0x1c28。ida反彙編後的結果也驗證了結果是beq loc_1c28。
接下來我們想修改彙編代碼為00001bcc: bne loc_1c2c。隻需要将”0a”變成”1a”,将”15”變成”16”即可。
該技巧是qever 在《msc的僞解題報告》中提到的。利用kill我們可以讓程式挂起,然後用ida挂載上去,擷取有用的資訊,然後可以再用kill将程式恢複運作。我們還是拿自毀程式密碼這個應用舉例,具體實行方法如下:
1 首先用ps擷取運作的app的pid。
2 然後用kill -19 [pid] 就可以将這個app挂起了。
3 随後我們用ida attach上這個app。因為整個程序都挂起了,是以這次ida挂載後app并沒有閃退。然後就可以在記憶體中找到答案了。
4 如果想要恢複app的運作,需要将ida退出,然後再使用kill -18 [pid]即可。
在現在的移動安全環境中,程式加殼已經成為家常便飯了,如果不會脫殼簡直沒法在破解界混的節奏。zjdroid作為一種萬能脫殼器是非常好用的,但是當作者公開釋出這個項目後就遭到了各種加殼器的針對,比如說搶占zjdroid的廣播接收器讓zjdroid無法接收指令等。我們也會在”安卓動态調試七種武器之多情環 - customized dvm”這篇文章中介紹另一種架構的萬能脫殼器。但工具就是工具,當我們釋出的時候可能也會遭到類似zjdroid那樣的針對。是以說手動脫殼這項技能還是需要學習的。在這一節中我們會介紹一下最基本的記憶體dump流程。在随後的文章中我們會介紹更多的技巧。
這裡我們拿alictf2014中的apk300作為例子來介紹一下ida脫簡單殼的基本流程。 首先我們用調試jni_onload的技巧将程式在運作前挂起:
![enter image description here][59]
然後在libdvm.so中的dvmdexfileopenpartial函數上下一個斷點:
然後我們點選繼續運作,程式就會在dvmdexfileopenpartial()這個函數處暫停,r0寄存器指向的位址就是dex檔案在記憶體中的位址,r1寄存器就是dex檔案的大小:
然後我們就可以使用ida的script command去dump記憶體中的dex檔案了。
1
2
3
4
5
6
7
8
9
staticmain(void)
{
auto fp, begin, end, dexbyte;
fp =fopen("c:\\dump.dex","wb");
begin = r0;
end = r0 + r1;
for( dexbyte = begin; dexbyte < end; dexbyte ++ )
fputc(byte(dexbyte), fp);
}
dump完dex檔案後,我們就可以用baksmali來反編譯這個dex檔案了。
因為過程有點繁瑣,我錄制了一個dump dex檔案的視訊在我的github,有興趣的同學可以去下載下傳觀看。
當然這隻是最簡單脫殼方法,很多進階殼會動态修改dex的結構體,比如将codeoffset指向記憶體中的其他位址,這樣的話你dump出來的dex檔案其實是不完整的,因為代碼段儲存在了記憶體中的其他位置。但你不用擔心,我們會在随後的文章中介紹一種非常簡單的解決方案,敬請期待。
有時我們想要将app中的某個函數的邏輯提取出來,用gcc重新編譯一個可執行檔案,比如我們想要寫一個注冊機,就需要把app生成key的邏輯提取出來。但是ida ”f5”過後的c語言直接編譯經常會有很多錯誤,比如未定義的宏,未定義的聲明等。這是因為這些宏都在ida的一個頭檔案裡。裡面定義了所有ida自定義的宏和聲明,比如說經常見到的byten()宏:
#define byten(x, n) (*((_byte*)&(x)+n))
#define byte1(x) byten(x, 1) // byte 1 (counting from 0)
#define byte2(x) byten(x, 2)
加上這個”defs.h”頭檔案後就可以正常的編譯ida ”f5”後的c語言了。
另外我們還可以自己建立一個ndk項目,然後自己編寫一個so或者elf利用dlopen()和dlsym()調用目标so中的函數。比如我們想要調用libdvm.so中的dvmgetcurrentjnimethod()函數,我們就可以在我們的ndk項目中這麼寫:
typedefvoid* (*dvmgetcurrentjnimethod_func)();
dvmgetcurrentjnimethod_func dvmgetcurrentjnimethod_fnptr;
dvm_hand= dlopen("libdvm.so", rtld_now);
dvmgetcurrentjnimethod_fnptr =dlsym(dvm_hand,"_z22dvmgetcurrentjnimethodv");
dvmgetcurrentjnimethod_fnptr();
還是那句話,寫了這麼多依然不能保證本文能夠覆寫到ida調試的方方面面,因為ida實在是太博大精深了。看官如有興趣可以繼續深入研究學習。另外文章中所有提到的代碼和工具都可以在我的github下載下傳到,位址是:https://github.com/zhengmin1989/thesevenweapons
msc解題報告 http://bbs.pediy.com/showthread.php?t=197235
僞·msc解題報告http://bbs.pediy.com/showthread.php?p=1349632
阿裡聚安全由阿裡巴巴移動安全部出品,面向企業和開發者提供企業安全解決方案,全面覆寫移動安全、資料風控、内容安全、實人認證等次元,并在業界率先提出“以業務為中心的安全”,賦能生态,與行業共享阿裡巴巴集團多年沉澱的專業安全能力。