Android 应用程序如何抓取 Coredump 项目介绍

项目介绍

github.com/Penguin38/O…

1
2
3
4
5
6
rust复制代码allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
1
2
3
4
arduino复制代码dependencies {
...
implementation 'com.github.Penguin38:OpenCoreSDK:opencore-1.4.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
scss复制代码{
// 初始化组件
Coredump.getInstance().init();

// 设置超时时间 (单位秒)
Coredump.getInstance().setCoreTimeout(Coredump.DEF_TIMEOUT);

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

// 设置 Coredump 文件名规则
Coredump.getInstance().setCoreFlag(Coredump.FLAG_CORE
| Coredump.FLAG_PROCESS_COMM
| Coredump.FLAG_PID
| Coredump.FLAG_THREAD_COMM
| Coredump.FLAG_TID
| Coredump.FLAG_TIMESTAMP);

// 设置过滤条件
Coredump.getInstance().setCoreFilter(Coredump.FILTER_SPECIAL_VMA
// | Coredump.FILTER_FILE_VMA
// | Coredump.FILTER_SHARED_VMA
| Coredump.FILTER_SANITIZER_SHADOW_VMA);

// 设置 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 信息,以及暂停线程
/proc/<PID/mem 用于 COPY2 模式直接复制父进程内存

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_PEEKTEXTPTRACE_PEEKDATAPTRACE_PEEKUSER 允许跟踪进程读取被跟踪进程的虚拟内存地址
PTRACE_POKETEXTPTRACE_POKEDATAPTRACE_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 命令恢复状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
C++复制代码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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
C++复制代码...
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 左右。

1
2
3
4
5
6
7
8
9
10
11
12
13
C++复制代码...
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 读取内存能节省大量的抓取时间。

直接内存拷贝

子进程访问 /proc/<PID>/mem 节点获取父进程所有可访问的虚拟内存,性能最高的模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码...
while(index < ehdr.e_phnum - 1) {
if (phdr[index].p_filesz > 0) {
switch (mode) {
...
case MODE_COPY2: {
int count = phdr[index].p_memsz / sizeof(zero);
for (int i = 0; i < count; i++) {
memset(&zero, 0x0, sizeof(zero));
pread(fd, &zero, sizeof(zero), phdr[index].p_vaddr + (i * sizeof(zero)));
uint64_t ret = fwrite(zero, sizeof(zero), 1, fp);
if (ret != 1) {
JNI_LOGE("[%p] write load segment fail. %s %s",
(void *)phdr[index].p_vaddr, strerror(errno), maps[ntfile[index].start].c_str());
}

}
} break;
...

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%