免責聲明:內容僅供學習參考,請合法利用知識,禁止進行違法犯罪活動!
內容參考于:圖靈Python學院
工具下載:
鏈接:https://pan.baidu.com/s/1bb8NhJc9eTuLzQr39lF55Q?pwd=zy89
提取碼:zy89
復制這段內容后打開百度網盤手機App,操作更方便哦
上一個內容:40.安卓逆向2-frida hook技術-過firda檢測(四)(通過攔截so文件的創建和攔截檢測frida函數過檢測)
本次通過查找app中檢測frida的函數,然后使用frida對檢測frida的函數進行hook,通過hook讓它失效來繞過檢測
首先還是通過hook加載so文件的函數,看看它是加載到什么文件時進行的退出,然后使用ida反編譯so文件,下方是檢測app加載so文件的frida代碼
function main(){// 這段代碼是給一個叫"Frida"的工具用的腳本
// Frida的作用是:可以鉆進手機里的APP內部,看看這個APP在偷偷做什么
// 我們這段腳本的具體任務是:盯著APP加載"特殊文件"的行為// 首先,我們要找到APP加載文件時會用到的兩個"工具函數"// 第一個工具函數叫"dlopen"
// 所有運行在Linux或安卓系統上的程序,要加載"動態鏈接庫"(一種特殊文件,后綴通常是.so)時,經常會用到它
// "Module.findExportByName(null, "dlopen")"的作用:
// 1. 在系統的所有功能里找(null表示不限制范圍)
// 2. 找到名字叫"dlopen"的那個功能,記錄下它在內存中的位置
// 3. 把找到的結果存在變量"dlopen"里,方便后面使用
var dlopen = Module.findExportByName(null, "dlopen");// 第二個工具函數叫"android_dlopen_ext"
// 這是安卓系統專門設計的加強版加載工具,功能比dlopen更多一點
// 有些安卓APP會用這個函數來加載特殊的.so文件
// 下面這行代碼的作用和上面類似:找到這個函數的位置,存在變量里
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");// 接下來,我們要給第一個工具函數"dlopen"裝個"監控器"
// 當APP調用dlopen加載文件時,我們就能立刻知道
// "Interceptor.attach"就是Frida提供的"裝監控器"的功能
Interceptor.attach(dlopen, {// 當APP剛開始調用dlopen函數時,會自動執行下面的代碼// 可以理解為:監控器發現"有人要開始用這個工具了"onEnter: function (args) {// "args"是APP傳給dlopen的參數(就像我們給工具傳遞的"指令")// dlopen的第一個參數很重要:它告訴工具"要加載的文件在哪里"// 這里的"args[0]"就是取第一個參數(計算機里計數從0開始)var path_ptr = args[0];// 剛才拿到的"path_ptr"其實是個"內存地址"(類似文件在倉庫里的貨架編號)// 我們需要根據這個編號,找到實際的文件路徑(比如"/data/lib/test.so")// "ptr(path_ptr)"是把編號轉成Frida能識別的格式// ".readCString()"是按照計算機存儲文字的規則,把地址對應的內容讀出來var path = ptr(path_ptr).readCString();// 最后,把我們發現的信息打印到屏幕上// 這樣我們就能清楚地看到:這個APP用dlopen加載了哪個文件console.log("[發現使用dlopen加載文件:] ", path);},// 當APP用完dlopen函數(加載文件完成后),會執行下面的代碼// 可以理解為:監控器發現"這個人用完工具了"onLeave: function (retval) {// 目前這里什么都沒做,留空是因為我們暫時只關心"開始加載"這個動作// 如果以后想知道"加載成功了嗎",可以在這里處理返回值retval}
});// 下面是給第二個工具函數"android_dlopen_ext"裝監控器,原理和上面完全一樣
Interceptor.attach(android_dlopen_ext, {// 當APP剛開始調用這個安卓特有的加載函數時onEnter: function (args) {// 同樣取第一個參數:要加載的文件地址var path_ptr = args[0];// 把地址轉成我們能看懂的文件路徑var path = ptr(path_ptr).readCString();// 打印信息:APP用安卓特有的工具加載了哪個文件console.log("[發現使用安卓專用dlopen_ext加載文件:] ", path);},// 當APP用完這個函數后onLeave: function (retval) {// 這里也暫時什么都不做}
});
/**
總結代碼的效果:就像我們在 APP 的 "文件加載通道" 上裝了兩個攝像頭,一個盯著普通加載通道,一個盯著安卓專用通道。
只要 APP 從這些通道加載文件(特別是.so 格式的文件),攝像頭就會立刻拍下 "文件地址" 并顯示出來,讓
我們清楚知道這個 APP 在運行時偷偷加載了哪些底層文件。
這種監控在分析 APP 的工作原理、查找惡意軟件行為時非常有用。
*/
}
main()
如下圖注入上方的代碼后,在加載了下圖紅框的so文件后,frida退出了,這說明 libmsaoaidsec.so 里面有frida檢測
查看線程,從下圖紅框可以看出,libmsaoaidsec.so創建了三個線程,然后退出了,這說明檢測frida的代碼在這三個線程中,記住這三個值一會要用, 1c544、1b8d4、 26e5c
![]()
// 定義一個函數,名字叫 hook_patch,作用是"鉤住"線程創建的行為 function hook_patch() { // 1. 找到系統里負責創建線程的函數(pthread_create)的地址 // 解釋: // - pthread_create 是 Linux/Android 系統中創建線程的核心函數,所有程序創建線程都要調用它 // - Module.findExportByName("libc.so", "pthread_create") 意思是:從 libc.so 這個系統庫中,查找導出的 pthread_create 函數的地址 // - libc.so 是系統基礎庫,包含了很多常用的系統函數(比如創建線程、文件操作等) var patch = Module.findExportByName("libc.so", "pthread_create");// 2. 打印找到的 pthread_create 函數的地址(調試用,確認是否找到了目標函數) // 比如可能會輸出:[pth_create] 0x7f8a8b2c3d40(這是一個內存地址) console.log("[pth_create]", patch);// 3. 攔截(Hook)這個 pthread_create 函數,監控它的調用 // Interceptor.attach 是 Frida 的攔截函數,第一個參數是要攔截的函數地址(這里就是上面找到的 patch) // 第二個參數是一個對象,里面定義了攔截后的行為(進入函數時做什么,離開函數時做什么) Interceptor.attach(patch, {// onEnter:當被攔截的函數(pthread_create)被調用時,會執行這里的代碼onEnter: function (args) {// args 是一個數組,存放了調用 pthread_create 時傳入的參數// pthread_create 的函數原型是:// int pthread_create(pthread_t* thread, const pthread_attr_t* attr, void*(*start_routine)(void*), void* arg)// 所以 args[0] = 線程ID指針,args[1] = 線程屬性,args[2] = 線程要執行的函數(核心!線程啟動后會跑這個函數),args[3] = 傳給線程函數的參數// 4. 通過線程要執行的函數地址(args[2]),找到它屬于哪個模塊(.so 文件)// Process.findModuleByAddress(地址) 會返回這個地址所在的模塊信息(比如模塊名、路徑等)var module = Process.findModuleByAddress(args[2]);// 5. 檢查是否成功找到模塊(避免空值報錯)if (module != null) {// 打印線程相關信息:// - module.name:模塊的名字(比如 libnative.so,就是這個模塊創建了線程)// - args[2].sub(module.base):計算線程函數在模塊中的偏移量(相對位置)// 偏移量 = 函數的實際地址 - 模塊的基地址(模塊加載到內存的起始地址)// 比如模塊基地址是 0x1000,函數地址是 0x1200,偏移量就是 0x200console.log("開啟線程-->", module.name, args[2].sub(module.base));}},// onLeave:當被攔截的函數(pthread_create)執行完畢,準備返回時,會執行這里的代碼// 這里暫時為空,說明不需要處理函數返回后的邏輯onLeave: function (retval) {} }); } function main(){ // 這段代碼是給一個叫"Frida"的工具用的腳本 // Frida的作用是:可以鉆進手機里的APP內部,看看這個APP在偷偷做什么 // 我們這段腳本的具體任務是:盯著APP加載"特殊文件"的行為// 首先,我們要找到APP加載文件時會用到的兩個"工具函數"// 第一個工具函數叫"dlopen" // 所有運行在Linux或安卓系統上的程序,要加載"動態鏈接庫"(一種特殊文件,后綴通常是.so)時,經常會用到它 // "Module.findExportByName(null, "dlopen")"的作用: // 1. 在系統的所有功能里找(null表示不限制范圍) // 2. 找到名字叫"dlopen"的那個功能,記錄下它在內存中的位置 // 3. 把找到的結果存在變量"dlopen"里,方便后面使用 var dlopen = Module.findExportByName(null, "dlopen");// 第二個工具函數叫"android_dlopen_ext" // 這是安卓系統專門設計的加強版加載工具,功能比dlopen更多一點 // 有些安卓APP會用這個函數來加載特殊的.so文件 // 下面這行代碼的作用和上面類似:找到這個函數的位置,存在變量里 var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");// 接下來,我們要給第一個工具函數"dlopen"裝個"監控器" // 當APP調用dlopen加載文件時,我們就能立刻知道 // "Interceptor.attach"就是Frida提供的"裝監控器"的功能 Interceptor.attach(dlopen, { // 當APP剛開始調用dlopen函數時,會自動執行下面的代碼 // 可以理解為:監控器發現"有人要開始用這個工具了" onEnter: function (args) {// "args"是APP傳給dlopen的參數(就像我們給工具傳遞的"指令")// dlopen的第一個參數很重要:它告訴工具"要加載的文件在哪里"// 這里的"args[0]"就是取第一個參數(計算機里計數從0開始)var path_ptr = args[0];// 剛才拿到的"path_ptr"其實是個"內存地址"(類似文件在倉庫里的貨架編號)// 我們需要根據這個編號,找到實際的文件路徑(比如"/data/lib/test.so")// "ptr(path_ptr)"是把編號轉成Frida能識別的格式// ".readCString()"是按照計算機存儲文字的規則,把地址對應的內容讀出來var path = ptr(path_ptr).readCString();// 最后,把我們發現的信息打印到屏幕上// 這樣我們就能清楚地看到:這個APP用dlopen加載了哪個文件console.log("[發現使用dlopen加載文件:] ", path);}, // 當APP用完dlopen函數(加載文件完成后),會執行下面的代碼 // 可以理解為:監控器發現"這個人用完工具了" onLeave: function (retval) {// 目前這里什么都沒做,留空是因為我們暫時只關心"開始加載"這個動作// 如果以后想知道"加載成功了嗎",可以在這里處理返回值retval } });// 下面是給第二個工具函數"android_dlopen_ext"裝監控器,原理和上面完全一樣 Interceptor.attach(android_dlopen_ext, { // 當APP剛開始調用這個安卓特有的加載函數時 onEnter: function (args) {// 同樣取第一個參數:要加載的文件地址var path_ptr = args[0];// 把地址轉成我們能看懂的文件路徑var path = ptr(path_ptr).readCString();// 打印信息:APP用安卓特有的工具加載了哪個文件console.log("[發現使用安卓專用dlopen_ext加載文件:] ", path);if(path.indexOf("libmsaoaidsec.so")!=-1){// 調用查看線程的函數hook_patch()} }, // 當APP用完這個函數后 onLeave: function (retval) {// 這里也暫時什么都不做 } }); /** 總結代碼的效果:就像我們在 APP 的 "文件加載通道" 上裝了兩個攝像頭,一個盯著普通加載通道,一個盯著安卓專用通道。 只要 APP 從這些通道加載文件(特別是.so 格式的文件),攝像頭就會立刻拍下 "文件地址" 并顯示出來,讓 我們清楚知道這個 APP 在運行時偷偷加載了哪些底層文件。 這種監控在分析 APP 的工作原理、查找惡意軟件行為時非常有用。 */ } main()
然后把apk進行解壓,找到它的 libmsaoaidsec.so 文件拖到ida中進行反編譯
然后ida加載完后,點擊下圖紅框,然后按CTRL+F
然后搜索1c544、1b8d4、 26e5c這三個,如下圖首先是搜索1c544,雙擊下圖紅框位置就可以跳轉到1c544了,跳轉之后按f5轉偽c代碼
直接把下圖紅框的代碼全選,然后復制給ai大模型,讓它解釋
如下圖ai的解釋,這個 1c544 做的是字符串相關和定時任務(無限循環執行周期性任務),跟檢測frida無關,所以下一個
然后搜索1b8d4,老樣子全選復制給ai大模型
如下圖大模型的解釋,它有點可疑,根據大模型的解釋,1b8d4會檢測,還會暫停,它可能是檢測firda然后暫停frida
然后接著看最后一個 26e5c
大模型的解釋,三個線程都分析完了,現在只有 1b8d4 是最可疑的,接下來通過調用進一步分析(它有一個特征)
再次回到1b8d4 中,鼠標點擊下圖紅框位置,然后按x
可以看誰調用了1b8d4,如下圖有兩個位置調用了它,但都是在1B924中調用的
1B924 調用 1b8d4,然后雙擊上圖任意一個,然后點擊下圖紅框位置,再次按x,查看 1B924 是誰調用的
如下圖只有一個地方調用了 1B924
然后現在的調用棧是 1BEC4 調用 1B924 調用 1b8d4
繼續重復上方的步驟,按 x 查看 1BEC4 誰調用的,如下圖只有一個位置對1BEC4進行了調用,然后雙擊它進入函數
然后特征就來了,如下調用1BEC4的函數叫做init_proc,現在的調用棧 init_proc 調用 1BEC4 調用 1B924 調用 1b8d4
然后 init_proc 不懂沒關系,給ai大模型,讓它解釋,如下圖ai大模型的結束,很詳細,所以 1b8d4 它必然是檢測frida的相關函數,然后 1b8d4 里面只做了暫停操作,沒有關閉frida的操作,所以調用 1b8d4 函數的1B924 函數才是檢測frida的函數,接下來只需要把 1B924 函數進行hook就可以過檢測了
hook代碼,注意 var 構造函數調用偏移 這個值,可能每個手機不一樣,它的找法繼續往下看,寫后面了
// 主函數:根據IDA中獲取的地址信息,替換目標SO中的指定函數
function 根據IDA地址替換函數() {// 1. 獲取linker64模塊的基地址// 來源:linker64是安卓系統自帶的64位動態鏈接器(負責加載所有SO文件),固定名稱為"linker64"// 作用:基地址是模塊加載到內存的起始位置,類似"小區大門的地址"var 鏈接器64基地址 = Module.getBaseAddress("linker64")// 打印基地址用于調試(實際值會隨系統/進程變化,例如0x7f8a0000)console.log("linker64模塊基地址(內存起始位置):", 鏈接器64基地址)// 2. 定義call_constructors函數的偏移量// 來源:這個值必須從IDA中獲取!步驟是:// a. 用IDA打開目標設備的linker64文件(通常在/system/bin/linker64)// b. 搜索函數名"call_constructors"// c. 記錄該函數相對于linker64基地址的偏移(例如0x50C00)// 作用:偏移量是函數在模塊內部的位置,類似"小區內某棟樓的門牌號"var 構造函數調用偏移 = 0x50C00 // 這里必須替換為你從IDA中看到的實際值!// 3. 計算call_constructors函數的實際內存地址// 公式:實際地址 = 模塊基地址 + 偏移量(類似"小區大門地址 + 門牌號 = 具體住戶地址")// 例如:基地址0x7f8a0000 + 偏移0x50C00 = 實際地址0x7f8f0C00var 構造函數調用地址 = 鏈接器64基地址.add(構造函數調用偏移)console.log("call_constructors函數實際地址:", 構造函數調用地址)// 4. 攔截call_constructors函數// 原因:這個函數是linker64加載SO文件時,用于初始化SO中構造函數的關鍵函數// 目的:在目標SO(libmsaoaidsec.so)加載完成并初始化時,及時執行替換操作var 攔截器 = Interceptor.attach(構造函數調用地址, {// 當call_constructors函數被調用時(即有SO正在初始化),執行以下代碼onEnter: function (參數列表) {console.log("檢測到SO文件正在初始化(進入call_constructors函數)")// 5. 檢查目標SO是否已加載// 來源:"libmsaoaidsec.so"是你要操作的目標SO文件名(需替換為你的實際SO名)// 作用:確認我們要修改的SO已經被系統加載到內存中var 目標模塊 = Process.findModuleByName("libmsaoaidsec.so")// 6. 如果目標SO已加載,則執行替換if (目標模塊 != null) {console.log("目標SO已加載:" + 目標模塊.name + ",基地址:" + 目標模塊.base)// 7. 替換目標SO中的指定函數// ① 目標函數地址計算:// 來源:0x1B924是從IDA中獲取的目標函數偏移量,步驟:// a. 用IDA打開libmsaoaidsec.so// b. 找到你要替換的函數(例如sub_1B924)// c. 記錄該函數相對于SO基地址的偏移// 公式:目標函數實際地址 = 目標SO基地址 + 偏移量// ② 新函數定義:// 返回值類型"void"和參數列表[]必須與原函數一致(從IDA中查看函數原型獲取)Interceptor.replace(目標模塊.base.add(0x1B924), // 目標函數的實際內存地址new NativeCallback(function () { // 替換后的新函數console.log("目標函數(偏移0x1B924)已被成功替換!")// 這里可以添加自定義邏輯,例如:// - 返回固定值(如return 0;)// - 修改原函數參數(需在參數列表中定義)// - 調用原函數后修改返回值}, "void", // 新函數返回值類型(必須與原函數一致,從IDA中查)[] // 新函數參數列表(必須與原函數一致,從IDA中查)))// 8. 替換完成后解除攔截// 原因:避免后續加載其他SO時重復執行替換操作攔截器.detach()console.log("已完成替換,解除對call_constructors的攔截")}},// 函數執行結束時的操作(這里不需要,留空)onLeave: function (返回值) {}})
}// 執行主函數,啟動整個替換流程
根據IDA地址替換函數()
如下圖成功繞過檢測,app正常啟動
然后 構造函數調用偏移 值的找法,把下圖紅框的文件,使用 adb pull 下載到電腦上
adb pull /system/bin/linker64 xxxxx
上方的指令執行完后,如下圖,就把 linker64 文件下載到電腦上了
然后把它拖到ida中,然后搜索 constructor,下圖紅框的就是我們要找的,
這個函數的地址50C00
不分析的方式過檢測,直接把 1c544、1b8d4、 26e5c 這三個線程全返回空
// 定義一個函數,用于將指定地址的代碼替換為"直接返回"
// 作用:讓目標函數被調用時直接退出,不執行原來的邏輯
function nop_addr(addr) {// 第一步:修改內存權限為"可讀可寫可執行"(rwx)// 原因:默認情況下代碼段可能沒有寫權限,無法修改指令// 參數說明:// - addr:要修改的內存地址// - 4:修改的內存大小(4字節,足夠存放一條返回指令)// - 'rwx':新的權限(read/write/execute)Memory.protect(addr, 4 , 'rwx');// 第二步:創建一個Arm64架構的指令寫入器// 注意:這里假設目標設備是64位ARM架構(手機幾乎都是ARM)var w = new Arm64Writer(addr);// 第三步:寫入"返回指令"(ret)// 效果:當程序執行到這里時,會直接退出當前函數,不執行后續代碼w.putRet();// 第四步:刷新寫入的指令(確保生效)w.flush();// 第五步:釋放寫入器資源(避免內存泄漏)w.dispose();
}// 定義主函數:Hook動態鏈接器的構造函數調用流程,監控目標SO加載
function hook_call_constructors() {// 聲明變量:用于存儲動態鏈接器(linker)的模塊信息let linker = null;// 判斷當前進程是32位還是64位// Process.pointerSize是指針大小:32位系統為4字節,64位為8字節if (Process.pointerSize === 4) {// 32位系統的動態鏈接器名為"linker"linker = Process.findModuleByName("linker");} else {// 64位系統的動態鏈接器名為"linker64"(大部分現代手機是64位)linker = Process.findModuleByName("linker64");}// 聲明變量:存儲找到的關鍵函數地址// call_constructors_addr:SO初始化函數的地址// get_soname:獲取SO文件名的函數(這里未實際使用)let call_constructors_addr, get_soname;// 枚舉動態鏈接器模塊中的所有符號(符號=函數名/變量名+地址)// 作用:從鏈接器中找到我們需要監控的函數let symbols = linker.enumerateSymbols();// 遍歷所有符號,篩選出需要的函數for (let index = 0; index < symbols.length; index++) {let symbol = symbols[index];// 匹配"__dl__ZN6soinfo17call_constructorsEv"符號// 這個符號對應的函數是:動態鏈接器加載SO時,執行SO內部構造函數的入口// 來源:安卓系統動態鏈接器的標準符號,可通過符號表查詢到if (symbol.name === "__dl__ZN6soinfo17call_constructorsEv") {call_constructors_addr = symbol.address; // 記錄這個函數的內存地址} // 匹配"__dl__ZNK6soinfo10get_sonameEv"符號(可選,備用)// 作用:通過這個函數可以獲取當前正在加載的SO的文件名else if (symbol.name === "__dl__ZNK6soinfo10get_sonameEv") {// 將函數地址包裝為NativeFunction,方便后續調用// 參數說明:// - symbol.address:函數的內存地址// - "pointer":函數返回值類型(返回SO文件名的字符串地址)// - ["pointer"]:函數參數(傳入soinfo結構體的指針)get_soname = new NativeFunction(symbol.address, "pointer", ["pointer"]);}}// 打印找到的call_constructors函數地址(調試用,確認是否找到)console.log("call_constructors函數地址:", call_constructors_addr);// 攔截call_constructors函數:監控所有SO的初始化過程var listener = Interceptor.attach(call_constructors_addr, {// 當call_constructors函數被調用時觸發(有SO正在加載初始化)onEnter: function (args) {console.log("檢測到SO文件正在初始化(進入call_constructors)");// 檢查我們關注的目標SO(libmsaoaidsec.so)是否已加載// 注意:這里的SO文件名需要替換為你實際要操作的SO名稱var module = Process.findModuleByName("libmsaoaidsec.so");// 如果目標SO已經加載到內存中if (module != null) {console.log("找到目標SO:" + module.name + ",基地址:" + module.base);// 對目標SO中的三個關鍵函數執行"替換為返回"操作// 0x1c544、0x1b8d4、0x26e5c是函數在SO中的偏移量// 偏移量來源:通過IDA/ Ghidra等反編譯工具分析目標SO得到// 計算實際地址公式:實際地址 = SO基地址(module.base) + 偏移量nop_addr(module.base.add(0x1c544)); // 處理第一個函數console.log("0x1c544: 已替換為返回指令(函數被跳過)");nop_addr(module.base.add(0x1b8d4)); // 處理第二個函數console.log("0x1b8d4: 已替換為返回指令(函數被跳過)");nop_addr(module.base.add(0x26e5c)); // 處理第三個函數console.log("0x26e5c: 已替換為返回指令(函數被跳過)");// 完成替換后,解除對call_constructors的攔截// 原因:避免后續加載其他SO時重復執行替換操作listener.detach();console.log("所有目標函數處理完畢,已解除攔截");}},// 函數執行結束時的操作(這里不需要處理,留空)onLeave: function (retval) {}});
}// 執行主函數,啟動整個Hook流程
hook_call_constructors();