C - Process Trace On Linux
Process Trace
操作系统对每个进程内存保护,一个进程是不能读写另一个进程的,试想一下如果进程间能够随意读写将会造成多大的安全问题,但是在某些情形下我们希望进程的内容可以被别的进程读取,例如:debugger应该能够读取/修改被调试进程的内容,当然读写别的进程内容并非任意进程间都允许的,那样的话就造成安全问题了
读写别的进程的模式
如果一个进程有权限随意读写任意进程的内容的话会造成安全问题,但是如果一个进程声明说,某某进程你可以读写我的内容,被授权的进程的读写就没有任何安全问题了,这里遵照的原则是:被读写进程拥有让受信任进程读写自身的授权权利
预备知识:系统调用
操作系统内核提供给外部使用的API又叫作系统调用,主要是调用操作系统底层的功能,例如:访问文件系统,操纵进程等功能,进程通过系统调用接口将任务传递给内核,为了识别不同的系统调用,每个系统调用都有唯一的编号。
系统调用过程有两个特殊状态称之为entry和exit,entry就是在系统调用开始时的状态,exit就是系统调用返回是的状态,在entry时寄存器%eax中存放的是系统调用号,在exit时%eax存放的是系统调用的返回值(i386机构是如此的)。
系统调用的实质过程,当进程调用了系统函数后,系统调用号写入%eax,系统调用的参数写入寄存器%ebx,%ecx,%edx,%esi,%edi,并触发软中断0x80(在linux中该中断将会导致进入内核模式,即内核开始执行系统调用函数)。
一个系统调用往往会在导致别的系统调用,其实就像自定义函数一样,系统调用也可能是嵌套的。
系统调用ptrace
读写别的进程在linux系统下是通过系统调用ptrace实现的,下面看一个例子:
/* 代码摘自(有略微改动): http://www.linuxjournal.com/article/6100 */
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h> /* system call number */
#include <sys/reg.h> /* For constants ORIG_EAX etc */
int main()
{ pid_t child;
long orig_eax;
child = fork();
if(child == 0) {
/* child process come here */
ptrace(PTRACE_TRACEME, 0, NULL, NULL); /* 子进程声明允许父进程trace自己 */
execl("/bin/ls", "ls", NULL);
}
else {
/* parent process come here */
wait(NULL); /* wait阻塞后面代码的执行,直到被唤醒 */
/* 父进程中可以访问子进程的寄存器内容 */
orig_eax = ptrace(PTRACE_PEEKUSER,
child, 4 * ORIG_EAX,
NULL);
printf("The child made a "
"system call %ld\n", orig_eax);
ptrace(PTRACE_CONT, child, NULL, NULL);
}
return 0;
}
下面通过执行时间的先后顺序来了解例子的运行过程:
1. 父进程处于wait状态等待被唤醒
2. 子进程调用`ptrace(PTRACE_TRACEME, 0, NULL, NULL);`使自己允许被trace
3. 子进程进行系统调用`execl("/bin/ls", "ls", NULL);`请求
4. 内核发现子进程是被trace的,所以将子进程的控制权移交给追踪它的进程(即父进程),并且机器中存放的与子进程相关的内容信息并未变化(例:寄存器内容)
5. 父进程被唤醒,父进程可以读取寄存器%eax(该寄存器内容仍然是刚刚子进程写入的,即execl对应的系统调用号)并输出其内容
6. 父进程调用`ptrace(PTRACE_CONT, child, NULL, NULL);`,以告知内核子进程请求的系统调用继续执行(这其实相当于父进程告知内核自己放弃了对子进程的监控)
7. 内核执行子进程的execl系统调用以及相关的系统调用,最终ls当前目录
读取子进程的内容
/* 代码摘自(略微改动): http://www.linuxjournal.com/article/6100?page=0,1 */
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h> /* For SYS_write etc */
int main()
{ pid_t child;
long orig_eax, eax;
long params[3];
int status;
int insyscall = 0;
struct user_regs_struct regs;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else {
while(1) {
/* 父进程进入wait状态,status表示子进程状态 */
wait(&status);
/* 如果子进程exited,则break循环,即父进程一直循环等待直到子进程退出 */
if(WIFEXITED(status))
break;
orig_eax = ptrace(PTRACE_PEEKUSER,
child, 4 * ORIG_EAX, NULL);
/* 注意: 上面子进程的execl("bin/ls", "ls", NULL);引发的第一个系统调用是SYS_execve,
之后还会引发一系列别的系统调用,因为我们这里只关心系统调用SYS_write,所以只对子进程
的SYS_write调用进行处理
ls命令显示结果时每输出一行内容,就是一次对stdout的写操作(即系统调用SYS_write)
*/
if(orig_eax == SYS_write) {
if(insyscall == 0) {
/* SYS_write entry
EBX, ECX, EDX存放了子进程的信息
*/
insyscall = 1;
/* 取出子进程 USER area的内容 */
params[0] = ptrace(PTRACE_PEEKUSER,
child, 4 * EBX,
NULL);
params[1] = ptrace(PTRACE_PEEKUSER,
child, 4 * ECX,
NULL);
params[2] = ptrace(PTRACE_PEEKUSER,
child, 4 * EDX,
NULL);
printf("Write called with "
"%ld, %ld, %ld\n",
params[0], params[1],
params[2]);
/* 或者可以使用sys/user.h中定义的user_regs_struct获取
ptrace(PTRACE_GETREGS, child,
NULL, ®s);
printf("Write called with "
"%ld, %ld, %ld\n",
regs.ebx, regs.ecx,
regs.edx);
*/
}
else {
/* SYS_write exit
系统调用将返回值写入EAX中
*/
eax = ptrace(PTRACE_PEEKUSER,
child, 4 * EAX, NULL);
printf("Write returned "
"with %ld\n", eax);
insyscall = 0;
}
}/* end of if SYS_write */
/* 告知内核子进程继续运行直到下一个系统调用entry或exit */
ptrace(PTRACE_SYSCALL,
child, NULL, NULL);
}/* end of while */
}
return 0;
}
该例子的执行过程:
1. 父进程进入循环等待直到子进程退出
2. 子进程调用`ptrace(PTRACE_TRACEME, 0, NULL, NULL);`使得自己被trace
3. 子进程进行execl系统调用请求进而引发一系列系统调用请求
4. 父进程被子进程的系统调用请求唤醒,每次被唤醒父进程都调用`ptrace(PTRACE_SYSCALL, child, NULL, NULL);`,以告知内核子进程继续运行直到下一个系统调用entry或exit
5. 子进程的一系列系统调用请求被执行,直到子进程发起SYS_write请求
6. 父进程中对SYS_write请求做了额外的处理,即分别提取子进程刚刚进入系统调用以及系统调用返回时的信息
读写子进程的内容
/* 代码摘自(略有改动): http://www.linuxjournal.com/article/6100?page=0,2 */
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
const int long_size = sizeof(long);
/* 反转字符串函数 */
void reverse(char *str)
{ int i, j;
char temp;
for(i = 0, j = strlen(str) - 2;
i <= j; ++i, --j) {
temp = str[i];
str[i] = str[j];
str[j] = temp;
}
}
/* 读取子进程内容 */
void getdata(pid_t child, long addr,
char *str, int len)
{ char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
}data;
i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
data.val = ptrace(PTRACE_PEEKDATA,
child, addr + i * 4,
NULL);
memcpy(laddr, data.chars, long_size);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
data.val = ptrace(PTRACE_PEEKDATA,
child, addr + i * 4,
NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
}
/* 写入子进程 */
void putdata(pid_t child, long addr,
char *str, int len)
{ char *laddr;
int i, j;
union u {
long val;
char chars[long_size];
}data;
i = 0;
j = len / long_size;
laddr = str;
while(i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * 4, data.val);
}
}
int main()
{
pid_t child;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else {
long orig_eax;
long params[3];
int status;
char *str, *laddr;
int toggle = 0;
while(1) {
wait(&status);
if(WIFEXITED(status))
break;
orig_eax = ptrace(PTRACE_PEEKUSER,
child, 4 * ORIG_EAX,
NULL);
if(orig_eax == SYS_write) {
if(toggle == 0) {
toggle = 1;
params[0] = ptrace(PTRACE_PEEKUSER,
child, 4 * EBX,
NULL);
/* 获取子进程缓存地址 */
params[1] = ptrace(PTRACE_PEEKUSER,
child, 4 * ECX,
NULL);
/* 获取子进程写入缓存内容的大小 */
params[2] = ptrace(PTRACE_PEEKUSER,
child, 4 * EDX,
NULL);
str = (char *)calloc(params[2]+1, sizeof(char));
/* 取出子进程写入内容复制到str */
getdata(child, params[1], str,
params[2]);
reverse(str);
/* 将str中的内容写入子进程的缓存中,也就是替换子进程自己写入的内容 */
putdata(child, params[1], str,
params[2]);
}
else {
toggle = 0;
}
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}
该例子的运行过程:
1. 子进程允许被trace以及父进程等待被唤醒和之前示例一样
2. 该例的不同之处在于,父进程对子进程SYS_write请求处理的逻辑
3. 子进程发生SYS_write请求时将内容写到子进程的缓存中
4. 父进程被唤醒,读取并修改子进程的缓存内容,再让子进程继续运行
ptrace接口
long ptrace(enum_ptrace_request request,
pid_t pid,
void *addr,
void *data);
1. 子进程允许被追踪,`ptrace(PTRACE_TRACEME, 0, NULL, NULL);`
2. 父进程访问子进程USER area内容,读取子进程系统调用号`orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL);`,读取子进程系统调用返回值`ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL);`,读取子进程缓存地址`ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL);`,读取子进程缓存内容大小`ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL);`
3. 父进程访问子进程内存,读取子进程缓存`ptrace(PTRACE_PEEKDATA, child, addr + i * 4, data.val);`,写入子进程缓存`ptrace(PTRACE_POKEDATA, child, addr + i * 4, data.val);`
4. 父进程将控制权交回给子进程直到子进程下次系统调用entry或exit,`ptrace(PTRACE_SYSCALL, child, NULL, NULL);`
5. 父进程放弃对子进程的控制权,`ptrace(PTRACE_CONT, child, NULL, NULL);`