Android 侦探事务所:bugreport 的取证故事

59 阅读9分钟

一、案件启动:指挥官与收集员的协作

在 Android 系统的侦探事务所里,bugreport 是经验丰富的指挥官,它的任务是在系统出现问题时,快速收集所有线索。当用户输入 adb bugreport 命令时,就像侦探事务所接到了紧急案件。

cpp

运行

int main() {
    // 启动dumpstate收集员服务
    property_set("ctl.start", "dumpstate");
    // 尝试20次联系收集员,每次间隔1秒
    int s;
    for (int i = 0; i < 20; i++) {
        s = socket_local_client("dumpstate", ANDROID_SOCKET_NAMESPACE_RESERVED, SOCK_STREAM);
        if (s >= 0) break;
        sleep(1);
    }
    // 设置3分钟超时,没收到线索就结束
    struct timeval tv = {3*60, 0};
    setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    // 持续接收收集员发来的线索并记录
    while (1) {
        char buffer[65536];
        ssize_t bytes_read = read(s, buffer, sizeof(buffer));
        if (bytes_read <= 0) break;
        write(STDOUT_FILENO, buffer, bytes_read);
    }
}

故事版解析

  • 指挥官 bugreport 首先给调度中心(init 进程)发消息,让 dumpstate 收集员准备工作
  • 指挥官用 "电话"(socket)联系收集员,打了 20 次才接通
  • 指挥官设定了 3 分钟的通话超时,如果收集员没说话就挂断
  • 收集员不断把线索通过电话传给指挥官,指挥官记录到笔记本上(标准输出)

二、收集员的准备工作:装备升级与权限管理

dumpstate 收集员接到任务后,开始做准备工作,确保自己不会被打断,并且有足够的权限收集线索。

cpp

运行

int main(int argc, char *argv[]) {
    // 检查权限:如果不是root,就强制调用bugreport(老版本兼容)
    if (getuid() != 0) {
        return execl("/system/bin/bugreport", "/system/bin/bugreport", NULL);
    }
    // 提高优先级,防止被OOM杀手(系统清理程序)赶走
    setpriority(PRIO_PROCESS, 0, -20);
    FILE *oom_adj = fopen("/proc/self/oom_adj", "we");
    if (oom_adj) { fputs("-17", oom_adj); fclose(oom_adj); }
    // 解析命令参数,比如是否要添加时间、输出到文件等
    while ((c = getopt(argc, argv, "dho:svqzpB")) != -1) {
        // 处理不同参数...
    }
    // 打开振动器,作为开始收集的提醒(手机震动)
    FILE *vibrator = fopen("/sys/class/timed_output/vibrator/enable", "we");
    if (vibrator) vibrate(vibrator, 150);
    // 收集前先记录系统启动命令行
    FILE *cmdline = fopen("/proc/cmdline", "re");
    if (cmdline) { fgets(cmdline_buf, sizeof(cmdline_buf), cmdline); fclose(cmdline); }
    // 收集虚拟机和native进程的堆栈线索
    dump_traces_path = dump_traces();
    // 获取tombstone(崩溃日志)文件描述符
    get_tombstone_fds(tombstone_data);
    // 切换到非root用户,但保留必要权限
    gid_t groups[] = { AID_LOG, AID_SDCARD_R, AID_SDCARD_RW, AID_MOUNT, AID_INET, AID_NET_BW_STATS };
    setgroups(sizeof(groups)/sizeof(groups[0]), groups);
    setgid(AID_SHELL); setuid(AID_SHELL);
    // 最终开始核心收集工作
    dumpstate();
}

故事版解析

  • 收集员检查身份:如果不是侦探所长(root),就叫真正的指挥官来
  • 收集员穿上 "防驱逐背心":提高优先级,让系统别把自己赶走
  • 戴上 "参数眼镜":根据不同参数(如 -d 加时间、-o 存文件)调整工作方式
  • 按下 "震动警报":告诉手机开始工作了(手机震动一下)
  • 先查看 "系统入场券"(/proc/cmdline),记录系统启动时的参数
  • 拿出 "堆栈扫描仪"(dump_traces)和 "崩溃日志探测器"(tombstone_fds)
  • 换上 "普通工作服"(非 root 用户),但保留查看日志等必要权限
  • 最后拿出 "核心收集手册",开始正式收集线索

三、核心取证:收集员的现场调查

dumpstate() 是收集员的核心工作,它会遍历系统的各个角落,收集所有可能的线索。

cpp

运行

static void dumpstate() {
    // 先记录系统基本信息
    printf("========================================================\n");
    printf("== dumpstate: %s\n", date);
    printf("Build: %s\n", build);
    printf("Build fingerprint: '%s'\n", fingerprint);
    // 记录系统运行时长
    run_command("UPTIME", 10, "uptime", NULL);
    // 内存信息
    dump_file("MEMORY INFO", "/proc/meminfo");
    // CPU和进程信息
    run_command("CPU INFO", 10, "top", "-n", "1", "-d", "1", "-m", "30", "-t", NULL);
    run_command("PROCRANK", 20, "procrank", NULL);
    // 内核日志
    do_dmesg();
    // 系统日志(logcat)
    run_command("SYSTEM LOG", timeout/1000, "logcat", "-v", "threadtime", "-d", "*:v", NULL);
    // 虚拟机堆栈线索(ANR等)
    if (dump_traces_path) dump_file("VM TRACES JUST NOW", dump_traces_path);
    // tombstone崩溃日志
    for (size_t i = 0; i < NUM_TOMBSTONES; i++) {
        if (tombstone_data[i].fd != -1) {
            dump_file_from_fd("TOMBSTONE", tombstone_data[i].name, tombstone_data[i].fd);
        }
    }
    // 网络信息
    run_command("NETWORK INTERFACES", 10, "ip", "link", NULL);
    // Binder通信信息(Android特有的进程通信)
    dump_file("BINDER STATS", "/sys/kernel/debug/binder/stats");
    // 最重要的dumpsys信息,收集所有系统服务状态
    run_command("DUMPSYS", 60, "dumpsys", NULL);
    // 应用相关信息
    run_command("APP ACTIVITIES", 30, "dumpsys", "activity", "all", NULL);
}

故事版解析

  • 收集员先填写 "案件基本信息表":系统版本、编译时间等
  • 查看 "系统运行时间表"(uptime),了解系统已经运行了多久
  • 进入 "内存仓库"(/proc/meminfo),记录内存使用情况
  • 到 "CPU 调度室"(top 命令),查看各个进程的 CPU 占用
  • 前往 "内核日志档案馆"(do_dmesg),调取内核启动以来的所有日志
  • 打开 "系统日志数据库"(logcat),导出系统运行日志
  • 检查 "虚拟机犯罪现场"(dump_traces),收集 ANR 和崩溃堆栈
  • 走访 "崩溃日志墓地"(tombstone),查看近期的程序崩溃记录
  • 到 "网络通信部"(ip 命令),记录网络连接状态
  • 调查 "Binder 通信中心"(binder stats),了解进程间通信情况
  • 最重要的环节:突击 "系统服务总部"(dumpsys),收集所有系统服务的状态
  • 最后检查 "应用活动现场"(dumpsys activity),查看哪些应用在运行

四、关键工具:收集员的取证装备

1. run_command:派遣手下执行任务

cpp

运行

int run_command(const char *title, int timeout_seconds, const char *command, ...) {
    // fork子进程执行命令
    pid_t pid = fork();
    if (pid == 0) {  // 子进程(手下)执行
        prctl(PR_SET_PDEATHSIG, SIGKILL);  // 确保自己能被杀死
        struct sigaction sigact; memset(&sigact, 0, sizeof(sigact));
        sigact.sa_handler = SIG_IGN; sigaction(SIGPIPE, &sigact, NULL);  // 忽略中断
        // 组装命令参数
        const char *args[1024] = {command};
        va_list ap; va_start(ap, command);
        for (size_t arg = 1; arg < 1024; ++arg) {
            args[arg] = va_arg(ap, const char *);
            if (args[arg] == NULL) break;
        }
        // 执行命令
        execvp(command, (char**)args);
        _exit(-1);  // 执行失败就退出
    } else {  // 父进程(收集员)等待
        int status;
        bool ret = waitpid_with_timeout(pid, timeout_seconds, &status);
        if (!ret) {  // 超时就杀死子进程
            kill(pid, SIGTERM);
            if (!waitpid_with_timeout(pid, 5, NULL)) kill(pid, SIGKILL);
        }
    }
}

故事版解析

  • 收集员派手下(子进程)去执行具体任务(如 top、dumpsys)
  • 给手下戴上 "自杀手环"(PR_SET_PDEATHSIG),任务完成或超时就自动结束
  • 手下组装好任务清单(命令参数),然后出发执行(execvp)
  • 收集员在原地等待,超时就发信号让手下回来,不听话就强行召回(SIGKILL)

2. dump_file:读取文件线索

cpp

运行

int dump_file(const char *title, const char *path) {
    // 打开文件
    int fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
    if (fd < 0) {  // 打不开就记录错误
        if (title) printf("------ %s (%s) ------\n", title, path);
        printf("*** %s: %s\n", path, strerror(errno));
        return -1;
    }
    // 读取文件内容并输出
    return _dump_file_from_fd(title, path, fd);
}

static int _dump_file_from_fd(const char *title, const char *path, int fd) {
    if (title) printf("------ %s (%s", title, path);
    // 如果是/proc或/sys文件,显示修改时间
    if (memcmp(path, "/proc/", 6) && memcmp(path, "/sys/", 5) && !fstat(fd, &st)) {
        char stamp[80];
        strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S", localtime(&st.st_mtime));
        printf(": %s", stamp);
    }
    printf(") ------\n");
    // 读取文件内容,30秒没数据就超时
    fd_set read_set; struct timeval tm = {30, 0};
    while (1) {
        FD_ZERO(&read_set); FD_SET(fd, &read_set);
        int ret = select(fd + 1, &read_set, NULL, NULL, &tm);
        if (ret <= 0) break;  // 超时或错误
        char buffer[65536];
        ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
        if (bytes_read > 0) fwrite(buffer, bytes_read, 1, stdout);
    }
    close(fd);
}

故事版解析

  • 收集员来到文件柜(path)前,尝试打开抽屉(open 文件)
  • 如果抽屉锁了(权限问题),就记录 "无法打开抽屉:原因..."
  • 如果打开了,就查看抽屉标签(title),并标注最后一次整理时间(修改时间)
  • 开始翻阅抽屉里的文件,每 30 秒没翻到新内容就停止

3. dump_traces:收集堆栈犯罪证据

cpp

运行

const char *dump_traces() {
    // 先保护上次的ANR日志
    property_get("dalvik.vm.stack-trace-file", traces_path, "");
    strlcpy(anr_traces_path, traces_path, sizeof(anr_traces_path));
    strlcat(anr_traces_path, ".anr", sizeof(anr_traces_path));
    rename(traces_path, anr_traces_path);
    // 创建新的日志文件
    int fd = open(traces_path, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    // 遍历所有进程,收集堆栈
    DIR *proc = opendir("/proc");
    while ((d = readdir(proc))) {
        int pid = atoi(d->d_name);
        if (pid <= 0) continue;
        // 读取进程信息
        char path[PATH_MAX];
        readlink("/proc/%d/exe", pid, data, sizeof(data)-1);
        // 如果是Java进程(app_process),发送SIGQUIT信号让它dump堆栈
        if (!strncmp(data, "/system/bin/app_process", ...)) {
            if (!strncmp(data, "zygote", ...)) continue;  // 跳过zygote
            kill(pid, SIGQUIT);  // 发送信号,让Java进程dump堆栈
            // 等待5秒,接收dump结果
            struct pollfd pfd = { ifd, POLLIN, 0 };
            poll(&pfd, 1, 5000);
        } else if (should_dump_native_traces(data)) {
            // 本地进程用debuggerd dump堆栈,超时20秒
            dump_backtrace_to_file_timeout(pid, fd, 20);
        }
    }
    // 重命名日志文件,保存本次收集结果
    strlcpy(dump_traces_path, traces_path, sizeof(dump_traces_path));
    strlcat(dump_traces_path, ".bugreport", sizeof(dump_traces_path));
    rename(traces_path, dump_traces_path);
    // 恢复上次的ANR日志
    rename(anr_traces_path, traces_path);
    return dump_traces_path;
}

故事版解析

  • 收集员先把上次的 ANR 日志(老证据)打包藏好(.anr 后缀)

  • 拿出新的笔记本(traces.txt)准备记录新证据

  • 走进 "进程大厦"(/proc 目录),逐个房间(进程)检查:

    • 看到 Java 程序的房间(app_process),就敲敲门(SIGQUIT 信号),让里面的程序把堆栈记录下来
    • 看到本地程序的房间,就找 debuggerd 帮忙,花 20 秒时间收集堆栈
  • 收集完后,把新证据笔记本重命名(.bugreport)保存

  • 最后把老证据笔记本放回原处(恢复 traces.txt)

五、案件总结:bugreport 的证据清单

通过以上步骤,bugreport 最终会生成一份包含五大类证据的报告:

  1. 当前日志:内核日志、系统日志、事件日志、无线电日志

  2. 历史日志:上次内核崩溃日志、上次系统日志

  3. 虚拟机线索:当前堆栈、上次 ANR 堆栈、崩溃墓碑日志

  4. 系统服务状态:所有系统服务 dump、电池 / 内存 / 网络统计

  5. 应用现场:正在运行的 Activity、Service、Provider

每类证据都以 ------ 证据类别 ------ 开头,就像侦探在报告里分章节记录线索,方便后续分析。

六、侦探事务所的工作流程总结

  1. 指挥官 bugreport 启动收集员 dumpstate,建立通信

  2. 收集员做好准备工作,提高优先级,获取必要权限

  3. 收集员遍历系统各角落,使用不同工具收集各类证据

  4. 收集员将证据通过 socket 传给指挥官,指挥官记录到文件

  5. 最终生成包含系统全面信息的 bugreport,为分析 bug 提供依据

通过这个故事,我们可以把复杂的 bugreport 源码流程理解为侦探事务所的一次案件调查,每个代码模块都是侦探的不同工具和步骤,最终目的是收集足够的线索来解决系统问题。