聲明
本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整代碼,抓包內容、敏感網址、數據接口等均已做脱敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!
本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯繫作者立即刪除!
逆向目標
- 目標:某當勞 APP
- apk 版本:7.0.15.1
- 下載地址:
aHR0cHM6Ly93d3cuZG93bmt1YWkuY29tL2FuZHJvaWQvMTU0OTg1Lmh0bWw=
逆向分析
直接注入 frida 代碼,frida 命令如下:
frida -U -f com.mcdonalds.gma.cn -l .\3.js
結果如下:
發現閃退,老樣子,按照之前的思路,我們可以先 hook dlopen 方法,監控動態庫的加載情況。
dlopen 原型函數:
void *dlopen(const char *filename, int flag);
| 參數 | 説明 |
|---|---|
filename |
so 文件的路徑,例如 "libfoo.so" 或完整路徑 /data/app/.../libfoo.so。傳 NULL 表示獲取主程序自身句柄。 |
flag |
加載選項,常見值:• RTLD_LAZY:按需解析符號(調用時綁定)• RTLD_NOW:立即解析所有未定義符號 • RTLD_GLOBAL:符號導出,可被後續庫使用 • RTLD_LOCAL:符號僅在本庫內可見(默認) |
hook android_dlopen_ext 代碼如下:
function hook_dlopen() {
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();
console.log("[android_dlopen_ext -> enter", path);
if (args[0].readCString() != null && args[0].readCString().indexOf("libmsaoaidsec.so") >= 0) {
// hook_call_constructors()
hook_pth()
}
},
onLeave: function (retval) {
console.log("android_dlopen_ext -> leave")
}
});
}
hook_dlopen()
可以看到,程序雖然在我們的 libmsaoaidsec.so 退出了,但是在這之前加載了一個 libDexHelper.so 文件,通過 MT 管理器查看可知,是某梆加固,而某梆加固都是 libDexHelper.so 進行 frida 檢測的,所以我們應該先分析這個文件:
so 文件分析
知道在這個 so 文件加密之後,我們直接 hook pthread_create 看看創建了哪些線程,hook 代碼如下:
function hook_pthread_create(){
var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
console.log("pthread_create_addr: ", pthread_create_addr);
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
console.log(args[2], Process.findModuleByAddress(args[2]).name);
},onLeave:function(retval){
}
});
}
hook_pthread_create();
發現並沒有 libDexHelper.so 相關的線程,這是什麼原因呢?遇事不決問 ai,下面是 GPT 給出的部分答案:
GPT 給出了重要結論,pthread_create 最終會調用 clone 方法。
clone 是 Linux/Android 系統的一個 底層系統調用,用於創建 線程或進程,它比 fork 更靈活,是 pthread_create 的底層實現基礎。
clone 函數原型如下:
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg,
... /* pid_t *ptid, void *tls, pid_t *ctid */);
| 參數 | 説明 |
|---|---|
fn |
子線程/進程起始函數 |
child_stack |
子線程棧頂地址 |
flags |
控制資源共享與行為 |
arg |
傳給 fn 的參數 |
ptid |
父線程寫入子線程 PID 的地址 |
tls |
子線程 TLS 基址 |
ctid |
子線程寫入自己 PID 的地址 |
那我們直接調用 clone 函數試看看,hook clone 代碼如下:
var clone = Module.findExportByName(null, 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// 獲取線程函數地址
var func = args[0];
// 獲取線程函數所在模塊
var module = Process.findModuleByAddress(func);
if (module) {
console.log("Thread function is located in module: " + module.name);
}
// 打印調用棧
console.log("Backtrace:");
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.join('\n'));
},
onLeave: function(retval) {
// 可在這裏打印返回值或做後續處理
}
});
發現多次調用同一個線程創建,通過下面命令,把 lib.so 文件拷貝到電腦:
adb pull /system/lib64/libc.so ./libc64.so
ida 分析
我們用 ida 打開 libc.so 文件,搜索 pthread_create 查看它的地址:
我們主要關注上面 a3 的值,按住 tab 鍵找到 pthread_create 的地址:
0x7278e88aa8 libc.so!pthread_create+0x290
根據上面的輸出加上偏移得到 clone 函數的最終地址為 0xAFAA8:
0x00000000000AF818 + 0x290 = 0x00000000000AFAA8
按 g 跳轉到該地址,如下所示:
clone(__pthread_start, v19, 4001536, v31, v31 + 16, v23 + 8, v31 + 16);
| 參數 | 説明 |
|---|---|
__pthread_start |
線程函數入口(pthread 內部包裝 start_routine) |
v19 |
新線程棧頂地址 |
4001536 |
clone flags(如 CLONE_VM) |
v31 |
入口函數參數(通常封裝了 start_routine + arg) |
v31 + 16 |
父線程寫入子線程 PID 的地址(ptid) |
v23 + 8 |
新線程 TLS 基址 |
v31 + 16 |
子線程寫自己 PID 的地址(ctid) |
在 pthread 內部,線程函數會存儲在線程控制塊中:
*(_QWORD *)(v31 + 96) = a3; // 將用户線程函數寫入線程控制塊
通過讀取 v31 + 96 的地址,我們可以獲取實際執行的線程函數,hook 代碼如下:
var clone = Module.findExportByName('libc.so', 'clone');
Interceptor.attach(clone, {
onEnter: function(args) {
// 只有當 args[3] 不為 NULL 時,才説明上層確實把 “線程控制塊指針” 傳進來了
if(args[3] != 0){
// 真正的用户線程函數地址
var addr = args[3].add(96).readPointer()
// 根據線程函數地址 addr,找它屬於哪個模塊
var so_name = Process.findModuleByAddress(addr).name;
// 獲取該 so 在進程裏的基址
var so_base = Module.getBaseAddress(so_name);
// 獲取相對於 so_base 的偏移
var offset = (addr - so_base);
console.log("===============>", so_name, addr,offset, offset.toString(16));
}
},
onLeave: function(retval) {
}
});
結果如下:
可以看到成功輸出了該 so 文件的線程函數,接下來,我們嘗試着先 nop 掉這幾個函數,看能否過檢測,hook 代碼如下:
function nopFunc(parg2) {
Memory.protect(parg2, 4, 'rwx'); // 修改該地址的權限為可讀可寫
var writer = new Arm64Writer(parg2);
writer.putRet(); // 直接跳到 ret 返回地方 ,不反回值
writer.flush(); // 寫入操作刷新到目標內存,使得寫入的指令生效。 從緩存中寫道內存
writer.dispose(); // 釋放 Arm64Writer 使用的資源。
console.log("nop " + parg2 + " success");
}
function hook_dlopen(so_name) {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("[android_dlopen_ext -> enter", path);
if (path.indexOf(so_name) !== -1) {
this.match = true
}
}
},
onLeave: function (retval) {
if (this.match) {
console.log(so_name, "加載成功");
var base = Module.findBaseAddress("libDexHelper.so")
nopFunc(base.add(308204));
nopFunc(base.add(362896));
nopFunc(base.add(332536));
nopFunc(base.add(366304));
nopFunc(base.add(385348));
}
console.log("android_dlopen_ext -> leave")
}
});
}
hook_dlopen("libDexHelper.so")
結果如下:
發現我們 nop 掉線程後,並沒有報錯,那證明我們 nop 掉沒有問題,但是卡在了最開始的 libmsaoaidsec.so 文件裏,對於這個文件,我們同樣直接 nop 掉裏面的線程即可:
function hook_pth() {
var pth_create = Module.findExportByName("libc.so", "pthread_create");
console.log("[pth_create]", pth_create);
Interceptor.attach(pth_create, {
onEnter: function (args) {
var module = Process.findModuleByAddress(args[2]);
if (module != null) {
console.log("開啓線程-->", module.name, args[2].sub(module.base));
if (module.name.indexOf("libmsaoaidsec.so") != -1) {
Interceptor.replace(module.base.add(0x175f8), new NativeCallback(function () {
console.log("替換成功")
}, "void", ["void"]))
Interceptor.replace(module.base.add(0x16d30), new NativeCallback(function () {
console.log("替換成功")
}, "void", ["void"]))
}
}
},
onLeave: function (retval) {
}
});
}
最終也是成功繞過了檢測,結果如下:
至此,該 app 的 frida 檢測分析流程就結束了。
相關 hook 腳本,會分享到知識星球當中,需要的小夥伴自取,僅供學習交流。