利用ptrace系统调用实现一个简单的软件调试器。基本功能包括能够截获被调试进程的信号,被调试进程进入断点后能够查看被调试进程任意内存区域的内容,能够查看任意CPU寄存器的内容,能够使被调试进程恢复运行。
Ubuntu-20.04 64位虚拟机
1,测试程序为test.c,这是后面要被测试的程序;它的可执行程序为test(采用-g进行编译)。
2,Ptrace系统调用的实现程序为ptrace.c。
查看阅读ptrace官方文档(man ptrace),从文档的相关说明中有了大体的实现思路,摘自手册:
A process can initiate a trace by calling fork and having the resulting child do a PTRACE_TRACEME, followed (typically) by an execve,Alternatively, one process may commence tracing another process using PTRACE_ATTACH or PTRACE_SEIZE.
其中第一种是被动的,让别人来调试自己;第二种是主动去调试别人。后面采用第一种方法。
Ptrace的函数原型为:
#include
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
参数request:请求ptrace执行的操作;参数pid:目标进程的ID;参数addr:目标进程的地址值;参数data:作用则根据request的不同而变化,如果需要向目标进程中写入数据,data存放的是需要写入的数据;如果从目标进程中读数据,data将存放返回的数据。如下图:
需注意:当采用参数request为PTRACE_PEEKTEXT时,代码段中的数据被保存在返回值中。返回值为一个long类型。(经测试long和long long在当前环境上均为8字节长)
在子进程中采用ptrace(PTRACE_TRACEME,0,0,0),来让父进程来调试它,这个方法的执行应该在exec()函数执行前,来保证执行exec()时,子进程已经处于被调试状态。
采用fork(),将父进程作为tracer,将子进程作为tracee;再通过exec ()函数族将待调试的程序装入到子进程中,并且exec()函数族在执行时若发现原进程处于调试状态下的话,当将新的代码装入后,会向自身发送信号SIGTRAP,中止执行,方便在父进程中来进行操作。
父进程中通过简单的循环操作来接受用户的输入,对不同的输入进行不同的操作。所接受的用户指令有:退出(exit),查看大部分寄存器内容(regs),继续执行被调试进程(continue),列出子进程代码,查看特定内存处的内容(examin),添加断点,查看一共有多少条指令(insc),单步调试(step),列出当前rip指向的指令(list)。
1,获取用户输入的待调试程序,(这里输入的程序要求:不能带有参数,单线程,且需要在当前目录下)
2,fork出一个子进程,采用execvp函数来装入待调试程序。
3,父进程实现具体的操作来对子进程进行调试工作。
ptrace.c:
#include
#include
#include
#include
#include //存放结构体 user_regs_struct 信息
#include
#include
#include
#include
int main()
{
const int SIZE=32;//表明用户输入的最大长度
char* arg[2];
for(int i=0;i<2;i++)
arg[i]=NULL;
printf("Input the program name as the tracee.(no parameter and in the pwd.)\n");
arg[0]=(char*)malloc(SIZE);
fgets(arg[0],SIZE-1,stdin);//获得用户输入,包括'\n'
int len=strlen(arg[0]);
arg[0][len-1]='\0';//将最后的'\n'变为'\0'
pid_t pid = fork();
if (pid < 0)
printf("error in fork().\n");
else if (pid == 0)
{
//printf("my pid is %d,I will be traced.\n\n", getpid());
if(ptrace(PTRACE_TRACEME,0,0,0)<0)
{
printf("error in ptrace().\n");
exit(-2);
}
if (execvp(arg[0],arg))//执行用户命令,收到系统产生的SIGTRAP信号。
{
printf("error in execvp().\n");
free(arg[0]);
exit(-1);
}
else
{
free(arg[0]);
exit(0);
}
}
else
{
int status;
int i,j,k;
char instruction[SIZE];
struct user_regs_struct regs;//存储子进程当前寄存器的值
int count;
//memset(instruction,0,SIZE*sizeof(char));
const char* e="exit";
const char* r="regs";
const char* c="continue";
const char* l="list";
const char* ic="insc";
const char* x="examin";//默认查看当前内存地址之后40个字
const char* s="step";
printf("Please wait...\n");
sleep(0.5);
while(1)
{
//wait(&status);
//printf("The signal child got: %s\n",strsignal(WSTOPSIG(status)));
printf("ptrace> ");
fgets(instruction,SIZE-1,stdin);
while(instruction[i]!='\n')
++i;
instruction[i]='\0';
if(strcmp(e,instruction)==0)//退出操作
{
ptrace(PTRACE_KILL,pid,NULL,NULL);
break;
}
else if(strcmp(c,instruction)==0)
{
ptrace(PTRACE_CONT,pid,NULL,NULL);
sleep(1);
break;
}
else if(strcmp(r,instruction)==0)//查询寄存器内容操作
{
ptrace(PTRACE_GETREGS,pid,NULL,®s);
printf("rax %llx\nrbx %llx\nrcx %llx\nrdx %llx\nrsi %llx\n"
"rdi %llx\nrbp %llx\nrsp %llx\nrip %llx\neflags %llx\n"
"cs %llx\nss %llx\nds %llx\nes %llx\n",
regs.rax,regs.rbx,regs.rcx,regs.rdx,regs.rsi,regs.rdi,regs.rbp,regs.rsp,regs.rip,regs.eflags,
regs.cs,regs.ss,regs.ds,regs.es);
}
else if(strcmp(l,instruction)==0)
{
// long 和long long在此都是8字节。
ptrace(PTRACE_GETREGS,pid,NULL,®s);
long data=ptrace(PTRACE_PEEKTEXT,pid,regs.rip,NULL);
printf("The rip is %llx, The present instruction is: %lx\n",regs.rip,data);
}
else if(strcmp(ic,instruction)==0)
{
count=0;
while(1)
{
wait(&status);
if(WIFEXITED(status))
{
printf("count is %d\n",count);
break;
}
ptrace(PTRACE_SINGLESTEP,pid,NULL,NULL);
count++;
}
}
else if(strcmp(x,instruction)==0)
{
printf("Please input 1,if you want to watch the 40 bytes after rip\n");
printf("Please input 2,if you want to watch the 40 bytes atrer the memory you will assign\n");
char ch;
ch=getc(stdin);
getchar();
unsigned char temp[40];
union u{
unsigned long data;
unsigned char t[8];
}d; //这里采用union联合类型来实现
ptrace(PTRACE_GETREGS,pid,NULL,®s);
if(ch=='1')
{
for(int i=0;i<5;++i)
{
d.data=ptrace(PTRACE_PEEKTEXT,pid,regs.rip+i*8,NULL);
memcpy(temp+8*i,d.t,8);
}
printf("The current location is %llx:\n The 40 bytes later are : ",regs.rip);
}
else if(ch =='2')
{
printf("The current rip is %llx: ,Please input offset: \n",regs.rip);//偏移单位为字节数,正负均可。
int offset;
scanf("%d",&offset);
getchar();
for(int i=0;i<5;++i)
{
d.data=ptrace(PTRACE_PEEKTEXT,pid,regs.rip+offset+i*8,NULL);
memcpy(temp+8*i,d.t,8);
}
printf("The current location is %llx:\n The 40 bytes later are : ",regs.rip);
}
count=0;
for(int i=0;i<40;i++)
{
printf("%.2x",temp[i]);
count++;
if(count==8)
{
count=0;
printf(" ");
}
}
printf("\n");
}
else if(strcmp(s,instruction)==0)
{
wait(&status);
if(WIFEXITED(status))
{
printf("Done.\n");
break;
}
ptrace(PTRACE_SINGLESTEP,pid,NULL,NULL);
}
else
{
printf("Invalid instruction!\n");
}
}
wait(&status);
printf("\nchild process quit with status %d.\n", status);
}
return 0;
}
test.c:
#include
#include
int main()
{
int i,j;
i=5;
j=10;
return 0;
}
1,编译两个.c文件:gcc –o p ptrace.c gcc –g test.c –o test
2,最终,父进程支持的命令操作如下:
a, exit 终止被调试程序(子进程),并且父进程退出
b, continue 恢复子进程的执行, 父进程稍后退出
c, regs 查看当前子进程的寄存器内容(很少用到的就不列出了)
d, list 显示当前rip的内容,以及需要待执行的指令(更好的说法应该是:rip指向的内存处的8B长度的值)。
e, step 单步执行
f, examin 查看当前rip所指向的内存后面40B长的内容。按照字节来显示。
g, insc 查看子进程需要单步执行的步数。
3,查看寄存器rip所指向地址后40个字节的内容。 命令 examin 后输入 1 即可。
查看指定内存处内容,内存地址不好输入,在此选择相对于rip的偏移来间接查看任意内存处的值。 命令 examin 后, 输入 2 ,输入相对于rip的偏移地址,
比如:下面分别输入+20 和 -20,内存处的内容如下图所示。并且通过 命令 continue 让被调试程序继续执行。此时退出状态为0