1. 程式人生 > >Android無原始碼除錯Native程式碼(使用GDB)

Android無原始碼除錯Native程式碼(使用GDB)

在前面的Android無原始碼除錯APK一篇中,介紹了一種可以在無原始碼的情況下除錯APKDalvik程式碼的方法。但是,現在越來越多的程式出於安全、效能或程式碼複用的考慮,使用JNI呼叫Native程式碼來實現某些功能。

其實,在Android平臺上,想要對Native程式進行除錯,過程非常簡單,主要是用到了GDB。大家知道,Android底層其實就是Linux,所謂的Dalvik虛擬機器什麼的都是在Linux系統上構建的,而JNI呼叫的Native程式只不過是一個載入進來的.so動態載入庫。所以,在Linux上功能非常強大的GDB除錯工具,也可以在Android平臺上發揮作用。

但是,與普通的

GDB除錯有點不同的是,在Android平臺上,是在PC上除錯Android裝置上的程式,所以需要採用GDB的遠端除錯模式。因此,在正式除錯之前,需要在Android裝置上有GDB伺服器端(gdbserver)程式,並且在PC上有GDB客戶端(gdb)程式。

如果打算在Android模擬器上除錯的話,因為其自帶gdbserver程式,所以不需要做什麼。但是,真機上是不自帶gdbserver程式的,因此如果想在真機上除錯的話,必須拷貝一個可用的gdbserver程式到裝置上。幸運的是在NDKprebuilt\android-arm\gdbserver目錄下可以找到一個Google預先編譯好的gdbserver

程式,省去了自己編譯的麻煩。直接將gdbserver通過adb push命令推送到Android手機上,將其屬性改成可執行。

至於GDB客戶端,NDK自帶了一個可在Windows上執行的版本,可以在NDK

toolchains\arm-linux-androideabi-4.8\prebuilt\windows-x86_64\bin目錄下找到,其檔名是arm-linux-androideabi-gdb.exe,能直接在PC上的命令列模式下執行起來。

最後一個條件是,請確保已經獲得了被除錯裝置的root許可權。

好了,現在萬事具備,已經可以使用GDB命令對裝置上的程式進行除錯了。具體來說,有以下幾個步驟:

1) 啟動要除錯的程式

直接在除錯裝置上點選你要啟動程式的圖示,就可以讓程式跑起來了。

光跑起來還不行,還需要知道這個程式在系統中當前的程序號是多少。可以在除錯裝置的adb shell上,通過下面的命令查到:

        ps | grep <PackageName>

其中,PackageName就是你要除錯程式對應的包名,例如:


可以看到,對於本例來說,其程序號是8143

2) 啟動GDB伺服器端

在除錯Android裝置上啟動gdbserver,並讓其attach到前一步執行的那個要除錯的程序上去。命令如下:

        gdbserver :<PORT>--attach:<PID>

這個命令必須要用root使用者來執行。命令中的PID引數就是前一步檢視到的被除錯程序號。由於要採用GDB遠端除錯模式,所以要讓gdbserver開啟一個埠,這裡的PORT引數就是指定要開啟哪個埠進行監聽。

還是接著上面的例子,假設開啟埠號是1234,並且從上一步檢視到的程序號是8143,則結果是:


對了,還有一點需要注意的是,Android4.4開始,強制打開了SELinux,其規則是不允許一個程序attach到一個非自己的子程序或兄弟程序上進行除錯的,哪怕這個程序是以root使用者啟動的也不行。想要知道當前SELinux的工作模式,可以在adb shell下鍵入getenforce命令,例如:


這是在我執行Android 5.0系統的Google Nexus 5上執行的結果,可以看出,其已經預設打開了強制(Enforcing)模式。所以,要想除錯成功,必須要關閉SELinux的強制模式,可以通過下面的命令來關閉:

        echo 0> /sys/fs/selinux/enforce

注意,這條命令必須用root使用者來執行。下面看看執行後的結果:


可以看出,SELinux的模式已經從強制變成了允許(Permissive)。

3)建立PCAndroid裝置間的埠轉發

前面的命令已經讓gdbserverAndroid裝置上打開了一個埠,監聽遠端GDB的除錯命令。不過,這個監聽埠只在Android裝置上有效,在PC端根本訪問不到。這時候,需要用adbAndroid裝置上的埠轉發到PC機上。請在PC上再開啟一個控制檯,並鍵入如下命令:

        adb forward tcp:<PC_PORT>tcp:<DEVICE_PORT>

這條命令的作用就是將發往PC機上埠為PC_PORTTCP報文,傳送到Android裝置上DEVICE_PORT埠上。

假設還是想在PC上開啟1234埠,對映到上一步在裝置上開啟的1234埠:


4)啟動GDB客戶端

GDB的客戶端啟動非常簡單,只要在命令列下或者Cygwin下執行NDK中自帶的gdb程式就可以了:


接下來,就要讓這個在PC上的GDB客戶端,連線上在Android裝置上的GDB伺服器了,命令如下:

        (gdb) target remote localhost:<PORT>

其中,引數PORT就是在第三步中,在PC上建立的轉發埠。對於前面的例子,就是:


這時,在gdbserver端就會顯示:


這就表示GDB客戶端與服務端已經連線成功了。

經過以上四個步驟之後,就可以在PC端使用常用的GDB命令,對Android裝置上指定的程式進行除錯了。

想進一步瞭解GDB命令的使用方法,可以參考《GDB常用命令》

但Android和普通的Linux平臺還是有點區別的,而且又無原始碼,所以除錯的時候需要使用一些技巧,下面稍微介紹幾個常用的:

1) 如何獲得.so檔案的載入地址

AndroidNative程式碼都是編譯成動態共享庫形式的.so檔案,其在載入的時候位置是不固定的。可以在裝置上的adb shell上,用下面的命令檢視到其具體的載入位置:

cat /proc/<PID>/maps | grep <NativeFileName>

例如,想檢視程序號為8143內,某個.so檔案的載入位置,結果如下:


可以看到,一共有三項,而且載入位置都不一樣。但是程式碼都是可執行的,而只有第一項有執行屬性(r-xp),所以這個.so的載入起始地址就是0xb3f93000

注意,每次程式被重新執行時,載入位置都有可能會改變,所以每次都要檢視一下。

2) 如何在指定程式碼的位置設定斷點

由於要除錯的Native程式無原始碼,而且基本上也不會是用debug模式編譯的,所以要想在指定的位置設定斷點,基本只能靠算。要想算出來,需要知道兩部分資訊:一是想要除錯的.so檔案,當前被載入到了什麼位置。關於這個問題,前面已經介紹過了。二是要知道想要設定斷點的程式碼距離.so頭部的偏移。最後只要將這兩個值相加就是程式碼在當前程序中的真實地址了。

關於第二個問題,可以通過IDA來輔助解決(筆者使用的版本是IDA 6.4)。

下面還是通過接著上面的例子來解釋,假設我想分析前面的.so檔案中的一個函式的具體邏輯。先用IDA將這個.so檔案開啟,在左邊的“Functionswindows”中:


找到那個感興趣的函式,雙擊它,右邊的程式碼顯示框會跳轉到你選擇的那個函式處:


在程式碼行的左邊,就是這個函式在.so檔案中的偏移,本例就是0x000235EC。而這個.so檔案在程序中的載入地址,前面我們也看到過了,是0xb3F93000。所以這個函式的載入地址是0xB3F93000+0x000235EC=0xB3FB65EC。下面,用GDB在這個位置設定斷點,繼續執行程式:


可以看到,程式碼真的斷在了這個位置。接下來就可以用ni來單步執行,具體分析了。

3) 如何定位對匯入函式的呼叫

正常情況下,Native程式都會呼叫別的動態連結庫中的功能,Android中也自帶了很多包含各種功能的.so動態連結庫檔案(例如最基本的libc.so,處理加密的libssl.so等)。我們經常想檢視在呼叫這些公共動態連結庫中的某個函式時,傳入的引數是什麼,這就需要先定位,到底在什麼地方呼叫了這些函式。

這裡先補充一點知識,對於這種要呼叫別的模組匯出函式的情況,在Linux上都要經過PLTProcedure Link Table,過程連結表)表(其實還有GOT表)來重定位。對應每一個要呼叫的外部函式,都有且只有一個PLT表項。而要除錯的.so模組中無論要呼叫多少次,都必須跳轉到這個PLT表項上。所以,如果想知道模組中到底在哪裡呼叫了這個外部函式,只要找到這個對應的PLT表項,再檢視到底哪些位置用跳轉指令跳轉到這個表項上就行了。

接著上面的例子,還是要用到IDA。如果我想知道哪些地方呼叫到了libssl.so模組提供的SSL_write函式,用IDA將這個.so檔案開啟,在左邊的“Functions windows”中找到SSL_write,雙擊它,右邊的程式碼顯示框會自動跳轉到對應SSL_write匯入函式的PLT表項上。右鍵點選SSL_write,選擇“List cross references to…”:


彈出的對話方塊中會列出所有跳轉到這個位置的程式碼位置:


雙擊每一項,都將自動跳轉到呼叫的程式碼位置,可以用上面的方法在其上設定斷點。

4) 如何檢視函式的入口引數

根據APCSARM ProcedureCall Standard)的規定,函式呼叫的前4個非浮點引數是通過ARMR0~R3暫存器來傳遞的(第一個引數對應R0,第二個引數對應R1,以此類推)。

所以,你在一個函式中,想檢視呼叫時的前4個非浮點引數的到底是什麼內容話,可以通過檢視ARM的暫存器來得到。

還是上面的例子,假設現在已經斷在了呼叫SSL_Write函式之前的位置,並且想檢視傳入的第二個引數是什麼(第二個引數就是指向要加密傳輸的原始字串的指標),則可以這樣:


先通過info register(i r)命令檢視存放在R1暫存器中指標執行的地址,然後再通過x命令檢視那個地址存放的字串到底是什麼。

5) 如何知道要除錯的程式碼是Thumb指令集的還是ARM指令集的

這個問題的答案其實很簡單,只要看一下指令程式碼的長度就可以了,Thumb指令長度是2個位元組,而ARM指令長度是4個位元組。

最後總結一下這種除錯方法的特點,大致有如下幾個:

1) 如果在真機上除錯的話,必須要求獲得手機的root許可權;

2) 不需要開啟被除錯程式的debug選項(AndroidManifest.xml中顯式申明android:debuggable=”true”);

3) 由於原理不同,可以結合Android無原始碼除錯APK的方法一起使用,達到聯合除錯Dalvik程式碼和Native程式碼的目的。請不要自找麻煩,用GDB來除錯Dalvik的程式碼。