关键词:Android、ART、eBPF、uprobe、DEX、Nterp、CodeItem、Rust、aya
项目地址:<6bdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6U0K9r3W2F1L8r3g2W2P5W2)9J5c8X3g2n7f1p5k6p5k6i4S2p5N6h3#2H3k6i4u0Q4x3X3c8J5M7#2)9J5y4X3N6@1i4K6y4n7
适用环境:Android 13-17 / ARM64 / root / 支持 eBPF 的内核
Android 加固方案在高版本系统上逐渐从完整 DEX 加密演化为方法级抽取、运行时短暂回填、Nterp 路径覆盖以及 native buffer 中转等多种形态。传统依赖自定义 ROM、Frida inline hook 或 ptrace 的脱壳方式通常存在侵入性强、易被检测、跨 Android 版本维护成本高等问题。
本文以开源项目 eBPFDexDumper-rs 为例,介绍一种基于 Linux eBPF uprobe/uretprobe 的 Android ART 运行时 DEX 采集方案。该方案不向目标进程注入 so,也不修改目标进程中的用户态代码逻辑;它通过在内核侧观察 ART 解释器入口、DexFile 生命周期入口以及 libc native buffer 操作,捕获 DEX 起始地址、文件大小和执行过的方法字节码,再由 Rust 用户态完成分块拼装、缺页兜底读取、DEX 去重、CodeItem 反扫和方法字节码回填。
文章重点讨论四个工程问题:第一,如何在 Android 13-17 的 ART 变化中定位可用 hook 目标;第二,如何把 ART 私有对象布局以运行时参数注入 eBPF 程序,降低 ROM 差异带来的重编译成本;第三,如何在 BPF verifier 限制下传输大体积 DEX 与变长方法字节码;第四,如何把采集到的 insns 回填到 DEX 并重算 SHA-1 / Adler-32,使输出结果能被 jadx、baksmali 等工具继续处理。
需要说明的是,本文讨论的是授权设备、授权应用和安全研究场景下的动态观测技术。它不试图绕过所有反分析能力,也不保证覆盖完全 native 化或 VMP 化的极端样本。
近几年常见 Android 加固方案有几个明显趋势:
因此,一个实用的运行时 DEX 采集工具不能只依赖单个 hook 点,也不能把 ART 私有布局写死在用户态或内核态逻辑里。
eBPF 的价值在于把“观测点”放到内核侧:BPF 程序在 uprobe 命中时读取寄存器和用户态内存,然后把事件写入 ringbuf。用户态只负责加载程序、attach 探针、消费事件和落盘。
eBPFDexDumper-rs 是一个 Rust 项目,主二进制为 eBPFDexDumper。与本文相关的核心模块如下:
命令行分为三个子命令:
整体数据流如下:
输出目录按目标自动分组:使用 --name 时以包名建目录;只指定 --pid 时尽量从 /proc/<pid>/cmdline 推断;只指定 --uid 时使用 uid_<num>/。
当前 dump 主流程默认 attach 的 ART 主入口包括:
这组 hook 负责捕获“正在执行的方法”。入口参数根据 ABI 分为两类:
拿到 ArtMethod* 后,BPF 程序尝试沿 ART 对象链解析 DEX:
Android 64 位 ART 通常使用 32-bit HeapReference。因此实现中会先判断压缩引用,避免把低 32 位对象引用当作 64 位用户态地址直接读取。同时,ARM64 顶字节可能带 MTE/PAC tag,读取前会统一 untag。
仅靠解释器入口会漏掉尚未执行的方法和某些 DexFile 生命周期事件。因此项目还会根据 src/art.rs 的解析结果 attach:
DexFile::DexFile 和 Android 16/17 上常见的 DexFileLoader::OpenOne 都符合“x1 位置可取得 DEX begin”的形态,因此可以复用同一个 BPF handler。RegisterDexFile 则从 DexFile* 读取 begin_,再检查 DEX header。
仓库中也包含 uprobe_libart_verifyClass 的 BPF handler 和 VerifyClass 目标定位逻辑,但当前 dump 主流程没有默认 attach 该探针。文档和使用预期应以实际 attach 路径为准,不能把它算作默认采集链路。
为覆盖“DEX 先在 native buffer 中短暂出现”的场景,项目默认尝试 attach 以下 libc 探针:
entry probe 记录参数,uretprobe 在函数返回后检查目标地址是否出现 dex\n magic 和合理的 file_size。BPF 侧只做轻量判断,完整 DEX header 校验、范围扫描和解析放到用户态完成,以减少 verifier 压力。
Android 高版本中 libart.so 的符号保留情况差异很大。src/art.rs 使用纯 ELF 解析定位目标,不依赖在目标进程中 dlopen 或执行 ART 代码。主要策略包括:
代码里的 TargetSource 目前分为 manual、symbol、pattern、string-ref 四类;分支扫描属于 pattern 路径的一种实现细节。
eBPF 程序通过 art_layout_t 描述读取 ART/DEX 关键字段所需的偏移:
默认布局对应 Android 13+ AOSP 主线 ARM64 常见结构:
用户态加载 BPF 后会把 layout 写入 art_layout_map[0]。这个 map 使用 BPF_MAP_TYPE_HASH 而不是单元素 array,是为了让 BPF 程序在用户态尚未写入时 lookup 返回 NULL,从而回退到内置的 default_art_layout,避免读到全 0 偏移。
Class.dex_cache_ 和 DexCache.dex_file_ 在不同 ROM 上可能出现在相邻 slot。主路径失败时,BPF 会在有限范围内做 4x4 网格尝试:
这里使用固定边界和 #pragma unroll 是为了满足 BPF verifier 对循环可证明性的要求。
DEX 文件可能达到几十甚至上百 MiB,不能作为单条 ringbuf 记录输出。项目用 dex_chunks ringbuf 加 dexProgress_map 实现分块续传:
每次同一个 DEX 再次被 hook 命中时,BPF 从 next_offset 继续发送固定大小 chunk。当前 chunk 记录大小为 RINGBUF_SIZE = 1 << 17,也就是 128 KiB;单次 BPF 调用最多推进 MAX_CHUNKS_PER_CALL = 128 个 chunk。
当 bpf_ringbuf_reserve 失败或 bpf_probe_read_user 读页失败时,BPF 发送 read_failures 事件,交给用户态兜底。
bpf_probe_read_user 是非阻塞读取,不会替目标进程触发缺页换入。如果目标页尚未驻留,BPF 侧读取会失败。用户态收到 read_failures 后调用 process_vm_readv:
在 root 场景下,这一 fallback 能把 BPF 侧不适合完成的阻塞式大块读取转移到用户态,降低 BPF 程序复杂度。
方法级抽取场景中,原始 DEX 文件本身可能存在,但目标方法的 insns 被抹掉。项目在解释器入口解析到 ArtMethod* 后,会进一步通过 ArtMethod.data_ 读取 CodeItem,采集:
BPF 中用 methodCodeCache_map 对 ArtMethod* 去重,避免同一方法重复发送。由于方法字节码是变长数据,项目使用 per-cpu 16 KiB 缓冲作为中转,再通过 bpf_ringbuf_output 输出到 method_events。
用户态会把这些记录累计到:
记录中保存方法名、method_idx 和 hex 编码的字节码。方法名解析失败时,回退为 method_idx_<n>。
实际样本里,任意单一路径都可能被绕开。因此项目把采集路径分成几层。
这是最准确的路径。解释器入口命中后,工具从 ArtMethod* 解析到 DEX begin/size,并同步采集当前方法的 CodeItem.insns。
适用场景:
DexFile::DexFile、DexFileLoader::OpenOne、ClassLinker::RegisterDexFile 用于捕获已经进入 ART DexFile 生命周期但尚未执行到具体方法的 DEX。
适用场景:
当 ART 链路解 DEX 失败,但 ArtMethod.data_ 仍像是 CodeItem* 时,BPF 发送 layout_debug_events。用户态从 code_item_ptr 所在页向前扫描 dex\n magic,最大回扫 64 MiB,并要求候选 DEX 范围包含该 code_item_ptr。
这条路径用于处理 dex_cache 链不稳定、字段偏移异常或部分 ART 私有路径失效的情况。
dump 默认启动一次 /proc/<pid>/maps 扫描,跳过 /apex/、/system/framework/、/data/dalvik-cache/ 等系统 DEX 路径,对可读 region 查找合法 DEX header。
native buffer 事件则来自 mmap、mprotect、memcpy、memmove。命中后,用户态会先尝试从事件地址读取 DEX;如果地址本身不是 DEX 起点,则在限定范围内继续扫描。
这两条路径覆盖率更宽,但精度低于 ART 链路,因此所有写盘前都要经过 DEX header 校验、DexParser 解析和 SHA-1 内容去重。
fix 阶段不会依赖外部 dexlib,而是直接解析 DEX 的 class_def_item 和 class_data_item。核心过程如下:
这样可以根据 dump 阶段记录的 method_idx 找回原 DEX 中对应 code_item 的位置。
标准 code_item 中:
回填时,工具会根据原 DEX 声明的 insns_size 计算期望长度,将采集到的字节码写入 insns 区域。若采集长度短于声明长度,剩余部分清零;若长度不一致,会记录 length_mismatch,便于后续人工审计。
DEX 修改后必须重算 header 中的 SHA-1 与 Adler-32:
项目会把修复版写入 fix/,再汇总到 final/:
这种目录设计方便后续工具只消费 final/,同时保留原始采集结果用于对比。
运行中第一次收到 SIGINT/SIGTERM 时,工具停止主 ringbuf 循环,但仍允许 flush JSON 和 auto-fix;第二次信号会进入强制退出状态,跳过后处理。
这一设计解决的是大应用 dump 时的常见问题:用户希望 Ctrl-C 后尽快停止 attach,但又不希望已经采集的数据因为未 flush 而丢失。
退出主循环后,用户态会先 drop ebpf,让所有 uprobe detach,再做最后一轮 ringbuf drain。否则目标进程仍在运行时,ringbuf 可能一边 drain 一边继续增长,导致收尾阶段 livelock。
部分 Android 内核没有暴露 /sys/kernel/btf/vmlinux。项目在 assets/ 中内嵌了精简 BTF 资源:
用户态检测不到系统 BTF 时,会根据内核 release 选择内置 BTF 解析,提升在 Android 设备和开发板上的加载成功率。
dump 默认会根据包名查找 APK 路径并清理目标应用的 oat 目录,促使下次启动更多走解释器或重新加载路径。退出时默认执行 auto-fix,将可回填的 DEX 直接整理到 final/。
这些行为都提供了关闭开关:
本文不把单次本地样本结果写成普适性能结论。不同设备的内核配置、ROM ART 编译方式、目标应用规模和壳行为都会显著影响命中率与开销。更稳妥的验证方式是按以下步骤复现。
macOS 宿主机示例:
产物路径:
先在设备上检查 libart.so 的定位结果:
重点关注:
建议验证:
以下场景可能无法完整恢复:
uprobe 的实现会在目标 ELF 对应位置设置断点机制。虽然这不是 Frida 式 so 注入,也不需要改目标进程业务代码,但它并不是“不可检测”。具备精细自检能力的壳仍可能发现探针痕迹。
因此,本项目更适合定位为安全研究、取证分析和授权逆向环境中的低侵入采集工具,而不是对抗所有商业壳的隐蔽执行框架。
eBPFDexDumper-rs 展示了一条不同于 Frida 注入和 ROM 修改的 Android DEX 运行时采集路线:把 ART/libc 关键路径上的观测逻辑放在 eBPF uprobe 中,把复杂的 DEX 拼装、缺页读取、CodeItem 反扫和回填逻辑放在 Rust 用户态中完成。
这种分层让工具在 Android 13-17 ARM64 上具备较好的工程可维护性:BPF 侧只做短路径判断和事件输出,用户态负责高复杂度处理;ART 字段布局通过 map 注入,ROM 差异可以在运行时修正;DEX 和方法字节码分别通过分块续传和变长事件传输,最终再统一落盘、去重和修复。
它的边界同样明确:需要 root 和 eBPF-capable kernel,无法覆盖完全脱离 ART 的 native/VMP 方法,也可能被专门的反 uprobe 检测发现。只要在授权场景下使用,并把输出结果与样本行为、jadx/baksmali 解析结果结合验证,它可以作为 Android 高版本 DEX 采集与方法回填的一条实用工程路径。
[1] LLeavesG. eBPFDexDumper . <7cbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6x3e0r3g2S2N6X3g2K6c8#2)9J5c8X3g2n7f1p5k6p5k6i4S2p5N6h3#2H3k6i4u0Q4x3U0k6Y4N6q4)9K6b7R3`.`.
[2] aya-rs. aya: an eBPF library for Rust . <799K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2P5h3q4Q4x3X3c8J5M7#2)9J5c8X3q4&6j5g2)9J5y4X3N6@1i4K6y4n7
[3] Android Open Source Project. ART runtime source . <959K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6S2M7Y4c8Q4x3V1k6Q4x3U0k6Y4N6q4)9K6b7R3`.`.
[4] Android Developers. Dalvik Executable format . <1b8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6L8%4g2J5j5$3g2Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3k6r3!0U0M7#2)9J5c8X3y4G2M7X3g2Q4x3V1k6J5N6h3&6@1K9h3#2W2i4K6u0r3k6r3g2^5i4K6u0V1k6X3!0J5L8h3q4@1i4K6t1$3k6%4c8Q4x3@1t1`.
[5] Linux Kernel Documentation. BPF Ring Buffer . <772K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2C8k6i4u0F1k6h3I4Q4x3X3g2G2M7X3N6Q4x3V1k6V1L8$3y4Q4x3V1k6Z5N6r3#2D9i4K6u0r3L8r3q4@1k6i4y4@1i4K6u0r3j5Y4m8X3i4K6u0r3M7X3W2F1k6$3u0#2k6W2)9J5k6h3S2@1L8h3I4Q4x3U0k6Y4N6q4)9K6b7R3`.`.
输出结构:
请仅在你有权分析的设备、应用和数据上使用本项目。
注:本项目代码与本文内容均在 AI 辅助下完成。
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 3天前
被PanzerT编辑
,原因: