介绍 GREASE:一款用于发现二进制代码中隐藏漏洞的开源工具

Langston Barrett, Ryan Scott, Ben Davis, and Matt Bauer

2025 年 3 月 19 日

主动且防御性地确保二进制代码中不存在漏洞,对于部署高保证系统至关重要。GREASE 是一款开源工具,利用欠约束符号执行来帮助软件逆向工程师分析二进制文件并发现难以发现的错误,最终增强系统安全性。这种二进制分析对于包含仅以二进制形式提供的 COTS 软件的系统尤其重要。

‍ GREASE 可以作为 Ghidra 逆向工程框架的插件使用,也可以作为独立的命令行工具或 Haskell 库使用。GREASE 支持分析 AArch32、PPC32、PPC64 和 x86_64 Linux ELF 二进制文件以及 LLVM bitcode。

演示

GREASE 可以帮助软件逆向工程师发现二进制文件中的错误。例如,考虑以下代码,该代码源自 libpng,演示了 CVE-2018-13785。即使在源代码级别,该错误也很难发现。你能看到它吗? (不必担心详细研究代码,这对于理解本文的其余部分不是必需的。)

void/* PRIVATE */png_check_chunk_length(png_const_structrp png_ptr, const unsigned int length){
  png_alloc_size_t limit = PNG_UINT_31_MAX;
# ifdef PNG_SET_USER_LIMITS_SUPPORTED
if (png_ptr->user_chunk_malloc_max > 0 &&
    png_ptr->user_chunk_malloc_max < limit)
   limit = png_ptr->user_chunk_malloc_max;
# elif PNG_USER_CHUNK_MALLOC_MAX > 0if (PNG_USER_CHUNK_MALLOC_MAX < limit)
   limit = PNG_USER_CHUNK_MALLOC_MAX;
# endif
if (png_ptr->chunk_name == png_IDAT)
  {
   png_alloc_size_t idat_limit = PNG_UINT_31_MAX;
   size_t row_factor =
     (png_ptr->width * png_ptr->channels * (png_ptr->bit_depth > 8? 2: 1)
     + 1 + (png_ptr->interlaced? 6: 0));
if (png_ptr->height > PNG_UINT_32_MAX/row_factor)
     idat_limit=PNG_UINT_31_MAX;
else     idat_limit = png_ptr->height * row_factor;
   row_factor = row_factor > 32566? 32566 : row_factor;
   idat_limit += 6 + 5*(idat_limit/row_factor+1); /* zlib+deflate overhead */   idat_limit=idat_limit < PNG_UINT_31_MAX? idat_limit : PNG_UINT_31_MAX;
   limit = limit < idat_limit? idat_limit : limit;
  }
// ...}

‍ GREASE 可以自动找到这个难以发现的错误:

$ clang test.c -o test
$ grease test
Finished analyzing 'png_check_chunk_length'. Possible bug(s):

At 0x100011bd:
div: denominator was zero
Concretized arguments:

rcx: 0000000000000000rdx: 0000000000000000rsi: 0000000000000000rdi: 000000+0000000000000000r8: 0000000000000000r9: 0000000000000000r10: 0000000000000000
000000: 5441444901000000 f9 ff ff ff 000000000080

‍ 此输出表明,当寄存器 rdi 保存指向包含字节 54 41 44 的分配的指针时,png_check_chunk_length 将除以零。实际上,如果我们添加以下 main 函数:

int main() {
 char data[] = {0x54, 0x41, 0x44, 0x49, 0xf9, 0x00, 0x00, 0x00, 0x01, 0xb7, 0x3e, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80};
 png_check_chunk_length((png_const_structrp)data, 0);
return0;
}

‍ 我们看到 GREASE 所描述的:

$ clang test.c -o test
$ ./test
Floating point exception (core dumped)

工作原理

从根本上讲,GREASE 的工作方式与 UC-Crux 非常相似,UC-Crux 是我们用于 LLVM 的欠约束符号执行工具。本质上,GREASE 通过在一组完全符号化的寄存器上运行目标二进制文件中的每个函数来分析该函数。如果发生错误(例如,如果程序从未初始化的内存中读取数据),GREASE 会使用启发式方法来改进此初始符号化前提条件(例如,通过初始化一些内存)并重新运行目标函数。此过程将继续,直到 GREASE 发现错误或得出结论:该函数在其输入的某些合理前提条件下是安全的。介绍 UC-Crux 的博客文章 详细描述了此算法。 GREASE 文档 中也提供了更多信息。

与上面来自 libpng 的示例相比,GREASE 的启发式方法 不会 将以下程序标记为可能存在问题。

$ cat test.c
int test(int *x) { return *x + 1; }

$ clang test.c -o test
$ grease test
– snip –
All goals passed!

‍ 如果我们向 GREASE 询问更多详细信息,我们可以看到它推断出 rdi 必须指向(至少)四个已初始化的字节。 启发式方法认为这是测试函数的一个合理前提条件。

$ grease test -v

rip: 0000000000401010– snip –
rdi: 000007+0000000000000000– snip –
000007: XX XX XX XX

‍ (在上面的输出中,XX 表示初始化为符号值的内存字节。GREASE 输出中使用的语言的语法在文档中中有详细说明。)

局限性

GREASE 有几个关键的局限性。最重要的是,GREASE 依赖于启发式方法来确定是否有故障的内存访问应报告为错误。这些启发式方法可能会导致误报(将正常的程序行为报告为可疑)或漏报(遗漏真实错误)。

像大多数程序分析工具一样,GREASE 也受到大量约束和警告的限制。GREASE 受 路径爆炸 的影响。GREASE 的内存模型无法表达无界堆数据结构(例如,符号长度的链表)。与其他基于符号执行的工具一样,GREASE 会展开循环和递归(可以选择最多达到最大限制),并且当循环条件保持符号化时可能会卡住。GREASE 无法分析机器代码的某些病态行为,例如运行时代码生成 (JIT)、自修改代码或跳转到指令的“中间”。文档中可以找到对 GREASE 局限性的更完整说明。

与其他工具的比较

GREASE 在二进制分析、符号执行和软件逆向工程工具领域中处于什么位置?

结论

我们很高兴以 BSD 3-clause 许可证与二进制分析研究社区分享 GREASE。GREASE 正在积极开发中。欢迎提出请求、问题和疑问!请联系 grease@galois.com 以开始对话。

本材料基于国防高级研究计划局在合同编号 W31P4Q-22-C-0017 和 W31P4Q-23-C-0020 下支持的工作。更深入: 软件与系统分析