EBPF 使用指南

eBPF 使用指南

eBPF (extended Berkeley Packet Filter) 是 Linux 内核中的一项革命性技术,允许在内核空间运行沙盒程序而无需修改内核代码。在 Android 逆向工程中,eBPF 提供了比 Frida 更底层、更隐蔽的追踪能力。


为什么选择 eBPF

与其他工具对比

特性eBPFFridaXposed
检测难度极难中等容易
内核级追踪原生支持不支持不支持
性能开销极低中等中等
系统调用追踪原生支持需要额外实现不支持
网络包捕获原生支持不支持不支持
Root 需求
Android 支持4.14+ 内核广泛广泛

eBPF 的优势

  1. 隐蔽性: 运行在内核空间,应用层检测极其困难
  2. 性能: JIT 编译,接近原生性能
  3. 安全性: 内核验证器确保程序安全
  4. 全面性: 可追踪系统调用、网络、文件系统等各个层面

环境准备

检查内核支持

1
2
3
4
5
6
7
8
# 检查内核版本(需要 4.14+)
adb shell uname -r

# 检查 eBPF 支持
adb shell ls /sys/fs/bpf/

# 检查 CONFIG_BPF 相关配置
adb shell zcat /proc/config.gz | grep -i bpf

需要确认以下内核配置启用:

1
2
3
4
5
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y

安装 bpftrace (推荐)

bpftrace 是 eBPF 的高级追踪语言,类似于 awk 和 DTrace。

在 Android 上安装:

1
2
3
4
5
6
7
8
9
# 下载预编译的 bpftrace (需要 root)
# https://github.com/aspect-build/bpftrace-android

# 推送到设备
adb push bpftrace /data/local/tmp/
adb shell chmod +x /data/local/tmp/bpftrace

# 测试运行
adb shell /data/local/tmp/bpftrace -e 'BEGIN { printf("eBPF works!\n"); }'

安装 BCC 工具集

BCC (BPF Compiler Collection) 提供了丰富的预置追踪工具。

1
2
3
4
5
6
7
8
# 交叉编译 BCC for Android
# 或使用预编译版本

# 常用工具
# - opensnoop: 追踪文件打开
# - execsnoop: 追踪进程执行
# - tcpconnect: 追踪 TCP 连接
# - trace: 通用函数追踪

快速入门

Hello World

1
2
3
4
5
6
7
8
9
# 最简单的 bpftrace 脚本
adb shell /data/local/tmp/bpftrace -e '
BEGIN {
    printf("开始追踪...\n");
}

END {
    printf("追踪结束\n");
}'

追踪系统调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 追踪特定应用的所有系统调用
adb shell /data/local/tmp/bpftrace -e '
tracepoint:raw_syscalls:sys_enter
/comm == "com.target.app"/
{
    @syscalls[args->id] = count();
}

END {
    print(@syscalls);
}'

追踪文件操作

1
2
3
4
5
6
7
# 追踪文件打开操作
adb shell /data/local/tmp/bpftrace -e '
tracepoint:syscalls:sys_enter_openat
/comm == "com.target.app"/
{
    printf("%s opened: %s\n", comm, str(args->filename));
}'

核心概念

程序类型

eBPF 支持多种程序类型:

类型用途Android 逆向场景
kprobe/kretprobe内核函数追踪追踪内核行为
uprobe/uretprobe用户态函数追踪追踪应用函数
tracepoint内核静态追踪点系统调用追踪
socket_filter网络包过滤网络流量分析
xdp高性能网络处理网络数据包分析

Maps 数据结构

eBPF Maps 用于在内核和用户空间之间共享数据:

1
2
3
4
5
// 常用 Map 类型
BPF_HASH(counts, u32);        // 哈希表
BPF_ARRAY(data, u64, 256);    // 数组
BPF_PERF_OUTPUT(events);      // 性能事件输出
BPF_STACK_TRACE(stacks, 128); // 调用栈追踪

辅助函数

1
2
3
4
5
6
// 常用 eBPF 辅助函数
bpf_get_current_pid_tgid()    // 获取进程/线程 ID
bpf_get_current_comm()        // 获取进程名
bpf_probe_read()              // 安全读取内存
bpf_ktime_get_ns()           // 获取时间戳
bpf_trace_printk()           // 调试输出

实战场景

💡 思路一句话: eBPF 的核心优势是「内核级无感知监控」— 在内核中插入探针,应用层完全无法检测到,适合对抗强反调试/反 hook 的场景。

场景1: 追踪加密函数调用

追踪应用调用的 OpenSSL 加密函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
adb shell /data/local/tmp/bpftrace -e '
uprobe:/system/lib64/libcrypto.so:EVP_EncryptInit_ex
/comm == "com.target.app"/
{
    printf("[%d] EVP_EncryptInit_ex called\n", pid);
    printf("  ctx: %p\n", arg0);
    printf("  type: %p\n", arg1);
}

uprobe:/system/lib64/libcrypto.so:EVP_EncryptUpdate
/comm == "com.target.app"/
{
    printf("[%d] EVP_EncryptUpdate\n", pid);
    printf("  input len: %d\n", arg3);
}
'

场景2: 监控网络连接

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
adb shell /data/local/tmp/bpftrace -e '
tracepoint:syscalls:sys_enter_connect
/comm == "com.target.app"/
{
    $sa = (struct sockaddr *)args->uservaddr;
    if ($sa->sa_family == 2) { // AF_INET
        $sin = (struct sockaddr_in *)$sa;
        printf("Connect to %s:%d\n",
            ntop(AF_INET, $sin->sin_addr.s_addr),
            ntohs($sin->sin_port));
    }
}'

场景3: 文件访问监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
adb shell /data/local/tmp/bpftrace -e '
tracepoint:syscalls:sys_enter_openat
/comm == "com.target.app"/
{
    @files[str(args->filename)] = count();
}

tracepoint:syscalls:sys_enter_read
/comm == "com.target.app"/
{
    @read_bytes = sum(args->count);
}

tracepoint:syscalls:sys_enter_write
/comm == "com.target.app"/
{
    @write_bytes = sum(args->count);
}

END {
    printf("\n=== 文件访问统计 ===\n");
    print(@files);
    printf("\n读取字节: %d\n", @read_bytes);
    printf("写入字节: %d\n", @write_bytes);
}'

场景4: JNI 调用追踪

1
2
3
4
5
6
7
# 追踪 JNI 函数调用
adb shell /data/local/tmp/bpftrace -e '
uprobe:/system/lib64/libart.so:*JNI*
/comm == "com.target.app"/
{
    printf("[JNI] %s\n", probe);
}'

场景5: 检测反调试行为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 监控常见的反调试系统调用
adb shell /data/local/tmp/bpftrace -e '
tracepoint:syscalls:sys_enter_ptrace
{
    printf("[Anti-Debug] ptrace called by %s (PID: %d)\n", comm, pid);
    printf("  request: %d\n", args->request);
}

tracepoint:syscalls:sys_enter_prctl
{
    printf("[Anti-Debug] prctl called by %s\n", comm);
    printf("  option: %d\n", args->option);
}

kprobe:proc_pid_status
{
    printf("[Anti-Debug] /proc/pid/status read by %s\n", comm);
}'

高级技巧

1. 条件过滤

1
2
3
4
5
6
7
# 只追踪特定 UID 的进程
adb shell /data/local/tmp/bpftrace -e '
tracepoint:syscalls:sys_enter_*
/uid == 10123/  // 目标应用的 UID
{
    @syscalls[probe] = count();
}'

2. 调用栈追踪

1
2
3
4
5
6
7
8
# 获取完整调用栈
adb shell /data/local/tmp/bpftrace -e '
uprobe:/data/app/com.target.app-*/lib/arm64/libnative.so:suspicious_function
{
    printf("Function called!\n");
    printf("User stack:\n%s\n", ustack);
    printf("Kernel stack:\n%s\n", kstack);
}'

3. 时间统计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 统计函数执行时间
adb shell /data/local/tmp/bpftrace -e '
uprobe:/data/app/com.target.app-*/lib/arm64/libnative.so:target_function
{
    @start[tid] = nsecs;
}

uretprobe:/data/app/com.target.app-*/lib/arm64/libnative.so:target_function
/@start[tid]/
{
    $duration = nsecs - @start[tid];
    @time = hist($duration);
    delete(@start[tid]);
}'

4. 内存访问追踪

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 追踪内存分配
adb shell /data/local/tmp/bpftrace -e '
uprobe:/apex/com.android.runtime/lib64/bionic/libc.so:malloc
/comm == "com.target.app"/
{
    @malloc_size = hist(arg0);
    @total_alloc = sum(arg0);
}

uretprobe:/apex/com.android.runtime/lib64/bionic/libc.so:malloc
/comm == "com.target.app"/
{
    printf("malloc returned: %p\n", retval);
}'

BCC Python 脚本

对于更复杂的追踪需求,可以使用 BCC Python API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env python3
from bcc import BPF

# eBPF 程序
prog = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>

struct data_t {
    u32 pid;
    u64 ts;
    char comm[TASK_COMM_LEN];
    char filename[256];
};

BPF_PERF_OUTPUT(events);

int trace_openat(struct pt_regs *ctx, int dfd, const char __user *filename) {
    struct data_t data = {};

    data.pid = bpf_get_current_pid_tgid() >> 32;
    data.ts = bpf_ktime_get_ns();
    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    bpf_probe_read_user_str(&data.filename, sizeof(data.filename), filename);

    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
}
"""

# 加载 BPF 程序
b = BPF(text=prog)
b.attach_kprobe(event="do_sys_openat2", fn_name="trace_openat")

# 处理事件
def print_event(cpu, data, size):
    event = b["events"].event(data)
    print(f"[{event.pid}] {event.comm.decode()}: {event.filename.decode()}")

b["events"].open_perf_buffer(print_event)

print("Tracing file opens... Ctrl+C to exit")
while True:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        break

常见问题

Q: Android 设备不支持 eBPF?

检查内核版本和配置:

1
2
3
4
5
# 内核版本需要 >= 4.14
adb shell uname -r

# 部分厂商可能禁用了 BPF
adb shell zcat /proc/config.gz | grep BPF

解决方案:

  1. 使用自定义内核 (如 LineageOS)
  2. 使用 Android 模拟器 (支持更好)
  3. 使用特定的 Root 方案

Q: bpftrace 报错 “Failed to attach”?

1
2
3
4
5
6
7
8
# 检查 SELinux 状态
adb shell getenforce

# 临时禁用 (需要 root)
adb shell setenforce 0

# 检查内核符号
adb shell cat /proc/kallsyms | head

Q: 如何追踪特定应用?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 方法1: 使用进程名
/comm == "com.target.app"/

# 方法2: 使用 PID
/pid == 12345/

# 方法3: 使用 UID
/uid == 10123/

# 获取应用 UID
adb shell dumpsys package com.target.app | grep userId

工具推荐

1. bpftrace

2. BCC

3. libbpf

4. eCapture

5. Tracee


延伸阅读


参考资料


相关内容

如果这篇文章对你有帮助,请我喝杯咖啡吧~
+5 支付宝支付宝
+5 微信微信