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

1622

积分

0

好友

232

主题
发表于 7 天前 | 查看: 26| 回复: 0

为保证结论稳健且可迁移,文中所有实验均在以下环境中完成:

  • LineageOS 21 / Nexus 5X
  • Magisk 29.0.0,LSPosed
  • Zygisk Frida Gadget开源模块(sucsand)
  • Frida Server 16.5.2

合规声明:本文仅用于安全研究与对抗评估,旨在帮助甲方团队识别自身加固薄弱点、完善自检与回归策略;不针对具体业务落地攻击,不提供可直接用于对第三方应用的利用脚本。

本文将系统性地拆解一个使用DexProtector加固的应用如何检测并阻止Frida,并分享一套完整的绕过思路与实操步骤。你将学习到以下核心技能:

  1. 入口卡位:不死盯 System.loadLibrary,而是改从 __loader_android_dlopen_ext 抓“真实装载面”,提早拿到证据和时序。
  2. 匿名段定位与转储:通过 JNI_OnLoad → 函数指针 → 匿名可执行段 这条线索,结合 /proc/<pid>/maps 定位内存段,并用Frida直接dump。
  3. 最小化修复与类型库引入:在IDA中手动补充区段、引入 android_arm64 / gnulnx_arm64 类型库,理顺JNI动态注册链条。
  4. 校验链路拆解:识别 xxHash / SHA256 / HMAC 等算法的落点,采用“等式化替换 + 调用点定位”策略进行最小侵入的绕过。
  5. 二分法定位:从“可卸载点”开始逐段排除,将“必崩区间”缩小到少量函数,再精确打补丁。
  6. 入口完整性绕过:遇到对“当前段基址”的校验,复制一份干净的text段,将参数基址替换为干净副本来绕过检测。
  7. 线程面处理:顺着 /proc/self/maps 的反向引用追溯到 pthread_create,定位并禁用监控线程。

在整个过程中,保持工程化习惯:每一步都预留“可回头验证”的观测点,避免“一刀切”,以降低误伤和回归压力。

1. 分析过程

1.1 样本获取与安装

  • 样本:Hyatt 6.8.0.apkm
  • 目标:安装相同版本,确保App可安装、可启动,并能完整复现检测与崩溃过程。
  • 安装方法:通过 APKMirror Installer 或 MT 管理器安装即可。

1.2 设备与运行环境

该基线用于复现与对比。不同 SoC / API level / ART 实现可能导致“加载顺序、符号可见性、maps 标记”等差异。

  • 设备:Nexus 5X(刷入LineageOS 21)。
  • Root环境:Magisk 29.0.0、LSPosed、Zygisk Frida Gadget 模块(sucsand)。
  • Frida:frida-server 16.5.2。
  • PC环境:使用集成了逆向常用工具的r0env Kali虚拟机,以简化环境配置。

运行现象

  • 不运行 frida-server 时,应用可进入主页,但会提示需要升级。
  • 一旦尝试使用Frida进行 attachspawn,目标进程会迅速崩溃。
  • App本身不运行Frida时不会崩溃,说明壳可能没有有效的Root检测,或未检测到Magisk/LSPosed。

应用正常运行提示升级
Frida附加导致崩溃

2. Java层入口识别

使用JADX对APK进行分析,通过 AndroidManifest.xmlApplication 类定位主壳入口与native库加载点。

2.1 AndroidManifest.xml分析

  1. 定位 application 节点,其 android:name 属性即为主壳入口点:com.Hyatt.hyt.ProtectedTopHyattApplication
  2. 查找带有 LAUNCHER 类别的 Activity,确认入口Activity为 SplashActivity

2.2 Application类分析

查看 ProtectedTopHyattApplication 类,在 attachBaseContext 等方法中发现了加载 so 库的操作(如 System.loadLibrary(“dpboot”))以及大量 native 方法声明。这表明Java层主要起引导作用,核心逻辑已下沉到native层。

3. Native层入口点定位

如果直接尝试Hook System.loadLibrary 函数,会导致进程崩溃。因此需要寻找更早的时机。更底层的加载函数是linker中的 __loader_android_dlopen_ext,这是一个全局符号,可以直接获取。

3.1 Hook dlopen 定位加载时机

  1. 启动 frida-server 并指定非默认端口(如14725),以规避部分端口检测。
  2. 执行Frida脚本Hook __loader_android_dlopen_ext
function hook_dlopen() {
    var android_dlopen_ext = Module.findExportByName(null, “__loader_android_dlopen_ext“)
    Interceptor.attach(android_dlopen_ext, {
        onEnter: function (args) {
            var pathptr = args[0];
            console.log(”path is => “, pathptr.readCString())
        },
        onLeave: function () {
            console.log(”结束“)
        }
    })
}
hook_dlopen()

分析日志:日志显示依次加载了 libalice.solibdpboot.solibdexprotector.so。崩溃发生在这些库“加载完成之后”,因此将入口库聚焦于 libdexprotector.so。检测逻辑很可能不在 .init_array,后续应重点分析 JNI_OnLoad 或动态注册链。

3.2 尝试Hook JNI_OnLoad

Hook libdexprotector.soJNI_OnLoad 函数,发现它能顺利执行并返回,但进程依然崩溃。这说明检测可能发生在 JNI_OnLoad 注册的JNI函数被外部调用时。

var libdexprotector = Process.findModuleByName(“libdexprotector.so“)
Interceptor.attach(libdexprotector.findExportByName(”JNI_OnLoad“), {
    onEnter: function (args) { console.log(”JNI_OnLoad onEnter”) },
    onLeave: function(ret){ console.log(”JNI_OnLoad 结束”) }
})

4. 定位检测函数至匿名内存段

检测函数可能位于其他native函数中。使用IDA分析 libdexprotector.soJNI_OnLoad,发现它只是将 JavaVM* 指针传递给了一个函数指针 off_C838

4.1 追踪函数指针

使用Frida读取 off_C838 指针的值,并检查其所属模块。

// ... 在 dlopen 的 onLeave 中判断 libdexprotector.so 加载后
var libdexprotector = Process.findModuleByName(“libdexprotector.so“)
console.log(”off_C838 is => “, libdexprotector.base.add(0xC838).readPointer())
console.log(”off_C838 module is => “, Process.findModuleByAddress(libdexprotector.base.add(0xC838).readPointer()))

结果:指针地址不属于任何已知模块(返回 null)。进一步查询该地址所在的内存段:

console.log(“off_C838 mem is => ”, JSON.stringify(Process.findRangeByAddress(libdexprotector.base.add(0xC838).readPointer())))
// 输出示例:{"base":"0x7e25afc000","size":507904,"protection":"r-x"}

通过 cat /proc/<pid>/maps 交叉验证,确认该地址位于一段 [anon:...] 匿名可执行内存映射中。这表明DexProtector将关键逻辑放入了运行时生成的匿名映射段,分析必须转向内存态dump与反汇编。

4.2 Dump匿名内存段

获取内存段基址、大小和进程PID后,使用Frida脚本将该段内存dump到本地文件(例如 libanon.so),以供后续静态分析。

结论:分析需基于内存dump,进行单段反汇编。

5. IDA手工修复匿名内存SO

  1. 加载dump文件:将dump下来的 libanon.so 拖入IDA,由于没有ELF头,需手动选择处理器类型为 ARM Little-endian [ARM]
  2. 导入类型库:视图 → 打开子视图 → 类型库,右键加载 android_arm64gnulnx_arm64 类型库,以识别JNIEnv、jclass等类型,以及 RegisterNativesFindClass 等API。
  3. 计算真实函数偏移:通过 函数指针地址 - 内存段基址 得到 off_C838 对应函数在dump文件中的偏移(例如 0x4E984),在IDA中跳转至此进行分析。
  4. 修复无效内存访问:反编译时可能遇到访问 [0x8A800] 等红字错误,这是因为dump文件中没有数据段。在IDA的“区段”视图中,手动添加区段(如名为 rodata),覆盖这些地址范围,即可消除错误。

6. JNIEnv恢复与动态注册链梳理

sub_4E984 的参数类型修正为 JavaVM* 后,代码可读性大幅提升。分析发现其调用了 sub_4EAA0sub_4EBE0sub_4EFC4 等函数。

重点跟进 sub_4EAA0,将其参数类型修正为 JNIEnv* 后,发现了明显的动态注册逻辑(RegisterNatives)。注册的函数列表中包含 sub_4F0F4(像是初始化函数)和 sub_4F254(一个非常冗长、包含复杂业务逻辑的函数),后者成为后续分析的重点。

7. 关键函数 sub_4F254 的初步探测

直接Hook sub_4F254,观察其返回值,并尝试修改返回值以测试是否能够绕过检测。

Interceptor.attach(libanon.base.add(0x4F254),{
    onEnter: function(){ console.log(“onEnter 0x4F254 ”) },
    onLeave: function(retval){
        console.log(“onLeave 0x4F254  “, retval.toInt32())
        // 尝试修改返回值: retval.replace(0)
    }
})

修改返回值为0后,App依然崩溃,说明检测逻辑嵌套在该函数内部的校验路径中,必须深入分析其内部的完整性或哈希校验分支。

8. 定位哈希校验算法(xxHash与HMAC-SHA256)

分析 sub_4F254,发现关键校验代码:

if ( v34 == sub_161E8(unk_82948, unk_82990 - unk_82948, &v75) )
    sub_14D24(byte_872AC, 64, v32, 32, v33);
  • sub_161E8:通过分析其指令特征(如大量 vaddq_s64veorq_s8 等NEON指令)和询问GPT,判断其为类似 xxHash 的非加密哈希算法,用于CRC校验。
  • sub_14D24:其内部调用了 sub_304A8sub_30B14,这两个函数中包含ARMv8内联的SHA256指令(如 SHA256SU0),可以判定 sub_14D24 是HMAC-SHA256的入口。

9. 等式化替换策略绕过CRC校验

目标是让 sub_161E8 的返回值与预期的内存值相等,从而“通过”校验。首先需要找到所有调用 sub_161E8 进行校验的位置。

Interceptor.attach(libanon.add(0x161E8),{
    onEnter:function(args){
        this.lr = this.context.lr.sub(libanon) // 保存调用地址偏移
    },
    onLeave:function(ret){
        // 定义不同调用点需要相等的目标内存地址
        var hash_crc = {“0x4f5a4”: 0x8A810, “0x4f6cc”: 0x8A810, “0x5c494”: 0x8AB20}
        var targetAddr = hash_crc[this.lr.toString()];
        if(targetAddr){
            ret.replace(libanon.add(targetAddr).readU64()); // 替换返回值
        }
    }
})

注意lr 寄存器(存有返回地址)需在 onEnter 阶段保存,因为 onLeave 时其可能已被Hook框架修改。

此策略成功让程序走进了 sub_14D24(HMAC-SHA256)的逻辑,但程序仍然崩溃,说明后续还有校验或副通道检测。

10. 字符串解密与 /proc/self/maps 检测点

在分析其他函数(如 sub_50130)时,发现了字符串解密函数(sub_55650, sub_40814)以及对 /proc/self/maps 的读取操作。Hook字符串函数后,日志显示应用确实在检测maps。

通过调用栈回溯(利用 lr),定位到读取maps的检测函数 sub_61974。Hook发现其返回值为786(表示异常),尝试修改返回值为0后,崩溃日志变多,说明maps检测只是整个检测体系的一部分。

11. 二分排除法缩小“必崩区间”

为了精确打击,采用“二分排除法”定位导致崩溃的核心代码区间。

  1. 切换至Zygisk Frida Gadget:使用sucsand模块,以绕过部分基于ptrace的检测,并为二分法创造条件。
  2. 逐步卸载Hook:从入口函数 sub_4F254 开始,逐步卸载后续Hook的代码块,观察App是否崩溃。
    • 卸载 sub_4F254 后不崩溃? → 崩溃点在它之后。
    • 卸载 sub_593FC 后不崩溃? → 崩溃点在 sub_4F254sub_593FC 之间。
    • 依次在中间点测试,如卸载 sub_3EA0C 后不崩溃,但卸载 sub_15128 后崩溃,则“必崩区间”被缩小到 sub_3EA0Csub_15128 之间。
      通过此方法,可以将问题范围从数百个函数急剧缩小到少数几个,极大提升分析效率。

12. 绕过入口完整性校验(基址检测)

在对缩小的区间进行分析时,发现 sub_304A8(SHA256相关函数)的入口参数中包含了待校验内存的基址,并且该校验可能针对当前匿名段的基址。

绕过方案:在匿名段加载后,立即将其text段内容拷贝一份到新分配的“干净”内存中。当检测函数校验基址时,将参数中的基址替换为这份干净副本的地址。

var origin = Memory.alloc(size);
origin.writeByteArray(libanon.base.readByteArray(size));

Interceptor.attach(libanon.base.add(0x304A8), {
    onEnter: function (args) {
        if (args[1].toString() === libanon.base.toString()) { // 如果检测当前段
            args[1] = origin; // 替换为干净副本基址
        }
    }
});

实施此操作后,应用成功绕过检测,进入主界面。但日志显示仍有大量线程在刷maps检测信息。

13. 定位并禁用监控线程

根据之前发现的 /proc/self/maps 调用栈,反向追踪到线程创建函数 sub_7A230(类似 pthread_create)。

  1. 在IDA中通过交叉引用(X键)查找所有调用 sub_7A230 的地方,获取其传入的线程入口函数地址(如 0x5A708, 0x5BE28 等)。
  2. 在运行时,使用Frida向这些线程入口地址写入 RET 指令,使新创建的线程立即返回,从而无法执行监控逻辑。
function retFunc(funcAddress) {
    Memory.protect(funcAddress, 4, ‘rwx’);
    var writer = new Arm64Writer(funcAddress);
    writer.putRet(); // 写入返回指令
    writer.flush();
    writer.dispose();
}

retFunc(libanon.base.add(0x5A708));
retFunc(libanon.base.add(0x5BE28));
// ... 禁用其他监控线程入口

执行后,maps检测日志停止刷屏,实现相对“干净”的绕过。

14. 结论回顾

  1. 入口选择:从 __loader_android_dlopen_ext 切入比 System.loadLibrary 更能把握早期时机。
  2. 主战场:DexProtector的关键逻辑常驻于运行时生成的匿名可执行段,需掌握内存dump与“仅text段”反汇编的技巧。
  3. 对抗核心:识别并处理多层校验链(如xxHash、HMAC-SHA256),采用等式替换等精准打击策略。
  4. 分析方法:善用二分排除法系统性缩小问题范围,定位到具体函数后再做补丁,而非盲目禁用。
  5. 对抗全面性:除了主校验逻辑,还需处理独立的监控线程。

15. 限制与风险

  • 环境敏感性:结论受ROM、内核、ART版本影响较大,不同环境下的内存布局、linker行为可能不同。
  • 时效性:部分绕过技巧(如特定偏移、匿名段特征)可能仅对当前样本或版本有效。
  • 工具链影响:Zygisk Frida Gadget 与标准 frida-server 在时序和可见性上存在差异。

本文涉及的工具与代码已整理,可通过相关渠道获取以供学习交流。




上一篇:MySQL数据库管理实战指南:权限控制、日志分析与备份恢复
下一篇:Fortinet防火墙CVE-2025-59718漏洞:SSO身份验证遭绕过,需紧急修复
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2025-12-24 20:53 , Processed in 0.226842 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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