Android逆向之旅---動態方式破解apk進階篇(IDA除錯so原始碼)

yangxi_001發表於2016-12-02

一、前言

今天我們繼續來看破解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:


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

下面我們還需要做一步,就是還原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 <stdio.h>
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。


相關文章