【模糊测试】如何编写一个Harness?
如何编写一个 Harness?(翻译)
https://research.checkpoint.com/2018/50-adobe-cves-in-50-days/
原作者针对 Adobe Acrobat Reader 进行了测试,测试目标是 JP2KLib.dll 库,该 dll 用于对 JPEG2000 格式的图片进行解析。下面内容作者介绍了如何针对 JP2KLib.dll 编写 harness。
我是想使用 fuzz 来测试 CVE-2018-20250 WinRAR 路径穿越漏洞,所以先看看这篇文章。
在对 JP2KLib.dll 进行逆向工程之前,先检查这个库是否开源,或者是否有符号表信息。这能节省大量事件。然而 JP2KLib.dll 并没有。
我们希望我们的 harness 能够尽可能地像 Adobe Reader 一样去使用 JP2KLib.dll,所以第一件事是找一个能够触发这些行为(指调用 JP2KLib.dll 库的函数) PDF 文件。这能帮助我们更容易地定位程序的有关部分(指引用 JP2KLib.dll 库的代码)。
在这个例子中,我们有一个庞大的 PDF 语料库供测试。我们筛选了字符串 “/JPXDecode”,它是针对 JPEG2000 的 PDF 过滤器。我们也谷歌搜索了一个样例文件,或者用 Acrobat Pro/Phantom PDF 来生成了测试用例。
Tip1:Acrobat Reader 有沙箱,会阻碍调试。但是这个沙箱可以被关闭 - https://forums.adobe.com/thread/2110951
Tip2:使用了堆调试工具 PageHeap 来辅助逆向工程。这个工具有助于追踪内存分配的位置和大小。
我们从我们的样例中提取了 jp2 文件,所以我们可以用这个 jp2 文件来编写 harness,而不必生成一个完整的 PDF。
我们已经有了一个最小化的工作环境,我们希望在 JP2KLib.dll 加载时中断。在 WinDbg 中使用 sxe ld jp2klib
。当断点触发时,我们再在所有的 JP2KLib 函数上设置断点。下面的语句会在断点触发时,打印打印函数的调用栈,打印函数的前几个参数,以及它的返回值。
1 | bm /a jp2klib!* ".echo callstack; k L5; .echo parameters:; dc esp L8; .echo return value: ; pt; " |
dc:以双字(四字节)打印内存变量。和 dd 的区别是右侧会再以字符串显示。(还有dw,dd,dq等。)
pt:执行直到程序返回
我们加载了样例 PDF ,在 WinDbg 中得到了下面的输出:
JP2KLibInitEx 是加载了 JP2KLib 之后的第一个函数。我们注意到 JP2KLibInitEx 只有一个参数。(0x600a6dec 是返回地址,0x47e82fe0 是参数)。继续分析:
dds/dps/dqs:显示给定范围内的内存内容,把内存中每个元素都视为一个符号对其进行解析。dds 将四字节视为一个符号,dqs 将八字节视为一个符号,dps 根据当前处理器架构来选择最合适的长度。
我们能看到,这个结构体的大小是 0x20(因为再往下就是???了),它包含了指向 AcroRd32.dll 的指针(函数指针?)。当我们遇到一个未知的函数时,不要急于逆向它,因为我们不知道目标代码是否真的会使用它。相反,我们将每个指针的值都置为一个不同的空函数,我们称为 nopX
(X 是一个数值)。
现在我们有足够的信息开始编写我们的 harness 框架了。
- 从命令行参数获得一个输入文件
- 加载 JP2KLib.dll
- 获得 JP2KLibInitEx 函数的指针然后调用这个函数,参数设置为 8 个不同的 nopX 函数。
我们使用了 LOAD_FUNC
作为一个方便的宏定义,来加载函数地址。同时我们也提供了若干个 nopX
函数。
我们编译这个程序,然后用 sample.jp2 来运行这个程序,然后它生效了(生效指和上述过程一样,触发了指定函数)。
回到之前的测试。接下来在 WinDbg 中按 g(continue)。我们就进入到了第二个函数 JP2KGetMemObjEx,这个函数没有参数。所以我们只是调用它并保存返回值。
再下一个函数是 JP2KDecOptCreate。它也不需要参数,所以我们也只是保存返回值。然而,我们注意到 JP2KDecOptCreate 调用了 nop4 和 nop7 函数,这意味着我们需要完善 nop4 和 nop7 函数。
我们下一步是弄清楚 nop4 函数做了什么。我们将断点设置在 nop4 替代的函数上,即 0x5fd702e2(AcroRd32!CTJPEGDecoderRelease+0xa992),然后继续执行:
bu 表示针对某个符号下断点。在代码被修改之后,该断点可以随着函数地址的改变而自动移动到最新位置。
此外,在模块没有被加载的时候,bp 断点会失败(因为地址不存在),而 bu 断点则可以成功。
上图中最后一条 jmp 指令将我们带到另一个地址:
再执行若干条指令后:
上述过程表名,nop4 实际上是封装了 malloc 的函数。我们据此在 harness 中实现并替换这个 nop4。我们对 nop7 重复了这个过程并发现他其实是 memset 函数。继续查看后,我们发现 nop5 和 nop6 是 free 和 memcpy 函数。
下一个调用的函数是 JP2KDecOptInitToDefaults,这个函数有一个参数,是 JP2KDecOptCreate 的返回值。所以我们将这个值传递给它。
下一个调用的函数,JP2KImageCreate,没有参数,所以调用并保存返回值。
目前为止,我们的 harness 函数如下:
下一个函数是 JP2KImageInitDecoderEx,它要求 5 个参数。我们匹配了这五个参数中的三个,分别是 JP2KImageCreate,JP2KDecOptCreate 和 Jp2KGetMemObjEx 的返回值。我们注意到第三个参数指向一个 vtable。我们分析它也用了相同的技巧(nopX 函数)。第二个参数指针指向一个结构体,这个结构体的成员看起来不像是函数指针。所以我们决定将它设置为常数 0xbaaddaab。
此时,我们的 harness 是这样:
我们运行了我们的 harness,很快运行到了 nop10 函数。我们在 Adobe Reader 内的相关函数设置了断点并显示函数调用栈:(原文的信息太少了。在哪里设置的断点?为什么关注这个函数?)
我们在 IDA 中查看 JP2KCodeStm::IsSeekable:(所以这个 dll 是包含了符号信息是吗。。?)
在 WinDbg 中查看,我们能看到 JP2KCodeStm 在偏移 0x24 字节处包含了 vtable,并在偏移 0x18 处包含了整数 0xbaaddaab。我们发现 JP2KCodeStm::IsSeekable 调用了我们的虚表里的一个函数,并传递了 0xbaaddaab 作为第一个参数。它只是调用 vtable 第七个函数的一个简单 wrapper。
总体上,每个解析器(parser)都有一些不同,但是通常他们都要求一个输入流,这个输入流可能是一个熟悉的接口(比如 FILE/ifstream)。更常见的是,这个输入流是一种定制的类型,抽象了某种底层的输入流(网络/文件/内存)。所以当我们看到 JP2KCodeStm 函数是如何被使用的时候,我们就知道我们看到的是什么。(但是我不知道啊)
回到刚才的场景,0xbaaddaab 是一个流对象(stream object),虚表函数操作了这个流对象。
我们在 IDA 中查看 JP2KCodeStm 类中的其它函数:
他们都很相似。所以我们先继续创建我们自己的文件对象,然后实现所有必要的方法。最终代码如下:
我们确信我们检查了 JP2KImageInitDecoderEx 函数的返回值并且处理了错误(?bailed in case of error.)。在我们的场景中,JP2KImageInitDecoderEx 函数返回 0 表示成功。我们做了很多次尝试来正确实现流函数(steam functions),最终还是得到了想要的返回值。
个人理解是构建了很多虚表的函数,传入进行调试。最终 JP2KImageInitDecoderEx 函数返回 0 时就代表构建正确了。
下一个函数 JP2KImageDataCreate,没有参数,返回值会被传递给 JP2KImageGetMaxRes。我们调用这两个函数然后继续。
我们进入了 JP2KImageDecodeTileInterleaved 函数,它要求 7 个参数。其中 3 个参数是 JP2KImageCreate, JP2KImageGetMaxRes, 和 JP2KImageDataCreate 参数的返回值。
通过 IDA 的交叉引用,我们发现第二和第六个参数设置为 NULL。
我们保留了第四和第五个参数。我们总结后人为他们依赖于 color depth(8/16) (???),所以我们决定用常数 depth 来 fuzz。
最后,我们调用了 JP2KImageDataDestroy 函数,JP2KImageDestroy 函数,和 JP2KDecOptDestroy 函数来释放我们创建的结构体,避免内存泄漏。这对于 WinAFL 和 fuzz_iterations 至关重要。
至此,我们获得了一个正常工作的 harness。
经过最终的调整,我们分理出了初始化代码——加载 JP2KLib 并定位函数地址。这个操作能够提高 fuzz 的效率。因为不必在 fuzz 的过程中循环加载和解析地址。我们将导出这个新函数 fuzzme 这样比从二进制程序中定位它的地址要容易一些。
PS:在 WinAFL 中测试 harness 时,我们发现 WinAFL 生成了重复的 magic。当我们 debug 后,我们发现 Adobe 使用不同的 SEEK 常量而不是定义在 libc 里的。这导致了我们弄混了 SEEK_SET 和 SEEK_CUR。