1. 程式人生 > 實用技巧 >華為手機核心程式碼的編譯及刷入教程【通過魔改華為P9 Android Kernel 對抗反除錯機制】

華為手機核心程式碼的編譯及刷入教程【通過魔改華為P9 Android Kernel 對抗反除錯機制】

0x00 寫在前面

攻防對立。程式除錯與反除錯之間的對抗是一個永恆的主題。在安卓逆向工程實踐中,通過修改和編譯安卓核心原始碼來對抗反除錯是一種常見的方法。但網上關於此類的資料比較少,且都是基於AOSP(即"Android 開放原始碼專案",可以理解為原生安卓原始碼)進行修改,然後編譯成二進位制映象再刷入Nexus 或者Pixel 等 谷歌親兒子手機。但因為谷歌的親兒子在國內沒有行貨銷售渠道,市場佔有率更多的是國產手機,而修改國產手機系統核心的教程卻很少,加之部分國產手機的安卓核心和主線 AOSP 存在些許差異,照搬原生安卓程式碼的修改方法無法在國產手機上實現某些功能,甚至無法編譯成功。所以本文以某國產手機為例,通過研究其核心原始碼,對關鍵程式碼進行分析、修改,編譯核心、打包成刷機映象,對全過程予以展示。

0x01 常見反除錯手段及對抗策略簡介

在安卓程式的開發過程中,反除錯的手段有很多種,簡單列舉若干:

(1) 檢測特定程序或埠號。如 IDA Pro 在對安卓應用進行除錯時,需要在手機端啟動除錯程式 android_server ,該除錯程式預設開啟埠23946。目標程式若發現手機裡有 android_server 程序或開啟了埠23946,目標程式就自動退出,以達到反除錯的目的。

(2)檢測某些關鍵檔案的狀態。如目標程式在除錯狀態時,Linux核心會向部分系統檔案內寫入一些程序狀態資訊,包括但不限於向 “ /proc/目標程式pid/status ” 這一檔案的 TracerPid 欄位寫入除錯程序的 pid 。有部分程式會檢查這些欄位,比如目標程式發現對應的 TracerPid 不等於 0 ,則說明自己本身正在被別的程式除錯,比如:

(Pid為19707的程序正在被Pid為24741的程序除錯)

(3)檢測軟體斷點。在對目標程式進行除錯的過程中,難免會出現斷點。有些程式會通過檢測在除錯狀態下的軟體斷點(如讀取ELF檔案在記憶體中的某些地址是否存在斷點指令)來判斷自己是否正在被除錯。

相應的,反除錯的對抗策略也層出不窮。比如相針對以上第(2)種的反除錯手段,在實戰中存在有以下幾種方案來對抗:

A.修改 Android 系統的 kernel 原始碼,對“程序狀態”相關的函式原始碼進行修改,然後對核心原始碼進行重新編譯並刷寫到手機裡以騙過反除錯檢測。

B.提取手機 boot.img ,用工具對 boot.img檔案進行解包處理,解包之後得到 Android 的二進位制核心檔案。使用 IDA 對其進行逆向分析及修改某些位置,其實質也是修改核心“程序狀態”相關函式,

C.hook 系統 fopen 函式,或者 hook 目標程式 對 /proc/pid/status 等檔案的讀取等,使其返回錯誤的值以騙過反除錯檢測。

綜合以上方案,不難看出,在核心層面進行修改無疑為一勞永逸的辦法。關於修改核心原始碼,網上當前的資料都是基於原生安卓原始碼進行修改。前面我們也說過,照搬原生安卓的修改辦法,往往並不能在國產手機上通過。本文便採取以上第 A 種方案,通過修改某手機的核心原始碼,並在Ubuntu 上進行交叉編譯,然後打包成刷機映象,刷入手機,對抗反除錯。

0x02 原始碼獲取及修改

不同於 AOSP 大大方方的開源,國產手機的開原始碼卻有點”遮遮掩掩“,不太好找。(但是 小米手機 除外,小米的開源做的是越來越好了,在他們的Github上公開了好多機型的程式碼。)而該手機的 kernel 原始碼就得在它的英文版網站上才能找到(以某手機為例):其核心原始碼下載,這個地址實在是不太好找。進入正題,我手頭上的是 Android 7.0, EMUI 5.0 的系統,我們下載對應的 kernel 原始碼,然後解壓到硬碟上,如圖(本文的原始碼存放目錄是 /home/lazarus/Huawei_Kernel/Code_Opensource ):

kernel 目錄裡是該手機 的核心原始碼,這是整個手機系統的核心,它負責著記憶體管理、CPU和程序管理、檔案系統、裝置管理和驅動、網路通訊,以及系統的初始化(引導)、系統呼叫等。經過分析研究以及查閱資料得知,我們要修改的原始檔位於 /Code_Opensource/kernel/fs/proc 目錄下,array.c 和 base.c 這兩個檔案,總共3處需要修改,如圖:

接下來,我們用文字編輯器分別開啟這兩個檔案,開始進行如下修改:

第1處, /Code_Opensource/kernel/fs/proc/array.c (115行):

具體操作如下:

static const char * const task_state_array[] = {
    "R (running)",        /*   0 */
    "S (sleeping)",        /*   1 */
    "D (disk sleep)",    /*   2 */
    "T (stopped)",        /*   4 */
    "S (sleeping)",        /*   1 */  //第二步,再加上一行,保持陣列大小不變
// "t (tracing stop)",    /*   8 */   //第一步,把這一行註釋掉(或刪掉)
    "X (dead)",        /*  16 */
    "Z (zombie)",        /*  32 */
};

這一處操作是修改Linux核心對程序狀態的描述,主要是改掉"t (tracing stop)",這表示程序處於跟蹤狀態或者暫停狀態,會寫入程序狀態的描述檔案的。修改時要注意保持陣列大小不變,因為後面的程式碼會檢查這個陣列大小,如果陣列大小變動了,編譯的時候會出錯。

第2處, /Code_Opensource/kernel/fs/proc/array.c (163行):

具體操作如下:

    tpid = 0;    //新增上這一行,將 tpid 重新賦值為 0
    seq_printf(m,
        "State:\t%s\n"
        "Tgid:\t%d\n"
        "Ngid:\t%d\n"
        "Pid:\t%d\n"
        "PPid:\t%d\n"
        "TracerPid:\t%d\n"
        "Uid:\t%d\t%d\t%d\t%d\n"
        "Gid:\t%d\t%d\t%d\t%d\n"
        "FDSize:\t%d\nGroups:\t",
        get_task_state(p),
        tgid, ngid, pid_nr_ns(pid, ns), ppid, tpid,
        from_kuid_munged(user_ns, cred->uid),
        from_kuid_munged(user_ns, cred->euid),
        from_kuid_munged(user_ns, cred->suid),
        from_kuid_munged(user_ns, cred->fsuid),
        from_kgid_munged(user_ns, cred->gid),
        from_kgid_munged(user_ns, cred->egid),
        from_kgid_munged(user_ns, cred->sgid),
        from_kgid_munged(user_ns, cred->fsgid),
        max_fds);

這一處操作是對 tpid 進行重新賦值。tpid 是描述程序狀態的一個變數,它關聯著程序狀態描述的TracerPid 的值,表示 ptrace 對應的程序 id ,可以理解為如果目標程式處於除錯狀態,tpid的值 == 除錯程式的pid;如果目標程式未處於除錯狀態,則 tpid 的值 == 0 。

第3處, /Code_Opensource/kernel/fs/proc/base.c (243行):

具體操作如下:

static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns,
              struct pid *pid, struct task_struct *task)
{
    unsigned long wchan;
    char symname[KSYM_NAME_LEN];

    wchan = get_wchan(task);

    if (wchan && ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)
            && !lookup_symbol_name(wchan, symname))

      //在此處增加程式碼如下:
            {
        if (strstr(symname, "trace")) { 
         seq_printf(m, "%s", "sys_epoll_wait"); 
       } 
      //增加到到這裡為止。

        seq_printf(m, "%s", symname);
    }
    else
        seq_putc(m, '0');

    return 0;

這一處操作是針對 proc_pid_wchan() 函式,它影響著 /proc/目標程序PID/wchan 這一檔案,當程序處於除錯狀態下, wchan檔案會顯示ptrace_stop。

以上就是對兩個檔案的修改及簡要講解。注意:在修改程式碼時注意不要出現語法錯誤,以免編譯的時候報錯。修改完畢之後,我們進入下一章,也就是緊張刺激的交叉編譯環節。

0x03 交叉編譯環境配置及編譯流程

建議使用 Liunx 系統編譯,我用的是 Ubuntu 。在開始編譯之前,我們當然要先對編譯環境進行一番配置。下載的原始碼中有個 “ README_Kernel.txt ” 的文字文件,裡面簡要描述了編譯要求,這裡我們展開再詳細講一下。該文件是這麼說的:

1. How to Build
- get Toolchain
From android git server, codesourcery and etc ..
- aarch64-linux-android-4.9
- edit Makefile
edit CROSS_COMPILE to right toolchain path(You downloaded).
Ex)   export PATH=$PATH:$(android platform directory you download)/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin
Ex)   export CROSS_COMPILE=aarch64-linux-android-
$ mkdir ../out
$ make ARCH=arm64 O=../out merge_hi3650_defconfig
$ make ARCH=arm64 O=../out -j8
2. Output files
- Kernel : out/arch/arm64/boot/Image.gz
- module : out/drivers/*/*.ko
3. How to Clean
$ make ARCH=arm64 distclean
$ rm -rf out

也就是說,第一步: 我們要先獲得交叉編譯的工具鏈(該手機是 aarch64 架構):

aarch64-linux-android-4.9

這個可以從網上下載,比如 Google 官方地址(因眾所周知的原因,訪問該URL可能需要某種手段)

https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/

當然也可以從github等處找到。(我用的是之前編譯 AOSP 時的工具鏈,所以我就沒有再下載。)

下載後解壓到某個目錄,比如我的在 /home/lazarus/aarch64-linux-android-4.9 這個目錄:

第二步: 把工具鏈的路徑放到系統變數裡面,好讓我們的作業系統在編譯的時候知道去哪兒找到工具鏈。開啟終端,輸入如下命令:

export PATH=$PATH:/home/lazarus/aarch64-linux-android-4.9/bin

(你需要將 /home/lazarus/aarch64-linux-android-4.9 換成你自己的工具鏈存放路徑)

第三步: 設定交叉編譯引數,在剛才的終端裡再輸入

export CROSS_COMPILE=aarch64-linux-android-

(不知為何,有的時候在開始編譯時 gcc會報錯,這時把 CROSS_COMPILE 後面的引數設為了完整路徑即:/home/lazarus/aarch64-linux-android-4.9/bin/aarch64-linux-android- 就好了 ……)

第4步:還記得我們下載的某手機kernel原始碼嗎?我們在該終端內輸入以下命令,來到kernel的原始碼目錄:

cd /home/lazarus/Huawei_Kernel/Code_Opensource/kernel

第5步:按照 “ README_Kernel.txt ”的說明,在kernel 目錄的上一級新建一個目錄(或稱之為資料夾也行),這個目錄將用來存放我們編譯出來的核心二進位制檔案:

mkdir ../out

第6步:設定編譯引數,將目標檔案存放路徑設為剛才的 out 目錄,編譯設定從 merge_hi3650_defconfig 中讀取:

make ARCH=arm64 O=../out merge_hi3650_defconfig

第7步:開始編譯。輸入以下命令,準備起飛吧!

make ARCH=arm64 O=../out -j8

第1~7步的輸入如下圖所示:

經過一番等待,編譯完成後,我們最會在 ~/Code_Opensource/out/arch/arm64/boot 目錄中發現 Image.gz 這一檔案,這個就是編譯完成後的二進位制核心檔案——的壓縮包。接下來我們需要做的,就是把這個核心放到手機系統裡,讓它跑起來就行了。不過……這個壓縮包怎麼寫入手機系統裡面呢?這是系統核心,可不是簡單複製貼上就能完事兒的。我們且看下一章節。

0x04 將核心刷入手機

經常刷機的朋友們想必知道 fastboot 。在安卓手機中,fastboot 是一種比 recovery 更底層的刷機模式(俗稱引導模式)。需要使用USB資料線連線手機,然後刷入相應的映象檔案。較為常見的映象大多是boot.img(核心/引導) ,recovery.img(恢復介面,大眾喜愛的第三方recovery TWRP 就是此類映象),system.img(這個一般比較大,裡面是安卓系統。常見的第三方ROM就是通過修改它得來的)。

我們這次需要通過給手機刷入 boot.img 來更新手機核心。簡單的說,boot.img 包含兩部分,分別為 kernel 和 ramdisk 。而其中的 kernel 就包含我們剛才編譯出來的核心檔案。那麼 boot.img 從哪裡可以搞的到呢?第一種方法:如果你硬盤裡存放的有這款手機的刷機包的話,可以通過解包等操作來獲取手機的 boot.img 。不過這種方法顯然略顯苛刻,那既然 boot.img 是被刷入手機中的,可不可以直接從手機中提取出來呢?答:可以(前提是手機已經 root ),這就是我們要講的第二種方法,看操作:

(1)找到 boot.img 的“藏身之處”

手機開啟開發者模式,勾選允許USB除錯,然後通過USB資料線接入電腦。在電腦端啟動一個終端,輸入如下命令:

adb shell
su
cd /dev/block/platform/hi_mci.0/by-name
ls -l boot

簡要解釋以下這段命令的意思:首先進入 ADB shell 並獲得 su 許可權(這也是需要手機已經 root 的原因),然後切換到 /dev/block/platform/hi_mci.0/by-name 這一目錄。如果你在手機裡面的檔案管理器中開啟這個目錄,會發現裡面是一堆類似於Windows系統中的“快捷方式”一類的東西,其實這個在Linux系統中叫做“軟連結”(或者叫“符號連結”),不同名字的軟連結會指向它們真正的所在的mmcblk(塊裝置)。比如 以上命令最後一句 ls -l boot 的意思就是顯示 boot 分割槽 所在的mmcblk。如下圖,boot 分割槽存放在“mmcblk0p28” 之中:

(2)將boot.img 提取到手機

找到了 boot 分割槽的存放位置,我們用 Linux 的 dd 命令將其提取到手機的內部儲存空間中:

dd if=/dev/block/mmcblk0p28 of=/sdcard/boot.img

簡單解釋下:dd 命令的用途是用指定大小的塊拷貝一個檔案,其中 “ if = ” 後面跟著的,是輸入檔名,也就是 我們上一步找到的 boot 分割槽藏身之處,而“ of= ” 後面跟著的,是輸出檔名,也就是我們想要的boot.img 。這樣,我們就把手機的boot f分割槽內容提取到了手機的 /sdcard 目錄中,你可以在手機的內部儲存空間裡找到它。

然後再開啟一個終端,將boot .img 從手機的/sdcard 目錄中複製到電腦上:

adb pull  /sdcard/boot.img  boot.img

這個命令很簡單,不用解釋了吧?複製完成後,我們可以在 電腦硬碟中找到 boot.img,如圖:

(3) 對 boot.img 進行修改,放入新核心

既然得到了 boot.img ,下一步就是把修改 boot.img 。我們需要先把 boot.img 解包,然後將新核心替換進去,再重新打包,然後刷入手機。這裡我們需要一個工具,叫做Android Image Kitchen(這一步你也可以在Windows上操作,但我用了Ubuntu,所以要下載這個工具的 Linux 版本,它也有Windows 版本你可以到這裡下載,也可以網上搜索)。下載後解壓到硬碟,同時為了方便操作,我們把剛才提取的 boot.img 也放到Android Image Kitchen所在的目錄中。然後再開一個終端,定位到該目錄,執行 ./unpackimg.sh 進行解包,如下圖:

解包完成後,目錄下會多出兩個資料夾。其中一個名叫 split_img ,我們要替換的 kernel 就在裡面存放著。我們開啟這個資料夾,會發現一個叫做boot.img-zImage的檔案——這個就是我們要找的東西了!還記得之前我們編譯出來的新核心檔案嗎?我們把新核心檔案重新命名為boot.img-zImage,複製到 split_img 資料夾,替換掉之前這個舊的核心檔案。然後執行 ./repackimg.sh 對 boot 映象進行重新打包,這樣會生成一個新的 boot 映象檔案 “image-new.img” ,如下圖:

到了這一步,工作基本上就接近尾聲了。接下來,我們要是把這個新映象刷入手機。

(4)通過fastboot刷入新核心

將該手機通過USB資料線連線電腦(記得開USB除錯),在剛才的終端內執行以下命令 進入fastboot:

adb reboot bootloader

手機會自動重啟到 fastboot 介面。進入fastboot介面以後,在終端內執行以下命令,將 image-new.img 刷入手機的 boot 分割槽:

su
fastboot flash boot image-new.img

一切順利的話,如下圖所示:

此處要宣告兩個情況:

(1)我的 Ubuntu 的fastboot 需要在 su 許可權下執行,也許有的人不需要 su 就可以。另外也可以用 Windows 系統也進行fastboot 。(2)如果出現 FAILED (remote: Command not allowed) 的錯誤資訊,很有可能是沒有關閉“手機找回”這一功能所致,需要在手機裡面關閉手機找回功能。可以參考該帖子的2樓回帖

刷入完畢後,對手機進行重啟:

fastboot reboot

重啟完畢後,你親手編譯的新核心就執行在你手機上了。看看新鮮出爐的核心版本:

0x05 真機測試

好了,大功告成。到了我們喜聞樂見的真機測試環節。我們將手機連線電腦,push IDA 的gdb偵錯程式到手機的 /data/local/tmp 目錄,啟動gdb偵錯程式,開啟埠轉發,啟動IDA Pro ……(具體操作自行查閱用IDA 除錯 Android 的方法。)這一章節我用了 Windows 10 系統,安裝的是 IDA Pro 7.0:

就以我手機上的 “com.example.root.myapplication” 這個程式來測試吧,記下它的 程序PID 是13819,我們附加上去開始除錯……

此時,我們再開一個命令列視窗,進入手機 adb shell ,用如下命令檢視PID 13819 的程序狀態:

cat /proc/13819/status

在我們對核心修改之前,TracerPid的值應該是 android_server 的 PID 。而現在,我們仔細觀察它的 TracerPid 欄位,是不是已經變成 0 了 ?說明我們編譯的核心已經正常執行,而且實現了我們想要的對抗反除錯的功能。然後你就擁有了一個開啟“無敵模式”的手機,某些(通過檢測自身狀態)帶有反除錯功能的程式在裡面將無法察覺自身的狀態,已然完全任你擺佈(除錯)了——用 IDA 附加上去,開始起飛吧!

【本文首發於 FreeBuf.COM ,在此做一備份記錄。連結地址--> https://www.freebuf.com/articles/terminal/229624.html 】