首页
社区
课程
招聘
[原创]AFLNET源码分析(一)
2023-3-1 21:09 8214

[原创]AFLNET源码分析(一)

2023-3-1 21:09
8214

AFLNET源自一篇论文:

 

论文下载

 

它是一种针对网络协议的灰盒模糊测试器,通过状态反馈来指导模糊测试,在模糊测试的过程中根据协议的反馈不断完善有限状态自动机并据此指导模糊测试框架的数据变异和状态选择。

 

以往针对协议的黑盒模糊测试框架boofuzz由于缺少代码插桩和覆盖率引导,所以测试数据往往以字典为主,无法主动变异出字典以外的数据。AFLNET以经典框架AFL为基础,改进了测试端和被测端的通信方式,使其能够适配网络协议,接下来这个系列将对AFLNET的源码进行分析。

 

首先分析的是AFLNET如何实现的测试框架和被测服务端的通信,我们知道在AFL中,框架首先用execv来初始化forkserver,后续的程序启动则通过管道向forkserver发送消息不断fork子进程来进行,输入多为执行参数中直接指定某个文件作为输入,但是对于网络协议来说输入需要以数据包的形式发送到server端,来看看AFLNET是如何实现。

 

首先对于forkserver的初始化阶段AFL与AFLNET没有任何区别,区别主要体现在forkserver初始化以后的fork操作中,首先关注直接涉及到运行程序的函数run_target

 

AFL:

1
2
3
4
5
6
7
8
9
if (dumb_mode == 1 || no_forkserver) {
  if (waitpid(child_pid, &status, 0) <= 0) PFATAL("waitpid() failed");
} else {
  s32 res;
  if ((res = read(fsrv_st_fd, &status, 4)) != 4) {
    if (stop_soon) return 0;
    RPFATAL(res, "Unable to communicate with fork server (OOM?)");
  }
}

AFLNET:

1
2
3
4
5
6
7
8
9
10
11
if (dumb_mode == 1 || no_forkserver) {
  if (use_net) send_over_network();
  if (waitpid(child_pid, &status, 0) <= 0) PFATAL("waitpid() failed");
} else {
  if (use_net) send_over_network();
  s32 res;
  if ((res = read(fsrv_st_fd, &status, 4)) != 4) {
    if (stop_soon) return 0;
    RPFATAL(res, "Unable to communicate with fork server (OOM?)");
  }
}

二者的主要区别在于AFLNET多了一个send_over_network函数。
在forkserver已经初始化并且是灰盒模式的情况下,AFL会直接读取状态管道内容,即等待程序执行完毕。而AFLNET会先调用send_over_network,然后再等待程序执行。这个函数起到了测试端向服务端喂数据的功能,无论是种子数据或者经过变异后的数据,都经过这个函数发送给server进行后续代码执行。

 

来看具体实现

 

首先是定义一些变量以及执行一些初始化操作

  • likely_buggy 用来标志服务端是否可能出现crash
  • serv_addr 服务端地址套接字地址
  • local_serv_addr 本地地址套接字地址

判断是否存在清除脚本,如果存在则执行,一般用于清除执行服务端所产生的痕迹。

 

sleep一定的时间用来等待服务端初始化

 

清除缓冲区并重置缓冲区大小

 

创建TCP/UDP套接字

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
  int n;
  u8 likely_buggy = 0;
  struct sockaddr_in serv_addr;
  struct sockaddr_in local_serv_addr;
  //Clean up the server if needed
  if (cleanup_script) system(cleanup_script);
  //Wait a bit for the server initialization
  usleep(server_wait_usecs);
  //Clear the response buffer and reset the response buffer size
  if (response_buf) {
    ck_free(response_buf);
    response_buf = NULL;
    response_buf_size = 0;
  }
  if (response_bytes) {
    ck_free(response_bytes);
    response_bytes = NULL;
  }
int sockfd = -1;
  if (net_protocol == PRO_TCP)
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
  else if (net_protocol == PRO_UDP)
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
  if (sockfd < 0) {
    PFATAL("Cannot create a socket");
  }

接下来是设置超时时间以及建立服务端套接字

1
2
3
4
5
6
7
8
9
10
//Set timeout for socket data sending/receiving -- otherwise it causes a big delay
  //if the server is still alive after processing all the requests
  struct timeval timeout;
  timeout.tv_sec = 0;
  timeout.tv_usec = socket_timeout_usecs;
  setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout));
  memset(&serv_addr, '0', sizeof(serv_addr));
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(net_port);
  serv_addr.sin_addr.s_addr = inet_addr(net_ip);

然后是检查local_port是否大于0,这里的local_port是由-l 参数指定的,从注释中也能理解到,有一些协议的响应包只会发给特定的端口,所以框架提供了-l 参数指定接收端口,并将sockfd用bind函数绑定到指定端口上,如果没有指定则无需bind

1
2
3
4
5
6
7
8
9
10
11
12
//This piece of code is only used for targets that send responses to a specific port number
  //The Kamailio SIP server is an example. After running this code, the intialized sockfd
  //will be bound to the given local port
  if(local_port > 0) {
    local_serv_addr.sin_family = AF_INET;
    local_serv_addr.sin_addr.s_addr = INADDR_ANY;
    local_serv_addr.sin_port = htons(local_port);
    local_serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    if (bind(sockfd, (struct sockaddr*) &local_serv_addr, sizeof(struct sockaddr_in)))  {
      FATAL("Unable to bind socket on local source port");
    }
  }

连接服务端,如果没有连上则等待一定时间之后继续尝试

1
2
3
4
5
6
7
8
9
10
11
12
if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
    //If it cannot connect to the server under test
    //try it again as the server initial startup time is varied
    for (n=0; n < 1000; n++) {
      if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == 0) break;
      usleep(1000);
    }
    if (n== 1000) {
      close(sockfd);
      return 1;
    }
  }

接下来就是正式发送数据的逻辑:

 

首先进行一次接收,这里的net_recv后面说,这里只需要知道如果正常接收的话会返回0即可,第一次接收是为了处理刚连接上服务端的时候服务端发来的一些先行消息。如果接收出现问题则会提前跳转到HANDLE_RESPONSES部分

 

然后初始化messages_sent,这个变量表示已发送了几次消息,然后kl_messages链表,发送每个状态对应的数据并进行接收

 

net_recv函数每次执行都会更新response_buf和response_buf_size,每发送完一个状态的数据后就将当前buf_size记录到prev_buf_size中,然后接收本次发送的数据所获得的响应数据。

 

更新response_bytes,它是一个全局变量,用于记录协议走到每个状态的接收缓冲区大小

 

如果buf_size在执行完net_recv后没有任何变化说明可能出现了crash,所以将likely_buggy置1

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
//retrieve early server response if needed
  if (net_recv(sockfd, timeout, poll_wait_msecs, &response_buf, &response_buf_size)) goto HANDLE_RESPONSES;
  //write the request messages
  kliter_t(lms) *it;
  messages_sent = 0;
  for (it = kl_begin(kl_messages); it != kl_end(kl_messages); it = kl_next(it)) {
    n = net_send(sockfd, timeout, kl_val(it)->mdata, kl_val(it)->msize);
    messages_sent++;
    //Allocate memory to store new accumulated response buffer size
    response_bytes = (u32 *) ck_realloc(response_bytes, messages_sent * sizeof(u32));
    //Jump out if something wrong leading to incomplete message sent
    if (n != kl_val(it)->msize) {
      goto HANDLE_RESPONSES;
    }
    //retrieve server response
    u32 prev_buf_size = response_buf_size;
    if (net_recv(sockfd, timeout, poll_wait_msecs, &response_buf, &response_buf_size)) {
      goto HANDLE_RESPONSES;
    }
    //Update accumulated response buffer size
    response_bytes[messages_sent - 1] = response_buf_size;
    //set likely_buggy flag if AFLNet does not receive any feedback from the server
    //it could be a signal of a potentiall server crash, like the case of CVE-2019-7314
    if (prev_buf_size == response_buf_size) likely_buggy = 1;
    else likely_buggy = 0;
  }

在看HANDLE_RESPONSES部分之前先来简单看看net_recv是怎么写的:

 

简历一个1000的缓冲区,然后以缓冲区为单位调用原生的recv函数进行循环接收,每次接收的字节数n如果小于0则返回1,说明出现问题,否则就用realloc函数扩展response_buf,并更新response_buf_size的值

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
int net_recv(int sockfd, struct timeval timeout, int poll_w, char **response_buf, unsigned int *len) {
  char temp_buf[1000];
  int n;
  struct pollfd pfd[1];
  pfd[0].fd = sockfd;
  pfd[0].events = POLLIN;
  int rv = poll(pfd, 1, poll_w);
  setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout));
  // data received
  if (rv > 0) {
    if (pfd[0].revents & POLLIN) {
      n = recv(sockfd, temp_buf, sizeof(temp_buf), 0);
      if ((n < 0) && (errno != EAGAIN)) {
        return 1;
      }
      while (n > 0) {
        usleep(10);
        *response_buf = (unsigned char *)ck_realloc(*response_buf, *len + n + 1);
        memcpy(&(*response_buf)[*len], temp_buf, n);
        (*response_buf)[(*len) + n] = '\0';
        *len = *len + n;
        n = recv(sockfd, temp_buf, sizeof(temp_buf), 0);
        if ((n < 0) && (errno != EAGAIN)) {
          return 1;
        }
      }
    }
  } else
    if (rv < 0) // an error was returned
      return 1;
  // rv == 0 poll timeout or all data pending after poll has been received successfully
  return 0;
}

最后来看HANDLE_RESPONSES部分

 

首先再进行一次接收,猜测是为了防止出现服务端反应较慢导致一些数据没有被接收到,然后更新最后一个状态的buf_size

 

然后是一个等待服务端执行完毕的逻辑,但是实现的方法比较有趣,先将session_virgin_bits初始化为全1,然后不断执行has_new_bits函数,因为在上一步初始化为全1,所以has_new_bits会随着服务端程序的执行不断的覆盖到新的路径,所以会不停的返回2,如果返回值不是2,即有任何新的路径覆盖到,说明服务端本次执行已经结束了。

 

(这里的逻辑我认为其实还是有一些缺陷的,如果有些服务端有一些逻辑需要一定的时间才能到达的话其实会被框架漏掉,但是这种情况比较少并且如果改成连续多少次返回值不是2的话会牺牲掉一定的效率,并不值得。)

 

关闭sockfd,kill子进程并为了使其能够优雅的退出进行一定的等待,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HANDLE_RESPONSES:
  net_recv(sockfd, timeout, poll_wait_msecs, &response_buf, &response_buf_size);
  if (messages_sent > 0 && response_bytes != NULL) {
    response_bytes[messages_sent - 1] = response_buf_size;
  }
  //wait a bit letting the server to complete its remaining task(s)
  memset(session_virgin_bits, 255, MAP_SIZE);
  while(1) {
    if (has_new_bits(session_virgin_bits) != 2) break;
  }
  close(sockfd);
  if (likely_buggy && false_negative_reduction) return 0;
  if (terminate_child && (child_pid > 0)) kill(child_pid, SIGTERM);
  //give the server a bit more time to gracefully terminate
  while(1) {
    int status = kill(child_pid, 0);
    if ((status != 0) && (errno == ESRCH)) break;
  }
  return 0;
}

AFLNET的网络通信部分基本就是这些,后面会继续阅读有关状态图的逻辑,搞清楚是如何做到在状态图中进行转换并以此指导模糊测试数据变异的。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞2
打赏
分享
最新回复 (1)
雪    币: 2264
活跃值: (1508)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
CatF1y 2 2023-3-1 22:59
2
0
Ayaka带带我
游客
登录 | 注册 方可回帖
返回