Linuxptrace详细分析系列一

治疗白癜风有哪些方法 http://pf.39.net/bdfyy/

本文为看雪论坛优秀文章

看雪论坛作者ID:有毒

备注:文章中使用的Linux内核源码版本为Linux5.9,使用的Linux版本为Linuxubuntu5..0-65-generic

一、简述

ptrace系统调用提供了一个进程(tracer)可以控制另一个进程(tracee)运行的方法,并且tracer可以监控和修改tracee的内存和寄存器,主要用作实现断点调试和系统调用追踪。tracee首先要被attach到tracer上,这里的attach以线程为对象,在多线程场景(这里的多线程场景指的使用cloneCLONE_THREADflag创建的线程组)下,每个线程可以分别被attach到tracer上。ptrace的命令总是以下面的调用格式发送到指定的tracee上:

ptrace(PTRACE_foom,pid,...)//pid为linux中对应的线程ID一个进程可以通过调用fork()函数来初始化一个跟踪,并让生成的子进程执行PTRACE_TRACEME,然后执行execve(一般情况下)来启动跟踪。进程也可以使用PTRACE_ATTACH或PTRACE_SEIZE进行跟踪。当处于被跟踪状态时,tracee每收到一个信号就会stop,即使是某些时候信号是被忽略的。tracer将在下一次调用waitpid或与wait相关的系统调用之一)时收到通知。该调用会返回一个状态值,包含tracee停止的原因。tracee发生stop时,tracer可以使用各种ptrace的request来检查和修改tracee。然后,tracer使tracee继续运行,选择性地忽略所传递的信号(甚至传递一个与原来不同的信号)。当tracer结束跟踪后,发送PTRACE_DETACH信号释放tracee,tracee可以在常规状态下继续运行。

二、函数原型及初步使用

1.函数原型ptrace的原型如下:

longptrace(enum__ptrace_requestrequest,pid_tpid,void*addr,void*data);其中request参数表明执行的行为(后续将重点介绍),pid参数标识目标进程,addr参数表明执行peek和poke操作的地址,data参数则对于poke操作,指明存放数据的地址,对于peek操作,指明获取数据的地址。返回值,成功执行时,PTRACE_PEEK请求返回所请求的数据,其他情况时返回0,失败则返回-1。.函数定义ptrace的内核实现在kernel/ptrace.c文件中,内核接口是SYSCALL_DEFINE(ptrace,long,request,long,pid,unsignedlong,addr,unsignedlong,data)。其代码如下,整体逻辑简单,需要注意的是对PTRACE_TRACEME和PTRACE_ATTACH进行了特殊处理(对于该函数的参数后续将进行深入解析)。

SYSCALL_DEFINE(ptrace,long,request,long,pid,unsignedlong,addr,unsignedlong,data){structtask_struct*child;longret;if(request==PTRACE_TRACEME){ret=ptrace_traceme();if(!ret)arch_ptrace_attach(current);gotoout;}child=find_get_task_by_vpid(pid);if(!child){ret=-ESRCH;gotoout;}if(request==PTRACE_ATTACH

request==PTRACE_SEIZE){ret=ptrace_attach(child,request,addr,data);/**Somearchitecturesneedtodobook-keepingafter*aptraceattach.*/if(!ret)arch_ptrace_attach(child);gotoout_put_task_struct;}ret=ptrace_check_attach(child,request==PTRACE_KILL

request==PTRACE_INTERRUPT);if(ret0)gotoout_put_task_struct;ret=arch_ptrace(child,request,addr,data);if(ret

request!=PTRACE_DETACH)ptrace_unfreeze_traced(child);out_put_task_struct:put_task_struct(child);out:returnret;}系统调用都改为了SYSCALL_DEFINE的方式。如何获得上面的定义的呢?这里需要穿插一下SYSCALL_DEFINE的定义(syscall.h):

#defineSYSCALL_DEFINE1(name,...)SYSCALL_DEFINEx(1,_##name,__VA_ARGS__)#defineSYSCALL_DEFINE(name,...)SYSCALL_DEFINEx(,_##name,__VA_ARGS__)#defineSYSCALL_DEFINE(name,...)SYSCALL_DEFINEx(,_##name,__VA_ARGS__)#defineSYSCALL_DEFINE(name,...)SYSCALL_DEFINEx(,_##name,__VA_ARGS__)#defineSYSCALL_DEFINE5(name,...)SYSCALL_DEFINEx(5,_##name,__VA_ARGS__)#defineSYSCALL_DEFINE6(name,...)SYSCALL_DEFINEx(6,_##name,__VA_ARGS__)宏定义进行展开:

#defineSYSCALL_DEFINEx(x,sname,...)\SYSCALL_METADATA(sname,x,__VA_ARGS__)\__SYSCALL_DEFINEx(x,sname,__VA_ARGS__)/**Theasmlinkagestubisaliasedtoafunctionnamed__se_sys_*()which*sign-extends-bitintstolongswheneverneeded.Theactualworkis*donewithin__do_sys_*().*/#ifndef__SYSCALL_DEFINEx#define__SYSCALL_DEFINEx(x,name,...)\__diag_push();\__diag_ignore(GCC,8,"-Wattribute-alias",\"Typealiasingisusedtosanitizesyscallarguments");\asmlinkagelongsys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\__attribute__((alias(__stringify(__se_sys##name))));\ALLOW_ERROR_INJECTION(sys##name,ERRNO);\staticinlinelong__do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\asmlinkagelong__se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));\asmlinkagelong__se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))\{\longret=__do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\__MAP(x,__SC_TEST,__VA_ARGS__);\__PROTECT(x,ret,__MAP(x,__SC_ARGS,__VA_ARGS__));\returnret;\}\__diag_pop();\staticinlinelong__do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))#endif/*__SYSCALL_DEFINEx*/__SYSCALL_DEFINEx中的x表示系统调用的参数个数,且sys_ptrace的宏定义如下:

/*kernel/ptrace.c*/asmlinkagelongsys_ptrace(longrequest,longpid,unsignedlongaddr,unsignedlongdata);所以对应的__SYSCALL_DEFINEx应该是SYSCALL_DEFINE,这与上面的定义SYSCALL_DEFINE(ptrace,long,request,long,pid,unsignedlong,addr,unsignedlong,data)一致。仔细观察上面的代码可以发现,函数定义其实在最后一行,结尾没有分号,然后再加上花括号即形成完整的函数定义。前面的几句代码并不是函数的实现(详细的分析可以跟踪源码,出于篇幅原因此处不放出每个宏定义的跟踪)。定义的转换过程:

SYSCALL_DEFINE(ptrace,long,request,long,pid,unsignedlong,addr,unsignedlong,data)--SYSCALL_DEFINEx(,_ptrace,__VA_ARGS__)--__SYSCALL_DEFINEx(,__ptrace,__VA_ARGS__)#define__SYSCALL_DEFINEx(x,name,...)\asmlinkagelongsys##name(__MAP(x,__SC_DECL,__VA_ARGS__))\--asmlinkagelongsys_ptrace(__MAP(,__SC_DECL,__VA_ARGS__))而对__MAP宏和__SC_DECL宏的定义如下:

/**__MAP-applyamacrotosyscallarguments*__MAP(n,m,t1,a1,t,a,...,tn,an)willexpandto*m(t1,a1),m(t,a),...,m(tn,an)*Thefirstargumentmustbeequaltotheamountoftype/name*pairsgiven.Notethatthislistofpairs(i.e.thearguments*of__MAPstartingatthethirdone)isinthesameformatas*forSYSCALL_DEFINEn/COMPAT_SYSCALL_DEFINEn*/#define__MAP0(m,...)#define__MAP1(m,t,a,...)m(t,a)#define__MAP(m,t,a,...)m(t,a),__MAP1(m,__VA_ARGS__)#define__MAP(m,t,a,...)m(t,a),__MAP(m,__VA_ARGS__)#define__MAP(m,t,a,...)m(t,a),__MAP(m,__VA_ARGS__)#define__MAP5(m,t,a,...)m(t,a),__MAP(m,__VA_ARGS__)#define__MAP6(m,t,a,...)m(t,a),__MAP5(m,__VA_ARGS__)#define__MAP(n,...)__MAP##n(__VA_ARGS__)#define__SC_DECL(t,a)ta按照如上定义继续进行展开

__MAP(,__SC_DECL,longrequest,longpid,unsignedlongaddr,unsignedlongdata)--__MAP(__SC_DECL,long,request,long,pid,unsignedlong,addr,unsignedlong,data)--__SC_DECL(long,request),__MAP(__SC_DECL,__VA_ARGS__)__MAP(__SC_DECL,long,pid,unsignedlong,addr,unsignedlong,data)--__SC_DECL(long,pid),__MAP(__SC_DECL,unsignedlong,addr,unsignedlong,data)--__SC_DECL(unsignedlong,addr),__MAP1(__SC_DECL,__VA_ARGS__)unsignedlongaddr,__SC_DECL(unsignedlong,data)--unsignedlongdatalongpid,__SC_DECL(unsignedlong,addr),__MAP1(__SC_DECL,__VA_ARGS__)--longpid,unsignedlongaddr,unsignedlongdata--longrequest,__SC_DECL(long,pid),__MAP(__SC_DECL,__VA_ARGS__)--longrequest,longpid,unsignedlongaddr,unsignedlongdata最后调用asmlinkagelongsys_ptrace(longrequest,longpid,unsignedlongaddr,unsignedlongdata);。为什么要将系统调用定义成宏?主要是因为个内核漏洞CVE--,CVE--01,Linux.6.8及以前版本的内核中,将系统调用中位参数传入6位的寄存器时无法作符号扩展,可能导致系统崩溃或提权漏洞。内核开发者通过将系统调用的所有输入参数都先转化成long类型(6位),再强制转化到相应的类型来规避这个漏洞。

asmlinkagelong__se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))\{\longret=__do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\__MAP(x,__SC_TEST,__VA_ARGS__);\__PROTECT(x,ret,__MAP(x,__SC_ARGS,__VA_ARGS__));\returnret;\}\define__TYPE_AS(t,v)__same_type((__forcet)0,v)/*判断t和v是否是同一个类型*/define__TYPE_IS_L(t)(__TYPE_AS(t,0L))/*判断t是否是long类型,是返回1*/define__TYPE_IS_UL(t)(__TYPE_AS(t,0UL))/*判断t是否是unsignedlong类型,是返回1*/define__TYPE_IS_LL(t)(__TYPE_AS(t,0LL)

__TYPE_AS(t,0ULL))/*是long类型就返回1*/define__SC_LONG(t,a)__typeof(__builtin_choose_expr(__TYPE_IS_LL(t),0LL,0L))a/*将参数转换成long类型*/define__SC_CAST(t,a)(__forcet)a/*转成成原来的类型*/define__force__attribute__((force))/*表示所定义的变量类型可以做强制类型转换*/.初步使用(1)最简单的ls跟踪首先通过一个简单的例子来熟悉一下ptrace的使用:

#includestdio.h#includeunistd.h#includesys/ptrace.h#includesys/wait.h#includesys/reg.h#includesys/types.hintmain(intargc,char*argv[]){pid_tchild;longorig_rax;child=fork();if(child==0){ptrace(PTRACE_TRACEME,0,NULL,NULL);//Tellkernel,tracemeexecl("/bin/ls","ls",NULL);}else{/*Receivecertificationafterchildprocessstopped*/wait(NULL);/*Readchildprocesssrax*/orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);printf("[+]Thechildmadeasystemcall%ld.\n",orig_rax);/*Continue*/ptrace(PTRACE_CONT,child,NULL,NULL);}return0;}运行结果如下:打印出系统调用号,并等待用户输入。查看/usr/include/x86_6-linux-gnu/asm/unistd_6.h文件(6位系统)查看59对应的系统调用:59号恰好为execve函数调用。对上面的过程进行简单总结:a.父进程通过调用fork()来创建子进程,在子进程中,执行execl()之前,先运行ptrace(),request参数设置为PTRACE_TRACEME来告诉kernel当前进程正在被trace。当有信号量传递到该进程,进程会stop,提醒父进程在wait()调用处继续执行。然后调用execl(),执行成功后,新程序运行前,SIGTRAP信号量被发送到该进程,子进程停止,父进程在wait()调用处收到通知,获取子进程的控制权,查看子进程内存和寄存器相关信息。b.当发生系统调用时,kernel保存了rax寄存器的原始内容,其中存放的是系统调用号,我们可以使用request参数为PTRACE_PEEKUSER的ptrace来从子进程的USER段读取出该值。c.系统调用检查结束后,子进程通过调用request参数为PTRACE_CONT的ptrace函数继续执行。()系统调用查看参数

#includesys/ptrace.h#includesys/types.h#includesys/wait.h#includeunistd.h#includesys/user.h#includesys/reg.h#includestdio.h#includesys/syscall.hintmain(intargc,char*argv[]){pid_tchild;longorig_rax,rax;longparams[];intstatus;intinsyscall=0;child=fork();if(child==0){ptrace(PTRACE_TRACEME,0,NULL,NULL);execl("/bin/ls","ls",NULL);}else{while(1){wait(status);if(WIFEXITED(status))break;orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);if(orig_rax==SYS_write){if(insyscall==0){insyscall=1;params[0]=ptrace(PTRACE_PEEKUSER,child,8*RBX,NULL);params[1]=ptrace(PTRACE_PEEKUSER,child,8*RCX,NULL);params[]=ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL);printf("Writecalledwith%ld,%ld,%ld\n",params[0],params[1],params[]);}else{rax=ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);printf("Writereturnedwith%ld\n",rax);insyscall=0;}}ptrace(PTRACE_SYSCALL,child,NULL,NULL);}}return0;}执行结果:在上面的程序中,跟踪的是wirte的系统调用,ls命令总计进行了三次write的调用。request参数为PTEACE_SYSCALL时的ptrace使kernel在进行系统调用进入或退出时stop子进程,这等价于执行PTRACE_CONT并在下一次系统调用进入或退出时stop。wait系统调用中的status变量用于检查子进程是否已退出,这是用来检查子进程是否被ptrace停掉或是否退出的典型方法。而宏WIFEXITED则表示了子进程是否正常结束(例如通过调用exit或者从main返回等),正常结束时返回true。()系统调用参数-改进版前面有介绍PTRACE_GETREGS参数,使用它来获取寄存器的值相比前面一种方法要简单很多:

#includestdio.h#includesys/reg.h#includesys/user.h#includesys/wait.h#includesys/ptrace.h#includesys/syscall.h#includesys/types.h#includeunistd.hintmain(intargc,char*argv[]){pid_tchild;longorig_rax,rax;longparams[];intstatus;intinsyscall=0;structuser_regs_structregs;child=fork();if(child==0){ptrace(PTRACE_TRACEME,child,8*ORIG_RAX,NULL);execl("/bin/ls","ls",NULL);}else{while(1){wait(status);if(WIFEXITED(status))break;orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);if(orig_rax==SYS_write){if(insyscall==0){insyscall==1;ptrace(PTRACE_GETREGS,child,NULL,regs);printf("Writecalledwith%lld,%lld,%lld\n",regs.rbx,regs.rcx,regs.rdx);}else{rax=ptrace(PTRACE_PEEKUSER,child,8*rax,NULL);printf("Writereturnedwith%ld\n",rax);insyscall=0;}}ptrace(PTRACE_SYSCALL,child,NULL,NULL);}}return0;}执行结果:整体输出与前面的代码无所差别,但在代码开发上使用了PTRACE_GETREGS来获取子进程的寄存器的值,简洁了很多。参考文献[1]


转载请注明:http://www.kelongbinga.com/klss/6946.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了