【S2E】路径引导功能设计
整理一下之前实现的路径引导相关的内容。
问题描述
已有使程序崩溃的 seed,在 S2E 中复现相同的崩溃,从而借助符号执行分析。
具体来说是,在 S2E 将程序输入替换为符号变量,但执行路径要遵循 seed 具体执行时的路径,从而在 S2E 中探索程序崩溃时的状态。
解决方案概述
已知符号执行在每次执行至分支时会 fork 出两个状态,分别探索这两个不同的分支。因为我们已经有 seed,因此可以判断出具体执行时会选择哪个分支,此时只需要探索该分支即可。根据这种方式,既能实现剪枝(减少开销),也能将符号执行最终引导至目标点(崩溃点)。
判断方法是类似污点分析的方式。每次执行到条件分支语句时,符号执行可以捕获条件表达式,进而分析出该条件被哪些输入的字节所影响。通过查找 seed 对应的字节,就能计算出具体执行时,该条件为 true 还是 false。此时我们也令符号执行选择和具体执行相同的分支去执行即可。
举栗说明
下面用一个栗子来说明。
1 |
|
该程序从 stdin 中读取 64 字节保存至 buf 缓冲区。后续有 2 个条件分支语句,产生 3 条分支。这两个分支表达式分别为:
buf[0] == 'X'
buf[1] + buf[2] == 'Y' + 'Z'
而在 S2E 中,这样的条件表达式使用 KQuery 语言描述。(KQuery 由 KLEE 定义,而 S2E 的符号执行是基于 KLEE 实现)。官方文档:https://klee.github.io/docs/kquery/
因此在 S2E 执行过程中,上述两条分支表达式分别为:
1 | 1. (Eq false |
阅读一下 KQuery 的官方文档,就可以对这个表达式进行解释了。需要先搞清楚这个表达式:v0___symfile____tmp_input_0___0_256_symfile___0
,这个是 S2E 中的独特写法,它是 KQuery 语言描述的一个数组,对应的是一字节的符号变量。这个字符串的命名规则在 https://github.com/S2E/s2e/blob/master/libs2eplugins/src/s2e/Plugins/ExecutionTracers/TestCaseGenerator.cpp 中有解释,下面直接引用了:
1 | v0___symfile____(guest_file_name)___(chunk_id)_(number_of_chunks)_symfile___0 |
(guest_file_name)
表示 guest 使用的符号文件的名字,这个例子是 tmp_input_0,因为 bootstrap.sh 里写了符号文件是 input-0;(chunk_id)
表示的是该 chunk 在符号化文件中的位置;(number_of_chun)
表示一共有多少个 chunk;注:默认每个 chunk 表示 1 字节。
所以,表达式 v0___symfile____tmp_input_0___0_256_symfile___0
的含义其实就是:符号化文件 input-0 中的第 0 字节,该符号化文件共有 256 字节。由于我启动 S2E 时,是以这样的形式运行目标程序的:cat "${SYMB_FILE}" | "${TARGET}"
,即把符号化文件的内容,重定向到目标程序作为程序的标准输入,因此表达式 v0___symfile____tmp_input_0___0_256_symfile___0
最终可以被解释为:
程序标准输入的第 0 个字节(且程序共接受 256 个字节作为标准输入)。
接下来继续回到第一个表达式:
1 | (Eq false |
Read 表示读取操作。前面简单的提到,v0___symfile____tmp_input_0___0_256_symfile___0
是一个数组,不过数组中仅有一个元素,大小为 1 字节。Read w8 0x0 array
操作表示从数组 array 中读取第 0 个元素,宽度为 8 bit,即 1 字节。
ZExt 是一个截断操作,且截断后所有高位设置为 0。由于我们已经知道仅读取 1 字节,所以这个截断 64 bit 等于什么也没做。
Add 是数学操作,把刚刚取出来的 1 字节表达式和 0xffffffffffffffa8 相加。0xffffffffffffffa8 实际上是 0x58 (即:ascii 'X'
)的相反数的补码。其实就是将取出的字节减去 0x58。
Eq 对比了刚才相减的结果是否为 0。再外面一层 Eq 含义是判断上次对比的结果是否为 False。
综上所述,该表达式可以被解释为:
取程序输入的第 0 个字节(符号变量),令其减去 0x58,判断结果是否为 0。
即:判断程序输入的第 0 字节是否为字符 ‘X’ 。
同时,当我们解析出该条件表达式和程序输入的第 0 字节有关时,我们可以直接从 seed 中取出第 0 字节,将其带入到表达式中,计算出结果。计算结果为 True,就表示具体执行会走向 True 分支,反之则表示具体执行会走向 False 分支。接下来我们令符号执行探索相同的路径即可。
分支处理
前面已经识别出应该选择 True 还是 False 分支了。下一步就是如何使 S2E 探索我们指定的分支,且避免探索另一条分支。
早期版本中,S2E 在 onStateFork 事件中并未提供条件表达式。当时的处理思路是:在 ExecutionState 中新增一个标志位,先允许 S2E fork 出两个不同的状态(ExecutionState),对于我们需要探索的路径对应的状态,将其标志位置为 True,反之置为 False。当 ExecutionState 执行时,如果标志位为 False,则直接 Terminate,从而阻止不必要路径的探索。
新版本中,onStateFork 事件提供了条件表达式:
1 | /* libs2ecore/include/s2e/CorePlugin.h */ |
因此,直接注册该事件,将表达式计算结果不匹配的状态直接杀死即可。