浅谈ARM上的Ptrace

//weibo: @少仲


0x0 前言

你想过怎么实现对系统调用的拦截吗?你尝试过通过改变系统调用的参数来愚弄你的系统kernel吗?你想过调试器是如何使运行中的进程暂停并且控制它吗?你可能会开始考虑怎么使用复杂的kernel编程来达到目的,那么,你错了.实际上Linux提供了一种优雅的机制来完成这些:ptrace系统函数
ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪.使用ptrace,你可以在用户层拦截和修改系统调用(sys call).关于ptrace不做过多的赘述,<<玩转PTRACE>>写的已经十分详细,这篇文章重点是分析如何实现arm版的ptrace.

0x1 实现原理

(1).首先写一个简单的target程序,代码如下:

[java] view plain copy
  1. //target.c  
  2. #include <stdio.h>  
  3.                                                                                                                                  
  4. int flag = 1;  
  5. int count = 0;  
  6. int main(int argc,char* argv[])  
  7. {  
  8.         while(flag)  
  9.         {  
  10.                 printf("Target is running:%d\n", count);  
  11.                 count++;  
  12.                 sleep(3);  
  13.         }  
  14.         return 0;  
  15. }  
(2).接下来写trace程序来捕获syscall

[java] view plain copy
  1. int main(int argc, char *argv[])  
  2. {     
  3.     if(argc != 2)   
  4.     {  
  5.        printf("please input pid...\n");  
  6.        return 1;  
  7.     }  
  8.             
  9.     pid_t traced_process;  
  10.     int status;  
  11.     traced_process = atoi(argv[1]);  
  12.   
  13.     if( ptrace(PTRACE_ATTACH, traced_process, NULL, NULL) != 0)  
  14.     {  
  15.         printf("Trace process failed:%d.\n", errno);  
  16.         return 1;  
  17.     }  
  18.     while(1)  
  19.     {  
  20.         wait(&status);  
  21.         if(WIFEXITED(status))  
  22.         {  
  23.             break;  
  24.         }  
  25.         tracePro(traced_process);  
  26.         ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);  
  27.     }  
  28.         
  29.     ptrace(PTRACE_DETACH, traced_process, NULL, NULL);  
  30.             
  31.     return 0;  
  32. }  

这部分代码和x86基本没有区别.因为没有设计到寄存器和中断这些架构问题.下面就是要考虑如何获取syscall的调用号.
先看x86的代码:

[java] view plain copy
  1. orig_eax =ptrace(PTRACE_PEEKUSER,child,4*ORIG_EAX,NULL);  
  2. printf("system call %ld\n",orig_eax);  

在x86架构上,Linux的系统调用都是通过int 0x80软中断实现,而触发软中断前会把调用号装入eax寄存器.这样系统调用处理程序一旦运行,就可以从eax中得到数据.由于系统调用表中的表项是以32位(4字节)类型存放的,所以内核需要将给定的系统调用号乘以4,然后用所得到的结果在该表中查询器位置.

而ARM架构上,也是通过SWI软中断实现,但是方式不同,因为有两个指令,EABI和OABI.

关于两者的区别请参考 http://bbs.chinaunix.NET/thread-1950213-1-1.html 说的十分详细

[EABI]
机器码:1110 1111 0000 0000 -- SWI 0
具体的调用号存放在寄存器r7中.

[OABI]
机器码:1101 1111 vvvv vvvv -- SWI immed_8
调用号进行转换以后得到指令中的立即数.立即数=调用号 | 0x900000

所以在获取syscall调用号的时候要做区分,要获取SWI指令判断是EABI还是OABI,如果是EABI就直接在r7中获取调用号,如果是OABI则需要获取立即数然后反向计算.

完整的trace代码如下:

[java] view plain copy
  1. #include <sys/ptrace.h>  
  2. #include <sys/types.h>  
  3. #include <sys/wait.h>  
  4. #include <unistd.h>  
  5. #include <stdio.h>  
  6. #include <errno.h>  
  7. #include <string.h>  
  8. #include <stdlib.h>  
  9. #include <sys/syscall.h>  
  10.         
  11. long getSysCallNo(int pid, struct pt_regs *regs)  
  12. {  
  13.     long scno = 0;  
  14.     ptrace(PTRACE_GETREGS, pid, NULL, regs);  
  15.     scno = ptrace(PTRACE_PEEKTEXT, pid, (void *)(regs->ARM_pc - 4), NULL);   
  16.     if(scno == 0)  
  17.         return 0;  
  18.              
  19.     if (scno == 0xef000000)   
  20.     {   
  21.         scno = regs->ARM_r7;   
  22.     }   
  23.     else   
  24.     {   
  25.         if ((scno & 0x0ff00000) != 0x0f900000)   
  26.         {   
  27.             return -1;   
  28.         }   
  29.         
  30.         scno &= 0x000fffff;   
  31.     }  
  32.     return scno;      
  33. }  
  34.         
  35. void tracePro(int pid)  
  36. {  
  37.     long scno=0;  
  38.     struct pt_regs regs;  
  39.   
  40.     scno = getSysCallNo(pid, &regs);  
  41.     printf("Target syscall no:%ld\n",scno);  
  42. }  
  43.         
  44. int main(int argc, char *argv[])  
  45. {     
  46.     if(argc != 2)   
  47.     {  
  48.        printf("please input pid...\n");  
  49.        return 1;  
  50.     }  
  51.             
  52.     pid_t traced_process;  
  53.     int status;  
  54.     traced_process = atoi(argv[1]);  
  55.   
  56.     if( ptrace(PTRACE_ATTACH, traced_process, NULL, NULL) != 0)  
  57.     {  
  58.         printf("Trace process failed:%d.\n", errno);  
  59.         return 1;  
  60.     }  
  61.     while(1)  
  62.     {  
  63.         wait(&status);  
  64.         if(WIFEXITED(status))  
  65.         {  
  66.             break;  
  67.         }  
  68.         tracePro(traced_process);  
  69.         ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);  
  70.     }  
  71.         
  72.     ptrace(PTRACE_DETACH, traced_process, NULL, NULL);  
  73.             
  74.     return 0;  
  75. }  
运行结果如图
浅谈ARM上的Ptrace

从结果上得出,每次printf都会引发两次__NR_write调用,每次sleep都会调用两次__NR_clock_nanosleep.

0x2 具体实例

在__NR_write上动手,添加一个"hahaha"

[java] view plain copy
  1. void tracePro(int pid)  
  2. {  
  3.     long scno=0;  
  4.     struct pt_regs regs;  
  5.   
  6.     scno = getSysCallNo(pid, &regs);  
  7.     if (scno == __NR_write)  
  8.     {  
  9.         printf("hahaha...\n");  
  10.     }  
  11.     printf("Target syscall no:%ld\n",scno);  
  12. }  
结果如下:

浅谈ARM上的Ptrace

参考文章:
<<玩转PTRACE>>
http://hi.baidu.com/harry_lime/item/cce5161e4af86d4a71d5e8ff