Linux沙箱之ptrace

第一次接触到ptrace是学习gdb的实现,它作为一个系统调用如其名提供对进程追踪的功能,能在一个进程里观察与控制另一个进程的运行状态,因此也可以作为沙箱保护的工具~

信号

ptrace是使用信号来进行进程间通信,所以先复习下信号,可以使用kill向进程发送信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ubuntu@VM-49-124-ubuntu:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

Linux下信号为软中断,程序在接收到信号后会中断现有执行,若用户事先绑定了处理例程那么内核将回到用户态调用此例程处理它,否则根据相应类型信号进行默认操作(终止进程或忽略),若程序未终止那么在处理完信号后又会回到中断处继续执行。比如比赛常见到的定时结束进程:

1
2
3
4
5
void handler(int signum){
perrorAndExit("timeout~");
}
signal(SIGALRM,handler); //绑定信号处理例程,当收到SIGALRM信号时,调用handler处理
alarm(time); //设置定时器,当时间到了将会发送SIGALRM信号

ptrace

作用

  1. 编写动态分析工具,如gdb,strace
  2. 反追踪,一个进程只能被一个进程追踪(注:一个进程能同时追踪多个进程),若此进程已被追踪,其他基于ptrace的追踪器将无法再追踪此进程,更进一步可以实现子母进程双线执行动态解密代码等更高级的反分析技术
  3. 代码注入,往其他进程里注入代码。
  4. 本篇关注点–做沙箱,它也能监控系统调用,因此可限制tracee的系统调用。

说明

其原型如下,根据第一个参数request表明要执行的操作,第二个参数pid表明要监视与控制(本篇统一叫作追踪追踪)的进程(其实是线程,后不作区分)ID,第三个与第四个参数是一对,由第一个参数指明其含义。

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

在ptrace中有两个角色:

  1. tracee:被追踪者,它是被监控的进程,通过ptrace系统调用的操作作用在它之上。
  2. tracer:追踪者,它负责监视并处理被追踪者传来的信息。

理所当然的不是任何进程都能追踪其他进程,可追踪又三种情况:

  1. 拥有root权限的进程是可以追踪所有进程的
  2. 父进程可以追踪子进程
  3. 进程可以指定被某个进程极其祖先进程追踪

在使用ptrace之前需要在两个进程间建立追踪关系,其中tracee可以不做任何事,也可使用prctlPTRACE_TRACEME来进行设置,ptrace编程的主要部分是tracer,它可以通过附着的方式与tracee建立追踪关系,建立之后,可以控制tracee在特定的时候暂停并向tracer发送相应信号,而tracer则通过循环等待waitpid来处理tracee发来的信号。

建立追踪关系

在进行追踪前需要先建立追踪关系,相关request有如下4个:

1
2
3
4
PTRACE_TRACEME:tracee表明自己想要被追踪,这会自动与父进程建立追踪关系,这也是唯一能被tracee使用的request,其他的request都由tracer指定。
PTRACE_ATTACH:tracer用来附着一个进程tracee,以建立追踪关系,并向其发送`SIGSTOP`信号使其暂停。
PTRACE_SEIZE:像PTRACE_ATTACH附着进程,但它不会让tracee暂停,addr参数须为0,data参数指定一位ptrace选项。
PTRACE_DETACH:解除追踪关系,tracee将继续运行。

其中建立关系时,tracer使用如下方法:

1
2
3
ptrace(PTRACE_ATTACH, pid, 0, 0);
//或
ptrace(PTRACE_SEIZE, pid, 0, PTRACE_O_flags); //指定追踪选项立即生效

当建立追踪关系后,即使tracer执行了execve这种追踪关系依然存在。

tracee状态

在ptrace下tracee被定义为两种状态:ptrace-running(即使tracee在系统调用中阻塞也属于running态) 和 ptrace-stopped.(PTRACE_LISTEN为一种特殊状态,下面提到)
除了PTRACE_ATTACH, PTRACE_SEIZE,PTRACE_TRACEME, PTRACE_INTERRUPT 和 PTRACE_KILL,其他类型的操作都需要tracee为ptrace-stop状态,实际上按照暂停发生的原因,还可以将ptrace-stop状态细分:

  1. signal-delivery-stop:当由kill等向整个进程发送除SIGKILL外的信号时,内核会任选一个线程去处理这个信号,而使用tgkill会明确的指定哪个线程处理该信号。无论如何,若被选中的线程正在被追踪,该线程将进入signal-delivery-stop状态,此时该信号将不会被直接投递给此线程,而是会被转交给tracer,tracer可以选择处理该信号,也可以在恢复tracee执行时将信号(可以为原来的信号也可以自己指定其他信号)交给tracee,这个过程被叫做Signal injection,tracer恢复tracee运行时可以把收到的信号投递给tracee:
    1
    ptrace(PTRACE_restart, pid, 0, sig) //若sig为0表示不注入信号
  2. group-stop
  3. Syscall-stops:如果tracee被PTRACE_SYSCALL或PTRACE_SYSEMU恢复,tracee在进入系统调用前会进入syscall-enter-stop状态,当使用PTRACE_SYSEMU恢复时,该系统调用将不会被执行,不管是使用哪种方式进入的syscall-entry-stop状态,只要再次使用PTRACE_SYSCALL恢复执行,在此次系统调用完成(或被信号中断)时tracee就会进入syscall-exit-stop状态。
  4. PTRACE_SINGLESTEP stops
  5. PTRACE_EVENT stops: 当tracer设置了PTRACE_O_TRACE_*选项,在对应事件发生时就会进入
    1
    2
    3
    4
    5
    6
    7
    8
    PTRACE_EVENT_VFORK:vfork和clone使用了CLONE_VFORK标识时,在他们返回前暂停
    PTRACE_EVENT_FORK:fork和clone退出,信号设置为SIGCHLD,返回前停止。
    PTRACE_EVENT_CLONE:停在clone返回前
    PTRACE_EVENT_VFORK_DONE:vfork和clone使用了CLONE_VFORK标识时,在他们返回前暂停
    PTRACE_EVENT_EXEC
    PTRACE_EVENT_EXIT
    PTRACE_EVENT_STOP
    PTRACE_EVENT_SECCOMP:PTRACE_O_TRACESECCOMP被设置,seccomp规则被触发

    读写数据

    当tracee处于ptrace-stopped状态时可以读写context和memory:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ptrace(PTRACE_PEEKTEXT/PEEKDATA/PEEKUSER, pid, addr, 0);//读数据,TEXT和DATA不做区分,即同义,USER为内核中进程定义的user结构体,定义在/usr/include/sys/user.h
    ptrace(PTRACE_POKETEXT/POKEDATA/POKEUSER, pid, addr, long_val);//写数据
    ptrace(PTRACE_GETREGS/GETFPREGS, pid, 0, &struct);//读寄存器
    ptrace(PTRACE_SETREGS/SETFPREGS, pid, 0, &struct);//写寄存器
    ptrace(PTRACE_GETREGSET, pid, NT_foo, &iov);
    ptrace(PTRACE_SETREGSET, pid, NT_foo, &iov);
    ptrace(PTRACE_GETSIGINFO, pid, 0, &siginfo);//读信号信息
    ptrace(PTRACE_SETSIGINFO, pid, 0, &siginfo);//写信号信息
    ptrace(PTRACE_GETEVENTMSG, pid, 0, &long_var);//可获取事件信息,对于PTRACE_EVENT_EXIT得到退出状态
    //对于PTRACE_EVENT_FORK, PTRACE_EVENT_VFORK,PTRACE_EVENT_VFORK_DONE, and PTRACE_EVENT_CLONE得到新进程的PID
    //对于PTRACE_EVENT_SECCOMP得到触发规则后的SECCOMP_RET_DATA。

    设置选项

    使用PTRACE_SETOPTIONS可设置选项,这样程序触发相应事件时将会执行预定操作,这在安全保护中极其重要:
    1
    ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_flags);
    PTRACE_O_flags可为以下值:
    1
    2
    3
    4
    5
    6
    7
    PTRACE_O_EXITKILL:当tracer终止时向tracee发送SIGKILL信号终止tracee,若不设置此选项线程将deattch并恢复运行,这个选项在安全保护中极其重要,不设置它子进程可以通过杀死父进程绕过保护。
    PTRACE_O_TRACEEXEC:当tracee执行`execve`时暂停,即使未设置该选项,在新程序开始执行前也会用`SIGTRAP`暂停,此时tracer还有机会决定是否继续trace。
    PTRACE_O_TRACEEXIT:当tracee执行`exit`时暂停,此时仍可读寄存器。
    PTRACE_O_TRACEFORK:当tracee执行`fork`时暂停,tracer会自动追踪新fork出来的进程。
    PTRACE_O_TRACEVFORK:当tracee执行`vfork`时暂停,tracer会自动追踪新vfork出来的进程。
    PTRACE_O_TRACECLONE:当执行`clone`时暂停,tracer会自动追踪新clone出来的进程。
    PTRACE_O_TRACEVFORKDONE:当下次tracee`vfork`执行完成时暂停。
    当设置PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK, or PTRACE_O_TRACECLONE,新进程将仍然被追踪,并且继承父进程的flags,新进程的PID可由PTRACE_GETEVENTMSG获取。

    继续运行

    当完成读写与设置选项操作以后,可以让tracee恢复执行,而且可以同时设置下次暂停的条件:
    1
    ptrace(cmd, pid, 0, sig);//在tracee为signal-delivery-stop状态且sig非0时,sig将作为signal被注入,否则一般会被忽略,要使tracee从ptrace-stop恢复运行,最好使sig为0.
    其中cmd可以为:
    1
    2
    3
    4
    5
    PTRACE_CONT:恢复tracee执行,若data非0,它将作为一个signal投递给tracee。
    PTRACE_LISTEN
    PTRACE_DETACH
    PTRACE_SYSCALL,PTRACE_SINGLESTEP:两者类似,都像PTRACE_CONT一样恢复tracee执行,更多的前者会让tracee在下一次进入或退出系统调用是暂停,后者会在执行单步指令之后暂停,并且都会在收到信号时也会暂停
    PTRACE_SYSEMU,PTRACE_SYSEMU_SINGLESTEP:当前只支持x86

    沙箱

    类似seccomp,只需要在系统调用前判断是否允许该调用即可,相对seccomp为程序开发者自己嵌入的代码,若要对无保护的程序添加保护需要修改源码或为可执行程序打补丁,ptrace可以在不修改被保护程序的前提下对程序进行保护,而且恶意程序是不会使用seccomp降权的,ptrace可以轻松作用到不受信任的程序上,以保护系统安全。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <stddef.h>

int main()
{
pid_t pid = fork();
if(pid){ //父进程作为tracer
int incall = 0; //由于syscall-stop会在进出系统调用时都暂停,这里简单的使用它只处理进入前的情况
while(1){ //循环处理
int status;
waitpid(pid,&status,0); //等待获取tracee传来的信号
if(WIFEXITED(status))break; //当tracee的信号非EXIT时才处理,否则tracee退出自己也就不用再等待了

long orig_rax = ptrace(PTRACE_PEEKUSER,pid, //从user结构里面取出原始rax的值
offsetof(struct user,regs.orig_rax),0);
long rsi = ptrace(PTRACE_PEEKUSER,pid, //当为write时,为buf起始地址
offsetof(struct user,regs.rsi),0);
long rdx = ptrace(PTRACE_PEEKUSER,pid, //当为write时,为长度
offsetof(struct user,regs.rdx),0);
if(incall){
printf("orig_rax = %ld\n",orig_rax); //当进入系统调用时,输出rax,即输出系统调用号
if(orig_rax==1){ //当为write时
printf("write ==> ");
for(int i=0;i<rdx;i++){
int c = ptrace(PTRACE_PEEKDATA,pid,rsi+i,0); //取出数据,虽然每次取一个word,但是为了简单就这样写
putchar(c&0xff);
}
}
}
incall= ~incall; //取反,其实用这种方法是有问题的,正常情况下系统调用进出因该是成对的,但是当系统调用被信号中断,将会破坏这种次序
//ptrace(PTRACE_CONT,pid,0,0);
ptrace(PTRACE_SYSCALL,pid,0,0); //恢复tracee的执行,并让其在下次系统调用进入或退出时再次暂停
}
}else{ //子进程
ptrace(PTRACE_TRACEME,0,0,0); //表明想要被追踪,与父进程建立追踪关系
execl("/bin/ls","ls",NULL); //执行系统调用
}
return 0;
}

程序运行可以得到结果:

1
2
3
4
5
6
7
8
9
10
11
.........
orig_rax = 217
orig_rax = 217
orig_rax = 3
orig_rax = 5
orig_rax = 1 #为write
write ==> a a.c Makefile test trace trace.c #父进程的输出,ptrace取出的数据
a a.c Makefile test trace trace.c #子进程自己的输出
orig_rax = 3 #为close
orig_rax = 3
orig_rax = 231 #为exit_group

逃逸

未正确使用ptrace的沙箱环境是可以逃逸的。例如:

  1. 未设置PTRACE_O_EXITKILL 杀掉tracer kill(-1,SIGKILL)
  2. 未设置PTRACE_O_TRACECLONE类,使用类fork逃离
  3. 利用上例方式判断系统调用时序,可通过在系统调用时中断它,来破坏时序 alarm(1) sleep(2)

参考

http://www.man7.org/linux/man-pages/man2/ptrace.2.html
stcs 2016 final