Android 应用程序如何抓取 Coredump

项目介绍

github.com/Penguin38/O…

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}
dependencies {
    ...
    implementation 'com.github.Penguin38:OpenCoreSDK:opencore-1.2.8'
}
{
    //  初始化组件
    Coredump.getInstance().init();

    //  设置模式
    Coredump.getInstance().setCoreMode(Coredump.MODE_PTRACE | Coredump.MODE_COPY);
    // Coredump.getInstance().setCoreMode(Coredump.MODE_PTRACE);
    // Coredump.getInstance().setCoreMode(Coredump.MODE_COPY);



    //  设置 Coredump 保存目录
    Coredump.getInstance().setCoreDir(...);
   
    //  Java Crash 生成 Coredump
    Coredump.getInstance().enable(Coredump.JAVA);
    
    //  Native Crash 生成 Coredump
    Coredump.getInstance().enable(Coredump.NATIVE);
    
    //  设置监听器
    Coredump.getInstance().setListener(new Coredump.Listener() {
        @Override
        public void onCompleted(String path) {
                // do anything
        }
    });

    //  主动生成当前时刻 Coredump
    Coredump.getInstance().doCoredump();
}

文件格式

UML diagram (16).jpg

名字 用途
ELF Header ELF 头部信息,记录该 ELF 文件类型为 Core,指令集类型等信息,由数据结构 ElfN_Ehdr N=32,64组成,对应的信息可用 readelf -h 查看
Program Headers 记录该 Core 文件所有的段信息,包含每一个段在文件内的偏移,以及对应的虚拟地址偏移,段大小等信息,每一个段为数据结构 ElfN_Phdr 组成,对应的信息可用 readelf -l 查看
PT_NOTE 记录该程序的线程状态信息,线程 TID、寄存器信息等,辅助调试信息 AUXV,对应的信息可用 readelf -n 查看
PT_LOAD 与程序的 /proc/self/maps 一一对应,记录的是程序运行时虚拟内存空间,存放内存元数据
AUXV 记录着执行程序的 PHDR 地址(AT_PHDR)ElfN_Phdr 数据大小(AT_PHENT),以及执行程序共有多少个段(AT_PHENT),这三个数据是调试器所需要的,用于找到 link_map 信息。链接地址(AT_BASE Android 平台则是 /system/bin/linker 地址),此处信息与 /proc/self/auxv 一一对应

工作原理

核心技术依赖 Linux 写时复制机制用于父子进程内存拷贝,以及 ptrace 系统调用,它可用于挂入到目标进程上,并且可访问进程内存、寄存器等信息。程序需访问的文件节点:

节点 项目用途
/proc/<PID>/maps 程序虚拟内存空间,用于解析生成 Core 文件的 Program Headers
/proc/<PID>/auxv 程序辅助调试信息,用于解析生成 Note 段的 AUXV 部分
/proc/<PID/task/* 程序的线程信息,用于解析生成 Note 段的 Register 信息,以及暂停线程

PTRACE API介绍

API官方文档使用:man7.org/linux/man-p…
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

参数类型 描述
PTRACE_ATTACH 挂入到目标PID进程上, 使目标PID进程成为调用进程的tracee,并可向tracee进程发送SIGSTOP信号来终止进程,不必等调用结束再来终止tracee,可使用waitpid(2)来等tracee结束
PTRACE_TRACEME 用于父子进程之间,表示该进程可被父进程跟踪
PTRACE_PEEKTEXT
PTRACE_PEEKDATA
PTRACE_PEEKUSER
允许跟踪进程读取被跟踪进程的虚拟内存地址
PTRACE_POKETEXT
PTRACE_POKEDATA
PTRACE_POKEUSER
允许跟踪进程修改被跟踪进程的虚拟内存地址
PTRACE_GETREGSET 允许跟踪进程读取被跟踪进程的寄存器信息
PTRACE_SETREGSET 允许跟踪进程修改被跟踪进程的寄存器信息
PTRACE_CONT 重启已经被终止的被跟踪进程
PTRACE_DETACH 会重启终止的被跟踪进程,并解除跟踪
PTRACE_SYSCALL 重启被跟踪进程,在下一个系统调用开始/退出时终止该进程。如strace工具
PTRACE_SINGLESTEP 重启进程,并且在下一条指令运行结束后切换到终止状态。如单步调试
PTRACE_GETSIGINFO 获取引起进程停止的信号信息,可获取siginfo_t结构信息进行修改,通过SETSIGINFO回传
PTRACE_SETSIGINFO
PTRACE_SETOPTIONS

暂停线程工作

使用 PTRACE_ATTACH 命令即可将目标线程暂停下来,线程进入 T 状态,在内核态上函数将会停留在 ptrace_stop 函数上等待,直到跟踪进程退出或接收到 PTRACE_CONTPTRACE_DETACH 命令恢复状态。

void OpencoreImpl::StopAllThread(pid_t pid)
{
    char task_dir[32];
    struct dirent *entry;
    snprintf(task_dir, sizeof(task_dir), "/proc/%d/task", pid);
    DIR *dp = opendir(task_dir);
    if (dp) {
        while ((entry=readdir(dp)) != NULL) {
            if (!strncmp(entry->d_name, ".", 1)) {
                continue;
            }


            pid_t tid = atoi(entry->d_name);
            if (ptrace(PTRACE_ATTACH, tid, NULL, 0) < 0) {
                JNI_LOGI("%s %d: %s\n", __func__ , tid, strerror(errno));
                continue;
            }
            int status = 0;
            waitpid(tid, &status, WUNTRACED);
        }
    }
}

获取线程寄存器

使用 PTRACE_GETREGSET 命令获取进程各个线程的寄存器信息,实际上内核返回的是线程内核态的上下文信息,对应的内核数据结构为 pt_regs

UML 图 (7).jpg

...

            int index = 0;
            while((entry=readdir(dp)) != NULL) {
                if(!strncmp(entry->d_name, ".", 1))
                    continue;

                pid_t tid = atoi(entry->d_name);
                prstatus[index].pr_pid = tid;


                uintptr_t regset = 1;
                struct iovec ioVec;


                ioVec.iov_base = &prstatus[index].pr_reg;
                ioVec.iov_len = sizeof(core_arm64_pt_regs);

                if (ptrace(PTRACE_GETREGSET, tid, regset, &ioVec) < 0) {
                    JNI_LOGI("%s %d: %s\n", __func__ , tid, strerror(errno));
                    index++;
                    continue;
                }

                index++;
            }
...

PTRACE 读取内存

使用 ptrace 读取内存缺点在于非常的慢,原因是每次 syscall 只能访问 4、8 字节,取决于目标操作系统是 32 位还是 64 位,每一段内存的转储过程中都需要进行反复遍历,受限与 ptrace 读取能力,要想完整的保存 Android 应用程序的内存,短则 5 min 起步,长则 20 min 左右。

...

    while(index < ehdr.e_phnum - 1) {
        if (phdr[index].p_filesz > 0) {
            switch (mode) {
                case MODE_PTRACE: {
                    Elf64_Addr target = phdr[index].p_vaddr;
                    while (target < phdr[index].p_vaddr + phdr[index].p_memsz) {
                        long mem = ptrace(PTRACE_PEEKTEXT, pid, target, 0x0);
                        fwrite(&mem, sizeof(mem), 1, fp);
                        target = target + sizeof(Elf64_Addr);
                    }
                }
...

父子进程内存拷贝

segmentfault.com/a/119000003…

在 Linux 系统中,调用 fork 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 写时复制 机制。

image.png
基于这个机制我们可以直接拷贝子进程虚拟内存空间,尽管子进程后边有修改过内存数据,但与父进程相识度非常的高,大多数情况下可以满足我们对本次内存分析的需求,并且相比纯 ptrace 读取内存能节省大量的抓取时间。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYHBvLXl' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片