查看: 144|回复: 0

[Reverse] 深入浅出Angr(一)

[复制链接]

22

主题

23

帖子

0

精华

VIP

Rank: 16

学币
10
荣耀
0
rank
0
违规
0

VIP

    发表于 2020-6-15 00:42:14 | 显示全部楼层 |阅读模式

    前言
      angr已经不在是从前的angr了。
      前一阵子为了学习ollvm混淆,纠结了很久angr的版本,可以说之前的angr很乱,版本稍一改动api就变了,早在暑假就听闻angr会有很大的变动,最近偶然间看到最新版的angr,于是又萌生了学习一番的冲动,毕竟angr在CTF解题中有许多应用。
    基本原理  什么是符号执行,参阅Wiki
      angr官网
      API文档
      CTF应用
    例题  官方给出了很多例题,这都是很好的学习资源呐!
      搭好环境后我们可以先来测试一个官方给的例子是否有效,
      就先测试耗时比较短的一题Whitehat CTF 2015 - Crypto 400

    深入浅出Angr(一)

    深入浅出Angr(一)
      可以看到喜人的输出结果。

    深入浅出Angr(一)

    深入浅出Angr(一)
    比较  改版之后的不同,我这刚好有早之前的angr的示例脚本,拿来比较着学习。
      旧版(7.x.x.x)
    #!/usr/bin/env python
    # coding: utf-8
    import angr
    import time
    
    def main():
    # Load the binary. This is a 64-bit C++ binary, pretty heavily obfuscated.
    p = angr.Project('wyvern')
    
    # This block constructs the initial program state for analysis.
    # Because we're going to have to step deep into the C++ standard libraries
    # for this to work, we need to run everyone's initializers. The full_init_state
    # will do that. In order to do this peformantly, we will use the unicorn engine!
    st = p.factory.full_init_state(args=['./wyvern'], add_options=angr.options.unicorn)
    
    # It's reasonably easy to tell from looking at the program in IDA that the key will
    # be 29 bytes long, and the last byte is a newline.
    
    # Constrain the first 28 bytes to be non-null and non-newline:
    for _ in xrange(28):
    k = st.posix.files[0].read_from(1)
    st.solver.add(k != 0)
    st.solver.add(k != 10)
    
    # Constrain the last byte to be a newline
    k = st.posix.files[0].read_from(1)
    st.solver.add(k == 10)
    
    # Reset the symbolic stdin's properties and set its length.
    st.posix.files[0].seek(0)
    st.posix.files[0].length = 29
    
    # Construct a SimulationManager to perform symbolic execution.
    # Step until there is nothing left to be stepped.
    sm = p.factory.simulation_manager(st)
    sm.run()
    
    # Get the stdout of every path that reached an exit syscall. The flag should be in one of these!
    out = ''
    for pp in sm.deadended:
    out = pp.posix.dumps(1)
    if 'flag{' in out:
    return filter(lambda s: 'flag{' in s, out.split())[0]
    
    # Runs in about 15 minutes!
    
    def test():
    assert main() == 'flag{dr4g0n_or_p4tric1an_it5_LLVM}'
    
    if __name__ == "__main__":
    before = time.time()
    print main()
    after = time.time()
    print "Time elapsed: {}".format(after - before)
      最新版8.18.10.25
    #!/usr/bin/env python
    # coding: utf-8
    import angr
    import claripy
    import time
    
    def main():
    # Load the binary. This is a 64-bit C++ binary, pretty heavily obfuscated.
    # its correct emulation by angr depends heavily on the libraries it is loaded with,
    # so if this script fails, try copying to this dir the .so files from our binaries repo:
    # [url]https://github.com/angr/binaries/tree/master/tests/x86_64[/url]
    p = angr.Project('wyvern')
    
    # It's reasonably easy to tell from looking at the program in IDA that the key will
    # be 29 bytes long, and the last byte is a newline. Let's construct a value of several
    # symbols that we can add constraints on once we have a state.
    
    flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(28)]
    flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])
    
    # This block constructs the initial program state for analysis.
    # Because we're going to have to step deep into the C++ standard libraries
    # for this to work, we need to run everyone's initializers. The full_init_state
    # will do that. In order to do this peformantly, we will use the unicorn engine!
    st = p.factory.full_init_state(
    args=['./wyvern'],
    add_options=angr.options.unicorn,
    stdin=flag,
    )
    
    # Constrain the first 28 bytes to be non-null and non-newline:
    for k in flag_chars:
    st.solver.add(k != 0)
    st.solver.add(k != 10)
    
    # Construct a SimulationManager to perform symbolic execution.
    # Step until there is nothing left to be stepped.
    sm = p.factory.simulation_manager(st)
    sm.run()
    
    # Get the stdout of every path that reached an exit syscall. The flag should be in one of these!
    out = b''
    for pp in sm.deadended:
    out = pp.posix.dumps(1)
    if b'flag{' in out:
    return next(filter(lambda s: b'flag{' in s, out.split()))
    
    # Runs in about 15 minutes!
    
    def test():
    assert main() == b'flag{dr4g0n_or_p4tric1an_it5_LLVM}'
    
    if __name__ == "__main__":
    before = time.time()
    print(main())
    after = time.time()
    print("Time elapsed: {}".format(after - before))
      试着运行旧版的脚本,如下报错

    深入浅出Angr(一)

    深入浅出Angr(一)
      可见st.posix.files中的files方法已经被移除了,之前了解过angr的同学应该都知道,这一段代码是用来做条件约束的,那么新版是如何进行约束的呢?
    for _ in xrange(28):
    k = st.posix.files[0].read_from(1)
    st.solver.add(k != 0)
    st.solver.add(k != 10)
      在angr==8.18.10.25中使用了claripy模块进行输入以及条件约束。
    flag_chars = [claripy.BVS('flag_%d' % i, 8) for i in range(28)]
    flag = claripy.Concat(*flag_chars + [claripy.BVV(b'\n')])
    
    for k in flag_chars:
    st.solver.add(k != 0)
    st.solver.add(k != 10)
      虽然形式发生了变化,但是理解起来是一样的。
      只希望angr的api不要再经常变动了,这对于想学习angr的同学来说可真不友好
    初涉angr  我们从比较简单的一题开始看起
    defcamp_r100  载入IDA

    深入浅出Angr(一)

    深入浅出Angr(一)
      逻辑很清楚了,sub_4006FD函数中只有一个简单的运算,对于angr这么点运算量实在是微不足道。
    import angr
    
    def main():
    p = angr.Project("r100",auto_load_libs=True)
    simgr = p.factory.simulation_manager(p.factory.full_init_state())
    simgr.explore(find=0x400844, avoid=0x400855)
    
    return simgr.found[0].posix.dumps(0).strip(b'\0\n')
    
    def test():
    assert main().startswith(b'Code_Talkers')
    
    if __name__ == '__main__':
    print(main())
      使用p.factory.simulation_manager创建一个simulation_manager进行模拟执行,其中传入一个SimState.
      SimState对象通常有三种
    1. blank_state(**kwargs)
    返回一个未初始化的state,此时需要主动设置入口地址,以及自己想要设置的参数。
    2. entry_state(**kwargs)
    返回程序入口地址的state,通常来说都会使用该状态
    3. full_init_state(**kwargs)
    同entry_state(**kwargs) 类似,但是调用在执行到达入口点之前应该调用每个初始化函数
      除此之外还需要了解一下auto_load_libs参数,该参数用来设置是否自动载入依赖的库,如果设置为True会自动载入依赖的库,然后分析到库函数调用时也会进入库函数,这样会增加分析的工作量。如果为False,程序调用函数时,会直接返回一个不受约束的符号值。
      simgr.explore(find=0x400844, avoid=0x400855)然后使用simgr.explore进行模拟执行find是想要执行分支,avoid是不希望执行的分支。

    深入浅出Angr(一)

    深入浅出Angr(一)
      执行完之后找到一个符合条件的分支。

    深入浅出Angr(一)

    深入浅出Angr(一)
      <SimulationManager with 12 avoid, 1 found, 2 active>,此时相关的状态已经保存在了simgr当中,我们可以通过simgr.found来访问所有符合条件的分支。

    深入浅出Angr(一)

    深入浅出Angr(一)
      此时我们可以试想一下,我们该如何获取angr正确执行到0x400844分支所进行的输入呢?
      在官方的文档上我们可以知道SimState都有哪些参数和方法。
      不过我有时候更习惯的是help(simgr.found[0])

    深入浅出Angr(一)

    深入浅出Angr(一)
      可以比较清楚的知道SimState中保存了当前状态的寄存器,内存,输入输出等信息,这里我们需要使用posix,接下来在看一下posix都由哪些方法可以供我们使用。

    深入浅出Angr(一)

    深入浅出Angr(一)
      我比较关注的是dump方法,它可以返回相应文件描述符的内容,很明显为了获取输入,应该使用dump(0),因此完整的代码就应该如下simgr.found[0].posix.dumps(0)
      到此,虽然题目解决了,但是相信大家也都有许多很多困惑的地方,<SimulationManager with 12 avoid, 1 found, 2 active>中avoid 和 active又是什么呢?
      我试着探究一番。逐个打印出simgr.avoid的输入,发现有趣的地方。

    深入浅出Angr(一)

    深入浅出Angr(一)
      可以看到这12个avoid就是angr在模拟执行到0x400855分支的过程。
      再来看看active

    深入浅出Angr(一)

    深入浅出Angr(一)
      其中0x4007e4对应的是一个死循环。

    深入浅出Angr(一)

    深入浅出Angr(一)
      至此我们已经掌握了angr的基本用法了,用来解决一般的题目没有任何问题,我们只需设置好find 和 avoid就可以了。
      !但是要想真正发挥出angr的作用,可不仅仅如此,我们需要知道如何进行条件约束,以及如何hook,从而在有限的时间内,充分发挥出angr的用处。
      下一篇了解一下angr如何进行命令行传参并求解



    评分

    参与人数 1学币 +1 收起 理由
    roger + 1 感谢您的热心解答,学到了!

    查看全部评分

    温馨提示:
    1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
    2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
    3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
    学逆向论坛-免费的逆向学习论坛
    微信公众号
    快速回复 返回顶部 返回列表