熟悉模糊测试的人都知道,模糊测试的主要思想是通过生成测试用例或对测试用例进行变异,让目标程序执行,从而发现其漏洞。本文中,主要结合当前的几款模糊测试器讨论执行目标程序的几种方法。
if ((pid = fork ()) != 0) //father process
{
child = pid;
signal (SIGALRM, &handle_alarm);
alarm (TIME_TO_DIE);
waitpid (pid, &status, 0); //wait son process
alarm (0);
…
}
else
{
…
execle (fullpath, get_random_string (), “-h”, “-z”, “-zz”, “----”, NULL,
environ);
perror (“execle”);
}
```
父进程首先fork()一个子进程,该子进程使用execve执行目标程序,父进程则使用waitpid()等待子进程的执行,通过判断waitpid的返回状态来判断子进程是否崩溃。
fork(),ptrace(),execve(),waitpid()
主要参考SPIKE的模糊测试框架,第一种方法只能通过检测子程序的返回值来判断子程序是否崩溃,如果希望捕获被应用处理的信号(因为这些信号是被应用而不是系统处理,所以用上面的方法无法发现),可以使用ptrace达成这个目的。
SPIKEfile工具的子进程部分代码:
if ( !(pid = fork ()) )
{ /* 子进程调用ptrace,向ptrace发送PTRACE_TRACEME,请求父进程对其进行跟踪。然后开始运行目标应用*/
ptrace (PTRACE_TRACEME, 0, NULL, NULL);
execve (argv[0], argv, envp);
}
else
{ /* 由于子进程使用了PTRACE_TRACEME请求,父进程能够接收到发送给子进程的所有信号。父进程循环接收发送给子进程的每一个信号,然后根据信号和子进程的状态采取不同的动作 /
c_pid = pid;
waitpid (pid, &status, 0);
if ( WIFEXITED (status) )
{ / 检测子程序正常退出 /
if ( !quiet )
printf (“Process %d exited with code %d\n”, pid,
WEXITSTATUS (status));
return(ERR_OK);
}
else if ( WIFSIGNALED (status) )
{ / 程序收到特定信号,退出 /
if ( !quiet )
printf (“Process %d terminated by unhandled signal %d\n”, pid,
WTERMSIG (status));
return(ERR_OK);
}
else if ( WIFSTOPPED (status) )
{ / 程序收到特定信号,停止 /
if ( !quiet )
fprintf (stderr, "Process %d stopped due to signal %d (%s) ",
pid,
WSTOPSIG (status), F_signum2ascii (WSTOPSIG (status)));
}
switch ( WSTOPSIG (status) )
{ / 下面通常是感兴趣的信号 /
case SIGILL: //非法指令
case SIGBUS: //总线错误
case SIGSEGV: //无效的内存引用
case SIGSYS: //错误的系统调用
pDumpfile = malloc(strlen(pOutfileProcess)+1+8+strlen("-dump.txt")+1);
F_getregs (pid, ®s);
sprintf(pDumpfile,"%s-%.8x-dump.txt",pBasefile,(unsigned)regs.eip);
if ( !(fp = fopen(pDumpfile,“w”)) )
{
perror(“fopen”);
abort();
}
fprintf(fp,“TYPE %d: FUZZ %d: BYTE %d\n”,type,fuzz,byte);
F_printregs (fp,regs);
F_libdis_print (fp,pid, 9, regs.eip);
F_memdump (fp,pid, regs.esp, 128,"%esp");
if ( (ptrace (PTRACE_CONT, pid, NULL,
(WSTOPSIG (status) == SIGTRAP) ? 0 : WSTOPSIG (status))) == -1 )
{
perror(“ptrace”);
}
ptrace(PTRACE_DETACH,pid,NULL,NULL);
fclose(fp);
return(ERR_CRASH);
}
/ 回传信号,继续跟踪 */
if ( !quiet )
fprintf (stderr, “Continuing…\n”);
if ( (ptrace (PTRACE_CONT, pid, NULL,
(WSTOPSIG (status) == SIGTRAP) ? 0 : WSTOPSIG (status))) == -1 )
{
perror(“ptrace”);
}
goto monitor;
}
return(ERR_OK);
在模糊测试器中,我们需要注意wait或waitpid这个系统调用,要确保每一个子进程,父进程都会对应的有一个waitpid,不然容易在系统中产生僵尸进程。僵尸进程是父进程创建并且已经执行完成的进程,但由于父进程没有调用wait或waitpid获取它的状态,导致该进程变成僵尸进程。僵尸进程的信息会一直存在与内核中,直到父进程调用wait或waitpid后,内核中该进程的信息才会被释放,这时进程才会真正结束。
3. forkserver方法
观察上面2种方法可以发现,对于目标程序的每一次测试都需要子程序执行一次execve,而 AFL 采用 forkserver 技术,只需进行一次 execve() 函数执行,之后的 fuzz 进程通过写时拷⻉技术从已经停止的 fuzz 进程镜像直接拷⻉。AFL主要是在对代码进行插桩时,将forkserver的汇编代码插入目标程序中执行,汇编代码与流程图如下:
```
"__afl_forkserver:\n"
"\n"
" /* Enter the fork server mode to avoid the overhead of execve() calls. We\n"
" push rdx (area ptr) twice to keep stack alignment neat. */\n"
"\n"
" pushq %rdx\n"
" pushq %rdx\n"
"\n"
" /* Phone home and tell the parent that we're OK. (Note that signals with\n"
" no SA_RESTART will mess it up). If this fails, assume that the fd is\n"
" closed because we were execve()d from an instrumented binary, or because\n"
" the parent doesn't want to use the fork server. */\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write")
"\n"
" cmpq $4, %rax\n"
" jne __afl_fork_resume\n"
"\n"
"__afl_fork_wait_loop:\n"
"\n"
" /* Wait for parent by reading from the pipe. Abort if read fails. */\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi /* file desc */\n"
CALL_L64("read")
" cmpq $4, %rax\n"
" jne __afl_die\n"
"\n"
" /* Once woken up, create a clone of our process. This is an excellent use\n"
" case for syscall(__NR_clone, 0, CLONE_PARENT), but glibc boneheadedly\n"
" caches getpid() results and offers no way to update the value, breaking\n"
" abort(), raise(), and a bunch of other things :-( */\n"
"\n"
CALL_L64("fork")
" cmpq $0, %rax\n"
" jl __afl_die\n"
" je __afl_fork_resume\n"
"\n"
" /* In parent process: write PID to pipe, then wait for child. */\n"
"\n"
" movl %eax, __afl_fork_pid(%rip)\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_fork_pid(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write")
"\n"
" movq $0, %rdx /* no flags */\n"
" leaq __afl_temp(%rip), %rsi /* status */\n"
" movq __afl_fork_pid(%rip), %rdi /* PID */\n"
CALL_L64("waitpid")
" cmpq $0, %rax\n"
" jle __afl_die\n"
"\n"
" /* Relay wait status to pipe, then loop back. */\n"
"\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write")
"\n"
" jmp __afl_fork_wait_loop\n"
"\n"
"__afl_fork_resume:\n"
"\n"
" /* In child process: close fds, resume execution. */\n"
"\n"
" movq $" STRINGIFY(FORKSRV_FD) ", %rdi\n"
CALL_L64("close")
```
![forkserver流程图](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNTIzMTQzODEyODg0?x-oss-process=image/format,png)
总结:
本文归纳了3中模糊测试器中执行目标程序的方法,其实应该算是2中,因为第三种AFL的forkserver模式其实也是通过fork,waitpid的方式实现,但所不同的是,第三种方法里面子进程与主进程通过管道通信,这样大大节省了通信时间与执行时间。