我在 Android 上面跑 root 這麼久,一直以來都是使用 Magisk。但最近遇到需要逆向一些有反逆向工程偵測的應用程式,正好讀到這篇《Root 隐藏基础教程》才知道原來有 KernelSU, APatch 這些 magisk 以外的 root 方案。KernelSU 的 root 比起 magisk 更難被應用程式偵測,因為 root 的功能是直接內建在核心裡面的。
在實體裝置上刷新的 kernel 這種事情我以前還真的沒做過,為了避免資料遺失,我決定先拿模擬器來跑 kernelsu 看看,畢竟如果隱藏效果夠好的話應該連模擬器特徵都可以隱藏才對。
KernelSU 的原理基本上就是去修改 Android 使用的 Linux kernel,讓 kernel 提供一個特別的功能可以讓 userspace app 獲得 root 權限(基本上就是個 rootkit?)。所以也就是說我們會需要一個特製的 kernel,這有兩種方法:
- GKI (Generic Kernel Image): 直接把 KernelSU 功能編譯進 kernel,也就是整個 kernel 要重編
- LKM (Linux Kernel Module): 把 KernelSU 編譯成一個 .ko 模組檔案,開機時插入 kernel,需注意的是
- 目前大部分 android kernel 都有啟用可以載入模組的功能,這點對我們有利
- KernelSU 官方有提供 .ko 模組可以直接下載,只要你的 kernel 是 GKI kernel(Google Treble 計劃後 kernel 有一個比較穩定統一的界面),都可以正確載入運作
- 但我研究了一下還是搞不懂 Image.gz , zImage, boot.img , ramdisk.img 到底要怎麼重包裝嵌入這個 .ko
- 而且 KernelSU 官方文件其實建議如果要用於模擬器那用 GKI 方式載入比較好
- 因此先專注於搞定 GKI
參考:Awesome Android Root(資訊有點過時但大致有用)
目前網路上能找到關於虛擬機內 kernelsu 的資訊只有 5ec1cff 的兩篇,但他的文章省略了很多細節,對於我這種第一次編譯 android kernel 的人來說實在看不出來實際的操作步驟。因此這邊記錄下我自己摸索出來的操作流程(核心概念還是來自於他的兩篇文章)。
下載 android kernel 原始碼
跟隨官方指引,使用 repo 指令下載原始碼。
找到正確原始碼版本
目前下載下來的是最新版本的原始碼,但如果直接編譯這個版本會遇到不相容的問題(5ec1cff 的第一篇就是這樣)。
因此我們要設法找到正確的 kernel 原始碼版本,因為我們接下來要修改原始碼然後重新編譯。幸好在第二篇他找到一個很方便的方法可以精確定位每一個虛擬機 kernel 的來源版本號 (commit hash)。這邊我就用這個方法來操作。
首先照尋常方法啟動虛擬機,然後 adb shell cat /proc/version,我的虛擬機(Android 15, kernel 6.6, host 是 macOS ARM)顯示如下:
ab11987101 這串數字表示 Android CI 的一次構建,可以在以下網址找到本次構建的資訊:
https://ci.android.com/builds/submitted/11987101/kernel_virt_aarch64/latest
直接將數字換成你自己找到的數字就可以,另外注意, kernel_virt_aarch64 是 macOS ARM 上面的 AVD 會使用的 kernel CPU architecture,如果是 x86 的話對應的字串是 kernel_virt_x86_64。
找到構建之後,打開 BUILD_INFO 檔案,裡面有這麼一部分:
"repo-dict": {
"kernel/build": "a9c2595a49f03227e30069b0b7ea4fa7ab69a906",
"kernel/common": "dd9c02ccfe27152c269eec860e74f845b94647e9",
"kernel/common-modules/virtual-device": "94e52d00fd4e9921efbad3c4b5f424f90b57dfce",
"kernel/configs": "7de932a4b942d7e7677b0116a2e257e050163bb9",
"kernel/manifest": "3ae476a46dfdb746553080768f8eb3f53116a8af",
"kernel/prebuilts/build-tools": "b46264b70e3cdf70d08c9ae2df6ea3002b242ebc",
"kernel/tests": "b46b107f40a525386657729465fefac179fc5404",
"platform/build/bazel_common_rules": "212bca45fa9e280adc98081483f649814baf4b61",
"platform/external/bazel-skylib": "6b103c40d8113f001475d5e13672922ef2aa0e5a",
"platform/external/bazelbuild-apple_support": "f6003e1e3763f8aad9fb9acae79cfa5fff9ae988",
"platform/external/bazelbuild-bazel-central-registry": "7018452ffac4fc070b29d5c5c872bac3c746a06a",
"platform/external/bazelbuild-platforms": "04d937168075c80205c96f31752000e9de759adb",
"platform/external/bazelbuild-rules_cc": "5d626dcf48e0e97933b48456a86966971799c9e9",
"platform/external/bazelbuild-rules_java": "8e548c7053dffd1717d565f0409a88992f401da1",
"platform/external/bazelbuild-rules_license": "f578df4fd057ffe2023728444759535685631548",
"platform/external/bazelbuild-rules_pkg": "632ab98c81cf295750dc096142d4ca76264084bc",
"platform/external/bazelbuild-rules_python": "f71847ac898655b67634bb14e77a7408c4fb5e00",
"platform/external/libcap": "8729338c078737da57a8b63ee88016799dfb4cdb",
"platform/external/libcap-ng": "2bcc92ae19481dd2b8d3ce3abdfbbee49261abe6",
"platform/external/lz4": "124599333514066524f55f5f41fbf0d46212e598",
"platform/external/pigz": "9bc9fa17d499ddde88b77820f6d063e16c0cdd42",
"platform/external/python/absl-py": "9ae5a78fc57c3cd539398373ae39601a8b923e62",
"platform/external/toybox": "91f90361fc563025e6d152d68d2072109500d602",
"platform/external/zlib": "b34cd2e4371556e913ad9801a924a891c74d1463",
"platform/external/zopfli": "36c79f00e5229800d2aaa13fc42c301ec8ef1153",
"platform/prebuilts/asuite": "659188034e89b2456c4cea7e38c80c28589dafa5",
"platform/prebuilts/build-tools": "5c0b50c49d7c958c05cd3050e43f4772003df800",
"platform/prebuilts/clang-tools": "1634c6a556d1f2c24897bf74156c6449486e8941",
"platform/prebuilts/clang/host/linux-x86": "7061673283909f372f4938e45149d23bd10cbd40",
"platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.17-4.8": "739d8a41b6096d384338a62995a04739aff8693d",
"platform/prebuilts/jdk/jdk11": "c6c90521b7c317f13d41bbd9336a8d45ee202cec",
"platform/prebuilts/rust": "442511af884f074018466f85b4daadd4b0ac0050",
"platform/system/tools/mkbootimg": "f5c284e8998dd8a085cf027bb518b6c2b84cf744",
"platform/tools/tradefederation/prebuilts": "fddb4c6cb91a580ee1f06536b8b5de15e98af638",
"toolchain/prebuilts/ndk/r26": "e535051ebc04204cec44bde38f62385d63180388"
},
這些就是本次構建使用的 commit hash。
接下來可以請 LLM 工具去編輯 android-kernel/.repo/manifests/default.xml 的內容,讓 default.xml 內指定這些 commit hash。也可以自己手動編輯,一一指定 revision=XXX,編輯完後我的 xml 長這樣:
然後跑一次 repo sync 來切換到剛剛指定的版本。
設定虛擬機使用自訂 system image
AVD 預設同系統版本的不同 VM 之間會使用同一個系統碟映像檔 (system image),為了避免 Kernelsu 安裝影響到其他虛擬機,這邊複製一份系統映像檔出來專供此虛擬機使用。
- 找到 system image 目錄
cd ~/Library/Android/sdk/system-images/android-XX/
- 複製一份
cp -cr google_apis_playstore ksu
- cp -c 在 macOS 上面會使用 APFS clone 功能(在檔案系統層做兩個檔案的 block copy on write),相當於 Linux 上面的 cp –reflink=always
- 修改 avd 設定檔裡面指定的 system image 路徑,設定檔通常在
~/.android/avd/XXXXXX.avd/hardware-qemu.ini
- 有提到
google_apis_playstore 的路徑都改成剛剛複製的一份 ksu
- AVD 的圖形界面現在應該會顯示 missing system images,不過如果用
emulator 指令其實可以正常開機 emulator -avd MYAVD -no-snapshot -show-kernel
若 VM 可以正常開機,恭喜,代表找到了正確的核心源碼版本,可以開始修改這份核心源碼了。
在核心源碼內加入 KernelSU 並重新編譯
參照文件在核心源碼內插入 KernelSU 即可。
實際的編譯指令也可以從 Android CI 的 BUILD_INFO 內取得(參見 5ec1cff’s blog),ARM 架構的核心編譯指令如下:
編譯產物會輸出到 virt/ 資料夾。
以 emulator 指令啟動虛擬機
將稍早編譯的 kernelsu 產物 Image.gz 複製到 system image 目錄並改名為 kernel-ranchu ,蓋掉本來的 kernel 就可以了。
需注意如果是 x86,編譯出來的核心執行檔是 bzImage。
接下來我開啟虛擬機測試,KernelSU 可以正常運作了!
進一步客製化 kernel:安裝 susfs
susfs 是在 KernelSU 對 kernel 源碼的修改上再增加一層修改。susfs 修改了主要是核心的 VFS 模組,讓核心可以刻意對 userspace 隱藏特定檔案、甚至偷偷重導向檔案內容,比如說在 userspace 呼叫 openat() syscall 打開某個路徑的檔案的時候,susfs 核心可以將 openat 重導向到另一個路徑。這些對於隱藏 root 和模改過的系統非常有幫助。
不過 susfs 系列 patch 安裝起來相當複雜(畢竟是 patch 的 patch),所幸官方 README 內的指引還算清楚準確。susfs patch 主要會對 KernelSU 源碼和核心源碼進行修改,對應的 patch 分別是 10_enable_susfs_for_ksu.patch 及 50_add_susfs_in_gki-android15-6.6.patch 。也就是說在套用 susfs patch 之前我們不只要確定核心源碼版本正確,也要確定目前安裝的 KernelSU 源碼版本正確,我在這步卡了一下。(註:我希望安裝的 susfs 版本是最新的開發版,因為 tag 的版本有些太舊)
如前文,我安裝 KernelSU 的時候直接選擇了 dev 分支來安裝,但是 susfs 的開發都是基於某個特定版本的 KernelSU 來開發,因此如果選錯 KernelSU 版本的話 susfs patch 就無法正確套用。至於要怎麼找到正確的 KernelSU 版本?要自己去翻 susfs4ksu 的 commit log,作者每次與 KernelSU 源碼同步都會在 commit log 裡面寫 KernelSU 的 commit hash。
舉例來說,我 1/17 想要安裝的 susfs4ksu repo 內最新的 commit 是 e64cd1f0,打開該 branch 的 commit log,一個一個 commit message 閱讀,找到在該 commit 之前,距離最近的一次 “Synced with official KernelSU main repo" 訊息,也就是 40cdafc0,此 commit 訊息稱與 KernelSU df6409 版本同步,因此我們要安裝的 KernelSU 就是這一個版本。
步驟 8 有個稍微麻煩的地方是對核心源碼打 patch 的時候經常會有 conflict,無法自動 patch,需要一些基本的 C 技能手動上 patch。
另外,susfs gitlab repo 內提供的 userspace 操控 CLI 工具 ksu_module_susfs 功能有些陽春,sidex15 的版本比較完整。
進一步客製化 kernel:自訂 defconfig
有些應用程式會使用 anti-debugging 技巧,讓 debugger (如 gdb, strace)無法正常 attach 上應用程式的 process。這個技巧就是開多個 process 然後從 parent process 先 attach 上 child process,這樣其他人就不能 attach 了,因為一次只能有一個 attach。
然後我查到 Linux 上面有個核心選項可以限制 attach (PTRACE_ATTACH),叫做 ptrace_scope。直接去虛擬機開這個選項看看發現不行,因為這個核心沒有內建提供這個限制功能的 SECURITY_YAMA 模組。
我就想說,既然我現在可以自己編譯核心,那我自然也可以自己去改核心編譯選項加上這個模組,但摸索了一下才找到正確指令開啟編譯選項的選單:
接下來找到 CONFIG_SECURITY_YAMA ,打開,再重新編譯。
重開機確認可以正常運行,我就把 ptrace_scope 調成 2 – admin-only attach,讓這個應用程式沒辦法用自己的 parent process attach child process。結果,應用程式直接啟動 crash。看來要繞過 anti-debugging 可沒那麼簡單啊。