本文基于 xiaoeeyu 的文章 做了一次复现,主要记录我自己的分析过程和实践结果。
0x00 环境
- 设备: Xiaomi MI 8(Android 10,已 Root)
- 工具: Frida 17.3.2
- App 版本: v5.12.0
0x01 查壳与威胁建模
使用 ApkCheckPack 对目标 APK 进行全面扫描,结果如下:

|
|
分析结论:
- 加固厂商:确认为 梆梆安全 (BangBang) 企业定制版。核心逻辑在
libDexHelper.so中。 - 防御机制:
- Root 检测:覆盖了文件路径、包名和属性检测。这也是需要 Shamiko 的主要原因。
- 环境检测:包含模拟器和反调试检测。
- 第三方 SDK:集成了腾讯云、阿里实人认证等 SDK,这些 SDK 本身也可能带有一定的安全校验,增加逆向干扰。
0x02 初步分析
打开应用后,很明显出现了 Root 检测提示。

采用隐藏 Magisk + Shamiko 的方式来绕过 Root 检测:隐藏 Magisk,刷入 Shamiko 模块,并在重启后配置排除列表。

处理完成后,打开 App,Root 检测提示消失。
但我并没有遇到原文作者提到的“frida-server 运行即闪退”情况,不管点击注册还是登录都未触发闪退。

并且我使用的确实是默认端口。

考虑到可能是 App 版本差异,我没有继续深究这个现象,先尝试简单 Hook 检测逻辑。

可以看到它会检测 Frida 的核心组件(frida-agent)并阻止注入。
0x03 Hook pthread_create
Hook 思路和原理分析
pthread_create 是 Android/Linux 开发中最常用的创建线程的 API。在加固或者反调试的场景中,壳通常会创建一些可以在后台偷偷运行的线程,即“检测线程”。
1 2 3 4int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
thread: 返回创建的线程 ID。attr: 线程属性(可以为 NULL)。start_routine: 线程执行函数。arg: 传入线程函数的参数。
这些线程的任务通常包括:
- 轮询文件系统:检查
/proc/self/maps是否有 Frida 相关 so,或者/sys/class/下是否有痕迹。 - 端口扫描:检查本地 27042 (frida-server 默认端口)。
- 完整性校验:检查特定函数(如
open,read)的头部字节是否被修改(Inline Hook 检测)。
因此,逆向分析的第一步通常是先记录线程入口地址。如果某个线程入口位于壳的 SO(如 libDexHelper.so)范围内,这个线程就值得重点关注。
第一次尝试
根据上述思路,我们首先尝试 Hook libc.so 的 pthread_create 函数,以此来定位检测线程的入口。
|
|

结果:Process terminated
App 启动即崩溃 (Crash)。
企业级加固壳(如梆梆)常会检查
pthread_create前几个字节是否被改写。如果检测到 Inline Hook,通常会触发崩溃或退出。因此,直接 Hook 这类敏感 libc 函数风险较高。
0x04 Hook clone
既然 pthread_create 被检测,那我们向下深入一层。在 Linux/Android 中,线程创建最终是通过 clone 系统调用实现的。
pthread_create的调用流程
1 2 3 4 5 6 7 8 9 10 11pthread_create() ↓ 分配线程控制块(TCB)、栈空间等 ↓ 设置调度策略/属性(可选) ↓ 调用 clone() ↓ 内核创建 task_struct(共享 mm、fs、files、sighand 等) ↓ 新线程执行 start_routine(arg)
策略:Hook libc.so 中的 clone 函数。
过程:
-
定位调用栈:首先 Hook
clone并打印堆栈,发现clone是被libc.so中的pthread_create调用的。 -
静态分析
libc:将手机中的/system/lib64/libc.so拉取出来,使用 IDA 反编译pthread_create函数。 -
寻找赋值点: 在
pthread_create的伪代码中,我们可以看到在调用clone之前,有一行关键的代码:1 2 3 4*(_QWORD *)(v28 + 96) = v51; *(_QWORD *)(v28 + 104) = v52; *(_DWORD *)(v28 + 20) = getpid(inited); if ( (unsigned int)clone(__pthread_start, v17, 4001536, v28, v28 + 16, v21 + 8, v28 + 16) == -1 )结论:
pthread_create会把线程入口函数写入结构体的 +96 偏移,并把该结构体指针传给clone。因此在 Hookclone时,读取args[3]指针的+96偏移,即可拿到真实线程入口。
注意:
在第一次反编译手机的
libc.so时,我看到偏移量是 48 (0x30) 而不是 96。 这是因为我分析的是 32位 (ARM) 的库 (/system/lib/libc.so)。 而目标 App 运行在 64位 (ARM64) 环境下,所以需要分析/system/lib64/libc.so,才能得到对应的 96 (0x60) 偏移。
验证脚本 (trace_clone.js):
为了增强脚本的兼容性(应对部分 Frida 版本在 Spawn 早期 Module.findExportByName 找不到符号的问题),我们在实际脚本中增加了一个 get_export 的封装。
|
|
结果: 成功输出了检测线程的偏移地址,随后 App 依然崩溃(因为我们只是观测,没阻止检测)。
|
|
我们捕获到了以下关键偏移 (基于 libDexHelper.so):
0x548140x513040x5C0340x5C56C0x628C8
0x05 Hook dlopen
我们需要在 libDexHelper.so 加载后、检测线程启动前尽快 Hook android_dlopen_ext 并完成 Patch。
- Patch 策略:将上述 5 个偏移处的函数代码,修改为直接返回 (RET)。
- 指令选择:
RET(Return):0xC0 03 5F D6(ARM64)
完整脚本 (hook_dlopen.js)
|
|
效果验证
运行命令:frida -U -f com.bybit.app -l hook_dlopen.js
终端输出(成功示例):

App 成功进入主界面,无闪退,Frida 连接稳定。
0x06 复盘总结
6.1 查壳
- 特征识别: 使用
ApkCheckPack这类工具可以快速确认加固特征。对梆梆企业壳来说,libDexHelper.so是比较明确的线索。 - Root 检测对抗: 传统的
su改名方式效果有限。实际测试里,Magisk + Shamiko这类按应用隔离 Root 痕迹的方案更稳定。
6.2 从 pthread_create 到 clone
-
API 调用链: Android 中的线程创建并非一步到位的,而是一个层层递进的过程:
Java Thread->Native pthread_create(NDK/Libc) ->clonesyscall (Kernel Interface) ->Kernel。 -
为什么 Hook
pthread_create失败? 壳在初始化阶段可能会校验pthread_create的指令头,或监控该函数是否被 Hook。直接附加到这个点位,容易触发崩溃。 -
为什么
clone有效?clone位于更底层,直接与内核交互。很多壳对它的防护没有上层 API 那么激进,因此是一个可行的观测点。 -
源码分析: 通过反编译手机中的
libc.so(64位),找到了pthread_create调用clone的部分:1 2 3 4 5 6 7 8 9 10 11 12 13 14// 伪代码片段 (IDA 64-bit libc.so) // v51: start_routine (用户线程函数) // v52: user_arg (用户参数) // v28: pthread_internal_t* (线程控制块指针) *(_QWORD *)(v28 + 96) = v51; // [关键] 将真实线程入口存入 +96 *(_QWORD *)(v28 + 104) = v52; // [关键] 将参数存入 +104 *(_DWORD *)(v28 + 20) = getpid(inited); // 调用 clone // 参数1: __pthread_start (系统包装函数) // 参数3: flags // 参数4: v28 (arg,即它的 pthread_internal_t 指针) if ( (unsigned int)clone(__pthread_start, v17, 4001536, v28, v28 + 16, v21 + 8, v28 + 16) == -1 )两个关键发现:
- Wrapper 机制:
clone的第一个参数不是业务线程函数,而是系统的__pthread_start,用于初始化线程环境(TLS、Stack Guard 等)。 - Arg 传递:
clone的第4个参数 (v28/arg) 实际上是一个指向pthread_internal_t的指针。系统把start_routine藏在了这个结构体的 +96 偏移处。
- Wrapper 机制:
6.3 动态库加载
android_dlopen_extvsdlopen: 在 Android 高版本中,系统加载 SO 往往直接通过android_dlopen_ext。Hook 这个函数可以让我们拦截到系统加载任何 SO 的动作。- 执行顺序:
- 检测到
libDexHelper.so正在加载。 - 此时 SO 的基址 (
base) 已经确定。 - 在
onLeave阶段通常可以拿到稳定基址并尽快完成 Patch(是否早于具体检测逻辑,取决于壳的实现时机)。 - 通过
Memory.patchCode修改目标函数入口,减少后续检测线程执行风险。
- 检测到
6.4 内存修补 (Patching)
- NOP (
0x1F2003D5):空指令,CPU 会继续执行下一条。如果后续仍有检测调用,NOP 本身不能阻断流程。 - RET (
0xC0035FD6):让函数立即返回。用于这种场景时,通常比 NOP 更直接。
(引用:本文思路参考 xiaoeeyu - 绕过某邦企业壳root、frida检测)