导语:
相信大家对GDB都不陌生,GDB原理也能说个一二,什么ptrace、调试寄存器等说的头头是道,也有很多朋友用ptrace亲手实现过简单的gdb调试功能。今天就让我用一种简单的方式,在目标进程添加一个系统调用并执行。
需要解决的问题有:
- 如何跟踪目标进程;
- 如何注入代码;
- 输入什么代码;
- 如何让目标进程执行注入的代码;
- 如何恢复现场;
如何跟踪目标进程 - ptrace
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
从ptrace函数声明看出,需要我们传入一个请求枚举值,我们要用到的是:
PTRACE_ATTACH - 跟踪一个进程,被跟踪进程将会因此停止运行
PTRACE_GETREGS - 获取被跟踪进程所有寄存器信息
PTRACE_SETREGS - 设置被跟踪进程所有寄存器信息
PTRACE_CONT - 使被跟踪进程继续运行
PTRACE_DETACH - 取消跟踪目标进程
如何注入代码
为了简单,我们只是简单的注入一个系统调用,所以需要写入的东西并不多,可以使用当前进程现有的地址空间已经映射的内存区域,比如说libc开始映射的位置(其实这个位置并不重要,随便一个代码段就可以),但需要注意的是,在替换代码->执行代码这两步结束后,别忘了最后一步,将原始代码替换回去。
那么如何注入代码呢?
我们看两个系统调用:
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
再看两个proc文件系统下的文件/proc/PID/mem和/proc/PID/maps。
将上面两个proc文件和两个系统调用结合起来,就可以访问和修改目标进程的内存。
/proc/PID/maps
这个文件大家比较熟悉了,先看看他长啥样:
![](https://i-blog.csdnimg.cn/blog_migrate/dbad2b390979c6b8bd1cef58e0f50f20.jpeg)
上面每一部分都是一个非常重要的知识点,比如:vDSO、vsyscall、vvar等,这个文件保存了一个进程镜像的布局,通过展现每个内存映射来实现,熟悉内核的朋友可能很熟悉VMA,没错,上面的每一行都是一个VMA结构(关于内核VMA本文当然也不介绍,内容太多泰国庞杂)。
这里先简单提一嘴,我们可以选地址范围为7fc0530b2000-7fc0530de000的libc的vma作为在目标进程执行系统调用的“中转”,也就是说,每次我们想要在目标进程中执行一个系统调用时,先将这个libc的vma对应的大小的内存保存起来,然后将程序写入这个vma起始位置,然后跳转到那个位置执行,最后,将第一步保存的代码写回到这个vma对应位置。
/proc/PID/mem
这个文件怎么解释呢?不好解释,还是看内核代码吧!
首先,每个进程都有一个/proc/PID/mem文件,在内核源码中,定位到代码 fs/proc/base.c,在创建新的进程时候,都会创建一个新的/proc/PID/mem文件。
REG("mem", S_IRUSR|S_IWUSR, proc_mem_operations),
对应的操作符(只关注打开,读,写):
static int mem_open(struct inode *inode, struct file *file)
{
int ret = __mem_open(inode, file, PTRACE_MODE_ATTACH);
/* OK to pass negative loff_t, we can catch out-of-range */
file->f_mode |= FMODE_UNSIGNED_OFFSET;
return ret;
}
static ssize_t mem_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
return mem_rw(file, buf, count, ppos, 0);
}
static ssize_t mem_write(struct file *file, const char __user