編輯:關於Android編程
隨著移動安全越來越火,各種調試工具也都層出不窮,但因為環境和需求的不同,並沒有工具是萬能的。另外工具是死的,人是活的,如果能搞懂工具的原理再結合上自身的經驗,你也可以創造出屬於自己的調試武器。因此,筆者將會在這一系列文章中分享一些自己經常用或原創的調試工具以及手段,希望能對國內移動安全的研究起到一些催化劑的作用。
目錄如下:
安卓動態調試七種武器之長生劍 - Smali Instrumentation
安卓動態調試七種武器之孔雀翎 – Ida Pro
安卓動態調試七種武器之離別鉤 - 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/
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的調試界面。
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”即可。
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的技巧將程序在運行前掛起:
adb shell am start -D -n com.ali.tg.testapp/.MainActivity
![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()宏:
1 2 3 #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項目中這麼寫:
1 2 3 4 5 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
作者:蒸米@阿裡聚安全,更多Android技術文章,請訪問阿裡聚安全博客
本文主要和大家分享如何在Android應用開發過程中如何進行單元測試,個人在做項目的過程中,覺得單元測試很有必要,以保證我們編寫程序的正確性。下面我們先大概了解下單元測試
main.xml代碼如下: .java代碼如下: package org.lxh.demo; import androi
直接附代碼:#import "MyView.h"#import // 行距const CGFloat kGlobalLineLeading = 5.0
1.說明: Service是Android中四大組件之一,在Android開發中起到非常重要的作用,先來看一下官方對Service的定義: A Service is