模糊测试器的几种构建思想

熟悉模糊测试的人都知道,模糊测试的主要思想是通过生成测试用例或对测试用例进行变异,让目标程序执行,从而发现其漏洞。本文中,主要结合当前的几款模糊测试器讨论执行目标程序的几种方法。

  1. fork(),execute(),wait()
    这种方法主要是通过检查wait或是waitpid返回的状态轻松的判断子进程是否崩溃。下面看一段iFUZZ工具的代码,iFUZZ是一个本地模糊测试器,下载地址:http://www.fuzzing.org/
    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的返回状态来判断子进程是否崩溃。
  1. 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的方式实现,但所不同的是,第三种方法里面子进程与主进程通过管道通信,这样大大节省了通信时间与执行时间。

你可能感兴趣的:(模糊测试)