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

4771

积分

0

好友

682

主题
发表于 昨天 21:02 | 查看: 6| 回复: 0

项目地址https://github.com/chinleez/eBPFDexDumper-rs
适用环境: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 化的极端样本。

ART运行时DEX采集技术抽象图  

1. 背景与问题

1.1 Android 高版本脱壳难点

近几年常见 Android 加固方案有几个明显趋势:

  1. 方法级抽取
    原 DEX 中的 class_data_item 仍在,但部分方法的 code_item.insns 被清零、替换或延迟填充。方法真正执行前,壳在运行时短暂恢复字节码。
  2. Nterp 成为主路径
    Android 11 之后,ARM64 上的 Nterp 解释器覆盖了大量解释执行场景。只盯早期 art::interpreter::Execute 很容易漏方法。
  3. DexFile 生命周期被弱化
    高版本 ART 中部分构造函数会被内联,符号也可能被 strip。仅依赖 DexFile::DexFile 一类入口不够稳。
  4. native buffer 中转
    壳可能先在 native 层 mmap 一段内存,把解密后的 DEX 或片段拷贝进去,再交给 ART 或自定义加载逻辑。
  5. 跨 ROM 布局漂移
    同为 Android 13+,不同 OEM ROM 中 mirror::ClassDexCache 等对象字段位置仍可能有细微变化。

因此,一个实用的运行时 DEX 采集工具不能只依赖单个 hook 点,也不能把 ART 私有布局写死在用户态或内核态逻辑里。

1.2 传统方案的局限

不同DEX采集方案对比表格  

eBPF 的价值在于把“观测点”放到内核侧:BPF 程序在 uprobe 命中时读取寄存器和用户态内存,然后把事件写入 ringbuf。用户态只负责加载程序、attach 探针、消费事件和落盘。

2. 项目结构与数据流

eBPFDexDumper-rs 是一个 Rust 项目,主二进制为 eBPFDexDumper。与本文相关的核心模块如下:

项目源码文件结构与功能说明表  

命令行分为三个子命令:

项目子命令及用途表格  

整体数据流如下:

Target App / ART / libc
        |
        | uprobe / uretprobe
        v
bpf/bpf.c
        |
        | events / dex_chunks / method_events
        | read_failures / layout_debug_events / native_buffer_events
        v
Rust 用户态
        |
        | process_vm_readv fallback
        | maps scan / CodeItem backscan / native buffer scan
        v
/data/local/tmp/dex_out/<package>/
        |-- dex_<begin>_<size>.dex
        |-- dex_<begin>_<size>_code.json
        |-- fix/
        `-- final/

输出目录按目标自动分组:使用 --name 时以包名建目录;只指定 --pid 时尽量从 /proc/<pid>/cmdline 推断;只指定 --uid 时使用 uid_<num>/

3. Hook 矩阵设计

3.1 ART 解释器入口

当前 dump 主流程默认 attach 的 ART 主入口包括:

SEC("uprobe/libart_execute")          // art::interpreter::Execute
SEC("uprobe/libart_executeNterpImpl") // ExecuteNterpImpl / ExecuteNterpWithClinitImpl
SEC("uprobe/libart_nterpOpInvoke")    // nterp_op_invoke_* pattern targets

这组 hook 负责捕获“正在执行的方法”。入口参数根据 ABI 分为两类:

  • art::interpreter::Execute
    从寄存器参数中取得 ShadowFrame*,再读取其中的 ArtMethod*
  • ExecuteNterpImplExecuteNterpWithClinitImplnterp_op_invoke_*:按当前实现假设 x0 中为 ArtMethod*

拿到 ArtMethod* 后,BPF 程序尝试沿 ART 对象链解析 DEX:

ArtMethod*
-> declaring_class_
-> mirror::Class.dex_cache_
-> mirror::DexCache.dex_file_
-> art::DexFile.begin_
-> DexHeader.file_size

Android 64 位 ART 通常使用 32-bit HeapReference。因此实现中会先判断压缩引用,避免把低 32 位对象引用当作 64 位用户态地址直接读取。同时,ARM64 顶字节可能带 MTE/PAC tag,读取前会统一 untag

3.2 DexFile 生命周期入口

仅靠解释器入口会漏掉尚未执行的方法和某些 DexFile 生命周期事件。因此项目还会根据 src/art.rs 的解析结果 attach:

SEC("uprobe/libart_dexFileCtor")      // DexFile::DexFile / DexFileLoader::OpenOne
SEC("uprobe/libart_registerDexFile")  // ClassLinker::RegisterDexFile

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 路径为准,不能把它算作默认采集链路。

3.3 libc native buffer 入口

为覆盖“DEX 先在 native buffer 中短暂出现”的场景,项目默认尝试 attach 以下 libc 探针:

("memcpy",   "uprobe_libc_memcpy",   Some("uretprobe_libc_memcpy"))
("memmove",  "uprobe_libc_memmove",  Some("uretprobe_libc_memmove"))
("mmap",     "uprobe_libc_mmap",     Some("uretprobe_libc_mmap"))
("mmap64",   "uprobe_libc_mmap",     Some("uretprobe_libc_mmap"))
("mprotect", "uprobe_libc_mprotect", None)

entry probe 记录参数,uretprobe 在函数返回后检查目标地址是否出现 dex\n magic 和合理的 file_size。BPF 侧只做轻量判断,完整 DEX header 校验、范围扫描和解析放到用户态完成,以减少 verifier 压力。

4. ART 目标定位与布局注入

4.1 Hook 目标定位

Android 高版本中 libart.so 的符号保留情况差异很大。src/art.rs 使用纯 ELF 解析定位目标,不依赖在目标进程中 dlopen 或执行 ART 代码。主要策略包括:

  • 符号匹配
    优先匹配 art::interpreter::ExecuteExecuteNterpImplExecuteNterpWithClinitImplDexFile::DexFileDexFileLoader::OpenOneClassLinker::RegisterDexFile 等符号。
  • 字节模式扫描
    在符号缺失时,用 Nterp 入口和 nterp_op_invoke_* 的 ARM64 指令特征定位候选地址。
  • 分支扫描
    围绕 ExecuteNterpImpl 查找直接跳转目标,推断 ExecuteNterpWithClinitImpl 一类变体。
  • 字符串引用反推
    通过 .rodata 中的 "Interpreting " 字符串和 .text 中的 ADR/ADRP+ADD 引用反推 Execute 附近入口。
  • 手工兜底
    通过 --execute-offset--nterp-offset--art-layout 接管自动定位失败的 ROM。

代码里的 TargetSource 目前分为 manualsymbolpatternstring-ref 四类;分支扫描属于 pattern 路径的一种实现细节。

4.2 ART layout 动态注入

eBPF 程序通过 art_layout_t 描述读取 ART/DEX 关键字段所需的偏移:

struct art_layout_t {
 u32 shadow_frame_method_offset;
 u32 art_method_declaring_class_offset;
 u32 art_method_dex_method_index_offset;
 u32 art_method_data_offset;
 u32 class_dex_cache_offset;
 u32 dex_cache_dex_file_offset;
 u32 dex_file_begin_offset;
 u32 dex_header_file_size_offset;
 u32 code_item_insns_size_offset;
 u32 code_item_insns_offset;
};

默认布局对应 Android 13+ AOSP 主线 ARM64 常见结构:

ShadowFrame.method              = 0x08
ArtMethod.declaring_class       = 0x00
ArtMethod.dex_method_index      = 0x08
ArtMethod.data                  = 0x10
Class.dex_cache                 = 0x10
DexCache.dex_file               = 0x10
DexFile.begin                   = 0x08
DexHeader.file_size             = 0x20
CodeItem.insns_size             = 0x0c
CodeItem.insns                  = 0x10

用户态加载 BPF 后会把 layout 写入 art_layout_map[0]。这个 map 使用 BPF_MAP_TYPE_HASH 而不是单元素 array,是为了让 BPF 程序在用户态尚未写入时 lookup 返回 NULL,从而回退到内置的 default_art_layout,避免读到全 0 偏移。

4.3 ROM 偏移漂移处理

Class.dex_cache_DexCache.dex_file_ 在不同 ROM 上可能出现在相邻 slot。主路径失败时,BPF 会在有限范围内做 4x4 网格尝试:

#pragma unroll
for (int class_slot = 0; class_slot < 4; class_slot++) {
    u32 class_dex_cache_offset = 0x10 + class_slot * 8;
    #pragma unroll
    for (int dex_cache_slot = 0; dex_cache_slot < 4; dex_cache_slot++) {
        u32 dex_cache_dex_file_offset = 0x10 + dex_cache_slot * 8;
        if (read_dex_from_art_method(...)) {
            return 1;
        }
    }
}

这里使用固定边界和 #pragma unroll 是为了满足 BPF verifier 对循环可证明性的要求。

5. 大体积 DEX 与方法字节码传输

5.1 DEX 分块续传

DEX 文件可能达到几十甚至上百 MiB,不能作为单条 ringbuf 记录输出。项目用 dex_chunks ringbuf 加 dexProgress_map 实现分块续传:

dexProgress_map[begin] = next_offset

每次同一个 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 事件,交给用户态兜底。

5.2 process_vm_readv 缺页兜底

bpf_probe_read_user 是非阻塞读取,不会替目标进程触发缺页换入。如果目标页尚未驻留,BPF 侧读取会失败。用户态收到 read_failures 后调用 process_vm_readv

libc::syscall(
    libc::SYS_process_vm_readv,
    pid as libc::pid_t,
    &mut local,
    1usize,
    &mut remote,
    1usize,
    0usize,
)

在 root 场景下,这一 fallback 能把 BPF 侧不适合完成的阻塞式大块读取转移到用户态,降低 BPF 程序复杂度。

5.3 方法字节码事件

方法级抽取场景中,原始 DEX 文件本身可能存在,但目标方法的 insns 被抹掉。项目在解释器入口解析到 ArtMethod* 后,会进一步通过 ArtMethod.data_ 读取 CodeItem,采集:

  • dex begin
  • method_idx
  • ArtMethod*
  • codeitem_size
  • insns 字节

BPF 中用 methodCodeCache_mapArtMethod* 去重,避免同一方法重复发送。由于方法字节码是变长数据,项目使用 per-cpu 16 KiB 缓冲作为中转,再通过 bpf_ringbuf_output 输出到 method_events

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

dex_<begin>_<size>_code.json

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

6. Fallback 采集链

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

6.1 L0:ART 方法链

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

适用场景:

  • 普通解释执行
  • 方法级抽取但执行前会恢复 insns
  • 仍通过 ART/Nterp 分发的方法

6.2 L1:DexFile 注册/打开路径

DexFile::DexFileDexFileLoader::OpenOneClassLinker::RegisterDexFile 用于捕获已经进入 ART DexFile 生命周期但尚未执行到具体方法的 DEX。

适用场景:

  • DEX 被 ART 加载但方法还未触发
  • 构造/注册阶段还能看到完整 DEX header

6.3 L2:CodeItem 反扫

当 ART 链路解 DEX 失败,但 ArtMethod.data_ 仍像是 CodeItem* 时,BPF 发送 layout_debug_events。用户态从 code_item_ptr 所在页向前扫描 dex\n magic,最大回扫 64 MiB,并要求候选 DEX 范围包含该 code_item_ptr

这条路径用于处理 dex_cache 链不稳定、字段偏移异常或部分 ART 私有路径失效的情况。这在你进行复杂样本的逆向分析时,能提供一个关键的兜底手段。

6.4 L3:maps / native buffer 扫描

dump 默认启动一次 /proc/<pid>/maps 扫描,跳过 /apex//system/framework//data/dalvik-cache/ 等系统 DEX 路径,对可读 region 查找合法 DEX header。

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

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

7. DEX 回填与输出整理

7.1 method_idx -> code_off 映射

fix 阶段不会依赖外部 dexlib,而是直接解析 DEX 的 class_def_itemclass_data_item

核心过程如下:

  • 遍历 class_defs
  • 读取每个类的 class_data_off
  • 解 ULEB128,跳过 static/instance fields。
  • 读取 direct methods 与 virtual methods。
  • 按 DEX 规范累加 method_idx_diff,记录 method_idx -> code_off

这样可以根据 dump 阶段记录的 method_idx 找回原 DEX 中对应 code_item 的位置。

7.2 回填 insns

标准 code_item 中:

code_off + 0x0c = insns_size_in_code_units
code_off + 0x10 = insns

回填时,工具会根据原 DEX 声明的 insns_size 计算期望长度,将采集到的字节码写入 insns 区域。若采集长度短于声明长度,剩余部分清零;若长度不一致,会记录 length_mismatch,便于后续人工审计。

7.3 重算 DEX 校验链

DEX 修改后必须重算 header 中的 SHA-1 与 Adler-32:

SHA-1   = sha1(dex[32..])
Adler32 = adler32(dex[12..])

项目会把修复版写入 fix/,再汇总到 final/

  • 有成功回填结果时,final/ 使用修复版。
  • 没有 _code.json 或修复失败时,final/ 回退原始 DEX。

这种目录设计方便后续工具只消费 final/,同时保留原始采集结果用于对比。

8. 工程细节

8.1 关停状态机

运行中第一次收到 SIGINT/SIGTERM 时,工具停止主 ringbuf 循环,但仍允许 flush JSON 和 auto-fix;第二次信号会进入强制退出状态,跳过后处理。

这一设计解决的是大应用 dump 时的常见问题:用户希望 Ctrl-C 后尽快停止 attach,但又不希望已经采集的数据因为未 flush 而丢失。

8.2 先 detach 再 drain

退出主循环后,用户态会先 drop ebpf,让所有 uprobe detach,再做最后一轮 ringbuf drain。否则目标进程仍在运行时,ringbuf 可能一边 drain 一边继续增长,导致收尾阶段 livelock。

8.3 BTF 资源内嵌

部分 Android 内核没有暴露 /sys/kernel/btf/vmlinux。项目在 assets/ 中内嵌了精简 BTF 资源:

assets/a12-5.10-arm64_min.btf
assets/rock5b-5.10-arm64_min.btf

用户态检测不到系统 BTF 时,会根据内核 release 选择内置 BTF 解析,提升在 Android 设备和开发板上的加载成功率。

8.4 oat 清理与自动修复

dump 默认会根据包名查找 APK 路径并清理目标应用的 oat 目录,促使下次启动更多走解释器或重新加载路径。退出时默认执行 auto-fix,将可回填的 DEX 直接整理到 final/

这些行为都提供了关闭开关:

--no-clean-oat
--no-auto-fix
--no-code-item-fallback
--no-maps-scan
--no-native-buffer-scan

9. 复现与验证建议

本文不把单次本地样本结果写成普适性能结论。不同设备的内核配置、ROM ART 编译方式、目标应用规模和壳行为都会显著影响命中率与开销。更稳妥的验证方式是按以下步骤复现。

9.1 构建

macOS 宿主机示例:

brew install llvm
export CLANG=/opt/homebrew/opt/llvm/bin/clang
sh build_android.sh

产物路径:

target/aarch64-linux-android/release/eBPFDexDumper

9.2 目标定位检查

先在设备上检查 libart.so 的定位结果:

adb shell su -c '/data/local/tmp/eBPFDexDumper offsets \
  -l /apex/com.android.art/lib64/libart.so --json'

重点关注:

  • 是否至少定位到 ExecuteNterpImpl 或其他主入口。
  • DexFile::DexFile / DexFileLoader::OpenOne 是否命中。
  • ClassLinker::RegisterDexFile 是否命中。
  • ART layout 是否符合当前 ROM;不符合时用 --art-layout 覆盖。

9.3 dump 与 fix

adb push target/aarch64-linux-android/release/eBPFDexDumper /data/local/tmp/
adb shell su -c 'chmod +x /data/local/tmp/eBPFDexDumper'

adb shell su -c '/data/local/tmp/eBPFDexDumper dump \
  -n com.example.app \
  -o /data/local/tmp/dex_out'

adb shell su -c '/data/local/tmp/eBPFDexDumper fix \
  -d /data/local/tmp/dex_out/com.example.app'

建议验证:

  • dex_*.dex 是否能被 jadxbaksmali 解析。
  • dex_*_code.json 中是否出现目标业务方法。
  • fix/final/ 中的 DEX 是否比原始 dump 多出有效方法体。
  • 多次运行是否因 SHA-1 去重避免大量重复文件。

10. 局限性与威胁模型

10.1 平台限制

  • 当前实现面向 Android ARM64。
  • 需要 root 权限。
  • 需要内核支持 eBPF、uprobe 和 ringbuf。
  • ART 布局默认值面向 Android 13+ AOSP 常见布局,强定制 ROM 可能需要 --art-layout

10.2 覆盖边界

以下场景可能无法完整恢复:

方案局限性分析表  

10.3 可观测性与隐蔽性

uprobe 的实现会在目标 ELF 对应位置设置断点机制。虽然这不是 Frida 式 so 注入,也不需要改目标进程业务代码,但它并不是“不可检测”。具备精细自检能力的壳仍可能发现探针痕迹。

因此,本项目更适合定位为安全研究、取证分析和授权逆向环境中的低侵入采集工具,而不是对抗所有商业壳的隐蔽执行框架。

11. 结论

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. https://github.com/LLeavesG/eBPFDexDumper
[2] aya-rs. aya: an eBPF library for Rust. https://github.com/aya-rs/aya
[3] Android Open Source Project. ART runtime source. https://android.googlesource.com/platform/art/
[4] Android Developers. Dalvik Executable format. https://source.android.com/docs/core/runtime/dex-format
[5] Linux Kernel Documentation. BPF Ring Buffer. https://www.kernel.org/doc/html/latest/bpf/ringbuf.html

附录:使用速览

# 查看帮助
./eBPFDexDumper --help

# 按包名 dump,退出时默认 auto-fix
su -c './eBPFDexDumper dump \
  -n com.example.app \
  -o /data/local/tmp/dex_out'

# 按 UID dump
su -c './eBPFDexDumper dump \
  -u 10123 \
  -o /data/local/tmp/dex_out'

# 单独修复一个输出目录
su -c './eBPFDexDumper fix \
  -d /data/local/tmp/dex_out/com.example.app'

# 只解析 libart hook 目标
su -c './eBPFDexDumper offsets \
  -l /apex/com.android.art/lib64/libart.so --json'

# ROM layout 不匹配时手工覆盖
su -c './eBPFDexDumper dump \
  -n com.example.app \
  --art-layout 0x08,0x00,0x08,0x10,0x10,0x10,0x08,0x20,0x0c,0x10'

输出结构:

/data/local/tmp/dex_out/com.example.app/
|-- dex_<begin>_<size>.dex
|-- dex_<begin>_<size>_code.json
|-- fix/
|   `-- dex_<begin>_<size>_fix.dex
`-- final/
    `-- dex_<begin>_<size>.dex

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

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

作者头像-戴眼镜的橙色卡通动物

本文由 PanzerT 原创,首发于看雪论坛。




上一篇:Claude Fable 5一天完成两月工程:未来最值钱的工程师长什么样?
下一篇:大促全链路压测方案设计:4张图搞定流量模型与隔离策略
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-6-11 06:09 , Processed in 0.901817 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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