基础
操作系统通过一个叫做“系统调用”的标准机制来对上层提供服务,他们提供了一系列标准的API来让上层应用程序获取底层的硬件和服务,比如文件系统。当一个进程想要进行一个系统调用的时候,它会把该系统调用所需要用到的参数放到寄存器里,然后执行软中断指令0x80. 这个软中断就像是一个门,通过它就能进入内核模式,进入内核模式后,内核将会检查系统调用的参数,然后执行该系统调用。
在 i386 平台下(本文所有代码都基于 i386), 系统调用的编号会被放在寄存器 %eax 中,而系统调用的参数会被依次放到 %ebx,%ecx,%edx,%exi 和 %edi中,比如说,对于下面的系统调用:
write(2, "Hello", 5)
编译后,它最后大概会被转化成下面这样子:
movl $4, %eax
movl $2, %ebx
movl $hello,%ecx
movl $5, %edx
int $0x80
其中 $hello 指向字符串 "Hello"。
这里是基于32位系统的寄存器名称,64位系统的寄存器名称为rax,rbx
看完上面简单的例子,现在我们来看看 ptrace 又是怎样执行的。首先,我们假设进程 A 要 ptrace 进程 B。在 ptrace 系统调用真正开始前,内核会检查一下我们将要 trace 的进程 B 是否当前已经正在被 traced 了,如果是,内核就会把该进程 B 停下来,并把控制权交给调用进程 A (任何时候,子进程只能被父进程这唯一一个进程所trace),这使得进程A有机会去检查和修改进程B的寄存器的值。
ptrace 的使用流程一般是这样的:父进程 fork() 出子进程,子进程中执行我们所想要 trace 的程序,在子进程调用 exec() 之前,子进程需要先调用一次 ptrace,以 PTRACE_TRACEME 为参数。这个调用是为了告诉内核,当前进程已经正在被 traced,当子进程执行 execve() 之后,子进程会进入暂停状态,把控制权转给它的父进程(SIG_CHLD信号), 而父进程在fork()之后,就调用 wait() 等子进程停下来,当 wait() 返回后,父进程就可以去查看子进程的寄存器或者对子进程做其它的事情了。
当系统调用发生时,内核会把当前的%eax中的内容(即系统调用的编号)保存到子进程的用户态代码段中(USER SEGMENT or USER CODE),我们可以像上面的例子那样通过调用Ptrace(传入PTRACE_PEEKUSER作为第一个参数)来读取这个%eax的值,当我们做完这些检查数据的事情之后,通过调用ptrace(PTRACE_CONT),可以让子进程重新恢复运行。
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
// #include <linux/user.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h>
const int long_size = sizeof(long);
#define LONG_SIZE 8
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 * LONG_SIZE,
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 * LONG_SIZE,
NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
// printf("getdata str=%s\n", str);
}
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 * LONG_SIZE, data.val);
++i;
laddr += long_size;
}
j = len % long_size;
if(j != 0) {
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child,
addr + i * LONG_SIZE, data.val);
}
}
int main(int argc, char *argv[])
{
int fd = 0;
char acBuf[4096] = {0};
fd = open("./model", O_WRONLY | O_TRUNC);
if (-1 == fd)
{
printf("open error!\n");
return 0;
}
sprintf(acBuf, "test model");
write(fd, acBuf, strlen(acBuf));
close(fd);
}
int main(int argc, char *argv[])
{
pid_t child;
child = fork();
if (child == 0)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
printf("before\n");
execl("/bin/ls", "ls", NULL);
printf("after\n");
}
else
{
long orig_eax;
long params[3];
int status;
char *str, *laddr;
int toggle = 0;
printf("1\n");
while (1)
{
wait(&status);
printf("status=%d\n", status);
if (WIFEXITED(status))
break;
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS,child,NULL,®s);
printf("Write called with %ld,%ld,%ld,%ld\n",regs.orig_rax,regs.rbx,regs.rcx,regs.rdx);
orig_eax = ptrace(PTRACE_PEEKUSER,
child, LONG_SIZE * ORIG_RAX,
NULL);
if (orig_eax == SYS_write)
{
printf("2\n");
if (toggle == 0)
{
toggle = 1;
params[0] = ptrace(PTRACE_PEEKUSER,
child, LONG_SIZE * RBX,
NULL);
params[1] = ptrace(PTRACE_PEEKUSER,
child, LONG_SIZE * RSI,
NULL);
params[2] = ptrace(PTRACE_PEEKUSER,
child, LONG_SIZE * RDX,
NULL);
printf("%d,%d,%d\n", params[0],params[1],params[2]);
str = (char *)calloc((params[2] + 1), sizeof(char));
getdata(child, params[1], str,
params[2]);
printf("str=%s\n", str);
reverse(str);
putdata(child, params[1], str,
params[2]);
}
else
{
toggle = 0;
}
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}
这里做的操作是,子进程会调用ls命令,而父进程会将子进程的结果反转后输出。