使用concolic解决crash时的输入约束问题
当产生了crash,能够找到导致crash的数据与输入数据的关系,对进行漏洞利用有着重要的意义。通过污点追踪的方式,例如temu,已经可以定位到导致crash的数据是由哪些输入所导致的。但简单的污点追踪无法给出导致crash数据与输入数据的具体约束关系。通过具体的约束关系,可以找出导致crash的数据的输入数据范围,从而可以使用多个输入来触发漏洞。(还可以用来干嘛?)
解决该问题主要有两种思路:
1、 使用trace之后的指令和数据进行离线符号执行;这种方式在vine中应该有初步使用,或者FuzzBall。可以进一步研究。
2、 使用符号执行中的concolic模式,将导致crash的输入作为初始的输入,这时符号执行引擎会按照输入的样本数据来优先执行路径,从而可以到达crash的地方。同时,符号执行引擎仍然开启了约束收集的功能,因此,在执行到crash的时候,路径的约束条件,以及相关变量的约束条件也会被记录。
对于第二种思路,使用S2E符号执行引擎就可以完全实现,几乎不需要改动代码。
我们实现了两个例子来验证,包括一个简单的示例程序来验证和一个真实漏洞。
例子一:代码如下:
例子中,我们给出了输入一个初始数据,即str[0]=’a’,str[1]=’a’,通过该输入,可以到达32行,并触发漏洞。(这里的漏洞是已知的,在此假设)。那么我们要得到的是到达此处时的与输入的关系问题。这个例子中只体现了路径约束问题,而没有涉及变量的约束问题。即只有可达问题,不涉及受控问题。
经过测试,我们得到到达该处的路径约束关系如下:
通过整理,可以得到关系如下:
str0 != 0x10 &&
str1 != 0x10 &&
str0 >= 0x61 &&
str0 <= 0x7a &&
str0 >= 0x30 &&
str0 > 0x39 &&
str0 == str1
即只要满足上述约束关系,就可以达到该路径。通过验证可以满足。
其实使用符号执行就可以完成上述工作。为什么需要concolic模式?主要原因是对于上述的小例子,符号执行时可以完成。但对于大的程序,符号执行所遍历的路径太多,难以()使得符号执行到达crash的地方。为使得符号执行能够达到crash的地方,需要有较好的路径选择算法,能使其达到程序深处。而concolic模式则是一种以原始输入为指导的简单的路径选择算法。依据该方式,符号执行引擎可以较快的到达crash的地方。
一个以bind平台为漏洞的例子。Bind是一个开53端口的dns服务器软件。目前可以通过一个数据包将bind所运行的named进程crash掉。
这里crash的原理很简单,畸形的输入使得程序中的lib/dns/db.c的REQUIRE(type != dns_rdatatype_any);出现错误。这里的REQUIRE中其实是一个assert,而当type != dns_rdatatype_any时(dns_rdatatype_any = 255), 则可以使得assert出错。从而导致程序crash。
其实这里需要知道的是此时的type与输入数据有着怎样的对应关系。以及达到该路径有哪些约束。
当前实现的仅是将路径约束输出,而没有将type输出。Type可以通过在s2e_assert中加入s2e_print_expression来输出。
Zj的s2e.h中修改的代码如下,
这里的关键我倒认为是对b的求解,b是一个约束表达式,而这里一个简单的if (!b),是通过求解之后的结果吗?
我认为此处并没有进行求解,而是实际值。因此,如果实际值没有导致crash则是无法检测crash的。但已足够给出其约束表达式。
进一步,如果是在此处进行求解,判断,那么则可以实现符号执行的检测功能。从而可以达到只要走到该路径,不需初始输入时crash的值,也可以检测出bug。
当产生了crash,能够找到导致crash的数据与输入数据的关系,对进行漏洞利用有着重要的意义。通过污点追踪的方式,例如temu,已经可以定位到导致crash的数据是由哪些输入所导致的。但简单的污点追踪无法给出导致crash数据与输入数据的具体约束关系。通过具体的约束关系,可以找出导致crash的数据的输入数据范围,从而可以使用多个输入来触发漏洞。(还可以用来干嘛?)
解决该问题主要有两种思路:
1、 使用trace之后的指令和数据进行离线符号执行;这种方式在vine中应该有初步使用,或者FuzzBall。可以进一步研究。
2、 使用符号执行中的concolic模式,将导致crash的输入作为初始的输入,这时符号执行引擎会按照输入的样本数据来优先执行路径,从而可以到达crash的地方。同时,符号执行引擎仍然开启了约束收集的功能,因此,在执行到crash的时候,路径的约束条件,以及相关变量的约束条件也会被记录。
对于第二种思路,使用S2E符号执行引擎就可以完全实现,几乎不需要改动代码。
我们实现了两个例子来验证,包括一个简单的示例程序来验证和一个真实漏洞。
例子一:代码如下:
1 #include <stdio.h>
2 #include <string.h>
3 #include "s2e.h"
4
5 int main(void)
6 {
7 char str[3];
8
9
10
11 str[0] = 'a';
12 str[1] = 'a';
13
14 s2e_disable_forking();
15
16 s2e_make_concolic(str, 2, "str");
17
18 if(str[0] == '\n' || str[1] == '\n') {
19 printf("Not enough characters\n");
20 } else {
21 if(str[0] >= 'a' && str[0] <= 'z')
22 printf("First char is lowercase\n");
23 else
24 printf("First char is not lowercase\n");
25
26 if(str[0] >= '0' && str[0] <= '9')
27 printf("First char is a digit\n");
28 else
29 printf("First char is not a digit\n");
30
31 if(str[0] == str[1])
32 printf("assume a bug here\n");
33 else
34 printf("First and second chars are not the same\n");
35 }
36
37
38
39 s2e_get_example(str, 2);
40 printf("'%c%c' %02x %02x\n", str[0], str[1],
41 (unsigned char) str[0], (unsigned char) str[1]);
42
43 s2e_kill_state(0, "kill state ");
44 return 0;
45 }
46
例子中,我们给出了输入一个初始数据,即str[0]=’a’,str[1]=’a’,通过该输入,可以到达32行,并触发漏洞。(这里的漏洞是已知的,在此假设)。那么我们要得到的是到达此处时的与输入的关系问题。这个例子中只体现了路径约束问题,而没有涉及变量的约束问题。即只有可达问题,不涉及受控问题。
经过测试,我们得到到达该处的路径约束关系如下:
Constraint: (Eq false
(Eq (w32 0x0)
(And w32 (Add w32 (w32 0xfffffff6)
(ZExt w32 (Read w8 0x0 v0_str_0)))
(w32 0xff))))
Constraint: (Eq false
(Eq (w32 0x0)
(And w32 (Add w32 (w32 0xfffffff6)
(ZExt w32 (Read w8 0x1 v0_str_0)))
(w32 0xff))))
Constraint: (Eq false
(Slt (Read w8 0x0 v0_str_0) (w8 0x61)))
Constraint: (Eq false
(Slt (w8 0x7a) (Read w8 0x0 v0_str_0)))
Constraint: (Eq false
(Slt (Read w8 0x0 v0_str_0) (w8 0x30)))
Constraint: (Slt (w8 0x39) (Read w8 0x0 v0_str_0))
Constraint: (Eq (w32 0x0)
(And w32 (Sub w32 (ZExt w32 (Read w8 0x0 v0_str_0))
(ZExt w32 (Read w8 0x1 v0_str_0)))
(w32 0xff)))
通过整理,可以得到关系如下:
str0 != 0x10 &&
str1 != 0x10 &&
str0 >= 0x61 &&
str0 <= 0x7a &&
str0 >= 0x30 &&
str0 > 0x39 &&
str0 == str1
即只要满足上述约束关系,就可以达到该路径。通过验证可以满足。
其实使用符号执行就可以完成上述工作。为什么需要concolic模式?主要原因是对于上述的小例子,符号执行时可以完成。但对于大的程序,符号执行所遍历的路径太多,难以()使得符号执行到达crash的地方。为使得符号执行能够达到crash的地方,需要有较好的路径选择算法,能使其达到程序深处。而concolic模式则是一种以原始输入为指导的简单的路径选择算法。依据该方式,符号执行引擎可以较快的到达crash的地方。
一个以bind平台为漏洞的例子。Bind是一个开53端口的dns服务器软件。目前可以通过一个数据包将bind所运行的named进程crash掉。
这里crash的原理很简单,畸形的输入使得程序中的lib/dns/db.c的REQUIRE(type != dns_rdatatype_any);出现错误。这里的REQUIRE中其实是一个assert,而当type != dns_rdatatype_any时(dns_rdatatype_any = 255), 则可以使得assert出错。从而导致程序crash。
其实这里需要知道的是此时的type与输入数据有着怎样的对应关系。以及达到该路径有哪些约束。
当前实现的仅是将路径约束输出,而没有将type输出。Type可以通过在s2e_assert中加入s2e_print_expression来输出。
Zj的s2e.h中修改的代码如下,
static inline void _s2e_assert(int b, const char *expression )之所以加入if , else,目的是增加一个分支,来逼上绝路。但我认为写成如下代码是一样的。这里的if (!b)本来就有。
{
S2e_enable_forking();
if (!b) {
s2e_message(“Hit s2e assert\n”);
s2e_kill_state(0, expression);
return 0
}else {
S2e_disable_forking();
Return 1;
}
}
static inline void _s2e_assert(int b, const char *expression )
{
S2e_enable_forking();
if (!b) {
s2e_kill_state(0, expression);
return 0;
}
S2e_disable_forking();
Return 1;
}
这里的关键我倒认为是对b的求解,b是一个约束表达式,而这里一个简单的if (!b),是通过求解之后的结果吗?
我认为此处并没有进行求解,而是实际值。因此,如果实际值没有导致crash则是无法检测crash的。但已足够给出其约束表达式。
进一步,如果是在此处进行求解,判断,那么则可以实现符号执行的检测功能。从而可以达到只要走到该路径,不需初始输入时crash的值,也可以检测出bug。