这几天通过《游戏安全——手游安全技术入门这本书》了解到linux系统中ptrace()这个函数可以实现外挂功能,于是在ubuntu 16.04 x86_64系统上对这个函数进行了学习。

参考资料:

Playing with ptrace, Part I

Playing with ptrace, Part II

这两篇文章里的代码都是在x86平台上运行的,本文中将其移植到了x86_64平台。

ptrace提供让一个进程来控制另一个进程的能力,包括检测,修改被控制进程的代码,数据,寄存器,进而实现设置断点,注入代码和跟踪系统调用的功能。

这里把使用ptrace函数的进程称为tracer,被控制的进程称为tracee。

使用ptrace函数来拦截系统调用(system call)

操作系统向上层提供标准的API来执行与底层硬件交互的操作,这些标准API称为系统调用,每个系统调用都有一个调用编号,可以在unistd.h中查询。当进程触发一个系统调用时它会把参数放入寄存器中,然后通过软中断进入内核模式,通过内核来执行这个系统调用的代码。

在X86_64体系中,系统调用号保存在rax,调用参数依次保存在rdi,rsi,rdx,rcx,r8和r9中;而在x86体系中,系统调用号保存在寄存器eax中,其余的参数依次保存在ebx,ecx,edx,esi中

例如控制台打印所执行的系统调用为

write(,"Hello",)

翻译为汇编代码为

mov rax,
mov rdi, message
mov rdx,
syscall
message:
db "Hello"

在执行系统调用时,内核先检测一个进程是否为tracee,如果是的话内核就会暂停该进程,然后把控制权转交给tracer,之后tracer就可以查看或者修改tracee的寄存器了。

示例代码如下

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <stdio.h> int main()
{
pid_t child;
long orig_rax;
child = fork();
if(child == )
{
ptrace(PTRACE_TRACEME,,NULL,NULL);
execl("/bin/ls","ls",NULL);
}
else
{
wait(NULL);
orig_rax = ptrace(PTRACE_PEEKUSER,child,*ORIG_RAX,NULL);
printf("the child made a system call %ld\n",orig_rax);
ptrace(PTRACE_CONT,child,NULL,NULL);
}
return ;
} //输出:the child made a system call 59

该程序通过fork创建出一个我们将要跟踪(trace)的子进程,在执行execl之前,子进程通过ptrace函数的PTRACE_TRACEME参数来告知内核自己将要被跟踪。

对于execl,这个函数实际上会触发execve这个系统调用,这时内核发现此进程为tracee,然后将其暂停,发送一个signal唤醒等待中的tracer(此程序中为主线程)。

当触发系统调用时,内核会将保存调用编号的rax寄存器的内容保存在orig_rax中,我们可以通过ptrace的PTRACE_PEEKUSER参数来读取。

ORIG_RAX为寄存器编号,保存在sys/reg.h中,而在64位系统中,每个寄存器有8个字节的大小,所以此处用8*ORIG_RAX来获取该寄存器地址。

当我们获取到系统调用编号以后,就可以通过ptrace的PTRACE_CONT参数来唤醒暂停中的子进程,让其继续执行。

ptrace参数

long ptrace(enum __ptrace_request request,pid_t pid,void addr, void *data);

参数request 控制ptrace函数的行为,定义在sys/ptrace.h中。

参数pid 指定tracee的进程号。

以上两个参数是必须的,之后两个参数分别为地址和数据,其含义由参数request控制。

具体request参数的取值及含义可查看帮助文档(控制台输入: man ptrace)

注意返回值,man手册上的说法是返回一个字的数据大小,在32位机器上是4个字节,在64位机器上是8个字节,都对应一个long的长度。百度可以搜到很多不负责的帖子说返回一个字节的数据是不对的!

读取系统调用参数

通过ptrace的PTRACE_PEEKUSER参数,我们可以查看USER区域的内容,例如查看寄存器的值。USER区域为一个结构体(定义在sys/user.h中的user结构体)。

内核将寄存器的值储存在该结构体中,便于tracer通过ptrace函数查看。

示例代码如下

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> int main()
{
pid_t child;
long orig_rax,rax;
long params[]={};
int status;
int insyscall = ;
child = fork();
if(child == )
{
ptrace(PTRACE_TRACEME,,NULL,NULL);
execl("/bin/ls","ls",NULL);
}
else
{
while()
{
wait(&status);
if(WIFEXITED(status))
break;
orig_rax = ptrace(PTRACE_PEEKUSER,child,*ORIG_RAX,NULL);
//printf("the child made a system call %ld\n",orig_rax);
if(orig_rax == SYS_write)
{
if(insyscall == )
{
insyscall = ;
params[] = ptrace(PTRACE_PEEKUSER,child,*RDI,NULL);
params[] = ptrace(PTRACE_PEEKUSER,child,*RSI,NULL);
params[] = ptrace(PTRACE_PEEKUSER,child,*RDX,NULL);
printf("write called with %ld, %ld, %ld\n",params[],params[],params[]);
}
else
{
rax = ptrace(PTRACE_PEEKUSER,child,*RAX,NULL);
printf("write returned with %ld\n",rax);
insyscall = ;
}
}
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
}
}
return ; }
/***
输出:
write called with 1, 25226320, 65
ptrace_1.c  ptrace_2.c    ptrace_3.C  ptrace_4.C    ptrace_5.c  test.c
write returned with 65
***/

以上代码中我们查看write系统调用(由ls命令向控制台打印文字触发)的参数。

为了追踪系统调用,我们使用ptrace的PTRACE_SYSCALL参数,它会使tracee在触发系统调用或者结束系统调用时暂停,同时向tracer发送signal。

在之前的例子中我们使用PTRACE_PEEKUSER参数来查看系统调用的参数,同样的,我们也可以查看保存在RAX寄存器中的系统调用返回值。

上边代码中的status变量时用来检测是否tracee已经执行结束,是否需要继续等待tracee执行。

读取所有寄存器的值

这个例子中演示一个获取寄存器值的简便方法

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h> int main()
{
pid_t child;
long orig_rax ,rax;
long params[] = {};
int status = ;
int insyscall = ;
struct user_regs_struct regs;
child = fork();
if(child == )
{
ptrace(PTRACE_TRACEME,,NULL,NULL);
execl("/bin/ls","ls",NULL);
}
else
{
while()
{
wait(&status);
if(WIFEXITED(status))
break;
orig_rax = ptrace(PTRACE_PEEKUSER,child,*ORIG_RAX,NULL);
if(orig_rax == SYS_write)
{
if(insyscall == )
{
insyscall = ;
ptrace(PTRACE_GETREGS,child,NULL,&regs);
printf("write called with %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx);
}
else
{
ptrace(PTRACE_GETREGS,child,NULL,&regs);
printf("write returned with %ld\n",regs.rax);
insyscall = ;
}
}
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
}
}
return ;
}

这个例子中通过PTRACE_GETREGS参数获取了所有的寄存器值。结构体user_regs_struct定义在sys/user.h中。

修改系统调用的参数

现在我们已经知道如何拦截一个系统调用并查看其参数了,接下来我们来修改它

#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h> #define LONG_SIZE 8
//获取参数
char* getdata(pid_t child,unsigned long addr,unsigned long len)
{
char *str =(char*) malloc(len + );
memset(str,,len +);
union u{
long int val;
char chars[LONG_SIZE];
}word;
int i, j;
for(i = ,j = len/LONG_SIZE; i<j; ++i)
{
word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL);
if(word.val == -)
perror("trace get data error");
memcpy(str+i*LONG_SIZE,word.chars,LONG_SIZE);
}
j = len % LONG_SIZE;
if(j != )
{
word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL);
if(word.val == -)
perror("trace get data error");
memcpy(str+i*LONG_SIZE,word.chars,j);
}
return str;
}
//提交参数
void putdata(pid_t child,unsigned long addr,unsigned long len, char *newstr)
{
union u
{
long val;
char chars[LONG_SIZE];
}word;
int i,j;
for(i = , j = len/LONG_SIZE; i<j ; ++i)
{
memcpy(word.chars,newstr+i*LONG_SIZE,LONG_SIZE);
if(ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val) == -)
perror("trace error"); }
j = len % LONG_SIZE;
if(j != )
{
memcpy(word.chars,newstr+i*LONG_SIZE,j);
ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val);
}
}

//修改参数
void reserve(char *str,unsigned int len)
{
int i,j;
char tmp;
for(i=,j=len-; i<=j; ++i,--j )
{
tmp = str[i];
str[i] = str[j];
str[j] = tmp;
}
} int main()
{
pid_t child;
child = fork();
if(child == )
{
ptrace(PTRACE_TRACEME,,NULL,NULL);
execl("/bin/ls","ls",NULL);
}
else
{
struct user_regs_struct regs;
int status = ;
int toggle = ;
while()
{
wait(&status);
if(WIFEXITED(status))
break;
memset(&regs,,sizeof(struct user_regs_struct));
if(ptrace(PTRACE_GETREGS,child,NULL,&regs) == -)
{
perror("trace error");
} if(regs.orig_rax == SYS_write)
{
if(toggle == )
{
toggle = ;
//in x86_64 system call ,pass params with %rdi, %rsi, %rdx, %rcx, %r8, %r9
//no system call has over six params
printf("make write call params %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx);
char *str = getdata(child,regs.rsi,regs.rdx);
printf("old str,len %lu:\n%s",strlen(str),str);
reserve(str,regs.rdx);
printf("hook str,len %lu:\n%s",strlen(str),str);
putdata(child,regs.rsi,regs.rdx,str);
free(str);
}
else
{
toggle = ;
}
}
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
}
}
return ;
}
/***
输出:
make write call params 1, 9493584, 66
old str,len 66:
ptrace        ptrace2    ptrace3     ptrace4    ptrace5     test    test.s
hook str,len 66:
s.tset    tset     5ecartp    4ecartp     3ecartp    2ecartp        ecartp
s.tset    tset     5ecartp    4ecartp     3ecartp    2ecartp        ecartp
make write call params 1, 9493584, 65
old str,len 65:
ptrace_1.c  ptrace_2.c    ptrace_3.C  ptrace_4.C    ptrace_5.c  test.c
hook str,len 65:
c.tset  c.5_ecartp    C.4_ecartp  C.3_ecartp    c.2_ecartp  c.1_ecartp
c.tset  c.5_ecartp    C.4_ecartp  C.3_ecartp    c.2_ecartp  c.1_ecartp
***/

这个例子中,综合了以上我们提到的所有知识。进一步得,我们使用了ptrace的PTRACE_POKEDATA参数来修改系统调用的参数值。

这个参数和PTRACE_PEEKDATA参数的作用相反,它可以修改tracee指定地址的数据。

单步调试

接下来介绍一个调试器中常用的操作,单步调试,它就用到了ptrace的PTRACE_SINGLESTEP参数。

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h> #define LONG_SIZE 8 void main()
{
pid_t chid;
chid = fork();
if(chid == )
{
ptrace(PTRACE_TRACEME,,NULL,NULL);
     //这里的test是一个输出hello world的小程序
execl("./test","test",NULL);
}
else
{
int status = ;
struct user_regs_struct regs;
int start = ;
long ins;
while()
{
wait(&status);
if(WIFEXITED(status))
break;
ptrace(PTRACE_GETREGS,chid,NULL,&regs);
if(start == )
{
ins = ptrace(PTRACE_PEEKTEXT,chid,regs.rip,NULL);
printf("EIP:%llx Instuction executed:%lx\n",regs.rip,ins);
}
if(regs.orig_rax == SYS_write)
{
start = ;
ptrace(PTRACE_SINGLESTEP,chid,NULL,NULL);
}else{
ptrace(PTRACE_SYSCALL,chid,NULL,NULL);
}
}
}
}

通过rip寄存器的值来获取下一条要执行指令的地址,然后用PTRACE_PEEKDATA读取。

这样,就可以看到要执行的每条指令的机器码。

注:本文对开头文章参考资料进行了一些翻译,所有代码均在ubuntu 16.04 64bit 中运行通过。

linux ptrace I的更多相关文章

  1. linux ptrace II

    第一篇 linux ptrace I 在之前的文章中我们用ptrace函数实现了查看系统调用参数的功能.在这篇文章中,我们会用ptrace函数实现设置断点,跟代码注入功能. 参考资料 Playing ...

  2. Linux Ptrace 详解

    转 https://blog.csdn.net/u012417380/article/details/60470075 Linux Ptrace 详解 2017年03月05日 18:59:58 阅读数 ...

  3. linux ptrace I【转】

    转自:https://www.cnblogs.com/mmmmar/p/6040325.html 这几天通过<游戏安全——手游安全技术入门这本书>了解到linux系统中ptrace()这个 ...

  4. linux ptrace学习

    ptrace提供了一种使父进程得以监视和控制其它进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪.学习linux的ptrace是为学习android adbi框 ...

  5. linux下 玩转ptrace

    译者序:在开发Hust Online Judge的过程中,查阅了不少资料,关于调试器技术的资料在网上是很少,即便是UNIX编程巨著<UNIX环境高级编程>中,相关内容也不多,直到我在 ht ...

  6. linux应用调试技术之GDB和GDBServer

    1.调试原理 GDB调试是应用程序在开发板上运行,然后在PC机上对开发板上得应用程序进行调试,PC机运行GDB,开发板上运行GDBServer.在应用程序调试的时候,pc机上的gdb向开发板上的GDB ...

  7. linux内核学习之二 一个精简内核的分析(基于时间片轮转)

    一   实验过程及效果 1.准备好相关的代码,分别是mymain.c,mypcb.h,myinterrupt.c ,如下图,make make成功: 在qemu创建的虚拟环境下的运行效果:(使用的命令 ...

  8. kernel/ptrace.c

    /* ptrace.c *//* By Ross Biro 1/23/92 *//* edited by Linus Torvalds */ #include <linux/head.h> ...

  9. Linux LSM(Linux Security Modules) Hook Technology

    目录 . 引言 . Linux Security Module Framework Introduction . LSM Sourcecode Analysis . LSMs Hook Engine: ...

随机推荐

  1. uboot环境变量区为何不能放在data段

    一.疑问 环境变量也是全局变量,为何不能像其他的全局变量放在data段呢?为什么要放在堆中或者使用ENV_IS_EMBEDDED定义的CFG_ENV_SIZE的空间大小,又为什么需要这么大的空间呢? ...

  2. 编程框架—Autofac

    Autofac是一款轻量级的IOC框架,性能高. Autofac基本使用步骤: 1.创建容器建造者(Builder): 2.对Builder注册类型. 3.Buildder创建容器(Container ...

  3. 自定义Web控件写事件

    --------------------myRegister1.ascx前台代码----------------------- <script src="js/Jquery1.7.js ...

  4. matlab操作之--读取指定文件夹下的“指定格式”文件

    %% 正负样本所在folder fext='*.png';%要读取的文件格式 positiveFolder='F:\课题\Crater detection\machingLearning\Positi ...

  5. Hadoop 学习笔记 (十) MapReduce实现排序 全局变量

    一些疑问:1 全排序的话,最后的应该sortJob.setNumReduceTasks(1);2 如果多个reduce task都去修改 一个静态的 IntWritable ,IntWritable会 ...

  6. Ubuntu 桌面歌词

    Ubuntu 有个用来显示歌词软件叫 osd-lyrics. 这个软件的强大之处在于他可以和各种播放器配合, 并且可以自动下载歌词. 自从升级到14.04后不能用了,便以为该软件被废弃了. 无意中发现 ...

  7. 小细节:Java中split()中的特殊分隔符 小数点

    这两天做项目过程中由于数据表字段设计的太恶心了,导致自己填坑 关于微信支付和支付宝的支付有一个不同点:就是金额的处理,支付宝金额的单位是0.01元,但是微信支付中1表示0.01元,当时设计价格的时候使 ...

  8. 【转】Android ProgressDialog的使用

    原文网址:http://blog.csdn.net/sjf0115/article/details/7255280 版权声明:本文为博主原创文章,未经博主允许不得转载. <1>简介 Pro ...

  9. hdoj 1061 Rightmost Digit【快速幂求模】

    Rightmost Digit Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)T ...

  10. 挣值管理不是搞数字游戏(3)——进阶指标:CV、SV、CPI、SPI、EAC

    摘要: 要考PMP(Project Management Professional ),挣值管理是必考的知识.软件项目有很大的特殊性,不少人认为挣值管理不太适用于软件项目.挣值管理相关资料也比较超多, ...