winAFL 的网络测试模式

在winAFL的目录下,有个 custom_net_fuzzer.dllcustom_winafl_server.dll

根据winafl的说明, custom_net_fuzzer.dll 是用来辅助网络连接测试的。

具体原理:

  • 当 winAFL 启动时,设置 -l 参数,附加 custom_net_fuzzer.dll

  • custom_net_fuzzer.dll 在运行时,要获取几个环境变量

    1
    2
    3
    4
    5
    6
    -a IP address - ip 地址,数据往这个地址发
    -U - 使用udp协议,默认是 tcp
    -p port - 端口,数据往这个端口发
    -w msec - 实际开始fuzz之前的毫秒延迟
    实际命令如下:
    set AFL_CUSTOM_DLL_ARGS=-U -p 7714 -a 127.0.0.1 -w 1000 && afl-fuzz.exe -l custom_net_fuzzer.dll -i in -o out -D E:\Fuzz\DynamoRIO-Windows-8.0.18971\bin32 -t 20000 -- -target_module test_netmode.exe -target_method recv_func -coverage_module test_netmode.exe -fuzz_iterations 5200 -nargs 1 -- test_netmode.exe
  • 然后,afl-fuzz 负责变异数据,然后将数据放到一个 buf 中,源码如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // afl-fuzz.c
    /* This function is used to call user-defined server routine to send data back into sample */
    static int process_test_case_into_dll(int fuzz_iterations)
    {
    int result;
    long fsize;

    char *buf = get_test_case(&fsize);
    ACTF("buf= %s\n",buf);
    result = dll_run_ptr(buf, fsize, fuzz_iterations); /* caller should copy the buffer */

    free(buf);

    if (result == 0)
    FATAL("Unable to process test case, the user-defined DLL returned 0");

    return 1;
    }

    再将 buf 传递给 custom_net_fuzzer.dll 中的 dll_run 函数,如下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // custom_net_fuzzer.c
    CUSTOM_SERVER_API int APIENTRY dll_run(char *data, long size, int fuzz_iterations) {
    if (is_TCP)
    send_data_tcp(data, size, fuzz_iterations);
    else
    send_data_udp(data, size, fuzz_iterations);
    return 1;
    }
    // fuzz_iterations 模糊迭代次数

    dll_run 函数负责最后一步的数据发送,根据是 udp 还是 tcp ,建立 socket 发送数据到目标地址的目标端口上。

    其中 tcp 发送数据之后,需要关闭 socket;udp 只需要建立socket ,sendto 即可。

覆盖率

在 afl-fuzz.c 中有个关键函数:create_target_process ,这个函数的调用在 run_target 中。

1
2
3
4
5
6
7
8
9
10
static u8 run_target(char** argv, u32 timeout) {
total_execs++;

......
// 如果进程还存活就不去创建新的进程
if(!is_child_running()) {
destroy_target_process(0);
create_target_process(argv);
fuzz_iterations_current = 0;
}

create_target_process 函数用来创建进程并且使用 dynamorio 监控。create_target_process 函数比较复杂,具体功能有:

  • 创建命名管道

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pipe_name = (char *)alloc_printf("\\\\.\\pipe\\afl_pipe_%s", fuzzer_id);

    pipe_handle = CreateNamedPipe(
    pipe_name, // pipe name
    PIPE_ACCESS_DUPLEX | // read/write access
    FILE_FLAG_OVERLAPPED, // overlapped mode
    0,
    1, // max. instances
    512, // output buffer size
    512, // input buffer size
    20000, // client time-out
    &sa);
    if (pipe_handle == INVALID_HANDLE_VALUE) {
    FATAL("CreateNamedPipe failed, GLE=%d.\n", GetLastError());
    }

    fuzzer_id 是一个随机值。管道的作用,是用来与 drrun 通信。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    else {
    if (drattach) {
    drattachpid = find_attach_pid(drattach_identifier);
    cmd = alloc_printf(
    "%s\\drrun.exe -attach %ld -no_follow_children %s %s -fuzzer_id %s",
    dynamorio_dir, drattachpid, client_invocation, client_params, fuzzer_id);
    } else {
    pidfile = alloc_printf("childpid_%s.txt", fuzzer_id); // fuzzer_id 随机值,把 pid 写入到这个 txt 文件
    if (persist_dr_cache) {
    cmd = alloc_printf(
    "%s\\drrun.exe -pidfile %s -no_follow_children -persist -persist_dir \"%s\\drcache\" %s %s -fuzzer_id %s -drpersist -- %s",
    dynamorio_dir, pidfile, out_dir, client_invocation, client_params, fuzzer_id, target_cmd);
    } else {
    cmd = alloc_printf(
    "%s\\drrun.exe -pidfile %s -no_follow_children %s %s -fuzzer_id %s -- %s",
    dynamorio_dir, pidfile, client_invocation, client_params, fuzzer_id, target_cmd);
    }
    }
    }

    pidfile 存放 PID 值,然后等待管道连接,并通过读取上述txt文件以获取目标进程id,主要用来后面超时中断进程。

    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
    // 根据cmd,来创建进程
    if(!CreateProcess(NULL, cmd, NULL, NULL, inherit_handles, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
    FATAL("CreateProcess failed, GLE=%d.\n", GetLastError());
    }

    child_handle = pi.hProcess;
    child_thread_handle = pi.hThread;
    ......
    // 减少线程的挂起计数。当挂起计数减为零时,恢复线程的执行。
    // child_thread_handle 是要重新启动的线程的句柄。
    ResumeThread(child_thread_handle);

    watchdog_timeout_time = get_cur_time() + exec_tmout;
    watchdog_enabled = 1;
    // OverlappedConnectNamedPipe 负责初始化,并连接前面创建的管道
    if(!OverlappedConnectNamedPipe(pipe_handle, &pipe_overlapped)) {
    FATAL("ConnectNamedPipe failed, GLE=%d.\n", GetLastError());
    }

    watchdog_enabled = 0;

    else if (drioless == 0) {
    // 打开 pidfile 文件,读取内容
    fp = fopen(pidfile, "rb");
    if(!fp) {
    FATAL("Error opening pidfile.txt");
    }
    .......
    ck_free(pidfile);
    }
    else {
    child_pid = pi.dwProcessId;
    }

之后的操作交给winafl.dll,在 winafl.c 中会先打开前面创建的命名管道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static void
setup_pipe() {
pipe = CreateFile(
options.pipe_name, // pipe name
GENERIC_READ | // read and write access
GENERIC_WRITE,
0, // no sharing
NULL, // default security attributes
OPEN_EXISTING, // opens existing pipe
0, // default attributes
NULL); // no template file

if (pipe == INVALID_HANDLE_VALUE) DR_ASSERT_MSG(false, "error connecting to pipe");
}

然后,访问共享内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void
setup_shmem() {
HANDLE map_file;

map_file = OpenFileMapping(
FILE_MAP_ALL_ACCESS, // read/write access
FALSE, // do not inherit the name
options.shm_name); // name of mapping object

if (map_file == NULL) DR_ASSERT_MSG(false, "error accesing shared memory");

winafl_data.afl_area = (unsigned char *) MapViewOfFile(map_file, // handle to map object
FILE_MAP_ALL_ACCESS, // read/write permission
0,
0,
MAP_SIZE);

if (winafl_data.afl_area == NULL) DR_ASSERT_MSG(false, "error accesing shared memory");
}

winafl_data.afl_area 就是用来存覆盖率信息的。

循环调用的关键函数 pre_fuzz_handler

pre_fuzz_handler函数,通过管道写入 P 命令,代表开始进入目标函数,afl-fuzz.exe 进程收到命令后,会向目标进程写入管道命令 F,并监测超时时间和循环调用次数。

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
static void
pre_fuzz_handler(void *wrapcxt, INOUT void **user_data)
{
char command = 0;
int i;
void *drcontext;

app_pc target_to_fuzz = drwrap_get_func(wrapcxt); // 获取目标函数地址
dr_mcontext_t *mc = drwrap_get_mcontext_ex(wrapcxt, DR_MC_ALL); // 获取目标函数当前内存上下文信息
drcontext = drwrap_get_drcontext(wrapcxt); // 获取DynamoRIO上下文

fuzz_target.xsp = mc->xsp;// 保存栈指针,xsp是各平台下的通用标记变量
fuzz_target.func_pc = target_to_fuzz; // 目标函数地址

// F 退出目标函数;P 进入目标函数;K 超时中断进程;C 崩溃;Q 退出进程
if(!options.debug_mode) {
WriteCommandToPipe('P');
command = ReadCommandFromPipe();

if(command != 'F') {
if(command == 'Q') {
dr_exit_process(0);
} else {
DR_ASSERT_MSG(false, "unrecognized command received over pipe");
}
}
} else {
debug_data.pre_hanlder_called++;
dr_fprintf(winafl_data.log, "In pre_fuzz_handler\n");
}
......
memset(winafl_data.afl_area, 0, MAP_SIZE);

当执行完了被 fuzz 的函数,就会调用 post_fuzz_handler 来恢复调用 fuzz 函数之前的状态。以此来实现循环调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void
post_fuzz_handler(void *wrapcxt, void *user_data)
{
dr_mcontext_t *mc;
void *drcontext;
mc = drwrap_get_mcontext(wrapcxt); // 获取上下文信息
......
/* 网络型的 fuzz 无需恢复 */
if (options.no_loop)
return;
// 设置的循环次数,超过就退出
fuzz_target.iteration++;
if(fuzz_target.iteration == options.fuzz_iterations) {
dr_exit_process(0);
}

mc->xsp = fuzz_target.xsp; // 恢复栈顶指针
mc->pc = fuzz_target.func_pc; // 恢复 pc 到 fuzz 函数的地址
drwrap_redirect_execution(wrapcxt);
}

instrument_bb_coverage , instrument_edge_coverage 函数,用来记录覆盖率的情况。

1
2
3
afl_map = winafl_data.afl_area;

opnd2 = OPND_CREATE_INTPTR((uint64)winafl_data.afl_area);