華為手機內核代碼的編譯及刷入教程【通過魔改華為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 】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM