安卓動態除錯七種武器之孔雀翎 – Ida Pro

wyzsk發表於2020-08-19
作者: 蒸米 · 2015/07/01 10:31

0x00 序


隨著移動安全越來越火,各種除錯工具也都層出不窮,但因為環境和需求的不同,並沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬於自己的除錯武器。因此,筆者將會在這一系列文章中分享一些自己經常用或原創的除錯工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。

目錄如下:

安卓動態除錯七種武器之長生劍 - Smali Instrumentation

安卓動態除錯七種武器之孔雀翎 – Ida Pro

安卓動態除錯七種武器之離別鉤 - Hooking

安卓動態除錯七種武器之碧玉刀- Customized DVM

安卓動態除錯七種武器之多情環- Customized Kernel

安卓動態除錯七種武器之霸王槍 - Anti Anti-debugging

安卓動態除錯七種武器之拳頭 - Tricks & Summary

0x01 孔雀翎


天下的暗器共有三百六十餘種,但其中最成功、最可怕的就是孔雀翎。它使用簡單,卻威力無邊。據說,孔雀翎發動之時,暗器四射,有如孔雀開屏,輝煌燦爛,而就在敵人目眩神迷之際,便已魂飛魄散。這武器的描述與Ida是何其的相似啊!所以說安卓動態除錯七種武器中的孔雀翎非Ida莫屬。因為Ida太有名了,相應的教程也是漫天飛,但很多並不是安卓相關的內容,所以筆者決定將一些經典的安卓除錯技巧總結歸納一下。因為篇幅原因,筆者並不能保證本文能夠覆蓋到ida除錯的方方面面,看官如有興趣可以再繼續深入研究學習。

0x02 還原JNI函式方法名


在android除錯中,你會經常見到這種型別的函式:

enter image description here

首先是一個指標加上一個數字,比如v3+676。然後將這個地址作為一個方法指標進行方法呼叫,並且第一個引數就是指標自己,比如(v3+676)(v3…)。這實際上就是我們在JNI裡經常用到的JNIEnv方法。因為Ida並不會自動的對這些方法進行識別,所以當我們對so檔案進行除錯的時候經常會見到卻搞不清楚這個函式究竟在幹什麼,因為這個函式實在是太抽象了。解決方法非常簡單,只需要對JNIEnv指標做一個型別轉換即可。比如說上面提到v3指標,我們選中後按一下”y”鍵,然後將型別宣告為”JNIEnv*”。

enter image description here

隨後IDA就會自動查詢對應的方法並且顯示出來了:

enter image description here

是不是瞬間清晰了很多?另外有人( 貌似是看雪論壇上的)還總結了所有JNIEnv方法對應的數字,地址以及方法宣告:

enter image description here

有興趣的同學可以去我的github下載。

0x03 除錯.init_array和JNI_OnLoad


我們知道so檔案在被載入的時候會首先執行.init_array中的函式,然後再執行JNI_OnLoad()函式。JNI_Onload()函式因為有符號表所以非常容易找到,但是.init_array裡的函式需要自己去找一下。首先開啟view ->Open subviews->Segments。然後點選.init.array就可以看到.init_array中的函式了。

enter image description here

enter image description here

enter image description here

但一般當我們使用ida進行attach的時候,.init_array和JNI_Onload()早已經執行完畢了,根本來不急除錯。這時候我們可以使用jdb這個工具來解決,這個工具是安裝完jdk以後自帶的,可以在jdk的bin目錄下找到。在這裡我們使用阿里移動安全挑戰賽2014的第二題作為例子講解一下如何除錯JNI_OnLoad()。

開啟程式後,介面是這樣的:

enter image description here

我們的目標就是獲取到密碼。使用ida反編譯一下so檔案會看到我們輸入後的密碼會和off_628c這個指標指向的字串進行比較。

enter image description here

於是我們檢視off_628c這個地址對應的指標,發現對應的字串是”wojiushidaan”。

enter image description here

enter image description here

於是我們把這個密碼輸入一下,發現密碼錯誤。看樣子so檔案在載入的時候對密碼字串進行了動態修改。既然動態修改了那我們用ida動態除錯一下好了,我們開啟程式,然後再用ida attach一下,發現程式直接閃退了,ida那邊也沒有任何有用資訊。原來這就是自毀程式的意思啊。既然如此我們動態除錯一下JNI_OnLoad()來看一下程式究竟做了什麼吧。步驟如下:

1 ddms

一定要開啟ddms,否則除錯埠是關閉的,就無法在程式剛開始的暫停了。我之前不知道要開啟ddms才能用jdb,還以為android系統或者sdk出問題了,重灌好幾次。汗。

2 adb push androidserver /data/local/tmp/

adb shell
su
chmod 777 /data/local/tmp/androidserver
/data/local/tmp/androidserver

這裡我們把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的除錯介面。

安卓動態除錯七種武器之孔雀翎 – Ida Pro

5 ida attach target app

這時候我們啟動ida並attach這個app的程式。

6 suspend on libary loading

我們在debugger setup裡勾選 suspend on library load。然後點選繼續。

enter image description here

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中的函式。

0x04 Ida雙開定位


Ida雙開定位的意思是先用ida靜態分析so檔案,然後再開一個ida動態除錯so檔案。因為在動態除錯中ida並不會對整個動態載入的so檔案進行詳細的分析,所以很多函式並無法識別出來。比如靜態分析中有很多的sub_XXXX函式:

enter image description here

但動態除錯中的ida是沒有這些資訊的。

enter image description here

所以我們需要雙開ida,然後透過ida靜態分析的內容來定位ida動態除錯的函式。當然很多時候我們也需要動態除錯的資訊來幫助理解靜態分析的函式。

在上一節中,我們提到.init.array中有個sub_2378(),但當ida動態載入so後我們並無法在module中找到這個函式。那該咋辦呢?這時候我們就要透過靜態分析的地址和so檔案在記憶體中的基址來定位目標函式。首先我們看到sub_2378()這個函式在靜態分析中的地址為.text:00002378。而在動態載入中這個so在記憶體中的基址為:4004F000。

enter image description here

因此sub_2378()這個函式在記憶體中真正的地址應該為4004F000 + 00002378 =40051378。下面我們在動態除錯視窗輸入”g”,跳轉到40051378這個地址。然後發現全是亂碼的節奏:

enter image description here

不要擔心,這是因為ida認為這裡是資料段。這時候我們只要按”P”或者選中部分資料按”c”,ida就會把這段資料當成彙編程式碼進行分析了:

enter image description here

我們隨後還可以按”F5”,將彙編程式碼反編譯為c語言。

enter image description here

是不是和靜態分析中的sub_2378()長的差不多?

enter image description here

我們隨後可以在這個位置加入斷點,再結合上一節提到的除錯技巧就可以對init.array中的函式進行動態除錯了。

我們接下來繼續分析自毀程式這道題,當我們在對init.array和JNI_OnLoad()進行除錯的時候,發現程式在執行完dowrd_400552B4()後就掛掉了。

enter image description here

於是我們在這裡按”F7”進入函式看一下:

enter image description here

原來是libc.so的phread_create()函式,估計是app本身開了一個新的執行緒進行反除錯檢測了。

有意思的是在靜態分析中我們並不清楚dword_62B4這個函式是做什麼的,因為這個函式的地址在.bss段還沒有被初始化:

enter image description here

enter image description here

但是當我們動態除錯的時候,這個地址的值已經修改為了phread_create()這個函式的地址了。:

enter image description here

enter image description here

所以說自毀程式密碼這個app會用pthread_create()開一個新的執行緒對app進行反除錯檢測。執行緒會執行sub_16A4()這個函式。於是我們對這個函式進行分析,發現裡面的內容有大量的混淆,看起來十分吃力。這裡我介紹個小trick:常見的反除錯方法都會用fopen開啟一些檔案來檢測自己的程式是否被attach,比如說status這個檔案中的tracerpid的值是否為0,如果為0說明沒有別的程式在除錯這個程式,如果不為0說明有程式在除錯。所以我們可以守株待兔,在libc.so中的fopen()處下一個斷點,然後我們在hex view視窗中設定資料與R0的值同步:

enter image description here

這樣的話,當函式在fopen處停住的時候我們就能看到程式開啟了哪些檔案。果不其然,程式開啟了/proc/[pid]/status這個檔案。我們”F8”繼續執行fopen函式,看看返回後的地址在哪。然後發現我們程式卡在了某個函式中間,PC上面都是資料,PC下面才是彙編。這該咋辦呢?

enter image description here

解決辦法還是ida雙開,我們知道現在PC的地址為40050420,libcrackme.so檔案的基址為4004F000。因此這段程式碼在so中的位置應該是:40050420 - 4004F000 = 1420。因此我們回到ida靜態分析介面,就可以定位到我們其實是在sub_130C()這個函式中。於是我們猜測這個函式就是用來做反除錯檢測的。

enter image description here

因此我們可以透過基址來定位sub_130C()這個函式在記憶體中的地址:40050420 + 130C = 4005030C。然後我們在4005030C這個地址處按”P”, ida就可以正確的識別整個函式了。

enter image description here

所以說動態除錯的時候可以幫我們瞭解到很多靜態分析很難獲取到的資訊。這也就是ida雙開的意義所在:靜態幫助動態定位函式地址,動態幫助靜態獲取執行時資訊。

0x05 Ida動態修改記憶體資料和暫存器數值


我們繼續分析自毀程式密碼這個app,我們發現該程式會用fopen ()開啟/proc/[pid]/status這個檔案,隨後會用fgets()和strstr()來獲取,於是我們在strstr()處下個斷點,然後讓hex view的資料與R0同步。每次點選繼續,我們都會看到strstr傳入的引數。當傳入的引數變為TracerPid:XXXX的時候我們停一下。因為在正常情況下,TracerPid的值應該是0。但是當被除錯的時候就會變成偵錯程式的pid。

enter image description here

enter image description here

為了防止程式發現我們在除錯,在這裡我們需要把值改回0。我們在hex view的2那裡點選右鍵,然後選擇edit。隨後我們輸入30和00,再點選”apply changes”。就可以把TracerPid改為0了。然後就可以bypass這一次的反除錯的檢測。

enter image description here

但這個程式檢測TracerPid的次數非常頻繁,我們要不斷的修改TracerPid的值才行,這種方法實在有點治標不治本,所以我們會在下一節介紹patch so檔案的方法來解決這個問題。

另外在ida動態除錯過程中,除了記憶體中的資料可以修改,暫存器的資料也是可以動態修改的。比如說程式執行到CMP R6, #0。本來R6的值是0,經過比較後,程式會跳轉到4082A3FC這個地址。

enter image description here

但是如果我們在PC執行到4082A1F8這條語句的時候,將R6的值動態修改為0。程式就不會進行跳轉了。

enter image description here

enter image description here

你甚至可以修改PC暫存器的值來控制程式跳轉到任何想要跳轉到的位置,簡直和ROP的原理一樣。但記得要注意棧平衡等問題。

enter image description here

0x06 Patch so檔案


在上文中,我們透過分析定位到sub_130C()這個函式有很大可能性是用來做反除錯檢測的,並且作者開了一個新的執行緒,並且用了一個while來不斷執行sub_130C()這個函式,所以說我們每次手動的修改TracerPid實在是不現實。

enter image description here

既然如此我們何不把sub_130C()這個函式給nop掉呢?為了防止nop出錯,我們先在”F5”介面選擇所有程式碼,然後用”Copy to assembly”功能,就可以把c語言程式碼註釋到彙編程式碼裡。

enter image description here

enter image description here

在這裡我們看到如果想要註釋掉sub_130C()函式,只需要註釋掉000016B8這個位置上的程式碼即可,如果我們想要註釋掉dword_62B0(3)這個函式,我們則需要註釋掉000016BC-000016C4這三個位置上的程式碼。接下來我們選中000016B8這一行,然後再點選HexView。HexView會幫我們自動定位到000016B8這個位置。

enter image description here

因為ARM是沒有單獨的NOP指令的。於是我們採用movs r0,r0作為NOP。對應的機器碼為”00 00 A0 E1”。所以我們把”13 FF FF EB”這段內容修改為”00 00 A0 E1”。

enter image description here

我們再回”F5”介面,就會發現sub_130C()函式已經沒有了。

enter image description here

最後我們點選”Edit->Plugins->modifyfile”,然後就可以儲存新的so檔案了。我們將這個so檔案覆蓋原apk中的so檔案,然後再重新簽名。

enter image description here

這次我們先執行程式,再用ida載入,app並沒有閃退,說明我們patch成功了。於是我們先在”Java_com_yaotong_crackme_MainActivity_securityCheck”處下斷點。然後在app隨便輸入一個密碼,點選app上的”輸入密碼”按鈕。

enter image description here

程式就會暫停在”Java_com_yaotong_crackme_MainActivity_securityCheck”處。我們先按”P”再按”F5”,就可以看到反彙編的c語言了。而這裡的unk_4005228C就是儲存了密碼字串指標的指標。

enter image description here

因為是指標的指標,所以我們先雙擊進入這個地址。

enter image description here

然後在這個地址上按三下”D”,將這裡的資料格式從字元轉化為指標形式。

enter image description here

然後我們再雙擊進入這個地址,就可以看到最後的flag了。答案是”aiyou,bucuoo”。

enter image description here

這道題裡我們只是用到了很簡單的patch so技巧,在實戰中我們不光可以NOP,我們還可以改變條件判斷語句,比如將”BNE”變為” BEQ”。我們甚至可以修改跳轉地址,比如直接讓程式B到某個地址去執行,這樣的話就不需要挨個的NOP很多語句了。要注意的是,ARM中的跳轉指令是根據相對地址計算的,所以你要根據當前指令地址和目標地址來計算出相對跳轉的值。

比如說00001BCC: BEQ loc_1C28對應的彙編程式碼為”15 00 00 0A”。

enter image description here

enter image description here

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”即可。

enter image description here

enter image description here

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”即可。

0x07 Kill除錯技巧


該技巧是QEver 在《MSC的偽解題報告》中提到的。利用kill我們可以讓程式掛起,然後用ida掛載上去,獲取有用的資訊,然後可以再用kill將程式恢復執行。我們還是拿自毀程式密碼這個應用舉例,具體實行方法如下:

1 首先用ps獲取執行的app的pid。

enter image description here

2 然後用kill -19 [pid] 就可以將這個app掛起了。

enter image description here

3 隨後我們用ida attach上這個app。因為整個程式都掛起了,所以這次ida掛載後app並沒有閃退。然後就可以在記憶體中找到答案了。

enter image description here

4 如果想要恢復app的執行,需要將ida退出,然後再使用kill -18 [pid]即可。

enter image description here

0x08 在記憶體中dump Dex檔案


在現在的移動安全環境中,程式加殼已經成為家常便飯了,如果不會脫殼簡直沒法在破解界混的節奏。ZJDroid作為一種萬能脫殼器是非常好用的,但是當作者公開發布這個專案後就遭到了各種加殼器的針對,比如說搶佔ZJDroid的廣播接收器讓ZJDroid無法接收命令等。我們也會在”安卓動態除錯七種武器之多情環 - Customized DVM”這篇文章中介紹另一種架構的萬能脫殼器。但工具就是工具,當我們釋出的時候可能也會遭到類似ZJDroid那樣的針對。所以說手動脫殼這項技能還是需要學習的。在這一節中我們會介紹一下最基本的記憶體dump流程。在隨後的文章中我們會介紹更多的技巧。

這裡我們拿alictf2014中的apk300作為例子來介紹一下ida脫簡單殼的基本流程。 首先我們用除錯JNI_OnLoad的技巧將程式在執行前掛起:

adb shell am start -D -n com.ali.tg.testapp/.MainActivity

![enter image description here][59]

然後在libdvm.so中的dvmDexFileOpenPartial函式上下一個斷點:

enter image description here

然後我們點選繼續執行,程式就會在dvmDexFileOpenPartial()這個函式處暫停,R0暫存器指向的地址就是dex檔案在記憶體中的地址,R1暫存器就是dex檔案的大小:

enter image description here

enter image description here

然後我們就可以使用ida的script command去dump記憶體中的dex檔案了。

enter image description here

enter image description here

#!c
static main(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檔案了。

enter image description here

因為過程有點繁瑣,我錄製了一個dump dex檔案的影片在我的github,有興趣的同學可以去下載觀看。

當然這只是最簡單脫殼方法,很多高階殼會動態修改dex的結構體,比如將codeoffset指向記憶體中的其他地址,這樣的話你dump出來的dex檔案其實是不完整的,因為程式碼段儲存在了記憶體中的其他位置。但你不用擔心,我們會在隨後的文章中介紹一種非常簡單的解決方案,敬請期待。

0x09 Function Rewrite函式重寫


有時我們想要將app中的某個函式的邏輯提取出來,用gcc重新編譯一個可執行檔案,比如我們想要寫一個序號產生器,就需要把app生成key的邏輯提取出來。但是ida ”F5”過後的c語言直接編譯經常會有很多錯誤,比如未定義的宏,未定義的宣告等。這是因為這些宏都在ida的一個標頭檔案裡。裡面定義了所有ida自定義的宏和宣告,比如說經常見到的BYTEn()宏:

#!c
#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專案中這麼寫:

#!c
typedef void* (*dvmGetCurrentJNIMethod_func)();
dvmGetCurrentJNIMethod_func dvmGetCurrentJNIMethod_fnPtr;
dvm_hand= dlopen("libdvm.so", RTLD_NOW);
dvmGetCurrentJNIMethod_fnPtr =dlsym(dvm_hand, "_Z22dvmGetCurrentJNIMethodv");
dvmGetCurrentJNIMethod_fnPtr();

0x10 小結


還是那句話,寫了這麼多依然不能保證本文能夠覆蓋到ida除錯的方方面面,因為ida實在是太博大精深了。看官如有興趣可以繼續深入研究學習。另外文章中所有提到的程式碼和工具都可以在我的github下載到,地址是:

https://github.com/zhengmin1989/TheSevenWeapons

0x06 參考文章


  1. MSC解題報告 http://bbs.pediy.com/showthread.php?t=197235
  2. 偽·MSC解題報告http://bbs.pediy.com/showthread.php?p=1349632
本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章