zoukankan      html  css  js  c++  java
  • 动若脱兔:深入浅出angr--初步理解符号执行以及angr架构

    一:概论 

     angr作为符号执行的工具,集成了过去的许多分析方式,它不仅能进行动态符号执行,而且还能进行很多静态分析,他在分析二进制程序中能发挥很大的作用,下面为一些应用:

      1:利用符号执行探究执行路径,自动解ctf逆向题

      2:利用angr获取程序控制流(CFG)

      3:利用angr生成rop链

      4:利用angr发现漏洞

      5:利用angr加密程序

      6:进行污点跟踪

      由上可以发现,angr的应用是非常多的,里面关于符号执行的应用和思路(特别是自动化相关的思路)是非常值得学习的,本篇不涉及angr的具体应用,主要讲一下angr整个设计的架构。

    二:符号执行

      先初略了解一下符号执行,对angr有个大致的了解,后续利用angr再对符号执行的理解进行加深。最传统的符号执行是静态符号执行,首先将输入的变量符号化,如果通俗点的话就是设置输入变量为x,然后通过静态分析程序的cfg流,转化为中间语言,获得符号化的变量在程序流程中的改变,从而输出一个带符号化变量的值。举个例子:

    a = raw_input()
    b = 2*a
    if( b == 10):
        print "win"
    else:
        print "lose"

      在这个简单的代码段里,传统的运行是 先输入a的值,再运行下来的代码。在静态符号执行的过程中,首先将a进行符号化,就是转化为x,所以b就是 2*x,当b == 2*x时,则走入”win"路径,如果 b!=2*x时,则走入lose路径。路径合起来称之为执行树,(b==2*x)和(b!=2*x)即为路径约束式,当符号执行结束时(程序正常或者异常退出),约束求解器就会对路径约束式进行求解(可以简单理解为解方程),解出的答案就是走到这个路径需要的值。

      当然,这种方式看起来很美丽,但是在实际执行过程中会出现很多问题,其中一个就是约束式无法通过约束式求解的问题,这里的解决方案是将传统的静态符号执行和实际执行结合起来,称之为动态符号执行(concolic execution),concolic维持了两个状态。一种是实际变量的状态,另一种是符号化的状态。实际状态将随机生成值映射到变量中,而符号化状态将变量进行符号化。concolic首先将实际状态运行,并收集实际运行时该路径的变量符号化的约束式,i求解。并将约束式取反,获取另一条路径的约束式并求解。过程不断重复,知道路径被探索完,或者达到用户设置的限制。

      以上面的代码为示例,Concolic随机生成变量(a = 7),然后实际运行走了lose路径。在判断语句中,根据收集的约束式取反(b== 2*x),可以得到另一条路径。通过实际运行这种方式,可以很好的避免了约束式无法识别和求解的问题。

    三:Angr架构

      angr架构非常清晰,主要分为下图这些模块,每个模块的功能以及彼此间的联系。

                                                                           

    3.1 CLE模块

      二进制的装载组建是CLE(CLE Load Everything),它负责装载二进制对象以及它所依赖的库,将自身无法执行的操作转移给angr的其它组件,最后生成地址空间,表示该程序已加载并可以准备运行。

    >>> import angr, monkeyhex
    >>> proj = angr.Project('/bin/true')
    >>> proj.loader
    <Loaded true, maps [0x400000:0x5008000]>
    

      cle.loader代表着将整个程序映射到某个地址空间,而地址空间的每个对象都可以由一个加载器后端加载,例如cle.elf用于加载linux的32位程序。下面是地址空间的分类

    >>> proj.loader.all_objects
    [<ELF Object fauxware, maps [0x400000:0x60105f]>,
     <ELF Object libc.so.6, maps [0x1000000:0x13c42bf]>,
     <ELF Object ld-linux-x86-64.so.2, maps [0x2000000:0x22241c7]>,
     <ELFTLSObject Object cle##tls, maps [0x3000000:0x300d010]>,
     <KernelObject Object cle##kernel, maps [0x4000000:0x4008000]>,
     <ExternObject Object cle##externs, maps [0x5000000:0x5008000]>
    

      其中,类型可以分为 proj.loader.main_object,proj.loader.share_object,proj.loader.kernel_object等等...获取特定object之后,可以与object进行交互获取更详细的信息

    3.2 ArchInfo模块

      archinfo是包含特定于体系结构的信息的类的集合。太过于底层,在日后的分析中逐步解释

    3.3 PyVex模块

      angr需要处理不同的架构,所以它选择一种中间语言来进行它的分析,angr使用Valgrind的中间语言——VEX来完成这方面的内容。VEX中间语言抽象了几种不同架构间的区别,允许在他们之上进行统一的分析。各种中间语言在设计理念上有很多的共通点,这里又会是一个很大的话题,所以暂且抛开,具体关于IR语言的语法规则请查阅 https://docs.angr.io/docs/ir.html。

    3.4 SimuVEX模块

      这里是中间语言VEX执行的模拟器,它允许你控制符号执行。

    3.5 Clarity

      这个模块主要专注于将变量符号化,生成约束式并求解约束式,这也是符号执行的核心所在,在angr中主要是利用微软提供的z3库去解约束式

    3.6 angr以及以上

      这些则为上层封装好的接口,后续使用时在描述。

    四:angr的输入输出

      一般来说,命令行程序主要有两种数据输入的方式,第一种是利用api(get,read),第二种是放在argc上,其它的方法有很多,最后也会提供一种通用的解法。

      当数据输入在argc上时,一般使用claripy库,将输入的数据符号化,具体代码如下:

    复制代码
    import angr
    import claripy
    p = angr.Project("test")
    args = claripy.BVS('args', 8*16)
    initial_state = prog.factory.entry_state(args=["./vul", args])
    for i in range(0,8):
      initial_state.add_constraints(argc.get_byte(0) >= argvc.get_byte(1)) pg = p.factory.path_group(initial_state) pg.explore(find=(0x4005d1,)) print pg # <PathGroup with 18 deadended, 4 active, 1 found> print pg.found[0] # <Path with 64 runs (at 0x4005d1)> print pg.found[0].state.posix.dumps(0)
    复制代码

       claripy库是求解器引擎,绝大部分只是用来做z3的前端,而在这里起到的作用主要是将参数符号化,核心代码为第四行。

      当利用api时,主要通过对st.posix.files[0]进行符号化,具体代码如下:

    复制代码
     p = angr.Project('wyvern')
     st = p.factory.full_init_state(args=['./wyvern'], add_options=angr.options.unicorn)
     for _ in xrange(28):
        k = st.posix.files[0].read_from(1)
        st.solver.add(k != 0)
        st.solver.add(k != 10)
     k = st.posix.files[0].read_from(1)
     st.solver.add(k == 10)
     st.posix.files[0].seek(0)
     st.posix.files[0].length = 29
    复制代码

      state.pix在angr中是  angr.state_plugins.posix.SimSystemPosix类,该类的主要作用是用于模拟符合posix环境的数据存储和输入输出。其中files[0]代表着数据的输入,read_from表示读取输入的数据。第3到第8行对输入的数据进行限制,

    最后两行将指针重新指向开头并设置长度。

      第三种是最通用的方式,直接访问并修改内存,无论程序是通过何种方式进行输入,输入的数据总是在内存中,可以通过对内存进行符号化。具体代码示例如下:

    复制代码
    import angr
    p = angr.Project('./vul')
    s = p.factory.blank_state(addr=0x80485c8)
    bvs = s.se.BVS('to_memory', 8*4)
    s.se.add(bvs > 1000)
    s.memory.store(0x08049b80, bvs, endness='Iend_LE')
    pg = p.factory.path_group(s, immutable=False)
    复制代码

      其中 endness有三个值,分别为

    Variables:  
    LE – little endian, least significant byte is stored at lowest address
    BE – big endian, most significant byte is stored at lowest address
    ME – Middle-endian. Yep.

      关于内存操作还可以多说一下,s.memory.store可以用于存储数据,s.memory.load用于读取数据.。

    五:angr解题步骤:

      这里本篇主要利用simulation_manager(老版本为factory_group)求解

    复制代码
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import angr
    import claripy
    
    angr.l.setLevel('DEBUG')
    p = angr.Project('./vul', load_options={"auto_load_libs": False})
    
    args = claripy.BVS('args', 8*100)
    initial_state = p.factory.entry_state(args=[p.filename, args], add_options={'BYPASS_UNSUPPORTED_SYSCALL'})
    
    #pg = p.factory.path_group(initial_state, immutable=False),在新版本被代替,和simlation_manager等效
    pg = p.factory.simulation_manager(initial_state)
    find_addrs = (0x400546, )
    avoid_addrs = ()
    pg.explore(find=find_addrs, avoid=avoid_addrs)
    print pg
    print ans = pg.found[0].state.se._solver.result.model

    print pg.found[0].state.posix.dumps(0) //代表该状态程序的所有输入
    print pg.found[0].state.posix.dumps(1) //代表该状态程序的所有输出
    复制代码

      simualtion_manager初始化运行之后,一般有以下几种状态   

      step()表示向下执行一个block(42bytes),step()函数产生active状态,表示该分支在执行中;

      run()表示运行到结束,run()函数产生deadended状态,表示分支结束;

      explore()可以对地址进行限制以减少符号执行遍历的路径。例如 sm.explore(find=0x400676,avoid=[0x40073d]) explore()产生found状态,表示探索的结果等等。也可以使用条件来进行find匹配

    1
    pg.explore(find=lambda s: "Congrats" in s.posix.dumps(1))

      simulation_manager式angr最重要的控制接口,模拟管理器的最基本功能是利用step()通过一个基本块将给定存储中的所有状态向前推进。当然,如果不对路径探究过程进行细致研究,只需要使用run()和explore()就好了,在简述run和explore之前,首先对路径的stashes进行描述。

      当explore()以find参数运行时,程序会一直运行,直到找到与查找条件相匹配的状态,该条件可以是要停止的指令的地址(地址列表),或者是一些运行时是否符合的状态。当条件存储的任何条件与find条件匹配时,他们将置于found存储中,同时也可以设置avoid,符合avoid条件时也会将其置于avoid中。和run()函数不同的是,run()函数会存储所有的路径状态,而expore()只会存储find的状态。

      在路径探索中,一般使用广度优先算法进行探究,当然我们也可以自由设置使用其它方法,例如深度优先...具体可以使用angr.exploration_techniques

  • 相关阅读:
    HDU 5492 Find a path
    codeforce gym 100548H The Problem to Make You Happy
    Topcoder SRM 144 Lottery
    codeforce 165E Compatible Numbers
    codeforce gym 100307H Hack Protection
    区间DP总结
    UESTC 1321 柱爷的恋爱 (区间DP)
    HDU 4283 You Are the One (区间DP)
    HDU 2476 String painter (区间DP)
    UESTC 426 Food Delivery (区间DP)
  • 原文地址:https://www.cnblogs.com/0xJDchen/p/9291335.html
Copyright © 2011-2022 走看看