前言
文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口均已做脱敏处理。严正声明禁止用于商业和非法用途,否则由此产生的一切后果与作者本人无关。
- appName:某货(
v8.65.0)
- 签名状态:
V1 + V2
- 加固状态:未加固(伪几维安全)
废话
本篇文章并不会对该app的各项参数进行逆向分析,只是记录下学习Frida工具时遇到的检测,以及绕过检测的过程。
简单说下使用到的工具:
- 手机:谷歌
Pxiel 6(安卓13系统)
- 系统环境:
Linux-kali
- Frida:
16.5.9版本
- Python:
3.11.14版本
- IDA Pro:
9.3版本
正题
- 开启手机,连接到kali虚拟机,打开我们要测试的
app程序。正常的流程我们可以先抓包,找到要逆向分析的参数,然后使用 Jadx或者 JEB去反编译 app程序,在 JAVA层先进行追踪需要分析的参数,不过这不是本篇文章的重点,本篇文章目前只研究如果绕过 app的 Frida检测。ok那么我们直接使用 Frida对 app进行 Hook。
- 俺习惯使用微软大战代码,来进行
hook,如图:

- 使用
adb shell进入设备的 shell环境。

- 启动好
Frida-server后,我们对 app进行 Hook。

- 这里可以看到
Frida给我们的报错。进程直接被中止了。(这里我用的是 -f Spawn的方式进行的注入,其实用 Attach也是一样的结果,有兴趣的小伙伴可以自行尝试。)
- 遇到这种情况,一般就是有
Frida检测。(Frida的检测点,网上文章巨多,我这里就不在赘述了。)
- 最容易过的就是检测
Frida服务端的进程命是否有 frida字样,如果存在则直接关闭 app。其二是端口检测,Frida的默认通信端口是 27042,有些 app会检测是否开启该端口,如果开启则直接关闭 app。
- 第一点,我们在给手机传入
Frida-server时,就直接修改了名字,当前案例 app未检测端口,所以我就没有修改端口号。
- 其他的检测点,我们后边遇到再说。
Frida的检测一般是 Native配合 Java层,所以我们可以 Hook dlopen函数来看,是加载到哪个 so文件,app被强制关闭的,Frida检测代码,一般就在最后加载的 so文件中。

- 可以看到最后加载的
so文件是 libmsaoaidsec.so文件,我们把该文件拖到桌面上,稍后会用到。
- 这时候基本可以确认,检测代码是在这个文件中,可是我们后边该怎么做呢?我们该报着什么思路去做呢?
- 有个前置知识点,
so文件的加载流程,网上有很多的大佬文章,我也是个小白,这里简单提几句,详细的内容可以看看其他大佬写的。从 System.loadLibrary开始,加载 so文件,从 Java层到底层 linker(链接器),核心就是将二进制文件映射到内存、解析符合,并初始化代码。而在 linker链接器时,有 DT_INIT和 DT_INIT_ARRAY俩个函数,用来初始化。他们的执行逻辑是:INIT --> INIT_ARRAY --> JNI_OnLoad --> dlopen。
- 小白可以这么浅的理解,实际情况是有所差距的,正是调用了
dlopen函数,才会执行前面的一系列初始化操作。上面说的执行逻辑,可以理解成 hook时机的先后。邪修可以先按我这个逻辑死记硬背。
- 说完这个前置知识,我们回到正题,确定了检测
so文件后,我们来 hook pthread_create 看看这个 so文件创建了那些线程。为什么要看创建的线程呢?很简单的道理,进程是有自己的工作的,它们负责保证 app正常运行,提供正常服务,而创建出来的线程就负责检测,有没有 Frida注入进来干坏事。就像公司之间分工明确,老板负责赚钱,牛马负责给老板赚钱一样。牛马就是线程,老板就是进程。
function hook_dlopen(so_name) {
let dlopen = Module.findExportByName(null, so_name);
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
let pathPtr = args[0];
if (pathPtr != undefined && pathPtr != null) {
let path = pathPtr.readCString();
if (path.indexOf("libmsaoaidsec.so") >= 0) {
console.log("load so::> ", path)
this.hookTarget = true;
hook_pthread_creat()
}
}
},
onLeave: function (retval) {
if (this.hookTarget) {
console.log("over!!")
}
}
})
}
};
function hook_pthread_creat() {
let pthread_create = Module.findExportByName("libc.so", "pthread_create");
let libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
if (!libmsaoaidsec) {
console.log("libmsaoaidsec.so is not found!!")
return;
}
if (pthread_create) {
Interceptor.attach(pthread_create, {
onEnter: function (args) {
let pthreadAddr = args[2];
if (pthreadAddr.compare(libmsaoaidsec.base) >= 0 && pthreadAddr.compare(pthreadAddr.add(libmsaoaidsec.size)) <= 0) {
console.log("libmsaoaidsec.so addr: " + pthreadAddr, "offset: ", pthreadAddr.sub(libmsaoaidsec.base))
}
},
onLeave: function (retval) { }
})
}
};
function main() {
hook_dlopen("android_dlopen_ext")
};
setImmediate(main)
- 再次执行这个代码,如图:

- 这里报错文件未找到,说明没
Hook到。为什么?这就用到了前置知识的内容。
- 就是因为此时的Hook时机不对,虽然
Hook到了该 so文件的路径,其实这个时候刚刚加载到 so文件,还没有把符号表映射到内存,所以我们 hook显示找不到,那就要寻找其他的 hook时机才行,就是更早的 init阶段。
- 这时候我们使用
ida反编译 libmsaoaidsec.so文件。

- 这里我们站在巨人的肩膀,在字符串窗口直接搜索
“sdk”,这个方法是用来抓取手机的 sdk版本的。方法内部如图:

- 看下图,这里可以看到,这个方法实际在
init_proc阶段有被调用过,所以也符合我们的 hook时机。

- 接下来修改我们的
hook代码,重新再试一下。
var hookPtrheadCreat = false;
function hook_dlopen(so_name) {
let dlopen = Module.findExportByName(null, so_name);
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
let pathPtr = args[0];
if (pathPtr != undefined && pathPtr != null) {
let path = pathPtr.readCString();
if (path.indexOf("libmsaoaidsec.so") >= 0) {
console.log("load so::> ", path)
this.hookTarget = true;
hook_system_property_get();
}
}
},
onLeave: function (retval) {
if (this.hookTarget) {
console.log("over!!")
}
}
})
}
};
function hook_pthread_creat() {
let pthread_create = Module.findExportByName("libc.so", "pthread_create");
let libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
if (!libmsaoaidsec) {
console.log("libmsaoaidsec.so is not found!!")
return;
}
if (pthread_create) {
Interceptor.attach(pthread_create, {
onEnter: function (args) {
let pthreadAddr = args[2];
if (pthreadAddr.compare(libmsaoaidsec.base) >= 0 && pthreadAddr.compare(libmsaoaidsec.base.add(libmsaoaidsec.size)) < 0) {
console.log("libmsaoaidsec.so addr: " + pthreadAddr, "offset: ", pthreadAddr.sub(libmsaoaidsec.base));
}
},
onLeave: function (retval) { }
})
}
};
function hook_system_property_get() {
let _system_property_get = Module.findExportByName("libc.so", "__system_property_get");
if (_system_property_get) {
Interceptor.attach(_system_property_get, {
onEnter: function (args) {
let argName = args[0].readCString();
if (argName === "ro.build.version.sdk") {
let callerAddr = this.returnAddress;
let module = Process.findModuleByAddress(callerAddr);
if (module && module.name.indexOf("libmsaoaidsec.so") >= 0) {
if (!hookPtrheadCreat) {
hook_pthread_creat();
hookPtrheadCreat = true;
}
}
}
},
onLeave: function (retval) { }
})
}
};
function main() {
hook_dlopen("android_dlopen_ext")
};
setImmediate(main)

- 这个
offset就是检测函数的偏移,偏移地址一般是不会变的,so文件加载进内存的地址是不固定的,所以我们每次都需要重新获取,获取到 so文件的基址加上这个偏移地址,就是检测函数的位置。
- 可以用
ida来简单看下检测函数。(0x1c544)

- 这是其中的一个检测位置,
fd检测,通俗的讲,就是检查 frida进程的句柄数量与进程的软链接指向。再看(0x1b8d4)

- 这个是在检测线程
ID相关的内容,检测线程 ID句柄中有没有与 Frida相关的。比如:frida、gmain、gum-js-loop、pool-frida等。再看最后一个(0x26e5c)

- 读取
proc/self/maps进行检测,如果不是魔改的 frida,依旧会在该目录中留下敏感的字眼,比如:frida、gum-js-loop等。
- 那么这些监测点该怎么绕过呢?可以针对每一处检测位置做
hook,给他伪装成无 frida痕迹的样子。也可以直接把这三个偏移函数置空。
- 建议是优先置空操作,如果置空后
app无法正常运行,那么就针对每一处监测点做 hook。本篇文章直接做了置空,已验证,可以绕过。
var hookPtrheadCreat = false;
function hook_dlopen(so_name) {
let dlopen = Module.findExportByName(null, so_name);
if (dlopen) {
Interceptor.attach(dlopen, {
onEnter: function (args) {
let pathPtr = args[0];
if (pathPtr != undefined && pathPtr != null) {
let path = pathPtr.readCString();
if (path.indexOf("libmsaoaidsec.so") >= 0) {
console.log("load so::> ", path)
this.hookTarget = true;
hook_system_property_get();
}
}
},
onLeave: function (retval) {
if (this.hookTarget) {
console.log("over!!")
}
}
})
}
};
function hook_pthread_creat() {
let pthread_create = Module.findExportByName("libc.so", "pthread_create");
let libmsaoaidsec = Process.findModuleByName("libmsaoaidsec.so");
if (!libmsaoaidsec) {
console.log("libmsaoaidsec.so is not found!!")
return;
}
let base = libmsaoaidsec.base;
let size = libmsaoaidsec.size;
if (pthread_create) {
Interceptor.attach(pthread_create, {
onEnter: function (args) {
let pthreadAddr = args[2];
let offset = pthreadAddr.sub(base);
if (pthreadAddr.compare(base) >= 0 && pthreadAddr.compare(base.add(size)) < 0) {
console.log("libmsaoaidsec.so addr: " + pthreadAddr, "offset: ", offset);
replace_func(pthreadAddr, offset);
}
},
onLeave: function (retval) { }
})
}
};
function hook_system_property_get() {
let _system_property_get = Module.findExportByName("libc.so", "__system_property_get");
if (_system_property_get) {
Interceptor.attach(_system_property_get, {
onEnter: function (args) {
let argName = args[0].readCString();
if (argName === "ro.build.version.sdk") {
let callerAddr = this.returnAddress;
let module = Process.findModuleByAddress(callerAddr);
if (module && module.name.indexOf("libmsaoaidsec.so") >= 0) {
if (!hookPtrheadCreat) {
hook_pthread_creat();
hookPtrheadCreat = true;
}
}
}
},
onLeave: function (retval) { }
})
}
};
function replace_func(base, offset) {
let module = Process.findModuleByAddress(base);
if (module) {
Interceptor.replace(module.base.add(offset), bypass())
}
};
function bypass() {
return new NativeCallback(function () {
console.log("replace is ok!!!")
}, 'void', [])
};
function main() {
hook_dlopen("android_dlopen_ext")
};
setImmediate(main)
- 这样就可以绕过
Frida检测,可以愉快的逆向分析加密参数啦!
总结
libmsaoaidsec.so文件的 Frida使用这个代码,几乎可以通杀,经测试某 BLBL也可以成功绕过。
- 像我一样的小白一定要死记硬背:
INIT --> INIT_ARRAY --> JNI_OnLoad --> dlopen。
- 本篇内容没有对每一处监测点进行绕过,感觉没什么必要(太懒了)。
- 还得继续学习,安卓要学习的知识点太多了,一个
Frida学习了好久。55555