找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2487

积分

0

好友

337

主题
发表于 4 小时前 | 查看: 3| 回复: 0

分析对象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 函数的特性,部署了一个内存完整性监控陷阱。一旦这个函数被篡改,即触发防御。
  • 执行架构
    1. 主线程 (sub_1B924):负责环境清洗、反模拟器检测,并根据不同的Android版本适配加载ART库的策略。
    2. 监控线程 (sub_1C544):通过 pthread_create 启动的后台“看门狗”,在一个死循环中持续扫描文件系统、内存映射和核心函数代码的完整性。
    3. 处决引擎 (sub_26334):一个高度混淆的状态机函数。一旦监控线程发现异常(如Hook),它便动态解密一段Shellcode并执行 exit_group(0),强制结束整个进程。
  • 隐蔽性:全程无显式字符串(所有字符串均在栈上动态解密),无直接系统调用(通过Shellcode间接完成),且不会产生常规的崩溃(Crash)日志,使得追踪和调试变得极为困难。

2. 详细分析:入口与初始化 (sub_1B924)

这是整个防御逻辑的起点,代码通过一系列繁琐的操作来隐藏其真实目的。

2.1 字符串解密:栈上异或 (Stack String Obfuscation)

攻击者没有将关键字符串(如库名、函数名)存储在易于分析的 .rodata 段,而是将字符串拆分为十六进制数值硬编码在代码中,并在运行时于栈上进行动态解密还原,这是一种对抗静态分析的有效手段。

C语言解密代码片段,展示了使用密钥循环异或还原字符串的过程

  • 代码定位
    v27 = 0xA700000099LL; // 密钥低位 0x99, 高位 0xA7
    v28 = 236;           // 密钥 0xEC
    // ...
    *(_QWORD *)v15 = 0xF69F89FA8ECEF5LL; // 密文数据
  • 解密算法
    • 密钥序列 (Key): 循环使用 [0x99, 0xA7, 0xEC]
    • 操作: Plaintext[i] = Ciphertext[i] ^ Key[i % 3]
  • 还原结果
    • v20 (库名): "libart.so" (或 libc.so,视上下文而定,此处用于获取线程函数)。
    • v21 (函数名): "pthread_create"

分析意义:这解释了后续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)。

展示不同Android版本下加载libart.so策略的代码
展示Android 7.0+使用自定义函数加载libart.so的代码

  • 代码逻辑
    1. 检查当前设备的SDK版本 (*off_47FB8)。
    2. 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)

代码片段显示解密出目标函数符号并准备Hook

  • 符号解密:函数内部再次使用异或解密技术,还原出一个C++修饰名(Mangled Name):_ZN3art9ArtMethod12PrettyMethodEb
  • 符号含义:这对应ART运行时中的 art::ArtMethod::PrettyMethod(bool) 函数。
  • 为什么选它?
    Frida源码截图,显示其必须Hook PrettyMethod函数以修复Bug
    1. Frida的 frida-java-bridge 组件源码中包含一个函数 fixupArtQuickDeliverExceptionBug
    2. 为了修复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))
    1. sub_1BFAC (挂载点检查):遍历 /proc/mounts 文件,查找Magisk Hide等Root隐藏工具可能留下的异常挂载痕迹(如 core/mirror、特定的tmpfs挂载)。
    2. sub_1C158 (符号链接检查):检查 /system/bin 等关键目录下的文件是否被符号链接重定向,这是另一种常见的Root隐藏手段。
    3. sub_1C26C (内存映射检查):读取 /proc/self/maps,这是进程的内存布局图。查找其中是否加载了 frida-agent.soio.swag.xposed.bridge 等已知的动态分析模块。这属于典型的特征码检测。
    4. sub_26334(a1) (核心完整性校验)这是最致命的一步。它接收传入的 PrettyMethod 函数地址 (a1),检查该地址处的函数头部指令是否被篡改(例如,是否被Inline Hook)。
    5. 休眠:调用 sleep(4),使线程休眠4秒,然后继续下一轮扫描,以此实现低功耗的持续监控。

5. 黑盒揭秘:处决引擎 (sub_26334)

这是整个防御体系中最坚固、最隐蔽的部分。它通过状态机混淆控制流,并最终通过动态生成与执行的Shellcode来完成“处决”。

5.1 状态机检测逻辑

函数内部维护一个状态变量 v4,其初始值为 293539132。整个函数的执行路径由这个状态值决定。

  • 检测阶段
    1. 读取 PrettyMethod 函数头部的前4字节机器码
    2. 比对特征值:1476395088 (十六进制:0x58000050)。
      • 特征含义0x58000050 对应ARM64指令 LDR X16, #8。这正是许多Inline Hook框架(包括Frida的某些模式)中Trampoline(蹦床)代码的标准起手式——先将跳转目标地址加载到X16寄存器,然后通过 BR X16 跳转。
    3. 分支判定
      • 如果匹配到该特征(发现Hook),则将状态跳转至 887579370 (处决状态)
      • 如果未匹配,则跳转至安全退出路径。

5.2 Shellcode 静态还原实战 (Step-by-Step)

当状态机判定进入“处决状态”后,代码会动态解密并执行一段Shellcode。以下是其完整的静态还原过程。

步骤 A: 提取密钥
内存数据截图,显示用于解密的密钥
另一处内存数据截图,显示加密的Shellcode数据

代码逻辑: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),程序则会进入一套备用的静态防御模式。

展示备用防御模式下的代码函数
展示进程伪装和反调试相关代码

  1. 进程伪装 (sub_1B144)
    • 读取 /proc/self/cmdline 获取原始进程名。
    • 使用 prctl(PR_SET_NAME) 系统调用和可能的Java反射方法修改进程名。
    • 目的:在 pstop 等系统命令的进程列表中,将自己伪装成某个合法的系统进程(如com.android.phone),增加被分析人员发现和手动终止的难度。
  2. 双进程反调试 (sub_1A0D8)
    • 利用 fork() 系统调用创建一个子进程。
    • 子进程对父进程执行 ptrace(PTRACE_ATTACH),抢占调试端口。
    • 目的:这是一种经典的“父子进程互锁”反调试技术。当外部调试器(如IDA Pro、GDB)试图通过 ptrace 附加(Attach)到目标进程时,会发现该进程已被其自身的子进程调试,导致附加操作因 EPERM(权限不足)或 EBUSY(资源忙)而失败。

7. 总结与防御绕过建议 (Bypass)

7.1 针对此防御体系的Bypass方案

面对这种“主动监控 + 瞬时自毁”的复杂架构,最有效的绕过思路不是硬碰硬,而是使其监控机制失效

  1. 方案A (推荐):沉默看门狗
    • 操作:在 sub_1CEF8 函数调用 pthread_create 之前或之时进行拦截。
    • 逻辑:Hook pthread_create 函数,检查其传入的线程执行体地址。如果该地址匹配 sub_1C544(看门狗函数),则将其替换为一个空函数(NOP函数)或直接返回成功而不创建线程。
    • 效果:“看门狗”线程根本不会被创建,或者创建后什么都不做,所有后续的扫描和自毁逻辑都不会被执行。
  2. 方案B (进阶):欺骗校验逻辑
    • 操作:Hook内存读取相关函数或系统调用(如针对 sub_26334 中对 PrettyMethod 地址的读取操作)。
    • 逻辑:当处决引擎尝试读取 PrettyMethod 函数头部的指令时,返回其原始的、未被Hook的指令字节流,而不是被Frida修改后的跳转指令。
    • 效果:完整性校验永远返回“安全”状态,看门狗不会触发自毁流程。
  3. 方案C (硬核):阻断自杀指令
    • 操作:在Shellcode执行路径上进行拦截。
    • 逻辑:监控 mmap 调用(申请28字节大小、RWX权限是一个强特征),并在Shellcode写入后将其关键指令(如SVC #0)修改为NOP;或者直接Patch sub_26334 函数中的状态机跳转逻辑,使其永远无法进入“处决状态”。
    • 效果:即便检测到Hook,自毁代码也无法成功执行。

希望这篇深入的逆向分析能帮助你理解现代Android应用保护中复杂的对抗技术。这类Android系统级别的攻防演练,对于安全研究人员深入理解底层机制至关重要。更多关于移动安全、ELF加载器原理及Hook技术的实践讨论,欢迎在云栈社区与大家交流。




上一篇:GEO与SEO的本质区别:从315曝光乱象看生成式引擎优化
下一篇:Cofense曝光新型SaaS钓鱼攻击:攻击者滥用LiveChat客服软件实施诈骗
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-18 10:46 , Processed in 0.682676 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表