基于快照的 Fuzz 工具 Wtf 的基础使用

安全 应用安全
在日常的 fuzz 的工作中,通常我们都需要先大致分析目标软件,然后对其输入点构造 harness,才可以使用工具对 harness 进行 fuzz,从而发现目标软件的潜在漏洞。

前言

wtf (https://github.com/0vercl0k/wtf</https://github.com/0vercl0k/wtf>) 是一种分布式、代码覆盖引导、可定制、基于快照的跨平台模糊器,设计用于 fuzz 在 Microsoft Windows 平台上运行的用户模式或内核模式的目标。

在日常的 fuzz 的工作中,通常我们都需要先大致分析目标软件,然后对其输入点构造 harness,才可以使用工具对 harness 进行 fuzz,从而发现目标软件的潜在漏洞。构造 harness 不是一件容易的事情,这取决于安全研究人员分析解构目标软件的程度,除此之外,在部分软件中,只有进行完整的、复杂的初始化操作和预设,才能保证 harness 调用的输入点函数能够正常运行。

针对这一问题,基于快照的 fuzz 工具 wtf 吸引了我的注意;我们可以对正常运行的目标软件打下内存快照,然后对该内存快照进行 fuzz,这种方式可以不必编写 harness,并在一定程度上减少分析目标软件的成本。

本文从基于快照这一个特性出发,介绍 wtf 工具的基础使用和注意事项。

本文实验环境:

Windows 10 x64 专业版
Visual Studio 2019
WinDBG
ProxmoxVE 7.2-3

1. wtf概要

在 github 上可以访问 wtf 的源码和 README(https://github.com/0vercl0k/wtf):

imageimage

作者提供了 4 篇使用 wtf 进行 fuzz 的实操文章:

  • Building a new snapshot fuzzer & fuzzing IDA
  • Fuzzing Modern UDP Game Protocols With Snapshot-based Fuzzers
  • Fuzzing RDPEGFX with "what the fuzz"
  • A Journey to Network Protocol Fuzzing – Dissecting Microsoft IMAP Client Protocol

其中第一篇针对 IDA 的 fuzz,也是作者开发 wtf 的初衷,其中讲诉了 wtf 的开发历程并介绍了 wtf 的实现原理。

通过以上文章,了解到使用 wtf 进行 fuzz 可以大致分为 3 个步骤:

1. 远程调试目标程序,并在合适的位置打下系统内存快照
2. 为内存快照编写 wtf 的 fuzz 模块/插件(也称为 harness)
3. 编译并启动 wtf 进行 fuzz

为了方便下文叙述,这里先介绍如何源码编译 wtf。wtf 使用 ninja 进行构建,首先我们下载 ninja(https://ninja-build.org/</https://ninja-build.org/>) 并添加到环境变量PATH中,随后从 github 下载 wtf 源码,打开 Visual Studio 的 x64 位开发者命令行工具(vs2019:x64 Native Tools Command Prompt for VS 2019):

# 进入 wtf 的 build 目录
$ cd C:\Users\john\Desktop\wtf-0.4\src\build
# 执行 bat 脚本
$ .\build-release.bat

执行如下:

imageimage

编译成功后,会在build目录下生成wtf.exe文件,该二进制文件将支撑我们全部的 fuzz 流程。

2. fuzz hevd

wtf 提供了完整的测试用例 hevd / tlv_server,能够帮助我们快速上手 wtf,这里我们以 hevd 为例进行介绍;hevd(HackSys Extreme Vulnerable Windows Driver)是 Windows 内核驱动漏洞靶场(https://github.com/hacksysteam/HackSysExtremeVulnerableDriver</https://github.com/hacksysteam/HackSysExtremeVulnerableDriver>),以帮助安全研究员学习漏洞原理。

首先第一步是创建 hevd 运行时的系统内存快照,作者已经提供好了 https://github.com/0vercl0k/wtf/releases/download/v0.4/target-hevd.7z</https://github.com/0vercl0k/wtf/releases/download/v0.4/target-hevd.7z>,下载解压后如下:

imageimage

第二步是编写 hevd 的 wtf fuzz 模块/插件,其插件代码属于 wtf 源码的一部分,在其中需要定义如何对快照进行初始化,如何向快照注入测试用例等操作,同样作者也提供了 https://github.com/0vercl0k/wtf/blob/main/src/wtf/fuzzer_hevd.cc</https://github.com/0vercl0k/wtf/blob/main/src/wtf/fuzzer_hevd.cc>,如下:

imageimage

通过build-release.bat重新编译 wtf 以添加 hevd 插件,target-hevd已经准备好了工作目录,如下:

$ tree target-hevd
target-hevd
├── inputs          输入/测试用例/样本种子文件
├── outputs         输出/wtf发现的可产生不同的路径的测试用例
├── coverage        覆盖率文件夹
├── crashes         保存触发 crash 的测试用例
└── state           快照文件夹,包含内存dump(`mem.dmp`)和CPU状态`regs.json`

$ hexdump -C target-hevd/inputs/ioctl.bin 
00000000  3b 20 22 00                                       |; ".|
00000004

随后便可以使用wtf.exe进行 fuzz,wtf 的 fuzz 分为 server 节点和 fuzz 节点,服务器节点负责聚合代码覆盖率、语料库,生成测试用例并将其分发给客户端,fuzz 节点运行由服务器生成和分发的测试用例,并将结果传回服务器(代码覆盖率/结果等)。

$ cd C:\Users\john\Desktop\wtf-0.4\src\build
# 首先启动 server 节点
# master 指定为 server 节点
# --name 指定插件模块为 hevd
# --max_len 设置最大测试用例长度
# --runs 设置生成测试用例的个数
# --target 设置工作目录
$ .\wtf.exe master --name hevd --max_len=1028 --runs=10000000 --target C:\Users\john\Desktop\target-hevd
# 再启动一个 fuzz 节点
# fuzz 指定为 fuzz 节点
# --backend 指定执行后端为 bochscpu(还支持 whv/kvm)
$ .\wtf.exe fuzz --name hevd --backend=bochscpu --target C:\Users\john\Desktop\target-hevd

执行如下:

imageimage

运行一段时间后我们将收获 crash,本文这里不进行扩展分析。

3. demo程序

target-hevd示例能够让我们快速的上手 wtf,现在我们提供个 demo 程序以便完整的学习 wtf 的基本使用,编写 demo 程序如下:

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

__declspec(dllexport) int fuzzme(char* data);

char* argv_0 = NULL;

int __declspec(noinline) fuzzme(char* data) {
	unsigned int len = strlen(data);

	if (!(len > 0 && data[0] == 't')) {
		printf("Error 0\n");
		return 0;
	}
	if (!(len > 1 && data[1] == 'e')) {
		printf("Error 1\n");
		return 0;
	}
	if (!(len > 2 && data[2] == 's')) {
		printf("Error 2\n");
		return 0;
	}
	if (!(len > 3 && data[3] == 't')) {
		printf("Error 3\n");
		return 0;
	}

	//
	printf("!!!!!!!!!!OK!!!!!!!!!!\n");

	char* crash = NULL;
	if (len > 4 && data[4] == '1') {
		crash[0] = NULL;
	} else if (len > 4 && data[4] == '2') {
		char buffer[5] = { 0 };
		// stack-based overflow to trigger the GS cookie corruption
		for (int i = 0; i < 5; ++i) {
			strcat(buffer, argv_0);
		}
		printf("buffer: %s\n", buffer);
	}
	else {
		printf("Error 4\n");
	}

	return 0;
}

int main(int argc, char* argv[]) {
	char ready = 0;
	char data[32] = { 0 };

	argv_0 = argv[0];

	printf("ready?\n");
	scanf("%c", &ready);

	printf("input data:\n");
	scanf("%16s", data);
	printf("%s\n", data);
	
	fuzzme(data);

	return 0;
}

在main()函数的起始位置,我们设置了一个ready标志等待用户输入一个字符后,再执行真正代码逻辑,这样可以方便我们后续调试该程序进行快照操作;随后 demo 程序接收用户输入的字符串,将该字符串传入核心函数fuzzme()进行处理,在该函数中将逐字节检查用户输入是否等于test,满足条件后若第 5 个字节为1,则手动触发空指针访问异常,若第 5 个字节为2,则手动触发 GS cookie 异常,其他则调用printf()输出。

我们将该 demo 程序编译为wtf_test.exe,接下来的目标则是使用 wtf 对该二进制程序的fuzzme()函数进行 fuzz,找到其中的两个异常错误;同样按照上文的三大步进行。

4. 内存快照

首先我们需要搭建双机调试环境来获取二进制运行时的内存快照,官方推荐使用 hyper-v 虚拟机作为执行环境,我这里使用 ProxmoxVE 虚拟机环境也一样;运行的虚拟机(debuggee)使用 1C4G 的配置,调试主机(debugger)的环境不做要求,需要注意一点,由于打内存快照涉及到大量的数据通信,双机调试一定要使用网络调试进行,实验环境搭建如下:

imageimage

在debuggee上使用命令开启 powershell 配置网络双机调试如下:

# 开启 debug
$ bcdedit /debug on
# 设置网络调试参数
# 设置 debugger 的 ip 地址为 10.0.25.191
# 设置被调试机的端口为 50000
# 设置被调试机的连接密码为 p.a.s.s
$ bcdedit /dbgsettings NET HOSTIP:10.0.25.191 PORT:50000 KEY:p.a.s.s
# 查看调试配置
$ bcdedit /dbgsettings

除此之外,我们还需要关闭 Windows 的 KVA(Kernel Virtual Address) shadow 功能,否则 wtf 执行快照时将出现内存分页错误的问题,可以使用作者提供的脚本(https://github.com/0vercl0k/wtf/blob/main/scripts/disable-kva.cmd</https://github.com/0vercl0k/wtf/blob/main/scripts/disable-kva.cmd>)进行关闭:

REM To disable mitigations for CVE-2017-5715 (Spectre Variant 2) and CVE-2017-5754 (Meltdown)
REM https://support.microsoft.com/en-us/help/4072698/windows-server-speculative-execution-side-channel-vulnerabilities
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverride /t REG_DWORD /d 3 /f
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" /v FeatureSettingsOverrideMask /t REG_DWORD /d 3 /f

如果关闭 KVA shadow 后若还出现内存分页错误,可尝试使用 https://github.com/0vercl0k/lockmem</https://github.com/0vercl0k/lockmem> 对内存进行锁定再进行快照。

随后使用debugger使用 WinDBG 网络调试连接debuggee,随后在debuggee上执行wtf_test.exe程序,在 WinDBG 中进入目标进程空间如下:

imageimage

此时,我们需要选择一个「合适」的内存快照位置,因为 wtf 将会以该快照作为起始运行状态,在wtf_test.exe中,我们的目标函数是fuzzme(),所以我们在fuzzme()函数入口打下断点,并在wtf_test.exe程序中分别输入g和1234以运行到断点处,如下:

imageimage

随后我们使用 WinDBG 的插件 bdump (https://github.com/yrp604/bdump</https://github.com/yrp604/bdump>)对此刻的运行状态进行快照:

# 加载 bdump.js 脚本
kd> .scriptload C:\Users\john\Desktop\bdump\bdump.js
# 打下内存快照,保存在 state 文件夹中
kd> !bdump_active_kernel "C:\\Users\\john\\Desktop\\state"

执行如下:

imageimage

在本文的实验环境下,打内存快照大约需要 40 min。

5. 编写wtf插件

随后我们为 demo 程序编写 wtf 的插件,我们可以以 https://github.com/0vercl0k/wtf/blob/main/src/wtf/fuzzer_hevd.cc作为模板进行改写。

插件主要需要实现Init()和InsertTestcase()两个函数,wtf 加载内存快照后,将停留在我们打下断点/打下快照的位置,然后首先调用一次插件的Init()函数,在该函数中一般定义了:1.快照运行到何处停止、2.如何处理异常错误、3.如何处理IO 等操作,完成初始化操作后,随后将在每次执行快照前,调用InsertTestcase()函数注入测试用例。

编写 demo 的 wtf 插件如下:

#include "backend.h"
#include "targets.h"
#include "crash_detection_umode.h"
#include <fmt/format.h>

namespace Demo {

bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) {
    // "int fuzzme(char* data)" => data == Rcx
    const Gva_t data = Gva_t(g_Backend->Rcx());
    if (!g_Backend->VirtWriteDirty(data, Buffer, BufferSize)) {
        fmt::print("VirtWriteDirty failed\n");
        return false;
    }

    return true;
}

bool Init(const Options_t &Opts, const CpuState_t &) {
    // Stop the test-case once we return back from the call [fuzzme]
    const Gva_t Rsp = Gva_t(g_Backend->Rsp());
    const Gva_t ReturnAddress = Gva_t(g_Backend->VirtRead8(Rsp));
    if (!g_Backend->SetBreakpoint(ReturnAddress, [](Backend_t *Backend) {
            fmt::print("Back from kernel!\n");
            Backend->Stop(Ok_t());
        })) {
        fmt::print("Failed to SetBreakpoint ReturnAddress\n");
        return false;
    }

    // setup usermode crash/exception detection hooks
    if (!SetupUsermodeCrashDetectionHooks()) {
        fmt::print("Failed to SetupUsermodeCrashDetectionHooks\n");
        return false;
    }

    // patch "printf" to stdio
    if (!g_Backend->SetBreakpoint("printf", [](Backend_t *Backend) {
            const Gva_t StrPtr = Backend->GetArgGva(0);
            const std::string &str = Backend->VirtReadString(StrPtr);
            fmt::print("Demo: {}", str);
            Backend->SimulateReturnFromFunction(0);
        })) {
        fmt::print("Failed to SetBreakpoint printf\n");
        return false;
    }

    return true;
}

// Register the target.
Target_t Demo("demo", Init, InsertTestcase);

} // namespace Demo

需要注意的是,由于快照不具备 IO 访问能力,发生 IO 操作时 wtf 无法正确处理,所以我们应该尽量选择一个「合适」的快照点;对于无法避免的 IO 操作,我们可以采用 patch 的方式修改逻辑,如 demo 程序中的printf()函数调用,我们在Init()中进行 patch,不会执行真正的printf()功能。

通过build-release.bat重新编译 wtf 以添加 demo 插件。

6. fuzz demo

首先我们准备好工作目录如下,其中state目录就是我们上文打下的内存快照:

$ tree workdir
workdir
├── inputs
├── outputs
├── coverage
├── crashes
└── state

$ cat workdir/inputs/1.txt
1234

在进行 fuzz 之前,我们先使用 wtf 的run子命令,检查内存快照和插件是否正确运行:

$ cd C:\Users\john\Desktop\wtf-0.4\src\build
# 指定为 run 子命令
# --name 指定插件模块为 demo
# --state 指定内存快照文件夹
# --backend 指定执行后端为 bochscpu
# --input 指定输入文件
$ .\wtf.exe run --name demo --state C:\Users\john\Desktop\workdir\state\ --backend=bochscpu --input C:\Users\john\Desktop\workdir\inputs\1.txt

执行如下:

imageimage

如果不能按预期执行,可以使用run命令生成路径覆盖,随后根据执行路径分析问题,下文我们将介绍在 wtf 如何获取路径覆盖。

随后我们便可以对其进行 fuzz:

$ cd C:\Users\john\Desktop\wtf-0.4\src\build
# 启动 server 节点
$ .\wtf.exe master --name demo --max_len=6 --runs=10000000 --target C:\Users\john\Desktop\workdir
# 启动一个 fuzz 节点
$ .\wtf.exe fuzz --name demo --backend=bochscpu --target C:\Users\john\Desktop\workdir

启动执行 2min 即可获得 crash 如下:

imageimage

其 crash 文件如下,顺序寻找到我们埋在 demo 程序中的两个异常错误:

imageimage

7. 覆盖率

使用 wtf 的run子命令还可以生成路径覆盖,配合 IDA 以及 lighthouse 插件可以帮助我们排查 fuzz 问题和衡量 fuzz 的质量,如下:

$ cd C:\Users\john\Desktop\wtf-0.4\src\build
# 指定为 run 子命令
# --trace-path 设置覆盖率输出文件夹
# --trace-type 设置路径类型为 cov
$ .\wtf.exe run --name demo --state C:\Users\john\Desktop\workdir\state\ --backend=bochscpu --input C:\Users\john\Desktop\workdir\inputs\1.txt --trace-path=C:\Users\john\Desktop\workdir\coverage --trace-type=cov

执行完毕后将在C:\Users\john\Desktop\workdir\coverage下生成1.txt.trace文件,该文件没有符号化不能直接使用,使用作者提供的工具 symbolizer(https://github.com/0vercl0k/symbolizer</https://github.com/0vercl0k/symbolizer>) 对其进行符号化处理:

$ cd C:\Users\john\Desktop\workdir\coverage

# --input 指定输入文件
# --crash-dump 指定内存快照文件
# -o 设置输出文件
# --style 指定符号化类型
$ C:\Users\john\Desktop\symbolizer\symbolizer.exe --input .\1.txt.trace --crash-dump C:\Users\john\Desktop\workdir\state\mem.dmp -o cov.txt --style modoff

执行完毕后,将在指定目录下生成符号化的路径覆盖文件cov.txt,如下:

imageimage

随后在 IDA 中加载wtf_test.exe文件,并使用 lighthouse 插件加载覆盖率文件如下:

imageimage

8. References

[1]https://github.com/0vercl0k/wtf

[2]https://github.com/0vercl0k/wtf/issues/14

[3]https://github.com/0vercl0k/lockmem[4]https://github.com/0vercl0k/symbolizer

[5]https://github.com/yrp604/bdump

[6]https://msrc.microsoft.com/blog/2018/03/kva-shadow-mitigating-meltdown-on-windows/

[7]https://blog.ret2.io/2021/07/21/wtf-snapshot-fuzzing/

责任编辑:武晓燕 来源: ​FreeBuf.COM
相关推荐

2016-12-05 15:48:37

2023-07-11 08:30:56

2009-08-18 22:15:38

VMware快照改进方

2010-03-08 13:07:07

NFSVMware数据保护

2017-01-11 22:41:05

2010-07-14 17:32:53

SQL Server

2017-10-18 19:44:20

数据库DatasetSQLAlchemy

2018-12-11 10:55:00

SQLFuzzWEB安全

2013-03-05 10:28:36

大数据流媒体公司

2011-05-24 10:24:29

创建与使用VMware

2017-05-03 11:57:37

LinuxGnome-scree GNOME工具

2014-05-12 16:40:13

Linux命令快照

2023-11-15 13:20:14

Python图像分割

2015-05-28 10:34:16

TCPsocket

2023-04-18 08:00:00

2015-07-27 11:13:41

MySQLMySQL安全数据库安全

2010-04-01 12:33:37

Oracle repo

2016-03-25 10:21:22

androidIntentServi代码

2015-10-10 16:41:09

曙光存储快照

2011-03-07 13:34:33

FileZilla上传工具
点赞
收藏

51CTO技术栈公众号