linux(4)-Ptrace 系统调用的使用


问题

      利用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 <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
      参数request:请求ptrace执行的操作;参数pid:目标进程的ID;参数addr:目标进程的地址值;参数data:作用则根据request的不同而变化,如果需要向目标进程中写入数据,data存放的是需要写入的数据;如果从目标进程中读数据,data将存放返回的数据。如下图:
littlleant

      需注意:当采用参数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<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ptrace.h>
#include<sys/user.h>//存放结构体 user_regs_struct 信息
#include<signal.h>
#include<string.h>
#include<stdlib.h>
#include<sys/wait.h>

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,&regs);
				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,&regs);
				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,&regs);
				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<stdio.h>
#include<unistd.h>
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 查看子进程需要单步执行的步数。

1,运行程序(此时被调试程序相当于进入断点,等待输入命令)
littleant

2,查看寄存器内容 regs list 和step命令
littleant

3,查看寄存器rip所指向地址后40个字节的内容。 命令 examin 后输入 1 即可。
查看指定内存处内容,内存地址不好输入,在此选择相对于rip的偏移来间接查看任意内存处的值。 命令 examin 后, 输入 2 ,输入相对于rip的偏移地址,
比如:下面分别输入+20 和 -20,内存处的内容如下图所示。并且通过 命令 continue 让被调试程序继续执行。此时退出状态为0
littleant

4,命令insc 会将被调试程序单步运行一遍,算出所需的机器指令数。 再通过命令 exit 退出。
littleant

Guess you like

Origin blog.csdn.net/Little_ant_/article/details/114029594