首页
社区
课程
招聘
[原创]基于eBPF的Android ART运行时DEX采集与方法字节码回填
发表于: 3天前 1457

[原创]基于eBPF的Android ART运行时DEX采集与方法字节码回填

3天前
1457

关键词:Android、ART、eBPF、uprobe、DEX、Nterp、CodeItem、Rust、aya

项目地址:<f34K9s2c8@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/&lt;pid&gt;/cmdline 推断;只指定 --uid 时使用 uid_&lt;num&gt;/

当前 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 目前分为 manualsymbolpatternstring-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_mapArtMethod* 去重,避免同一方法重复发送。由于方法字节码是变长数据,项目使用 per-cpu 16 KiB 缓冲作为中转,再通过 bpf_ringbuf_output 输出到 method_events

用户态会把这些记录累计到:

记录中保存方法名、method_idx 和 hex 编码的字节码。方法名解析失败时,回退为 method_idx_&lt;n&gt;

实际样本里,任意单一路径都可能被绕开。因此项目把采集路径分成几层。

这是最准确的路径。解释器入口命中后,工具从 ArtMethod* 解析到 DEX begin/size,并同步采集当前方法的 CodeItem.insns

适用场景:

DexFile::DexFileDexFileLoader::OpenOneClassLinker::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/&lt;pid&gt;/maps 扫描,跳过 /apex//system/framework//data/dalvik-cache/ 等系统 DEX 路径,对可读 region 查找合法 DEX header。

native buffer 事件则来自 mmapmprotectmemcpymemmove。命中后,用户态会先尝试从事件地址读取 DEX;如果地址本身不是 DEX 起点,则在限定范围内继续扫描。

这两条路径覆盖率更宽,但精度低于 ART 链路,因此所有写盘前都要经过 DEX header 校验、DexParser 解析和 SHA-1 内容去重。

fix 阶段不会依赖外部 dexlib,而是直接解析 DEX 的 class_def_itemclass_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. <6cbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6x3e0r3g2S2N6X3g2K6c8#2)9J5c8X3g2n7f1p5k6p5k6i4S2p5N6h3#2H3k6i4u0Q4x3U0k6Y4N6q4)9K6b7R3`.`.

[2] aya-rs. aya: an eBPF library for Rust. <36dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6S2P5h3q4Q4x3X3c8J5M7#2)9J5c8X3q4&6j5g2)9J5y4X3N6@1i4K6y4n7

[3] Android Open Source Project. ART runtime source. <348K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6S2M7Y4c8Q4x3V1k6Q4x3U0k6Y4N6q4)9K6b7R3`.`.

[4] Android Developers. Dalvik Executable format. <748K9s2c8@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. <33dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2C8k6i4u0F1k6h3I4Q4x3X3g2G2M7X3N6Q4x3V1k6V1L8$3y4Q4x3V1k6Z5N6r3#2D9i4K6u0r3L8r3q4@1k6i4y4@1i4K6u0r3j5Y4m8X3i4K6u0r3M7X3W2F1k6$3u0#2k6W2)9J5k6h3S2@1L8h3I4Q4x3U0k6Y4N6q4)9K6b7R3`.`.

输出结构:

请仅在你有权分析的设备、应用和数据上使用本项目。

注:本项目代码与本文内容均在 AI 辅助下完成。


[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 3天前 被PanzerT编辑 ,原因:
收藏
免费 60
支持
分享
最新回复 (23)
雪    币: 491
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
感觉有点东西啊
3天前
0
雪    币: 4110
活跃值: (6707)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
感谢分享
3天前
0
雪    币: 76
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
6
3天前
0
雪    币: 8752
活跃值: (5468)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
666
3天前
0
雪    币: 155
活跃值: (4641)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
谢谢分享 
3天前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
tql
3天前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
3天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
谢谢分享
2天前
0
雪    币: 829
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
tql
2天前
0
雪    币: 11
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
这已经在生产上使用了吗
2天前
0
雪    币: 11
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
1. [但是由于eBPF的局限性,其无法替代FART等基于主动调用的脱壳工具](9b2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2L8r3I4W2j5i4k6W2M7$3N6Q4x3X3g2@1L8%4m8Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6f1u0b7c8V1c8W2P5p5c8#2L8i4m8W2M7W2)9J5z5b7`.`.
有什么局限,导致功能上有什么区别吗
2 它不试图绕过所有反分析能力,也不保证覆盖完全 native 化或 VMP 化的极端样本。能具体详述吗?
2天前
0
雪    币: 5872
活跃值: (2390)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
cecini 1. [但是由于eBPF的局限性,其无法替代FART等基于主动调用的脱壳工具](b73K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2L8r3I4W2j5i4k6W2M7$3N6Q4x3X3g2@1L8%4m8Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6f1u0b7c8V1c8W2P5p5c8#2L8i4m8W2M7W2)9J5z5b7`.`. 有什么局限,导致功能 ...
1. eBPF 局限带来的功能区别
  eBPF/uProbe 方案的限制主要在“只能看到发生过的事”:

  - 没执行到的方法,不一定能拿到真实 CodeItem。壳如果按需解密方法体,只有方法真正进入解释器/ART 路径时,eBPF 才有机会记录。
  - 它不能主动枚举类和方法,也不能替你调用每个方法。FART 的强项正是主动遍历类方法、触发方法解密,再 dump CodeItem 并回填。
  - 如果代码走 AOT/JIT/native 快路径,绕开你挂的 ART 解释器点,eBPF 可能只能拿到整体 DEX,拿不到执行时还原的方法体。
  - 如果内存中没有连续合法 DEX,只是 native 层临时拼出碎片、解密后马上擦除,通用 eBPF DEX 扫描也不一定能拼回完整文件。
  - eBPF 内核侧不能随意做复杂逻辑,受 verifier、栈大小、ring buffer、用户内存读取限制影响。文章里也提到过内核侧分片传 DEX 容易丢数据,后来才转向用户态 process_vm_readv 读取。

  所以功能上可以这么理解:

  - eBPFDexDumper-rs 更适合:低侵入、被动捕获、拿运行时已加载的 DEX、记录已执行方法字节码、适合常规壳和动态加载场景。
  - FART 更适合:代码抽取壳、方法体被 nop/占位替换、需要主动触发每个方法恢复真实 CodeItem 的场景。
  - eBPF 的优势是隐蔽和部署轻;FART 的优势是覆盖率和主动性,但侵入更强,通常依赖 ART/系统改造或更重的运行时控制。

  2. “不绕过所有反分析,不保证 native/VMP 极端样本”具体指什么
  “反分析能力”这里不是一句泛泛而谈,具体包括这些:

  - 目标可以检测 root、调试环境、异常系统属性、可疑文件、SELinux 状态、Magisk 痕迹等。这个项目不会帮你隐藏这些环境特征。
  - uProbe 不是完全无痕。它通常会在目标映射上留下可检测的断点式痕迹,强对抗样本可以校验 libart.so 代码页、扫描异常指令、检测性能/时序异常。
  - 目标可以检测 BPF/tracing 相关状态,比如内核 tracing、perf/uProbe 行为、BPF program/map 痕迹。当前项目提供 --probe-mode lifecycle|maps-only 降低探针面,但不是“反检测框架”。
  - 目标可以不让敏感代码进入 ART 字节码路径,例如把核心逻辑搬到 native .so,Java 层只剩 JNI 壳或空方法。这种情况下 dump 出 DEX 也看不到核心算法。
  - “完全 native 化”指业务逻辑已经变成 ARM64 机器码,DEX 里只有声明、桥接、加载器或少量壳代码。eBPF 盯 ART/DexFile 只能拿 Java 层结构,不能自动还原 native 算法。
  - “VMP 化”指原始字节码被翻译成壳自定义虚拟机指令,运行时由 native VM 解释器执行。此时 DEX 里可能只剩 VM 入口和字节码 blob,真实语义在 native VM 指令集中,dump DEX 不等于还原源码。
  - “代码抽取”指 DEX 方法体被 nop、空实现或占位结构替代,真实 CodeItem 在执行前才临时恢复。被动方案只能覆盖触发过的部分;没触发的仍然缺失。
2天前
0
雪    币: 534
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
666
2天前
0
雪    币: 11
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
15
PanzerT 1. eBPF 局限带来的功能区别 eBPF/uProbe 方案的限制主要在“只能看到发生过的事”: - 没执行到的方法,不一定能拿到真实 CodeItem。壳如果按需解密方法体,只有 ...
多谢详尽的解释
2天前
0
雪    币: 22
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
看看
2天前
0
雪    币: 104
活跃值: (8432)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
tql
1天前
0
雪    币: 48
活跃值: (263)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
18
tql
1天前
0
雪    币: 10
活跃值: (2543)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
19
666
1天前
0
雪    币: 190
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
666
9小时前
0
雪    币: 0
活跃值: (2055)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
感谢分享。学习了
6小时前
0
雪    币: 3483
活跃值: (6970)
能力值: ( LV11,RANK:185 )
在线值:
发帖
回帖
粉丝
22
1
6小时前
0
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
23
感谢分享
5小时前
0
雪    币: 9
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
感谢老铁
4小时前
0
游客
登录 | 注册 方可回帖
返回