项目介绍
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();
}
文件格式
名字 | 用途 |
---|---|
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_CONT
或 PTRACE_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
。
...
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);
}
}
...
父子进程内存拷贝
在 Linux 系统中,调用 fork
系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 —— 这就是著名的 写时复制
机制。
基于这个机制我们可以直接拷贝子进程虚拟内存空间,尽管子进程后边有修改过内存数据,但与父进程相识度非常的高,大多数情况下可以满足我们对本次内存分析的需求,并且相比纯 ptrace
读取内存能节省大量的抓取时间。