最新版某货app bypassFrida检测
文章中所有内容仅供学习交流使用,不用于其他任何目的,抓包内容、敏感网址、数据接口均已做脱敏处理。严正声明禁止用于商业和非法用途,否则由此产生的一切后果与作者本人无关。
- 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(); // 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(libmsaoaidsec.base.add(libmsaoaidsec.size)) < 0) { console.log("libmsaoaidsec.so addr: " + pthreadAddr, "offset: ", pthreadAddr.sub(libmsaoaidsec.base)); } }, onLeave: function (retval) { } }) } }; // _system_property_get("ro.build.version.sdk", v1); 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(); // 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; } 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) { } }) } }; // _system_property_get("ro.build.version.sdk", v1); 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检测,可以愉快的逆向分析加密参数啦!
