分析对象:sub_1B924及其完整调用链
分析目标:还原代码逻辑、提取核心对抗算法、复现Shellcode、制定防御策略
分析深度:指令级/内核级
技术标签:Anti-Frida, Watchdog, Shellcode Injection, State Machine, ELF Parsing, Ptrace
说明:本文仅作为安全技术交流,如有侵权请联系删除。
1. 核心结论 (Executive Summary)
经过对提供的C伪代码进行逐行审计与静态还原,可以确认该模块是一个针对Frida框架的高级主动防御引擎。这套机制的巧妙之处在于,它没有采取传统的被动检测,而是布置了一个主动的“陷阱”。
- 核心机制:利用了Frida框架为了修复Android运行时(ART)的一个Bug而必须Hook
art::ArtMethod::PrettyMethod 函数的特性,部署了一个内存完整性监控陷阱。一旦这个函数被篡改,即触发防御。
- 执行架构:
- 主线程 (
sub_1B924):负责环境清洗、反模拟器检测,并根据不同的Android版本适配加载ART库的策略。
- 监控线程 (
sub_1C544):通过 pthread_create 启动的后台“看门狗”,在一个死循环中持续扫描文件系统、内存映射和核心函数代码的完整性。
- 处决引擎 (
sub_26334):一个高度混淆的状态机函数。一旦监控线程发现异常(如Hook),它便动态解密一段Shellcode并执行 exit_group(0),强制结束整个进程。
- 隐蔽性:全程无显式字符串(所有字符串均在栈上动态解密),无直接系统调用(通过Shellcode间接完成),且不会产生常规的崩溃(Crash)日志,使得追踪和调试变得极为困难。
2. 详细分析:入口与初始化 (sub_1B924)
这是整个防御逻辑的起点,代码通过一系列繁琐的操作来隐藏其真实目的。
2.1 字符串解密:栈上异或 (Stack String Obfuscation)
攻击者没有将关键字符串(如库名、函数名)存储在易于分析的 .rodata 段,而是将字符串拆分为十六进制数值硬编码在代码中,并在运行时于栈上进行动态解密还原,这是一种对抗静态分析的有效手段。

分析意义:这解释了后续v26这个函数指针的真实身份——它是用于创建监控线程的函数,而非Hook函数本身,这在一定程度上混淆了分析人员的视线。
2.2 动态加载 (Dynamic Loading)
result = dlopen(v20, 2); // 加载 libc.so/libart.so
if ( result ) {
v25 = dlsym(v22, v21); // 获取 pthread_create 地址
v26 = ...; // v26 保存 pthread_create 指针
}
- 目的:防止在ELF文件的
Import Table(导入表)中留下pthread_create的痕迹,从而对抗静态分析工具的交叉引用分析。
2.3 环境指纹检测 (Anti-Environment)
在启动核心防御逻辑之前,代码执行了严格的环境检查,以判断是否处于分析或模拟环境。

- 配置检查 (
sub_CAA8):检查一个全局变量 dword_48810 的值是否为 248 (0xF8)。这通常用于SDK版本适配或作为某个核心功能的开关。
- 硬件黑名单 (
sub_12D9C):
- 逻辑:读取系统属性
ro.product.model,检查其是否包含字符串 "Firefly-RK3399"。
- 对抗意图:Firefly开发板是安全研究人员常用的低成本ARM逆向平台。代码一旦检测到运行在此类特定设备上,便会直接跳过核心的监控和注入逻辑,导致分析人员无法在目标设备上复现其恶意行为,实现“装死”以逃避检测。
3. 核心部署:陷阱安装 (sub_1CEF8)
此函数负责定位具体的攻击目标(即art::ArtMethod::PrettyMethod函数),并启动“看门狗”监控线程。它是连接初始化模块与持续监控模块的桥梁。
3.1 绕过 Android 7.0+ 命名空间限制
Android 7.0 (SDK 24) 及以上版本引入了Linker Namespace机制,禁止应用程序直接使用 dlopen 加载系统私有库(如 libart.so)。


- 代码逻辑:
- 检查当前设备的SDK版本 (
*off_47FB8)。
- 若 SDK >= 24,则调用自定义函数
sub_18D54("libart.so")。
sub_18D54 原理:这是一个手动ELF加载器。它通过读取 /proc/self/maps 文件来定位libart.so在内存中的基址,然后手动解析ELF Header、Program Header和Dynamic Segment,从而在不通过系统Linker的情况下查找所需符号地址。这是一种绕过Android系统限制的经典手法。
3.2 锁定“诱饵” (Targeting PrettyMethod)

- 符号解密:函数内部再次使用异或解密技术,还原出一个C++修饰名(Mangled Name):
_ZN3art9ArtMethod12PrettyMethodEb。
- 符号含义:这对应ART运行时中的
art::ArtMethod::PrettyMethod(bool) 函数。
- 为什么选它?
- Frida的
frida-java-bridge 组件源码中包含一个函数 fixupArtQuickDeliverExceptionBug。
- 为了修复Native线程调用Java方法抛出异常时可能发生的崩溃,Frida必须Hook这个
PrettyMethod 函数。
- 结论:攻击者巧妙地利用了Frida框架的“刚需”,将
PrettyMethod 设置为一个无法回避的陷阱。任何试图使用Frida进行动态分析的尝试,都会自动触发这个陷阱。
3.3 启动监控线程
// a1: pthread_create (由上层传入)
// sub_1C544: 线程执行体 (看门狗)
// v20: PrettyMethod 的内存地址 (作为参数传递)
return a1(&thread_id, 0, sub_1C544, v20);
至此,监控线程被创建,并将PrettyMethod函数的地址作为参数传入,为后续的持续校验做好准备。
4. 终极监控:看门狗线程 (sub_1C544)
这是一个被标记为 __noreturn 的死循环函数,作为后台常驻的“哨兵”,负责全方位的环境安全扫描。

4.1 扫描逻辑详解

- 初始化:首先解密出一系列安全工具和框架的黑名单字符串,例如
"Xposed"、"Frida"、"Magisk" 等。
- 循环体 (
while(1)):
sub_1BFAC (挂载点检查):遍历 /proc/mounts 文件,查找Magisk Hide等Root隐藏工具可能留下的异常挂载痕迹(如 core/mirror、特定的tmpfs挂载)。
sub_1C158 (符号链接检查):检查 /system/bin 等关键目录下的文件是否被符号链接重定向,这是另一种常见的Root隐藏手段。
sub_1C26C (内存映射检查):读取 /proc/self/maps,这是进程的内存布局图。查找其中是否加载了 frida-agent.so、io.swag.xposed.bridge 等已知的动态分析模块。这属于典型的特征码检测。
sub_26334(a1) (核心完整性校验):这是最致命的一步。它接收传入的 PrettyMethod 函数地址 (a1),检查该地址处的函数头部指令是否被篡改(例如,是否被Inline Hook)。
- 休眠:调用
sleep(4),使线程休眠4秒,然后继续下一轮扫描,以此实现低功耗的持续监控。
5. 黑盒揭秘:处决引擎 (sub_26334)
这是整个防御体系中最坚固、最隐蔽的部分。它通过状态机混淆控制流,并最终通过动态生成与执行的Shellcode来完成“处决”。
5.1 状态机检测逻辑
函数内部维护一个状态变量 v4,其初始值为 293539132。整个函数的执行路径由这个状态值决定。
- 检测阶段:
- 读取
PrettyMethod 函数头部的前4字节机器码。
- 比对特征值:
1476395088 (十六进制:0x58000050)。
- 特征含义:
0x58000050 对应ARM64指令 LDR X16, #8。这正是许多Inline Hook框架(包括Frida的某些模式)中Trampoline(蹦床)代码的标准起手式——先将跳转目标地址加载到X16寄存器,然后通过 BR X16 跳转。
- 分支判定:
- 如果匹配到该特征(发现Hook),则将状态跳转至
887579370 (处决状态)。
- 如果未匹配,则跳转至安全退出路径。
5.2 Shellcode 静态还原实战 (Step-by-Step)
当状态机判定进入“处决状态”后,代码会动态解密并执行一段Shellcode。以下是其完整的静态还原过程。
步骤 A: 提取密钥


代码逻辑:v10 = *((_DWORD *)&qword_30794 + v8 + -3 * (v9 / 3) + 1);
- 基址:
0x30794
- 内存值:
0x99, 0xA7, 0xA9, ...
- 推导出的密钥序列:
[0xA7, 0xA9, 0x99] (循环使用)
步骤 B: 数据解密

源数据位于 xmmword_30760。代码首先将其首字节强制设置为 0x08,然后使用上述密钥进行循环异或解密。
| Index |
原始字节 |
Key |
运算 (XOR) |
结果 (Hex) |
| 0 |
0x08 |
- |
Set to 0x08 |
08 |
| 1 |
0xA7 |
0xA7 |
A7 ^ A7 |
00 |
| 2 |
0x29 |
0xA9 |
29 ^ A9 |
80 |
| 3 |
0x4B |
0x99 |
4B ^ 99 |
D2 |
| 4 |
0xA6 |
0xA7 |
A6 ^ A7 |
01 |
| 5 |
0xA9 |
0xA9 |
A9 ^ A9 |
00 |
| ... |
... |
... |
... |
... |
经过27轮循环后,得到解密后的原始字节流。
步骤 C: 最终修正 (The Final Trick)

代码执行:*(_DWORD *)v16 += 3008; (对前4字节进行整数加法)。
- 前4字节 (Little Endian):
0xD2800008
- 加数:
3008 (0xBC0)
- 运算:
0xD2800008 + 0x00000BC0 = 0xD2800BC8
- 修正后前4字节:
C8 0B 80 D2
步骤 D: 最终载荷 (The Payload)
将修正后的完整字节流解释为ARM64汇编指令:
| Hex (指令) |
汇编指令 |
含义 |
C8 0B 80 D2 |
MOV X8, #94 |
将系统调用号 94 放入X8寄存器。在ARM64 Linux中,94对应 __NR_exit_group。 |
01 00 00 D4 |
SVC #0 |
触发系统调用(陷入内核),执行 exit_group(0),强制结束整个进程组。 |
C0 03 5F D6 |
RET |
返回指令。由于上一条指令已导致进程退出,此处为不可达代码。 |
5.3 处决机制详解
- 行为:
mmap分配一块可读、可写、可执行(RWX)的内存 -> 将解密并修正后的Shellcode写入该内存 -> 调用 __clear_cache 清除CPU指令缓存以确保新代码生效 -> 跳转到该内存地址执行Shellcode。
- 效果:调用
exit_group(0)。
- 杀伤力:
exit_group() 比 exit() 更底层、更彻底。它会立即杀死当前进程组中的所有线程。由于没有抛出任何Java或Native异常,Frida等工具甚至来不及捕获进程分离(Detaching)事件,被保护的应用程序就会“瞬间”从系统中消失,不留痕迹。
6. 辅助防御体系 (sub_1B380)
如果根据环境检测(如2.3节),配置不允许启动主动Hook和监控(状态码非249),程序则会进入一套备用的静态防御模式。


- 进程伪装 (
sub_1B144):
- 读取
/proc/self/cmdline 获取原始进程名。
- 使用
prctl(PR_SET_NAME) 系统调用和可能的Java反射方法修改进程名。
- 目的:在
ps、top 等系统命令的进程列表中,将自己伪装成某个合法的系统进程(如com.android.phone),增加被分析人员发现和手动终止的难度。
- 双进程反调试 (
sub_1A0D8):
- 利用
fork() 系统调用创建一个子进程。
- 子进程对父进程执行
ptrace(PTRACE_ATTACH),抢占调试端口。
- 目的:这是一种经典的“父子进程互锁”反调试技术。当外部调试器(如IDA Pro、GDB)试图通过
ptrace 附加(Attach)到目标进程时,会发现该进程已被其自身的子进程调试,导致附加操作因 EPERM(权限不足)或 EBUSY(资源忙)而失败。
7. 总结与防御绕过建议 (Bypass)
7.1 针对此防御体系的Bypass方案
面对这种“主动监控 + 瞬时自毁”的复杂架构,最有效的绕过思路不是硬碰硬,而是使其监控机制失效。
- 方案A (推荐):沉默看门狗
- 操作:在
sub_1CEF8 函数调用 pthread_create 之前或之时进行拦截。
- 逻辑:Hook
pthread_create 函数,检查其传入的线程执行体地址。如果该地址匹配 sub_1C544(看门狗函数),则将其替换为一个空函数(NOP函数)或直接返回成功而不创建线程。
- 效果:“看门狗”线程根本不会被创建,或者创建后什么都不做,所有后续的扫描和自毁逻辑都不会被执行。
- 方案B (进阶):欺骗校验逻辑
- 操作:Hook内存读取相关函数或系统调用(如针对
sub_26334 中对 PrettyMethod 地址的读取操作)。
- 逻辑:当处决引擎尝试读取
PrettyMethod 函数头部的指令时,返回其原始的、未被Hook的指令字节流,而不是被Frida修改后的跳转指令。
- 效果:完整性校验永远返回“安全”状态,看门狗不会触发自毁流程。
- 方案C (硬核):阻断自杀指令
- 操作:在Shellcode执行路径上进行拦截。
- 逻辑:监控
mmap 调用(申请28字节大小、RWX权限是一个强特征),并在Shellcode写入后将其关键指令(如SVC #0)修改为NOP;或者直接Patch sub_26334 函数中的状态机跳转逻辑,使其永远无法进入“处决状态”。
- 效果:即便检测到Hook,自毁代码也无法成功执行。
希望这篇深入的逆向分析能帮助你理解现代Android应用保护中复杂的对抗技术。这类Android系统级别的攻防演练,对于安全研究人员深入理解底层机制至关重要。更多关于移动安全、ELF加载器原理及Hook技术的实践讨论,欢迎在云栈社区与大家交流。