初始概念介绍 动态符号执行的核心思想是在所谓的符号数据域上模拟程序执行
cpu工作在1和0上,如果使用read系统调用读入用户输入,得到的是1和0.作为发送程序的人,无法给它发送一个未知值的变量,但是可以创造一个模拟器来实现它,可以是0,1或者x.随着知道已知数据是什么,未知数据是什么,根据cmp比较未知数据,并基于结果进行跳转
1 2 3 4 5 6 mov rax, 0 > rax = 0 mov rdi, 0 >rax = 0 , rdi = 0 mov rdx, 1 > rax = 0 , rdi = 0 , rdx = 1 syscall - >rax = 0 , rdi = 0 , rdx = 1 , [rsi] = user_input cmp qword ptr [rsi], 42 >rax = 0 , rdi = 0 , rdx = 1 , [rsi] = user_input, user_input == 42 ? je .get_flag
就可以创造出一个公式,描述程序的行为,获得程序的输入、检查条件和持续输出之间的关系
1 2 (user_inp(user_input == 42 AND program_output == the_flag) OR (user input != 42 AND program output == "INCORRECT" )ut == 42 AND program_output == the_flag) OR(user input != 42 AND program output == "INCORRECT" )
然后加上一些约束关系,就可以得到有效答案
1 2 3 4 ( (user_input == 42 AND program_output == the_flag) OR (user_input != 42 AND program_output == "INCORRECT" ) >user_input == 42 ) AND (program_output != "INCORRECT" )
原理 现代的动态符号执行会模拟代码,转化为数据公式
符号执行必须像普通CPU一样按顺序执行代码,不过它有一个可修改的模拟状态。
在它执行程序时,每当遇到分支,模拟器就会多分出一个内部状态,一个状态根据条件进入一个分支,而另一个不会进入同一个分支。当这些状态分离后它们的行为就不一样了,
angr 一些其他工具
angr: our binary analysis framework that does (among other things) symboli
execution.
miasm: another binary analysis framework that does symbolic execution.
s2e: another binary analysis framework that does symbolic execution.
manticore: another binary analysis framework that does symbolic execution.
klee: symbolic execution on source code.
angr 安装 (加个镜像源安装更快)
https://github.com/angr/angr
1 pip install angr -i https:
angr
依赖于 pycparser
来解析 C 代码。如果没有安装 pycparser
,就会出现错误。
1 pip install pycparser -i https:
如果还有报错,就尝试更新
1 pip install --upgrade pycparser
angr基本使用
使用的测试代码 do.c
1 2 3 4 5 6 7 8 9 10 11 12 int main () { char buffer[1024 ]; read(0 , buffer, 1 ); if (buffer[0 ] == 42 ) puts ("Success!" ); else puts ("Failure!" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 In [1 ]: import angr In [3 ]: p=angr.Project("do" ) In [4 ]: hex(p.entry) Out[4 ]: '0x401050' In [5 ]: p.factory.block(0x401050 ).pp() _start: 401050 xor ebp, ebp401052 mov r9, rdx401055 pop rsi401056 mov rdx, rsp401059 and rsp, 0xfffffffffffffff0 40105 d push rax40105 e push rsp40105f xor r8d, r8d401062 xor ecx, ecx401064 mov rdi, main40106b call qword ptr [0x403fd8 ]In [6 ]: sm=p.factory.simulation_manager() In [7 ]: sm.active Out[7 ]: [<SimState @ 0x401050 >] In [8 ]: sm.active[0 ] Out[8 ]: <SimState @ 0x401050 > In [9 ]: sm.active[0 ].regs Out[9 ]: <angr.state_plugins.view.SimRegNameView at 0x7fa4c530f190 > In [10 ]: sm.active[0 ].regs.rax Out[10 ]: <BV64 0x1c > In [11 ]: sm.active[0 ].regs.rsp Out[11 ]: <BV64 0x7fffffffffeff98 > In [12 ]: sm.active[0 ].memory.load(0x400000 ,4 ) Out[12 ]: <BV32 0x7f454c46 > In [13 ]: "\x7f\x45\x4c\x46" Out[13 ]: '\x7fELF'
现在来看执行,模拟程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 In [14 ]: sm.step() Out[14 ]: <SimulationManager with 1 active> In [15 ]: sm.step() Out[15 ]: <SimulationManager with 1 active> In [16 ]: sm.step() Out[16 ]: <SimulationManager with 1 active> In [17 ]: sm.step() Out[17 ]: <SimulationManager with 1 active> In [18 ]: sm.step() Out[18 ]: <SimulationManager with 1 active>
我们可以看到之前的状态,它在执行过程中走过的路径
1 2 3 4 5 6 7 In [19 ]: list (sm.active[0 ].history.descriptions) Out[19 ]: ['<IRSB from 0x401050: 1 sat>' , '<SimProcedure __libc_start_main from 0x527cc0: 1 sat>' , '<IRSB from 0x401136: 1 sat>' , '<IRSB from 0x401040: 1 sat>' , '<SimProcedure read from 0x5fea10: 1 sat>' ]
继续尝试步进,这次使用函数来步进
这条指令让符号执行开始运行,并在活动路径数量超过 1 时停止。换句话说,符号执行会继续探索路径,直到程序出现了多个可能的执行路径。这通常用于查找分支点或分析程序中不同输入条件引起的执行分歧。
1 2 In [20 ]: sm.run(until=lambda sm2 :len(sm.active) >1 ) Out[20 ]: <SimulationManager with 2 active>
来看看发生了什么,这是2者的执行路径,它们最后执行的都是 '<IRSB from 0x40115f: 2 sat>']
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 In [21 ]: list (sm.active[0 ].history.descriptions) Out[21 ]: ['<IRSB from 0x401050: 1 sat>' , '<SimProcedure __libc_start_main from 0x527cc0: 1 sat>' , '<IRSB from 0x401136: 1 sat>' , '<IRSB from 0x401040: 1 sat>' , '<SimProcedure read from 0x5fea10: 1 sat>' , '<IRSB from 0x40115f: 2 sat>' ] In [22 ]: list (sm.active[1 ].history.descriptions) Out[22 ]: ['<IRSB from 0x401050: 1 sat>' , '<SimProcedure __libc_start_main from 0x527cc0: 1 sat>' , '<IRSB from 0x401136: 1 sat>' , '<IRSB from 0x401040: 1 sat>' , '<SimProcedure read from 0x5fea10: 1 sat>' , '<IRSB from 0x40115f: 2 sat>' ]
查看产生分支的地方,这里就是cmp比较区域,也是一开始程序要点处
1 2 3 4 In [23 ]: p.factory.block(0x40115f ).pp() 40115f movzx eax, byte ptr [rbp-0x400 ]401166 cmp al, 0x2a 401168 jne 0x40117b
可以来看这两个模拟器的状态,到底是产生了什么分歧。可以看到是“约束条件”
1 2 3 4 5 In [24 ]: sm.active[0 ].solver.constraints Out[24 ]: [<Bool packet_0_stdin_55_8 == 42 >] In [25 ]: sm.active[1 ].solver.constraints Out[25 ]: [<Bool packet_0_stdin_55_8 != 42 >]
现在来看看“运行”会是怎样,成功运行并找到了分歧。但这是一个非常简单的程序,所以可以运行完并有结果。
1 2 In [26 ]: sm.run() Out[26 ]: <SimulationManager with 2 deadended>
可以看一下现在的约束条件,还是42
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 In [27 ]: sm.active[1 ].solver.constraints --------------------------------------------------------------------------- IndexError Traceback (most recent call last) Cell In[27], line 1 ----> 1 sm.active[1].solver.constraints IndexError: list index out of range In [28]: sm.deadended[1].solver.constraints Out[28]: [<Bool packet_0_stdin_55_8 == 42 >] In [29 ]: sm.deadended[0 ].solver.constraints Out[29 ]: [<Bool packet_0_stdin_55_8 != 42 >]
还可以看看他们的标准输出(可以称为“具体化”,具体化是查询约束求解器的过程)
1 2 3 4 5 In [30 ]: sm.deadended[0 ].posix.stdout .concretize() Out[30 ]: [b'F ailure!', b' \n' ] In [31 ]: sm.deadended[1 ].posix.stdout .concretize() Out[31 ]: [b' Success!', b' \n' ]
可以利用这个计算出标准输入
1 2 In [32 ]: sm.deadended[1 ].posix.stdin .concretize() Out[32 ]: [b' *']
一些问题 看起来很完美,但是符号执行有严重的限制:它必须追踪每一条路径来理解正在发生的事情
路径爆炸 比如此代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 unsigned long atoi (char *s) { unsigned long n = 0 ; while (*s != '\0' ) { if (*s == '0' ) n = n * 10 ; else if (*s == '1' ) n = n * 10 + 1 ; else if (*s == '2' ) n = n * 10 + 2 ; else if (*s == '3' ) n = n * 10 + 3 ; else if (*s == '4' ) n = n * 10 + 4 ; else if (*s == '5' ) n = n * 10 + 5 ; else if (*s == '6' ) n = n * 10 + 6 ; else if (*s == '7' ) n = n * 10 + 7 ; else if (*s == '8' ) n = n * 10 + 8 ; else if (*s == '9' ) n = n * 10 + 9 ; else break ; s++; } return n; } int main () { char buffer[1024 ]; read(0 , buffer, 10 ); int i = atoi(buffer); if (i == 42 ) { puts ("Success!" ); } else { puts ("Failure!" ); } return 0 ; }
每个状态都会再分出10个不同的状态,最终会导致状态爆炸,这就是符号执行不可行的首要原因,它会遍历每一条路径,面对复杂流程的代码就先一步卡在了路径遍历上。
现在执行此代码,来看到实时的状态爆炸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 In [1 ]: import angr In [2 ]: p=angr.Project("atoi" ) In [3 ]: sm=p.factory.simulation_manager() In [4 ]: sm.run(until=lamnda sm2 :len(sm.active)>1 ) Cell In[4 ], line 1 sm.run(until=lamnda sm2 :len(sm.active)>1 ) ^ SyntaxError: invalid syntax. Perhaps you forgot a comma? In [5 ]: sm.run(until=lambda sm2 :len(sm.active)>1 ) Out[5 ]: <SimulationManager with 2 active> In [6 ]: sm.step() Out[6 ]: <SimulationManager with 3 active> In [7 ]: sm.step() Out[7 ]: <SimulationManager with 4 active> In [8 ]: sm.step() Out[8 ]: <SimulationManager with 6 active> In [9 ]: sm.step() Out[9 ]: <SimulationManager with 9 active> In [10 ]: sm.step() Out[10 ]: <SimulationManager with 13 active> In [11 ]: sm.step() Out[11 ]: <SimulationManager with 19 active> In [12 ]: sm.step() Out[12 ]: <SimulationManager with 28 active> In [13 ]: sm.step() Out[13 ]: <SimulationManager with 41 active> In [14 ]: sm.step() Out[14 ]: <SimulationManager with 59 active, 1 deadended> In [15 ]: sm.deadended[0 ].posix.stdin .concretizer() --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[15 ], line 1 ----> 1 sm.deadended[0 ].posix.stdin .concretizer() AttributeError: 'SimPacketsStream' object has no attribute 'concretizer' In [16 ]: sm.deadended[0 ].posix.stdin .concretize() Out[16 ]: [b' \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ]
这是第一个结束状态,得到的都是空字节,继续步进,会发现需要的时间越来愈长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 In [17 ]: sm.step() Out[17 ]: <SimulationManager with 87 active, 1 deadended> In [18 ]: sm.step() Out[18 ]: <SimulationManager with 128 active, 1 deadended> In [19 ]: sm.step() Out[19 ]: <SimulationManager with 186 active, 2 deadended> In [20 ]: sm.deadended[0 ].posix.stdin .concretize() Out[20 ]: [b' \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ] In [21 ]: sm.deadended[1 ].posix.stdin .concretize() Out[21 ]: [b'0 \x00\x00\x00\x00\x00\x00\x00\x00\x00' ] In [22 ]: sm.step() Out[22 ]: <SimulationManager with 271 active, 3 deadended> In [23 ]: sm.deadended[2 ].posix.stdin .concretize() Out[23 ]: [b'1 \x00\x00\x00\x00\x00\x00\x00\x00\x00' ]
可以得到数字0,1,但这些离42还有很远,而且越到后面路径越多
如何解决? 可以使用一些技术合并状态,使用复杂的数学公式,将多种状态合并成一种
但也有可能得到越来越复杂的数学公式
约束一般使用 SMT解答器来解答,可满足性模块化理论
SMT的基本思想是:理论本质上编码了不同类型的简化结果,将一些常见的情况套用那些化简,然后不断地用那些理论简化结果,用逻辑表达式构建一个模型,它表示实际涉及的比特,即使这些比特是未知的,然后通过布尔可满足性(SAT)原理解决问题,SAT是一个NP完全解决的问题https://en.wikipedia.org/wiki/Boolean_satisfiability_problem
这意味着我们没有一个多项式时间的算法来解决这个问题,求解SAT模型的算法是指数型的,所以解决复杂的程序会很慢
当面对复杂数据,复杂操作时,符号执行会失败,不只是路径爆炸的问题,也由于约束求解的局限性
环境建模 模拟CPU:相对简单
模拟OS:难
符号执行引擎必须识别各种内存错误(如缓冲区溢出、空指针解引用),这需要精确地跟踪内存块的边界、访问权限等信息。内存模拟中出现的错误检测约束,会进一步增加符号执行的复杂性。
动态符号执行中需要模拟对指针、数组、结构体等复杂数据结构的访问。这些结构通常涉及多级指针和嵌套的内存引用,导致在执行过程中需要频繁地进行地址计算和内存对象查找。例如,访问链表、树等数据结构时,指针的符号化会导致路径爆炸,增加符号执行的开销。如何准确地追踪指针的指向并处理其符号化表达式,是内存模拟中的一大挑战
在符号执行中,程序中的变量、地址、数组索引等都可能被符号化,从而表示为一组符号变量,而非具体值。这样一来,内存地址本身也可能是符号化的,这意味着一个内存访问操作(如读、写)可能对应多个潜在的内存位置。当使用符号化地址进行内存访问时,符号执行引擎必须考虑所有可能的地址,并在模拟内存时维护这些可能性。这极大地增加了内存访问的复杂性和计算成本
动态符号执行在探索程序路径时,需要不断克隆内存状态,以探索不同的执行分支。每当程序执行一条条件分支指令,符号执行引擎就会创建两个新的状态,并且需要分别模拟它们的内存操作。这会导致大量的内存复制和状态克隆操作。
符号执行引擎通常使用一种分层表示 的方法来共享内存对象。然而,这种优化需要精确地管理哪些对象被共享,哪些对象需要克隆,且要确保在不同执行路径上的内存操作不会互相干扰
参考资料 pwn.college Module 12 - Automated Vulnerability Discovery - Dynamic Symbolic Execution
https://www.diag.uniroma1.it/~delia/papers/svtr19.pdf
https://srg.doc.ic.ac.uk/files/papers/ngmem-ase-2019.pdf