定制 ART 以绕过反调试

为什么要定制 ART

在 Android 逆向工程中,反调试(Anti-Debugging)是 APP 最常见的自我保护手段之一。APP 通过各种方式检测自己是否正在被调试,如果检测到调试器的存在,就会采取保护措施(如退出进程、返回错误数据、触发反作弊等)。

常见的反调试手段包括:

  • 检查 TracerPid 字段
  • 检测 ptrace 系统调用
  • 时间差检测
  • 检测调试端口
  • 检测 Frida / Xposed 等工具

虽然可以通过 Hook 等手段在应用层绕过这些检测,但存在以下问题:

  1. Hook 本身也可能被检测:APP 可以检查 Hook 框架的特征
  2. 检测时机不可控:APP 可能在多个位置、多个时机进行检测
  3. 绕过成本高:每个 APP 都需要单独编写绕过脚本

定制 ART(Android Runtime)是更彻底的解决方案——直接在系统层面修改,让所有反调试检测"天然"失效。这种方法对上层应用透明,一次修改,全局生效。

常见反调试技术及其原理

1. TracerPid 检测

TracerPid/proc/self/status 文件中的一个字段。当一个进程被 ptrace 附加(如调试器附加)时,TracerPid 字段会显示调试器的 PID。

# 正常进程
$ cat /proc/1234/status | grep TracerPid
TracerPid:    0

# 被调试的进程
$ cat /proc/1234/status | grep TracerPid
TracerPid:    5678    # 5678 是调试器的 PID

APP 通过读取并检查 TracerPid 是否为 0 来判断是否被调试:

// C 层实现
int is_debugged() {
    FILE* f = fopen("/proc/self/status", "r");
    char line[256];
    while (fgets(line, sizeof(line), f)) {
        if (strncmp(line, "TracerPid:", 10) == 0) {
            int pid = atoi(line + 10);
            fclose(f);
            return pid != 0;  // TracerPid 非零说明被调试
        }
    }
    fclose(f);
    return 0;
}
// Java 层实现
public boolean isDebugged() {
    try {
        BufferedReader reader = new BufferedReader(
            new FileReader("/proc/self/status"));
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.startsWith("TracerPid:")) {
                int pid = Integer.parseInt(line.split(":")[1].trim());
                reader.close();
                return pid != 0;
            }
        }
        reader.close();
    } catch (Exception e) {}
    return false;
}

2. ptrace 检测

Linux 中,一个进程只能被一个调试器 ptrace 附加。APP 可以通过自己 ptrace 自己来"占位",阻止其他调试器附加:

// 反调试的经典技巧:自己 ptrace 自己
void anti_ptrace() {
    if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
        // ptrace 失败,说明已有调试器附加
        exit(0);  // 退出进程
    }
    // 成功后不能再被其他调试器附加
}

更高级的 ptrace 检测会检查 ptrace 返回值的 errno:

void advanced_ptrace_check() {
    // 先尝试 PTRACE_ATTACH 自己的子进程
    pid_t child = fork();
    if (child == 0) {
        // 子进程
        sleep(1);
        exit(0);
    }
    
    // 父进程尝试 ptrace 子进程
    int ret = ptrace(PTRACE_ATTACH, child, NULL, NULL);
    if (ret < 0 && errno == EPERM) {
        // 权限被拒绝,可能是因为子进程已经被调试
        kill(child, SIGKILL);
        exit(0);
    }
    
    waitpid(child, NULL, 0);
    ptrace(PTRACE_DETACH, child, NULL, NULL);
}

3. 时间检测

调试器在执行时会产生额外的时间开销。APP 通过测量关键代码段的执行时间,如果时间异常地长,则认为正在被调试:

// C 层时间检测
void time_check() {
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);
    
    // 执行一段计算
    volatile int sum = 0;
    for (int i = 0; i < 10000; i++) {
        sum += i;
    }
    
    clock_gettime(CLOCK_MONOTONIC, &end);
    long elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000L 
                    + (end.tv_nsec - start.tv_nsec);
    
    if (elapsed_ns > 10000000L) {  // 超过 10ms 认为被调试
        // 正常执行不应超过 1ms
        exit(0);
    }
}

4. 检测调试端口和特征文件

APP 可能检查以下调试特征:

void check_debug_features() {
    // 检查 Frida 默认端口
    FILE* f = fopen("/proc/net/tcp", "r");
    // 检查 27042 端口是否开放(Frida 默认端口)
    
    // 检查 Frida 特征文件
    const char* frida_files[] = {
        "/tmp/frida-server",
        "/data/local/tmp/frida-server",
        "/data/local/tmp/frida-socket",
        NULL
    };
    
    // 检查 Xposed 特征
    const char* xposed_files[] = {
        "/system/framework/XposedBridge.jar",
        "/system/lib/libxposed_art.so",
        NULL
    };
    
    // 检查 Maps 文件中的可疑条目
    f = fopen("/proc/self/maps", "r");
    // 搜索 "frida" "xposed" "substrate" 等关键字
}

在 ART 源码中定位反调试检测点

AOSP 源码获取

# 获取 Android 源码(以 Android 12 为例)
repo init -u https://android.googlesource.com/platform/manifest -b android-12.0.0_r34
repo sync

# ART 源码路径
cd art/

关键文件位置

ART 中与反调试相关的关键文件:

art/
├── runtime/
│   ├── art_method.cc              # JNI/Java 方法执行
│   ├── debugger.cc                # JDWP 调试器支持
│   ├── jni/
│   │   ├── java_lang_Runtime.cc   # Runtime.exec 等实现
│   │   └── java_lang_Process.cc   # 进程相关实现
│   ├── native/
│   │   └── java_lang_Runtime.cc   # Runtime 类的 native 方法
│   ├── os.h                       # 系统调用封装
│   ├── procfs.cc                  # /proc 文件系统操作
│   └── thread.cc                  # 线程管理
└── libcore/
    └── luni/
        └── src/
            └── main/
                └── java/
                    └── java/
                        └── lang/
                            └── Runtime.java

修改 ART 绕过 ptrace 检测

定位 ptrace 调用

在 ART 中,ptrace 调用可能出现在多个位置。首先搜索源码:

cd art/
grep -rn "ptrace" --include="*.cc" --include="*.h"

主要关注以下调用:

  • PTRACE_TRACEME:进程主动请求被跟踪
  • PTRACE_ATTACH:附加到目标进程
  • PTRACE_DETACH:分离

修改 ptrace 返回值

runtime/os.hruntime/oat/runtime_arch.cc 中找到 ptrace 的系统调用封装:

// 原始代码(简化)
pid_t Syscall::ptrace(int request, pid_t pid, void* addr, void* data) {
    return ::ptrace(request, pid, addr, data);
}

修改为始终返回成功:

// 修改后:绕过 ptrace 检测
pid_t Syscall::ptrace(int request, pid_t pid, void* addr, void* data) {
    // 对于 TRACEME 请求,始终返回成功
    if (request == PTRACE_TRACEME) {
        return 0;  // 假装 ptrace 成功
    }
    
    // 对于 ATTACH 请求,也返回成功
    if (request == PTRACE_ATTACH) {
        return 0;  // 不阻止调试器附加
    }
    
    // 其他请求正常执行
    return ::ptrace(request, pid, addr, data);
}

在 ART 级别拦截

另一种方式是在 ART 的 JNI 层面拦截:

// 修改 art/runtime/jni/java_lang_Runtime.cc

// 拦截 Runtime.exec(),防止 APP 通过 fork + exec 启动反调试进程
static jobject Runtime_execInternal(JNIEnv* env, jobject, jobjectArray cmdArray, ...) {
    // 检查执行的命令是否包含反调试检测
    // ...
    return original_exec(env, cmdArray, ...);
}

伪造 /proc/self/status 信息

定位文件读取逻辑

APP 读取 /proc/self/status 通常通过以下路径:

  1. C 层fopen("/proc/self/status", "r") → 系统调用 openat + read
  2. Java 层new FileReader("/proc/self/status") → 最终也是系统调用

方法一:内核层面修改

如果你有内核源码访问权限,可以在内核中修改 proc 文件系统的输出:

// 内核源码:fs/proc/array.c
static int proc_pid_status(struct seq_file *m, struct pid_namespace *ns,
                           struct pid *pid, struct task_struct *task) {
    // ...
    seq_printf(m, "TracerPid:\t%d\n", ...);
    // ...
}

TracerPid 始终输出为 0:

// 修改后
seq_printf(m, "TracerPid:\t0\n");  // 始终返回 0

方法二:ART 层面拦截文件读取

在 ART 中拦截 /proc/self/status 的读取:

// 修改 art/runtime/runtime.cc 或 art/runtime/thread.cc

// 在文件读取时检查路径,如果是 /proc/self/status,则过滤 TracerPid
static ssize_t hooked_read(int fd, void* buf, size_t count) {
    ssize_t ret = original_read(fd, buf, count);
    
    // 检查是否读取的是 /proc/self/status
    char path[256];
    char link[256];
    snprintf(link, sizeof(link), "/proc/self/fd/%d", fd);
    ssize_t len = readlink(link, path, sizeof(path) - 1);
    if (len > 0) {
        path[len] = '\0';
        if (strcmp(path, "/proc/self/status") == 0 ||
            strstr(path, "/proc/") != NULL) {
            // 在缓冲区中查找并替换 TracerPid
            char* content = (char*)buf;
            char* tracer = strstr(content, "TracerPid:");
            if (tracer != NULL) {
                // 找到 TracerPid 行,替换为 0
                char* newline = strchr(tracer, '\n');
                if (newline != NULL) {
                    // 保留 "TracerPid:\t",将后面的数字替换为 0
                    char* tab = strchr(tracer, '\t');
                    if (tab != NULL) {
                        // 移动内容,将 PID 替换为 0
                        int pid_len = newline - tab - 1;
                        memmove(tab + 2, newline, strlen(newline) + 1);
                        tab[1] = '0';
                        tab[2] = '\n';
                        ret -= (pid_len - 1);
                    }
                }
            }
        }
    }
    
    return ret;
}

方法三:使用 seccomp-bpf 过滤

在 ART 启动时安装 seccomp-bpf 规则,过滤对 /proc/self/status 的访问:

// 在 ART 初始化时安装 seccomp 过滤
void install_proc_filter() {
    struct sock_filter filter[] = {
        // 允许除了 openat /proc/self/status 之外的所有操作
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
        // ... BPF 规则定义
    };
    
    struct sock_fprog prog = {
        .len = sizeof(filter) / sizeof(filter[0]),
        .filter = filter,
    };
    
    syscall(SYS_seccomp, SECCOMP_SET_MODE_FILTER, 0, &prog);
}

处理时间检测反调试

时间检测反调试通过测量执行时间来判断是否被调试。在 ART 层面处理:

修改 clock_gettime

// 修改 art/runtime/runtime.cc 或系统调用层

// 记录时间检测的基线
static bool time_check_active = false;
static struct timespec time_check_start;

// Hook clock_gettime
static int hooked_clock_gettime(clockid_t clk_id, struct timespec* tp) {
    int ret = original_clock_gettime(clk_id, tp);
    
    if (clk_id == CLOCK_MONOTONIC && time_check_active) {
        // 缩短时间差,使时间检测失效
        // 保留时间精度但压缩差值
        // 注意:这只是一种方案,需要更精细的控制
    }
    
    return ret;
}

更优雅的方案:在编译器层面优化

在 ART 编译 Dalvik 字节码时,识别出时间检测模式并消除:

// 修改 art/compiler/dex/optimizing_compiler.cc

// 识别以下模式:
// invoke-static System.currentTimeMillis() → vA
// invoke-static System.nanoTime() → vB
// ... 一些代码 ...
// invoke-static System.currentTimeMillis() → vC
// sub vD, vC, vA
// if-gt vD, threshold → exit
//
// 将其优化为 NOP(空操作)

处理 fork / Runtime.exec 的反调试

APP 可能通过 fork() 创建子进程来进行反调试检测(子进程检查父进程的 TracerPid,或子进程 ptrace 父进程)。

拦截 fork

// 修改 ART 的 fork 实现

// art/runtime/runtime.cc
pid_t Runtime::ForkAndSpecializeCommon(...) {
    // 在 fork 之前,临时清除 TracerPid
    // fork 之后,子进程中 TracerPid 为 0
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        // 确保子进程无法检测到调试
        // 可以修改子进程的 /proc/pid/status
        
        return pid;
    }
    
    return pid;
}

拦截 Runtime.exec

// 修改 libcore/luni/src/main/java/java/lang/Runtime.java

public Process exec(String[] cmdarray, ...) throws IOException {
    // 检查命令是否是反调试相关的
    for (String cmd : cmdarray) {
        if (is_anti_debug_command(cmd)) {
            // 返回一个假的 Process,不实际执行
            return createFakeProcess();
        }
    }
    return execInternal(cmdarray, ...);
}

编译和刷入定制 ART

编译 ART

# 在 AOSP 源码根目录
source build/envsetup.sh
lunch aosp_arm64-userdebug

# 仅编译 ART 模块
make art -j$(nproc)

# 编译产物路径
# out/target/product/<device>/system/lib64/libart.so
# out/target/product/<device>/system/lib/libart.so

编译完整系统镜像

# 编译完整系统(包含定制 ART)
make -j$(nproc)

# 产物
# out/target/product/<device>/system.img

刷入设备

# 进入 fastboot 模式
adb reboot bootloader

# 刷入系统镜像
fastboot flash system system.img

# 重启
fastboot reboot

仅替换 ART SO 文件(更快的方式)

如果你只想替换 ART 的 SO 文件而不刷入完整系统:

# 重新挂载 system 为可写
adb root
adb remount

# 推送定制的 libart.so
adb push out/target/product/<device>/system/lib64/libart.so /system/lib64/
adb push out/target/product/<device>/system/lib/libart.so /system/lib/

# 重启
adb reboot

测试验证绕过效果

测试 TracerPid 检测

// 测试代码:读取 /proc/self/status 中的 TracerPid
public void testTracerPid() {
    try {
        BufferedReader reader = new BufferedReader(
            new FileReader("/proc/self/status"));
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.startsWith("TracerPid:")) {
                Log.d("TEST", line);
                // 预期:TracerPid:    0(即使正在被调试)
                break;
            }
        }
        reader.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

使用 IDA 附加到进程后运行此测试,确认 TracerPid 始终返回 0。

测试 ptrace 检测

// 测试代码:尝试 ptrace 自己
public native int testPtrace();

// Native 实现
int testPtrace() {
    long ret = ptrace(PTRACE_TRACEME, 0, NULL, NULL);
    // 预期:ret == 0(始终成功)
    return (int)ret;
}

测试时间检测

// 测试代码
public void testTimeCheck() {
    long start = System.nanoTime();
    
    // 模拟被调试时的延迟
    volatile int sum = 0;
    for (int i = 0; i < 1000000; i++) {
        sum += i;
    }
    
    long end = System.nanoTime();
    long elapsed = end - start;
    
    Log.d("TEST", "Elapsed: " + elapsed + " ns");
    // 即使在调试状态下,时间也应该正常
}

综合测试

编写一个包含多种反调试检测的测试 APP,在定制 ART 环境下运行:

public class AntiDebugTest extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        boolean debugged = false;
        
        // 测试1:TracerPid
        if (checkTracerPid()) debugged = true;
        
        // 测试2:ptrace
        if (checkPtrace()) debugged = true;
        
        // 测试3:时间检测
        if (checkTime()) debugged = true;
        
        // 测试4:端口检测
        if (checkPorts()) debugged = true;
        
        // 测试5:特征文件检测
        if (checkFiles()) debugged = true;
        
        Log.d("TEST", "Debugged: " + debugged);
        // 预期:即使附加了调试器,所有检测都应返回 false
    }
}

注意事项与风险

稳定性风险

  • 修改 ART 可能导致系统不稳定
  • 部分修改可能导致某些正常功能异常
  • 建议在测试设备上进行,不要在生产设备上使用

版本兼容性

  • 不同 Android 版本的 ART 源码差异较大
  • 修改需要针对特定版本进行
  • Android 系统更新后需要重新编译

法律和道德考量

  • 定制 ART 仅应用于安全研究和学习
  • 不得用于非法目的
  • 尊重软件开发者的知识产权

总结

定制 ART 绕过反调试是一种系统级的解决方案。通过修改 ptrace 返回值、伪造 /proc/self/status 中的 TracerPid、处理时间检测和 fork/exec 反调试等手段,可以在 ART 层面彻底绕过常见的反调试检测。相比应用层的 Hook 方案,定制 ART 更加隐蔽和通用,但需要更多的编译和刷机工作。在实际项目中,需要根据具体需求选择合适的绕过方案。