上周六日打了一场de1ctf,做了几个逆向题,感觉signal vm还比较有意思,做完这道题目感觉对于ptrace系统调用以及信号的一些东西有了更深层次的理解,趁着这个机会总结一下ptrace,signal以及线程的一些机制与底层原理
首先来说这道题目,虽然是静态编译,但是一些库函数以及系统调用其实是很明显的,比如sys_ptrace与sys_wait4等,运行程序的时候,会fork出两个进程,父进程进入vmloop,子进程进入到了一个奇怪的地方,这里首先将几乎所有的寄存器设0,然后执行ptrace(调用号为0),等待父进程的调试,而父进程的loop中开头一个wait(&status)等待子进程的信号,然后进行vmloop.
在vmloop中,父进程通过ptrace读取子进程此时的寄存器值,以及此时子进程rip的值,从子进程的ptraceme下顺次执行,**驱动vm的运行**.当然在理清逻辑以后,依旧是常规vm的方法,打log逆向发现是矩阵相乘,直接上z3-solver即可,但是我觉得这个题目的精华并不在vm部分,而在于这个由信号驱动vm执行的出题思路.
先来简单的说一下wait函数,这个函数的原型是int wait(int*),其中wait的返回值是子进程的pid,而传入的参数还会有两种情况,wait(0)的含义是传入一个空指针,此时wait函数不会获取到子进程的一些状态信息,依旧会返回pid,而wait(&status)的话,不仅会返回pid,还会返回子进程的状态,相同的是在执行wait时,父进程会挂起等待子进程的信号.
再来说一下fork()这个函数,比较特殊的是,不同于其他函数,fork()函数是有两个返回值的,当程序执行到fork函数的时候,fork函数会执行以下操作:
1. 代码段不改变
2. 复制一份fork上文的数据(包括指针,消息等)
3. 返回
在返回后,对于fork出的新进程,fork的返回值为0,而对于父进程来说,fork则返回子进程的pid,自此以后,子进程的数据与父进程就毫无关联了,因为他们操作的就是两个不一样的数据段了.在此以后,两个进程之间可能会发生以下的一些状况:
- 正常运行
- 父进程先于子进程退出
- 父进程后于子进程退出 - 有wait
- 父进程后与子进程退出 - 无wait
来说一下后三种状况,首先来说如果父进程先与子进程退出的话,此时的子进程就会变成**孤儿进程**,此时的孤儿进程将会被init进程所收养,此后init进程为孤儿进程进行状态回收的工作,这种情况下不会给系统带来很多的负担,而如果父进程后于子进程退出时,如果父进程设置了wait,那么父进程就会完成对子进程的状态回收,而如果父进程没有设置了wait的话,那么此时的子进程将会变成**僵尸进程**,此时的子进程将会继续占用系统资源
那么为什么子进程退出后还会占用系统资源呢,这个要从进程退出的原理来说,一般情况下,一个进程的退出是执行了exit(0),但是exit(0)其实**不是一个进程的退出**,在执行exit(0)以后,内核将会释放这个进程所占用的资源,文件,内存等,但是会留下来一个存放这个pid退出状态的数据结构,而这个数据结构是需要父进程的wait来进行内存回收的,而linux中的pid数是有限的,如果一个程序产生了大量的垃圾进程,那么系统将会受到很大的影响.
而当父进程调用了wait(&status)后,父进程将会被挂起,等待子进程发来的信号,然后父进程才会继续进行,如果收到的是退出信号,那么父进程将会回收子进程的数据,否则将会进行后续的逻辑处理,就此题来说,opcode会触发几个异常(非法指令,内存错误)wait接受到异常后,根据异常的不同,来选择case进行运算
再来说调试器的原理,执行fork函数,子进程执行ptraceme,等待父进程的调试,并且执行exec(filename),而父进程会等待子进程所接收的信号,再来进行调试,随后就是用到了ptrace来进行对于数据,寄存器的读写操作
ptrace的声明在sys/ptrace.h里可以找到,ptrace是一个系统调用函数,函数原型是ptrace(enum _ptrace_request request,pid_t pid,void * addr ,void *data);下面是一些request对应的功能
```c
PTRACE_TRACEME = 0,
/ Return the word in the process's text space at address ADDR. /
PTRACE_PEEKTEXT = 1,
/ Return the word in the process's data space at address ADDR. /
PTRACE_PEEKDATA = 2,
/ Return the word in the process's user area at offset ADDR. /
PTRACE_PEEKUSER = 3,
/ Write the word DATA into the process's text space at address ADDR. /
PTRACE_POKETEXT = 4,
/ Write the word DATA into the process's data space at address ADDR. /
PTRACE_POKEDATA = 5,
/ Write the word DATA into the process's user area at offset ADDR. /
PTRACE_POKEUSER = 6,
/ Continue the process. /
PTRACE_CONT = 7,
/ Kill the process. /
PTRACE_KILL = 8,
/ Single step the process.
This is not supported on all machines. /
PTRACE_SINGLESTEP = 9,
/ Get all general purpose registers used by a processes.
This is not supported on all machines. /
PTRACE_GETREGS = 12,
/ Set all general purpose registers used by a processes.
This is not supported on all machines. /
PTRACE_SETREGS = 13,
/ Get all floating point registers used by a processes.
This is not supported on all machines. /
PTRACE_GETFPREGS = 14,
/ Set all floating point registers used by a processes.
This is not supported on all machines. /
PTRACE_SETFPREGS = 15,
/ Attach to a process that is already running. /
PTRACE_ATTACH = 16,
/ Detach from a process attached to with PTRACE_ATTACH. /
PTRACE_DETACH = 17,
/ Get all extended floating point registers used by a processes.
This is not supported on all machines. /
PTRACE_GETFPXREGS = 18,
/ Set all extended floating point registers used by a processes.
This is not supported on all machines. /
PTRACE_SETFPXREGS = 19,
/ Continue and stop at the next (return from) syscall. /
PTRACE_SYSCALL = 24,
可以看到ptrace甚至很贴心的给了单步运行的rq号,我们甚至可以直接利用这东西来写一个调试器.下面是一个简单的demo:
```c
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/user.h>
typedef int pid_t;
int main()
{
pid_t pid = fork();
if(!pid)
{
int a = 0;
ptrace(0,0,0,0);
asm(".byte 0xcc");
puts("Child!");
exit(0);
}
else
{
struct user_regs_struct regs;
int status;
wait(&status);
getchar();
ptrace(7,pid,0,0); //continue
printf("status : %x\n",status);
ptrace(12,pid,0,®s);// get reg
int x = ptrace(1,pid,regs.rip,0);
printf("%x\n",x);
wait(&status);
printf("status : %x\n",status); //exit(0) --ret--> 0;
}
}
这段代码中如果最后的wait不存在的话,则会导致子进程变成僵尸进程,在编译后,运行,wait挂起等待子进程的信号,子进程遇到int 3发送中断信号,等待父进程的处理,此后父进程getchar等待用户输入,用户输入后发送continue信号,进行继续的运行,并在最后设置wait进程子进程的回收
加上一些其他的case已经输入的处理,就是一个可以用的调试器了.