Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android逆向之旅---動態方式破解apk進階篇(IDA調試so源碼)

Android逆向之旅---動態方式破解apk進階篇(IDA調試so源碼)

編輯:關於Android編程

一、前言

今天我們繼續來看破解apk的相關知識,在前一篇:Eclipse動態調試smali源碼破解apk我們今天主要來看如何使用IDA來調試Android中的native源碼,因為現在一些app,為了安全或者效率問題,會把一些重要的功能放到native層,那麼這樣一來,我們前篇說到的Eclipse調試smali源碼就顯得很無力了,因為核心的都在native層,Android中一般native層使用的是so庫文件,所以我們這篇就來介紹如何調試so文件的內容,從而讓我們破解成功率達到更高的一層。


二、知識准備

我們在介紹如何調試so文件的時候,先來看一下准備知識:

第一、IDA工具的使用

早在之前的一篇文章:Android中通過靜態分析技術破解apk中使用IDA工具靜態分析so文件,通過分析arm指令,來獲取破解信息,比如打印的log信息,來破解apk的,在那時候我們就已經介紹了如何使用IDA工具:


這裡有多個窗口,也有多個視圖,用到最多的就是:

1、Function Window對應的so函數區域:這裡我們可以使用ctrl+f進行函數的搜索

2、IDA View對應的so中代碼指令視圖:這裡我們可以查看具體函數對應的arm指令代碼

3、Hex View對應的so的十六進制數據視圖:我們可以查看arm指令對應的數據等


當然在IDA中我們還需要知道一些常用的快捷鍵:

1、強大的F5快捷鍵可以將arm指令轉化成可讀的C語言,幫助分析


首先選中需要翻譯成C語言的函數,然後按下F5:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD48cD48aW1nIHNyYz0="/uploadfile/Collfiles/20160527/20160527091149286.png" alt="\" />

看到了,立馬感覺清爽多了,這些代碼看起來應該會好點了。

下面我們還需要做一步,就是還原JNI函數方法名
一般JNI函數方法名首先是一個指針加上一個數字,比如v3+676。然後將這個地址作為一個方法指針進行方法調用,並且第一個參數就是指針自己,比如(v3+676)(v3…)。這實際上就是我們在JNI裡經常用到的JNIEnv方法。因為Ida並不會自動的對這些方法進行識別,所以當我們對so文件進行調試的時候經常會見到卻搞不清楚這個函數究竟在干什麼,因為這個函數實在是太抽象了。解決方法非常簡單,只需要對JNIEnv指針做一個類型轉換即可。比如說上面提到a1和v4指針:


我們可以選中a1變量,然後按一下y鍵:


然後將類型聲明為:JNIEnv*。


確定之後再來看:


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



2、Shirt+F12快捷鍵,速度打開so中所有的字符串內容窗口


有時候,字符串是一個非常重要的信息,特別是對於破解的時候,可能就是密碼,或者是密碼庫信息。


3、Ctrl+S快捷鍵,有兩個用途,在正常打開so文件的IDA View視圖的時候,可以查看so對應的Segement信息


可以快速得到,一個段的開始位置和結束位置,不過這個位置是相對位置,不是so映射到內存之後的位置,關於so中的段信息,不了解的同學可以參看這篇文章:Android中so文件格式詳解這篇文章介紹的很很清楚了,這裡就不在作介紹了。

當在調試頁面的時候,ctrl+s可以快速定位到我們想要調試的so文件映射到內存的地址:


因為一般一個程序,肯定會包含多個so文件的,比如系統的so就有好多的,一般都是在/system/lib下面,當然也有我們自己的so,這裡我們看到這裡的開始位置和結束位置就是這個so文件映射到內存中:


這裡我們可以使用cat命令查看一個進程的內存映射信息:cat /proc/[pid]/maps

我們看到映射信息中有多so文件,其實這個不是多個so文件,而是so文件中對應的不同Segement信息被映射到內存中的,一般是代碼段,數據段等,因為我們需要調試代碼,所以我們只關心代碼段,代碼段有一個特點就是具有執行權限x,所以我們只需要找到權限中有x的那段數據即可。


4、G快捷鍵:在IDA調試頁面的時候,我們可以使用S鍵快速跳轉到指定的內存位置


這裡的跳轉地址,是可以算出來的,比如我現在想跳轉到A函數,然後下斷點,那麼我們可以使用上面說到的ctrl+s查找到so文件的內存開始的基地址,然後再用IDA View中查看A函數對應的相對地址,相加就是絕對地址,然後跳轉到即可,比如這裡的:

Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals 函數的IDA View中的相對地址(也就是so文件的地址):E9C


上面看到so文件映射到內存的基地址:74FE4000


那麼跳轉地址就是:74FE4000+E9C=74FE4E9C

注意:

一般這裡的基地址只要程序沒有退出,在運行中,那麼他的值就不會變,因為程序的數據已經加載到內存中了,基地址不會變的,除非程序退出,又重新運行把數據加載內存中了,同時相對地址是永遠不會變的,只有在修改so文件的時候,文件的大小改變了,可能相對地址會改變,其他情況下不會改變,相對地址就是數據在整個so文件中的位置。


這裡我們可以看到函數映射到內存中的絕對地址了。

注意:

有時候我們發現跳轉到指定位置之後,看到的全是DCB數據,這時候我們選擇函數地址,點擊P鍵就可以看到arm指令源碼了:



5、調試快捷鍵:F8單步調試,F7單步進入調試


上面找到函數地址之後,我們可以下斷點了,下斷點很簡單,點擊簽名的綠色圈點,變成紅色條目即可,然後我們可以點擊F9快捷鍵,或者是點擊運行按鈕,即可運行程序:


其中還有暫停和結束按鈕。我們運行之後,然後在點擊so的native函數,觸發斷點邏輯:


這時候,我們看到進入調試界面,點擊F8可以單步調試,看到有一個PC指示器,其實在arm中PC是一個特殊的寄存器,用來存儲當前指令的地址,這個下面會介紹到。


好了到這裡,我們就大致說了一下關於IDA在調試so文件的時候,需要用到的快捷鍵:

1、Shift+F12快速查看so文件中包含的字符串信息

2、F5快捷鍵可以將arm指令轉化成可讀的C代碼,這裡同時可以使用Y鍵,修改JNIEnv的函數方法名

3、Ctrl+S有兩個用途,在IDA View頁面中可以查看so文件的所有段信息,在調試頁面可以查看程序所有so文件映射到內存的基地址

4、G鍵可以在調試界面,快速跳轉到指定的絕對地址,進行下斷點調試,這裡如果跳轉到目的地址之後,發現是DCB數據的話,可以在使用P鍵,進行轉化即可,關於DCB數據,下面會介紹的。

5、F7鍵可以單步進入調試,F8鍵可以單步調試


第二、常用的ARM指令集知識

我們在上面看到IDA打開so之後,看到的是純種的匯編指令代碼,所以這就要求我們必須會看懂匯編代碼,就類似於我們在調試Java層代碼的時候一樣,必須會smali語法,慶幸的是,這兩種語法都不是很復雜,所以我們知道一些大體的語法和指令就可以了,下面我們來看看arm指令中的尋址方式,寄存器,常用指令,看完這三個知識點,我們就會對arm指令有一個大體的了解,對於看arm指令代碼也是有一個大體的認知了。

1、arm指令中的尋址方式

1>. 立即數尋址
也叫立即尋址,是一種特殊的尋址方式,操作數本身包含在指令中,只要取出指令也就取到了操作數。這個操作數叫做立即數,對應的尋址方式叫做立即尋址。例如:
MOV R0,#64 ;R0 ← 64
2>. 寄存器尋址
寄存器尋址就是利用寄存器中的數值作為操作數,也稱為寄存器直接尋址。
例如:ADD R0,R1, R2 ;R0 ← R1 + R2
3>. 寄存器間接尋址
寄存器間接尋址就是把寄存器中的值作為地址,再通過這個地址去取得操作數,操作數本身存放在存儲器中。
例如:
LDR R0,[R1] ;R0 ←[R1]
4>. 寄存器偏移尋址
這是ARM指令集特有的尋址方式,它是在寄存器尋址得到操作數後再進行移位操作,得到最終的操作數。
例如:
MOV R0,R2,LSL #3 ;R0 ← R2 * 8 ,R2的值左移3位,結果賦給R0。
5>. 寄存器基址變址尋址
寄存器基址變址尋址又稱為基址變址尋址,它是在寄存器間接尋址的基礎上擴展來的。它將寄存器(該寄存器一般稱作基址寄存器)中的值與指令中給出的地址偏移量相加,從而得到一個地址,通過這個地址取得操作數。
例如:
LDR R0,[R1,#4] ;R0 ←[R1 + 4],將R1的內容加上4形成操作數的地址,取得的操作數存入寄存器R0中。
6>. 多寄存器尋址
這種尋址方式可以一次完成多個寄存器值的傳送。例如:
LDMIA R0,{R1,R2,R3,R4} ;R1←[R0],R2←[R0+4],R3←[R0+8],R4←[R0+12]
7>. 堆棧尋址
堆棧是一種數據結構,按先進後出(First In Last Out,FILO)的方式工作,使用堆棧指針(Stack Pointer, SP)指示當前的操作位置,堆棧指針總是指向棧頂。
堆棧尋址舉例如下:
STMFD SP!,{R1-R7, LR} ;將R1-R7, LR壓入堆棧。滿遞減堆棧。
LDMED SP!,{R1-R7, LR} ;將堆棧中的數據取回到R1-R7, LR寄存器。空遞減堆棧。


2、ARM中的寄存器

R0-R3:用於函數參數及返回值的傳遞
R4-R6, R8, R10-R11:沒有特殊規定,就是普通的通用寄存器
R7:棧幀指針(Frame Pointer).指向前一個保存的棧幀(stack frame)和鏈接寄存器(link register, lr)在棧上的地址。
R9:操作系統保留
R12:又叫IP(intra-procedure scratch )
R13:又叫SP(stack pointer),是棧頂指針
R14:又叫LR(link register),存放函數的返回地址。
R15:又叫PC(program counter),指向當前指令地址。


3、ARM中的常用指令含義

ADD 加指令
SUB 減指令
STR 把寄存器內容存到棧上去
LDR 把棧上內容載入一寄存器中
.W 是一個可選的指令寬度說明符。它不會影響為此指令的行為,它只是確保生成 32 位指令。Infocenter.arm.com的詳細信息
BL 執行函數調用,並把使lr指向調用者(caller)的下一條指令,即函數的返回地址
BLX 同上,但是在ARM和thumb指令集間切換。
CMP 指令進行比較兩個操作數的大小


4、ARM指令簡單代碼段分析

C代碼:

#include
int func(int a, int b, int c, int d, int e, int f)
{
int g = a + b + c + d + e + f;
return g;
}

對應的ARM指令:

add r0, r1 將參數a和參數b相加再把結果賦值給r0
ldr.w r12, [sp] 把最的一個參數f從棧上裝載到r12寄存器
add r0, r2 把參數c累加到r0上
ldr.w r9, [sp, #4] 把參數e從棧上裝載到r9寄存器
add r0, r3 累加d累加到r0
add r0, r12 累加參數f到r0
add r0, r9 累加參數e到r0


三、構造so案例

好了,關於ARM指令的相關知識,就介紹這麼多了,不過我們在調試分析的時候,肯定不能做到全部的了解,因為本身ARM指令語法就比較復雜,不過幸好大學學習了匯編語言,所以稍微能看懂點,如果不懂匯編的同學那就可能需要補習一下了,因為我們在使用IDA分析so文件的時候,不會匯編的話,那是肯定行不通的,所以我們必須要看懂匯編代碼的,如果遇到特殊指令不了解的同學,可以網上搜一下即可。


上面我們的准備知識做完了,一個是IDA工具的時候,一個是ARM指令的了解,下面我們就來開始操刀了,為了方便開始,我們先自己寫一個簡單的Android native層代碼,然後進行IDA進行分析即可。

這裡可以使用AndroidStudio中進行新建一個簡單工程,然後創建JNI即可:



這裡順便簡單說一下AndroidStudio中如何進行NDK的開發吧:

第一步:在工程中新建jni目錄


第二步:使用javah生成native的頭文件


注意:

javah執行的目錄,必須是類包名路徑的最上層,然後執行:

javah 類全名

注意沒有後綴名java哦


第三步:配置項目的NDK目錄



選擇模塊的設置選線:Open Module Settings


設置NDK目錄即可


第四步:copy頭文件到jni目錄下,然後配置gradle中的ndk選項


這裡只需要設置編譯之後的模塊名,就是so文件的名稱,需要產生那幾個平台下的so文件,還有就是需要用到的lib庫,這裡我們看到我們用到了Android中打印log的庫文件。


第五步:編譯運行,在build目錄下生成指定的so文件,copy到工程的libs目錄下即可



好了,到這裡我們就快速的在AndroidStudio中新建了一個Native項目,這裡關於native項目的代碼不想解釋太多,就是Java層

傳遞了用戶輸入的密碼,然後native做了校驗過程,把校驗結果返回到Java層即可:


具體的校驗過程這裡不再解釋了。我們運行項目之後,得到apk文件,那麼下面我們就開始我們的破解旅程了


四、開始破解so文件

開始破解我們編譯之後的apk文件

第一、首先我們可以使用最簡單的壓縮軟件,打開apk文件,然後解壓出他的so文件


我們得到libencrypt.so文件之後,使用IDA打開它:



我們知道一般so中的函數方法名都是:Java_類名_方法名

那麼這裡我們直接搜:Java關鍵字即可,或者使用jd-gui工具找到指定的native方法


雙擊,即可在右邊的IDA View頁面中看到Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals 函數的指令代碼:


我們可以簡單的分析一下這段指令代碼:

1>、PUSH {r3-r7,lr} 是保存r3,r4,r5,r6,r7,lr 的值到內存的棧中,那麼最後當執行完某操作後,你想返回到lr指向的地方執行,當然要給pc了,因為pc保留下一條CPU即將執行的指令,只有給了pc,下一條指令才會執行到lr指向的地方

pc:程序寄存器,保留下一條CPU即將執行的指令
lr: 連接返回寄存器,保留函數返回後,下一條應執行的指令

這個和函數最後面的POP {r3-r7,pc}是相對應的。

2>、然後是調用了strlen,malloc,strcpy等系統函數,在每次使用BLX和BL指令調用這些函數的時候,我們都發現了一個規律:就是在調用他們之前一般都是由MOV指令,用來傳遞參數值的,比如這裡的R5裡面存儲的就是strlen函數的參數,R0就是is_number函數的參數,所以我們這樣分析之後,在後面的動態調試的過程中可以得到函數的入口參數值,這樣就能得到一些重要信息



3>、在每次調用有返回值的函數之後的命令,一般都是比較指令,比如CMP,CBZ,或者是strcmp等,這裡是我們破解的突破點,因為一般加密再怎麼牛逼,最後比較的參數肯定是正確的密碼(或者是正確的加密之後的密碼)和我們輸入的密碼(或者是加密之後的輸入密碼),我們在這裡就可以得到正確密碼,或者是加密之後的密碼:


到這裡,我們就分析完了native層的密碼比較函數:Java_cn_wjdiankong_encryptdemo_MainActivity_isEquals

如果覺得上面的ARM指令看的吃力,可以使用F5鍵,查看他的C語言代碼:


我們這裡看到其實有兩個函數是核心點:

1>is_number函數,這個函數我們看名字應該猜到是判斷是不是數字,我們可以使用F5鍵,查看他對應的C語言代碼:


這裡簡單一看,主要是看return語句和if判斷語句,看到這裡有一個循環,然後獲取_BYTE*這裡地址的值,並且自增加一,然後存到v2中,如果v3為"\0'的話,就結束循環,然後做一次判斷,就是v2-48是否大於9,那麼這裡我們知道48對應的是ASCII中的數字0,所以這裡可以確定的是就是:用一個循環遍歷_BYTE*這裡存的字符串是否為數字串。

2>get_encrypt_str函數,這個函數我們看到名字可以猜測,他是獲取我們輸入的密碼加密之後的值,再次使用F5快捷鍵查看:


這裡我們看到,首先是一個if語句,用來判斷傳遞的參數是否為NULL,如果是的話,直接返回,不是的話,使用strlen函數獲取字符串的長度保存到v2中,然後使用malloc申請一塊堆內存,首指針保存到result,大小是v2+1也就是傳遞進來的字符串長度+1,然後就開始進入循環,首指針result,賦值給i指針,開始循環,v3是通過v1-1獲取到的,就是函數傳遞進來字符串的地址,那麼v6就是獲取傳遞進來字符串的字符值,然後減去48,賦值給v7,這裡我們可以猜到了,這裡想做字符轉化,把char轉化成int類型,繼續往下看,如果v6==48的話,v7=1,也就是說這裡如果遇到字符"0',就賦值1,在往下看,看到我們上面得到的v7值,被用來取key_src數組中的值,那麼這裡我們雙擊key_src變量,就跳轉到了他的值地方,果不其然,這裡保存了一個字符數組,看到他的長度正好是18,那麼這裡我們應該明白了,這裡通過傳遞進來的字符串,循環遍歷字符串,獲取字符,然後轉化成數字,在倒序獲取key_src中的字符,保存到result中。然後返回。

好了,到這裡我們就分析完了這兩個重要的函數的功能,一個是判斷輸入的內容是否為數字字符串,一個是通過輸入的內容獲取密碼內容,然後和正確的加密密碼:ssBCqpBssP 作比較。


第二、開始使用IDA進行調試設置

那麼下面我們就用動態調試來跟蹤傳入的字符串值,和加密之後的值,這裡我們看到沒有打印log的函數,所以很難知道具體的參數和寄存器的值,所以這裡需要開始調試,得知每個函數執行之後的寄存器的值,我們在用IDA進行調試so的時候,需要以下准備步驟:

1、在IDA安裝目錄下獲取android_server命令文件


IDA安裝目錄\dbgsrv\android_server,這個文件是干嘛的呢?他怎麼運行呢?下面來介紹一下:

我們是否還記得之前一篇文章:Android中run-as命令帶來的安全問題 這篇文章中我們介紹了Android中的調試原理,其實是使用gdb和gdbserver來做到的,gdb和gdbserver在調試的時候,必須注入到被調試的程序進程中,但是非root設備的話,注入別的進程中只能借助於run-as這個命令了,所以我們知道,如果要調試一個應用進程的話,必須要注入他內部,那麼IDA調試so也是這個原理,他需要注入(Attach附加)進程,才能進行調試,但是IDA沒有自己弄了一個類似於gdbserver這樣的工具,那就是android_server了,所以他需要運行在設備中,保證和PC端的IDA進行通信,比如獲取設備的進程信息,具體進程的so內存地址,調試信息等。

所以我們把android_server保存到設備的/data目錄下,修改一下他的運行權限,然後必須在root環境下運行,因為他要做注入進程操作,必須要root。



注意:

這裡把他放在了/data目錄下,然後./android_server運行,這裡提示了IDA Android 32-bit,所以後面我們在打開IDA的時候一定要是32位的IDA,不是64位的,不然保存,IDA在安裝之後都是有兩個可執行的程序,一個是32位,一個是64位的,如果沒打開正確會報這樣的錯誤:


同樣還有一類問題:error: only position independent executables (PIE) are supported

這個主要是Android5.0以上的編譯選項默認開啟了pie,在5.0以下編譯的原生應用不能運行,有兩種解決辦法,一種是用Android5.0以下的手機進行操作,還有一種就是用IDA6.6+版本即可。


然後我們再看,這裡開始監聽了設備的23946端口,那麼如果要想讓IDA和這個android_server進行通信,那麼必須讓PC端的IDA也連上這個端口,那麼這時候就需要借助於adb的一個命令了:

adb forward tcp:遠端設備端口號(進行調試程序端) tcp:本地設備端口(被調試程序端)

那麼這裡,我們就可以把android_server端口轉發出去:


然後這時候,我們只要在PC端使用IDA連接上23946這個端口就可以了,這裡面有人好奇了,為什麼遠程端的端口號也是23946,因為後面我們在使用IDA進行連接的時候,發現IDA他把這個端口設置死了,就是23946,所以我們沒辦法自定義這個端口了。

我們可以使用netstat命令查看端口23946的使用情況,看到是ida在使用這個端口



2、上面就准備好了android_server,運行成功,下面就來用IDA進行嘗試連接,獲取信息,進行進程附加注入

我們這時候需要在打開一個IDA,之前打開一個IDA是用來分析so文件的,一般用於靜態分析,我們要調試so的話,需要在打開一個IDA來進行,所以這裡一般都是需要打開兩個IDA,也叫作雙開IDA操作。動靜結合策略。


這裡記得選擇go這個選項,就是不需要打開so文件了,進入是一個空白頁:


我們選擇Debugger選項,選擇Attach,看到有很多debugger,所以說IDA工具真的很強大,做到很多debugger的兼容,可以調試很多平台下的程序。這裡我們選擇Android debugger:


這裡看到,端口是寫死的:23946,不能進行修改,所以上面的adb forward進行端口轉發的時候必須是23946。這裡PC本地機就是調試端,所以host就是本機的ip地址:127.0.0.1,點擊確定:


這裡可以看到設備中所有的進程信息就列舉出來的,其實都是android_server干的事,獲取設備進程信息傳遞給IDA進行展示。

注意:

如果我們當初沒有用root身份去運行android_server:


這裡就會IDA是不會列舉出設備的進程信息:


還有一個注意的地方,就是IDA和android_server一定要保持一致。


我們這裡可以ctrl+F搜索我們需要調試的進程,當然這裡我們必須運行起來我們需要調試的進程,不然也是找不到這個進程的


雙擊進程,即可進入調試頁面:


這裡為什麼會斷在libc.so中呢?

android系統中libc是c層中最基本的函數庫,libc中封裝了io、文件、socket等基本系統調用。所有上層的調用都需要經過libc封裝層。所以libc.so是最基本的,所以會斷在這裡,而且我們還需要知道一些常用的系統so,比如linker:


我們知道,這個linker是用於加載so文件的模塊,所以後面我們在分析如何在.init_array處下斷點

還有一個就是libdvm.so文件,他包含了DVM中所有的底層加載dex的一些方法:


我們在後面動態調試需要dump出加密之後的dex文件,就需要調試這個so文件了。


3、找到函數地址,下斷點,開始調試

我們使用Ctrl+S找到需要調試so的基地址:74FE4000


然後通過另外一個IDA打開so文件,查看函數的相對地址:E9C


那麼得到了函數的絕對地址就是:74FE4E9C,使用G鍵快速跳轉到這個絕對地址:


跳轉到指定地址之後,開始下斷點,點擊最左邊的綠色圓點即可下斷點:


然後點擊左上角的綠色按鈕,運行,也可以使用F9鍵運行程序:


我們點擊程序中的按鈕:


觸發native函數的運行:


看到了,進入調試階段了,這時候,我們可以使用F8進行單步調試,F7進行單步進入調試:


我們點擊F8進行單步調試,達到is_number函數調用出,看到R0是出入的參數值,我們可以查看R0寄存器的內容,然後看到是123456,這個就是Java層傳入的密碼字符串,接著往下走:


這裡把is_number函數返回值保存到R0寄存中,然後調用CBZ指令,判斷是否為0,如果為0就跳轉到locret_74FE4EEC處,查看R0寄存器的值不是0,繼續往下走:


看到了get_encrypt_str函數的調用,函數的返回值保存在R1寄存器中,查看內容:zytyrTRA*B了,那麼看到,上層傳遞的:123456=》zytyrTRA*B了,前面我們靜態分析了get_encrypt_str函數的邏輯,繼續往下看:


看到了,這裡把上面得到的字符串和ssBCqpBssP作比較,那麼這裡ssBCqpBssP就是正確的加密密碼了,那麼我們現在的資源是:

正確的加密密碼:ssBCqpBssP,加密密鑰庫:zytyrTRA*BniqCPpVs,加密邏輯get_encrypt_str

那麼我們可以寫一個逆向的加密方法,去解析正確的加密密碼得到值即可,這裡為了給大家一個破解的機會,這裡就不公布正確答案了,這個apk我隨後會上傳,手癢的同學可以嘗試破解一下。

加密apk下載地址:http://download.csdn.net/detail/jiangwei0910410003/9531638


第三、總結IDA調試的流程

到這裡,我們就分析了如何破解apk的流程,下面來總結一下:

1、我們通過解壓apk文件,得到對應的so文件,然後使用IDA工具打開so,找到指定的native層函數

2、通過IDA中的一些快捷鍵:F5,Ctrl+S,Y等鍵來靜態分析函數的arm指令,大致了解函數的執行流程

3、再次打開一個IDA來進行調試so

1>將IDA目錄中的android_server拷貝到設備的指定目錄下,修改android_server的運行權限,用Root身份運行android_server

2>使用adb forward進行端口轉發,讓遠程調試端IDA可以連接到被調試端

3>使用IDA連接上轉發的端口,查看設備的所有進程,找到我們需要調試的進程。

4>通過打開so文件,找到需要調試的函數的相對地址,然後在調試頁面使用Ctrl+S找到so文件的基地址,相加之後得到絕對地址,使用G鍵,跳轉到函數的地址處,下好斷點。點擊運行或者F9鍵。

5>觸發native層的函數,使用F8和F7進行單步調試,查看關鍵的寄存器中的值,比如函數的參數,和函數的返回值等信息

總結就是:在調試so的時候,需要雙開IDA,動靜結合分析。


五、使用IDA來解決反調試問題

那麼到這裡我們就結束了我們這期的破解旅程了?答案是否定的,因為我們看到上面的例子其實是我自己先寫了一個apk,目的就是為了給大家演示,如何使用IDA來進行動態調試so,那麼下面我們還有一個操刀動手的案例,就是2014年,阿裡安全挑戰賽的第二題:AliCrackme_2:


阿裡真會制造氛圍,還記得我們破解的第一題嗎,這次看到了第二題,好吧,下面來看看破解流程吧:

首先使用aapt命令查看他的AndroidManifest.xml文件,得到入口的Activity類:


然後使用dex2jar和jd-gui查看他的源碼類:com.yaotong.crackme.MainActivity:


看到,他的判斷,是securityCheck方法,是一個native層的,所以這時候我們去解壓apk文件,獲取他的so文件,使用IDA打開查看native函數的相對地址:11A8


這裡的ARM指令代碼不在分析了,大家自行查看即可,我們直接進入調試即可:

在打開一個IDA進行關聯調試:


選擇對應的調試進程,然後確定:


使用Ctrl+S鍵找到對應so文件的基地址:74EA9000

和上面得到的相對地址相加得到絕對地址:74EA9000+11A8=74EAA1A8 使用G鍵直接跳到這個地址:


下個斷點,然後點擊F9運行程序:


擦,IDA退出調試頁面了,我們再次進入調試頁面,運行,還是退出調試頁面了,好了,這下蛋疼了,沒法調試了。

這裡其實是阿裡做了反調試偵查,如果發現自己的程序被調試了,就直接退出程序,那麼這裡有問題了,為什麼知道是反調試呢?這個主要還是看後續自己的破解經驗了,沒技術可言,還有一個就是阿裡如何做到的反調試策略的,這裡限於篇幅,只是簡單介紹一下原理:

前面說到,IDA是使用android_server在root環境下注入到被調試的進程中,那麼這裡用到一個技術就是Linux中的ptrace,關於這個這裡也不解釋了,大家可以自行的去搜一下ptrace的相關知識,那麼Android中如果一個進程被另外一個進程ptrace了之後,在他的status文件中有一個字段:TracerPid 可以標識是被哪個進程trace了,我們可以使用命令查看我們的被調試的進行信息:

status文件在:/proc/[pid]/status


看到了,這裡的進程被9187進程trace了,我們在用ps命令看看9187是哪個進程:


果不其然,是我們的android_server進程,好了,我們知道原理了,也大致猜到了阿裡在底層做了一個循環檢測這個字段如果不為0,那麼代表自己進程在被人trace,那麼就直接停止退出程序,這個反檢測技術用在很多安全防護的地方,也算是一個重要的知識點了。


那麼下面就來看看如何應對這個反調試?

我們剛剛看到,只要一運行程序,就退出了調試界面,說明,這個循環檢測程序執行的時機非常早,那麼我們現在知道的最早的兩個時機是:一個是.init_array,一個是JNI_OnLoad

.init_array是一個so最先加載的一個段信息,時機最早,現在一般so解密操作都是在這裡做的

JNI_OnLoad是so被System.loadLibrary調用的時候執行,他的時機要早於哪些native方法執行,但是沒有.init_array時機早

那麼知道了這兩個時機,下面我們先來看看是不是在JNI_OnLoad函數中做的策略,所以我們需要先動態調試JNI_OnLoad函數

我們既然知道了JNI_OnLoad函數的時機,如果阿裡把檢測函數放在這裡的話,我們不能用之前的方式去調試了,因為之前的那種方式時機太晚了,只要運行就已經執行了JNI_OnLoad函數,所以就會退出調試頁面,幸好這裡IDA提供了在so文件load的時機,我們只需要在Debug Option中設置一下就可以了:

在調試頁面的Debugger 選擇 Debugger Option選項:


然後勾選Suspend on library load/unload即可


這樣設置之後,還是不行,因為我們程序已經開始運行,就在static代碼塊中加載so文件了,static的時機非常早,所以這時候,我們需要讓程序停在加載so文件之前即可。

那麼我想到的就是添加代碼waitForDebugger代碼了,這個方法就是等待debug,我們還記得在之前的調試smali代碼的時候,就是用這種方式讓程序停在了啟動出,然後等待我們去用jdb進行attach操作。

那麼這一次我們可以在System.loadLibrary方法之前加入waitForDebugger代碼即可,但是這裡我們不這麼干了,還有一種更簡單的方式就是用am命令,本身am命令可以啟動一個程序,當然可以用debug方式啟動:

adb shell am start -D -n com.yaotong.crackme/.MainActivity

這裡一個重要參數就是-D,用debug方式啟動


運行完之後,設備是出於一個等待Debugger的狀態:


這時候,我們再次使用IDA進行進程的附加,然後進入調試頁面,同時設置一下Debugger Option選項,然後定位到JNI_OnLoad函數的絕對地址。


但是我們發現,這裡沒有RX權限的so文件,說明so文件沒有加載到內存中,想一想還是對的,以為我們現在的程序是wait Debugger,也就是還沒有走System.loadLibrary方法,so文件當然沒有加載到內存中,所以我們需要讓我們程序跑起來,這時候我們可以使用jdb命令去attach等待的程序,命令如下:

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

其實這條命令的功能類似於,我們前一篇說到用Eclipse調試smali源碼的時候,在Eclipse中設置遠程調試工程一樣,選擇Attach方式,調試機的ip地址和端口,還記得8700端口是默認的端口,但是我們運行這個命令之後,出現了一個錯誤:


擦,無法連接到目標的VM,那麼這種問題大部分都出現在被調試程序不可調試,我們可以查看apk的android:debuggable屬性:


果不其然,這裡沒有debug屬性,所以這個apk是不可以調試的,所以我們需要添加這個屬性,然後在回編譯即可:


回編譯:java -jar apktool.jar b -d out -o debug.apk

簽名apk:java -jar .\sign\signapk.jar .\sign\testkey.x509.pem .\sign\testkey.pk8 debug.apk debug.sig.apk

然後在次安裝,使用am 命令啟動:

第一步:運行:adb shell am start -D -n com.yaotong.crackme/.MainActivity

出現Debugger的等待狀態

第二步:啟動IDA 進行目標進程的Attach操作

第三步:運行:jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700


第三步:設置Debugger Option選項

第四步:點擊IDA運行按鈕,或者F9快捷鍵,運行


看到了,這次jdb成功的attach住了,debug消失,正常運行了,

但是同時彈出了一個選擇提示:


這時候,不用管它,全部選擇取消按鈕,然後就運行到了linker模塊了:


這時候,說明so已經加載進來了,我們再去獲取JNI_OnLoad函數的絕對地址


Ctrl+S查找到了基地址:7515A000

用靜態方式IDA打開so查看相對地址:1B9C


相加得到絕對地址:7515A000+1B9C=7515BB9C,然後點擊S鍵,跳轉:


跳轉到指定的函數位置:


這時候再次點擊運行,進入了JNI_OnLoad處的斷點:


下面咋們就開始單步調試了,但是當我們每次到達BLX R7這條指令執行完之後,就JNI_OnLoad就退出了:


經過好幾次嘗試都是一樣的結果,所以我們發現這個地方有問題,可能就是反調試的地方了

我們再次進入調試,看見BLX跳轉的地方R7寄存器中是pthread_create函數,這個是Linux中新建一個線程的方法

所以阿裡的反調試就在這裡開啟一個線程進行輪訓操作,去讀取/proc/[pid]/status文件中的TrackerPid字段值,如果發現不為0,就表示有人在調試本應用,在JNI_OnLoad中直接退出。其實這裡可以再詳細進入查看具體代碼實現的,但是這裡限於篇幅問題,不詳細解釋了,後續在寫一篇文章我們自己可以實現這種反調試機制的。本文的重點是能夠動態調試即可。



那麼問題找到了,我們現在怎麼操作呢?

其實很簡單,我們只要把BLX R7這段指令干掉即可,如果是smali代碼的話,我們可以直接刪除這行代碼即可,但是so文件不一樣,他是匯編指令,如果直接刪除這條指令的話,文件會發生錯亂,因為本身so文件就有固定的格式,比如很多Segement的內容,每個Segement的偏移值也是有保存的,如果這樣去刪除會影響這些偏移值,會破壞so文件格式,導致so加載出錯的,所以這裡我們不能手動的去刪除這條指令,我們還有另外一種方法,就是把這條指令變成空指令,在匯編語言中,nop指令就是一個空指令,他什麼都不干,所以這裡我們直接改一下指令即可,arm中對應的nop指令是:00 00 00 00

那麼我們看到BLX R7對應的指令位置為:1C58


查看他的Hex內容是:37 FF 2F E1


我們可以使用一些二進制文件軟件進行內容的修改,這裡使用010Editor工具進行修改:


這裡直接修改成00 00 00 00:


這時候,保存修改之後的so文件,我們再次使用IDA進行打開查看:


哈哈,指令被修改成了:ANDEQ R0,R0,R0了


那麼修改了之後,我們在替換原來的so文件,再次重新回編譯,簽名安裝,再次按照之前的邏輯給主要的加密函數下斷點,這裡不需要在給JNI_OnLoad函數下斷點了,因為我們已經修改了反調試功能了,所以這裡我們只需要按照這麼簡單幾步即可:

第一步:啟動程序

第二步:使用IDA進行進程的attach

第三步:找到Java_com_yaotong_crackme_MainActivity_securityCheck函數的絕對地址

第四步:打上斷點,點擊運行,進行單步調試


看到了吧,這裡我們可以單步調試進來了啦啦,說明我們修改反調試指令成功了。

下面就繼續F8單步調試:


調試到這裡,發現一個問題,就是CMP指令之後,BNE 指令就開始跳轉到loc_74FAF2D0處了,那麼我們就可以猜到了,CMP指令比較的應該就是我們輸入的密碼和正確的密碼,我們再次從新調試,看看R3和R1寄存器的值


看到了這裡的R3寄存器的值就是用寄存器尋址方式,賦值字符串的,這裡R2寄存器就是存放字符串的地址,我們看到的內容是aiyou...但是這裡肯定不是全部字符串,因為我們沒看到字符串的結束符:"\0',我們點擊R2寄存器,進入查看完整內容:


這裡是全部內容:aiyou,bucuoo

我們繼續查看R1寄存器的內容:


這裡也是同樣用寄存器尋址,R0寄存器存儲的是R1中字符串的地址,我們看到這裡的字符串內容是:jiangwei

這個就是我輸入的內容,那麼這裡就可以豁然開朗了,密碼是上面的:aiyou,bucuoo

我們再次輸入這個密碼:


哈哈哈,破解成功啦啦~~


手癢的同學可以下載項目來玩玩~~

項目下載:http://download.csdn.net/detail/jiangwei0910410003/9531957


六、技術總結

到這裡我們算是講解完了如何使用IDA來調試so代碼,從而破解apk的知識了,因為這裡IDA工具比較復雜,所以這篇文章篇幅有點長,所以同學們可以多看幾遍,就差不多了。下面我們來整理一下這篇文章中涉及到的知識點吧:

第一、IDA中的常用快捷鍵使用

1、Shift+F12可以快速查看so中的常量字符串內容,有時候,字符串內容是一個很大的突破點

2、使用強大的F5鍵,可以查看arm匯編指令對應的C語言代碼,同時可以使用Y鍵,進行JNIEnv*方法的還原

3、使用Ctrl+S鍵,可以在IDA View頁面中查看so的所有段信息,在調試頁面可以查找對應so文件映射到內存的基地址,這裡我們還可以使用G鍵,進行地址的跳轉

4、使用F8進行單步調試,F7進行單步跳入調試,同時可以使用F9運行程序

第二、ARM匯編指令相關知識

1、了解了幾種尋址方式,有利於我們簡單的讀懂arm匯編指令代碼

2、了解了arm中的幾種寄存器的作用,特別是PC寄存器

3、了解了arm中常用的指令,比如:MOV,ADD,SUB,LDR,STR,CMP,CBZ,BL,BLX

第三、使用IDA進行調試so的步驟,這裡分兩種情況

1、IDA調試無反調試的so代碼步驟:

1》把IDA安裝目錄中的android_server拷貝到設備的指定目錄中,修改android_server的權限,並且用root方式運行起來,監聽23946端口

2》使用adb forward命令進行端口的轉發,將設備被調試端的端口轉發到遠程調試端中

3》雙開IDA工具,一個是用來打開so文件,進行文件分析,比如簡單分析arm指令代碼,知道大體邏輯,還有就是找到具體函數的相對位置等信息,還有一個IDA是用來調試so文件的,我們在Debugger選項中設置Debugger Option,然後附加需要調試的進程

4》進入調試頁面之後,通過Ctrl+S和G快捷鍵,定位到需要調試的關鍵函數,進行下斷點

5》點擊運行或者快捷鍵F9,觸發程序的關鍵函數,然後進入斷點,使用F8單步調試,F7單步跳入調試,在調試的過程中主要觀察BL,BLX指令,以及CMP和CBZ等比較指令,然後在查看具體的寄存器的值。

2、IDA調試有反調試的so代碼步驟:

1》查看apk是否為可調式狀態,可以使用aapt命令查看他的AndroidManifest.xml文件中的android:debuggeable屬性是否為true,如果不是debug狀態,那麼就需要手動的添加這個屬性,然後回編譯,在簽名打包從新安裝

2》使用adb shell am start -D -n com.yaotong.crackme/.MainActivity 命令啟動程序,出於wait Debug狀態

3》打開IDA,進行進程附加,進入到調試頁面

4》使用jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 命令attach之前的debug狀態,讓程序正常運行

5》設置Debug Option選項,設置Suspend on library start/exit/Suspend on library load/unload/Suspend on process entry point選項

6》點擊運行按鈕或者F9鍵,程序運行停止在linker模塊中,這時候表示so文件加載進來了,我們通過Ctrl+S和G鍵跳轉到JNI_OnLoad函數出,進行下斷點

7》然後繼續運行,進入JNI_OnLoad斷點處,使用F8進行單步調試,F7進行單步跳入調試,找到反調試代碼處

8》然後使用二進制軟件修改反調試代碼為nop指令,即00值

9》修改之後,在替換原來的so文件,進行回編譯,從新簽名打包安裝即可

10》按照上面的無反調試的so代碼步驟即可

第四、學習了如何做到反調試檢測

現在很多應用防止別的進程調試或者注入,通常會用自我檢測裝置,原理就是循環檢測/proc/[mypid]/status文件,查看他的TracerPid字段是否為0,如果不為0,表示被其他進程trace了,那麼這時候就直接退出程序。因為現在的IDA調試時需要進程的注入,進程注入現在都是使用Linux中的ptrace機制,那麼這裡的TracePid就可以記錄trace的pid,我們可以發現我們的程序被那個進程注入了,或者是被他在調試。進而采取一些措施。

第五、IDA調試的整體原理

我們知道了上面的IDA調試步驟,其實我們可以仔細想一想,他的調試原理大致是這樣的:

首先他得在被調試端安放一個程序,用於IDA端和調試設備通信,這個程序就是android_server,因為要附加進程,所以這個程序必須要用root身份運行,這個程序起來之後,就會開啟一個端口23946,我們在使用adb forward進行端口轉發到遠程調試端,這時候IDA就可以和調試端的android_server進行通信了。後面獲取設備的進程列表,附加進程,傳遞調試信息,都可以使用這個通信機制完成即可。IDA可以獲取被調試的進程的內存數據,一般是在 /proc/[pid]maps 文件中,所以我們在使用Ctrl+S可以查看所有的so文件的基地址,可以遍歷maps文件即可做到。


破解法則:時刻需要注意關鍵的BL/BLX等跳轉指令,在他們執行完之後,肯定會有一些CMP/CBZ等比較指令,這時候就可以查看重要的寄存器內容來獲取重要信息。


七、總結

總算是說完了IDA調試so了這個知識點,我們也知道了一種全新的方式去破解native層的代碼,現在有些程序依然把關鍵代碼放在了Java層,那麼這裡我們可以使用Eclipse調試samli即可破解,如果程序為了安全,可能還會把關鍵代碼放到native層,那麼這時候,我們可以使用IDA來調試so代碼來破解,當然破解和加密總是相生相克的,現在程序為了安全做了加固策略,那麼這也是我們下一篇文章需要介紹的,如何去破解那些加固的apk。


更多內容:點擊進入


PS: 關注公眾號,最新Android技術實時推送


  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved