angr符号执行用例解析——cmu_binary_bomb

解题源码以及二进制文件在:https://github.com/angr/angr-doc/tree/master/examples/cmu_binary_bomb

一道有趣的题目!

首先运行bomb文件,随便输入个字符串,bomb! 炸了!

总共有6关要闯,每次只要输入满足要求的数据才能进入下一关,否则就爆炸。

拖到IDA里面分析,发现第一关的字符串比较还蛮简单,下面几关就需要计算了。

int __fastcall phase_1(__int64 a1)
{
  int result; // eax@1

  result = strings_not_equal(a1, "Border relations with Canada have never been better.");
  if ( result )
    explode_bomb(a1, "Border relations with Canada have never been better.");
  return result;

还是直接angr符号执行进行分析算了。

当然angr符号执行分析肯定也不简单,还是需要分析的。

(在我十分高估angr的时候我写出过如下代码:

大意就是模拟执行,找到包含0x400E49的路径,求解约束

import angr
import claripy

b=angr.Project('bomb',load_options={'auto_load_libs':False})
sm=b.factory.simulation_manager(b.factory.entry_state())
sm.explore(find=0x400E49)
print len(sm.found)
found=sm.found[0]
solution1 = found.posix.dumps(0)
print "print"
print repr(solution1)

Error:    found=sm.found[0]

IndexError: list index out of range

呵,native!)

要想符号执行,首先要让angr能够模拟执行下去,如果都执行不下去,肯定是找不到包含目标地址的路径的啊。

那angr什么时候会执行不下去呢?我们需要提供哪些辅助呢?这个。。以后再说。先看这道题的用例都做了些什么。

首先是phase_1:

从IDA中,我们发现它的验证逻辑就是字符串比较,而且字符串以及明文在代码中了,所以逻辑非常简单。同时这个函数没有调用任何库函数以及系统调用,所以angr肯定能模拟执行的啦。

使用angr分析时,为了避免执行到angr无法模拟的函数或系统调用,用例代码只对phase_1函数进行了符号执行分析。

创建一个空状态,起始地址就是phase_1的起始地址;

创建一个符号变量,逆向发现phase_1是从reg.rdi寄存器读取输入数据的,因此将reg.rdi赋值为符号变量;

注意注意:reg.rdi存放的是地址,phase_1是先从reg.rdi取址,然后从这个地址读取字符串的。IDA里可以看到,read_line函数是从窗口读取数据放入到地址 0x603780中,然后将这个地址传递个reg.rdi的。(测试过,这个地址可以为任意值,只要不影响后面的执行就可以,毕竟只是提供了一个存放符号变量的媒介。)

初始化符号执行,找到phase_1的返回地址end = 0x400ef7,求解此时符号变量的值。

phase_2:

phase_2部分是输入6个数字进行判断,第一个数是否是1,然后后面一个数是否是前一个数的2倍。

phase_2函数调用了库 ___isoc99_sscanf,似乎angr不能处理呢,所以跳过了这个函数,符号执行的起始地址为:0x400f0a。

由于判断逻辑是从栈里面取数据,所以我们需要创建6个符号变量,并压入栈中。然后开始执行符号执行,结束地址就是phase_2函数的结束地址就可以。

phase_3:

phase_3部分是输入两个整数,switch case语句,根据第一个整数判断第二是否匹配。

与phase_2类型,不过这个函数有多个解,所以可以重点关注一下,如何求解多个解的方法。

对于explore发现的所有活跃(active)状态(or 路径)都加入队列里,分别求解它们的值。

同时对于found集合,不再只是取第一个found[0]而是对所有的found都进行遍历求解。

phase_4:

phase_4部分是输入两个整数,第二个整数不变,第一个整数递归计算,最后结果均为0。所以第二个数为0,第一个数利用angr求解为7。这里测试了一下栈数据的读取。有的时候通过found.regs.rsp - 0x18 + 0x8读取第一个参数,有的时候通过found.stack_pop()读取参数。其实它们是等价的。

rsp指向的是返回地址,所以会先found.stack_pop() 弹出返回地址 使rsp+0x8

rsp+0x8指向的是第一个参数 这时候found.stack_pop()弹出的才是第一个参数。

rsp+0xc指向的是第二个参数。

具体的内容可以学习一下:逆向基础 堆栈详解 —— https://mp.weixin.qq.com/s/PlLHRJ3XbJ6z6C5-rPXfEQ

phase_5:

phase_5需要接收长度6的字符串,遍历字符串的字符取byte值去一个字符数组中索引,拼接成一个新的字符串。新的字符串应该为"flyers"。

这次它利用了proj.kb.obj.get_symbol函数,通过函数名来获取想要分析的函数地址。

    start = proj.kb.obj.get_symbol('phase_5').rebased_addr
    avoid = proj.kb.obj.get_symbol('explode_bomb').rebased_addr
    # let's stop at the end of the function
    find = proj.kb.functions.get('phase_5').ret_sites[0].addr

需要再次注意的是,phase_5没有调用scanf函数所以此时参数的地址不是存放在栈里了,也就是说不是通过stack.pop来访问参数的。函数访问时是从寄存器rdi读取,就是phase_5函数参数寄存器。此时需要修改寄存器rdi中的内容,rdi指向的是新开辟的一段栈空间,里面存放的是符号变量(??目前是这样猜的,在不创建符号变量时,所以的变量都是符号的??)。然后求解这个地址的值就好了。(如果我将rdi赋值为任意一个内存地址是不行的,似乎一定时某个栈地址才可以)

phase_6:

phase_6里面好多while循环啊,而且还有while(1),正好学习一下如何避免路径爆炸。

phase_6中关闭了,LAZY_SOLVES选项(remove_options={angr.options.LAZY_SOLVES})。

LAZY_SOLVES:在LAZY_SOLVES打开的情况下,只要路径在基本块的末尾分支为几个,angr就不会主动检查每个成功的状态是否可以满足。相反,angr认为他们都是可以满足的,并且继续创造成功的路径。如果这些成功状态中的任何一个不成立,并且稍后必须发生一个解决方案(例如,具体化一个符号内存写入目标),定理证明者将无法找到在该状态下收集的所有约束的解决方案(因为它不满足),以及会引发异常SimMemoryAddressError。 简而言之,LAZY_SOLVES是在路径爆炸(创建更多路径)和花费在约束求解上的时间之间的平衡。

在angr的issues中,很多人讨论了关于这个LAZY_SOLVES选项,一些人认为应该默认关闭。因为,往往它会将一些不满足的状态加入到路径中,最后求解时会报错,同时还会引起路径爆炸。感兴趣的可以去参考一下。

总之,默认LAZY_SOLVES是开启的,它可以加快我们分析的速度,因为它不会对每个状态都进行判断。

如果遇到while循环等可能存在路径爆炸的问题,则需要把这个选项关闭(remove_options={angr.options.LAZY_SOLVES})。

此外呢,phase_6终于没有跳过read_six_num()->scanf()这两个函数,而是采用hook的方式,对这个函数进行了替换。hook函数的使用方法就不在赘述啦,不过这一次传入的是一个实现了angr.SimProcedure接口的类。这个地方实在是写的妙啊,所以把代码贴出来。

原来的函数:

int __fastcall read_six_numbers(__int64 a1, __int64 a2)
{
  int result; // eax@1

  result = __isoc99_sscanf(a1, (__int64)&unk_4025C3, a2, a2 + 4);
  if ( result <= 5 )
    explode_bomb(a1, &unk_4025C3);
  return result;
}
hook后的函数:
class read_6_ints(angr.SimProcedure):
    answer_ints = []  # class variable
    int_addrs = []

    def run(self, s1_addr, int_addr):
        self.int_addrs.append(int_addr)
        for i in range(6):
            bvs = self.state.solver.BVS("phase6_int_%d" % i, 32)
            self.answer_ints.append(bvs)
            self.state.mem[int_addr].int.array(6)[i] = bvs

        return 6

hook传入的类要实现一个run函数,它将替换原来的read_six_numbers函数的功能。其中s1_addr就是read_six_numbers的参数a1,int_addr就是原函数参数a2,也就是存放用户输入6个整数的存放地址。在run函数中,我们创建6个符号变量,并依次按照整型数组存放在int_addr地址中。这可真的是从源头将变量符号化了。

接下来就简单了,只需要求解就可以了。一次性求解6个符号变量值的语句为:

answer = [found.solver.eval(x) for x in read_6_ints.answer_ints]

cool!

secret_phase:

接收一个整数,递归计算,看最后的结果是否为2。还有添加一个约束条件,就是输入的整数长度不能大于1000。

这个函数里又有了递归,一般有递归就需要把LAZY_SOLVES关闭了(避免路径爆炸)。

这个约束条件就非常简单了。其他地方已经在前面都讲过了,包括为什么hook strtol函数。直接看用例代码就可以了。

同时我还对用例代码进行更改测试,就是从地址 start=0x401248开始符号执行(也就是read_line函数之后),也可以得到正确的结果。

用例代码:

import angr
import claripy
import logging
from struct import unpack

class readline_hook(angr.SimProcedure):
    def run(self):
        pass

class strtol_hook(angr.SimProcedure):
    def run(self, str, end, base):
        return self.state.solver.BVS("flag", 64, explicit_name=True)

def solve_flag_1():

    # shutdown some warning produced by this example
    #logging.getLogger('angr.engines.vex.irsb').setLevel(logging.ERROR)

    proj = angr.Project('bomb', auto_load_libs=False)

    start = 0x400ee0
    bomb_explode = 0x40143a
    end = 0x400ef7

    # initial state is at the beginning of phase_one()
    state = proj.factory.blank_state(addr=start)

    # a symbolic input string with a length up to 128 bytes
    arg = state.solver.BVS("input_string", 8 * 128)

    # read_line() reads a line from stdin and stores it a this address
    bind_addr = 0x603780

    # bind the symbolic string at this address
    state.memory.store(bind_addr, arg)

    # phase_one reads the string [rdi]
    state.add_constraints(state.regs.rdi == bind_addr)

    # Attempt to find a path to the end of the phase_1 function while avoiding the bomb_explode
    simgr = proj.factory.simulation_manager(state)
    simgr.explore(find=end, avoid=bomb_explode)

    if simgr.found:
        found = simgr.found[0]
        return found.solver.eval(arg, cast_to=str).rstrip(chr(0)) # remove ending \0
    else:
        raise Exception("angr failed to find a path to the solution :(")

def solve_flag_2():

    proj = angr.Project('bomb', auto_load_libs=False)
    bomb_explode = 0x40143a

    # Start analysis at the phase_2 function after the sscanf
    state = proj.factory.blank_state(addr=0x400f0a)

    # Sscanf is looking for '%d %d %d %d %d %d' which ends up dropping 6 ints onto the stack
    # We will create 6 symbolic values onto the stack to mimic this
    for i in xrange(6):
        state.stack_push(state.solver.BVS('int{}'.format(i), 4*8))

    # Attempt to find a path to the end of the phase_2 function while avoiding the bomb_explode
    ex = proj.surveyors.Explorer(start=state, find=(0x400f3c,),
                                 avoid=(bomb_explode,),
                                 enable_veritesting=True)
    ex.run()

    if ex.found:
        found = ex.found[0]

        answer = []

        for _ in xrange(3):
            curr_int = found.solver.eval(found.stack_pop())

            # We are popping off 8 bytes at a time
            # 0x0000000200000001
            # This is just one way to extract the individual numbers from this popped value
            answer.append(str(curr_int & 0xffffffff))
            answer.append(str(curr_int>>32 & 0xffffffff))

        return ' '.join(answer)

def solve_flag_3():

    args = []

    proj = angr.Project('bomb', auto_load_libs=False)

    start = 0x400f6a # phase_3 after scanf()
    bomb_explode = 0x40143a
    end = 0x400fc9 # phase_3 before ret

    state = proj.factory.blank_state(addr=start)

    # we want to enumerate all solutions... let's have a queue
    queue = [state, ]
    while len(queue) > 0:

        state = queue.pop()
        #print "\nStarting symbolic execution..."

        ex = proj.surveyors.Explorer(start=state, find=(end,),
                                     avoid=(bomb_explode,),
                                     enable_veritesting=True,
                                     max_active=8)
        ex.run()

        #print "Inserting in queue " + str(len(ex.active)) + " paths (not yet finished)"
        for p in ex.active:
            queue.append(p)

        #print "Found states are " + str(len(ex.found))
        #print "Enumerating up to 10 solutions for each found state"

        if ex.found:
            for p in ex.found:
                found = p
                found.stack_pop() # ignore, our args start at offset 0x8

                iter_sol = found.solver.eval_upto(found.stack_pop(), 10) # ask for up to 10 solutions if possible
                for sol in iter_sol:

                    if sol == None:
                        break

                    a = sol & 0xffffffff
                    b = (sol >> 32) & 0xffffffff

                    #print "Solution: " + str(a) + " " + str(b)
                    args.append(str(a) + " " + str(b))

    return args


def solve_flag_4():

    avoid = 0x40143A
    find = 0x401061
    proj = angr.Project("./bomb", auto_load_libs=False)

    state = proj.factory.blank_state(
        # let's get the address via its symbol
        # after a proj.analysis.CFG it can be recovered by
        # addr=proj.kb.functions.get('phase_4').addr,
        # we will just use the obj's symbol directly
        addr=proj.kb.obj.get_symbol('phase_4').rebased_addr,
        remove_options={angr.options.LAZY_SOLVES})
    sm = proj.factory.simulation_manager(state)
    sm.explore(find=find, avoid=avoid)

    found = sm.found[0]

    # stopped on the ret account for the stack
    # that has already been moved

    answer = unpack('II', found.solver.eval(
        found.memory.load(found.regs.rsp - 0x18 + 0x8, 8), cast_to=str))

    return ' '.join(map(str, answer))


def solve_flag_5():

    def is_alnum(state, c):
        # set some constraints on the char, let it
        # be a null char or alphanumeric
        is_num = state.solver.And(c >= ord("0"), c <= ord("9"))
        is_alpha_lower = state.solver.And(c >= ord("a"), c <= ord("z"))
        is_alpha_upper = state.solver.And(c >= ord("A"), c <= ord("Z"))
        is_zero = (c == ord('\x00'))
        isalphanum = state.solver.Or(
            is_num, is_alpha_lower, is_alpha_upper, is_zero)
        return isalphanum

    # getting more lazy, let angr find the functions, and build the CFG
    proj = angr.Project("./bomb", auto_load_libs=False)

    proj.analyses.CFG()

    start = proj.kb.obj.get_symbol('phase_5').rebased_addr
    avoid = proj.kb.obj.get_symbol('explode_bomb').rebased_addr
    # let's stop at the end of the function
    find = proj.kb.functions.get('phase_5').ret_sites[0].addr

    state = proj.factory.blank_state(
        addr=start, remove_options={angr.options.LAZY_SOLVES})
    # retrofit the input string on the stack
    state.regs.rdi = state.regs.rsp - 0x1000
    string_addr = state.regs.rdi
    sm = proj.factory.simulation_manager(state)
    sm.explore(find=find, avoid=avoid)
    found = sm.found[0]

    mem = found.memory.load(string_addr, 32)
    for i in xrange(32):
        found.add_constraints(is_alnum(found, mem.get_byte(i)))
    return found.solver.eval(mem, cast_to=str).split('\x00')[0]
    # more than one solution could, for example, be returned like this:
    # return map(lambda s: s.split('\x00')[0], found.solver.eval_upto(mem, 10, cast_to=str))


class read_6_ints(angr.SimProcedure):
    answer_ints = []  # class variable
    int_addrs = []

    def run(self, s1_addr, int_addr):
        self.int_addrs.append(int_addr)
        for i in range(6):
            bvs = self.state.solver.BVS("phase6_int_%d" % i, 32)
            self.answer_ints.append(bvs)
            self.state.mem[int_addr].int.array(6)[i] = bvs

        return 6

def solve_flag_6():
    start = 0x4010f4
    read_num = 0x40145c
    find = 0x4011f7
    avoid = 0x40143A
    p = angr.Project("./bomb", auto_load_libs=False)
    p.hook(read_num, read_6_ints())
    state = p.factory.blank_state(addr=start, remove_options={angr.options.LAZY_SOLVES})
    sm = p.factory.simulation_manager(state)
    sm.explore(find=find, avoid=avoid)
    found = sm.found[0]

    answer = [found.solver.eval(x) for x in read_6_ints.answer_ints]
    return ' '.join(map(str, answer))

def solve_secret():
    start = 0x401242
    find = 0x401282
    avoid = (0x40127d, 0x401267,)
    readline = 0x40149e
    strtol = 0x400bd0

    p = angr.Project("./bomb", auto_load_libs=False)
    p.hook(readline, readline_hook)
    p.hook(strtol, strtol_hook)
    state = p.factory.blank_state(addr=start, remove_options={angr.options.LAZY_SOLVES})
    flag = claripy.BVS("flag", 64, explicit_name=True)
    state.add_constraints(flag -1 <= 0x3e8)
    sm = p.factory.simulation_manager(state)
    sm.explore(find=find, avoid=avoid)
    ### flag found
    found = sm.found[0]
    flag = found.solver.BVS("flag", 64, explicit_name="True")
    return str(found.solver.eval(flag))

def main():
    print "Flag    1: " + solve_flag_1()
    print "Flag    2: " + solve_flag_2()
    print "Flag(s) 3: " + str(solve_flag_3())
    print "Flag    4: " + solve_flag_4()
    print "Flag    5: " + solve_flag_5()
    print "Flag    6: " + solve_flag_6()
    print "Secret   : " + solve_secret()

def test():
    assert solve_flag_1() == 'Border relations with Canada have never been better.'
    print "Stage 1 ok!"

    assert solve_flag_2() == '1 2 4 8 16 32'
    print "Stage 2 ok!"

    args_3 = ["0 207", "1 311", "2 707", "3 256", "4 389", "5 206", "6 682", "7 327"]
    res_3 = solve_flag_3()
    assert len(res_3) == len(args_3)
    for s in args_3:
        assert s in res_3
    print "Stage 3 ok!"

    assert solve_flag_4() == '7 0'
    print "Stage 4 ok!"

    assert solve_flag_5().lower() == 'ionefg'
    print "Stage 5 ok!"

    assert solve_flag_6() == '4 3 2 1 6 5'
    print "Stage 6 ok!"

    assert solve_secret() == '22'
    print "Secret stage ok!"

if __name__ == '__main__':

    # logging.basicConfig()
    # logging.getLogger('angr.surveyors.explorer').setLevel(logging.DEBUG)

    main()

你可能感兴趣的:(漏洞挖掘,物联网)