用angr找到strcpy的栈溢出漏洞

这篇文章其实是分析angr文档里给出的一个挖漏洞的小例子。

原文链接在此:https://docs.angr.io/examples#beginner-vulnerability-discovery-example-strcpy_find

这是第一个用angr在二进制里找可利用条件的教程。第一个例子是一个非常简单的程序。这个angr脚本会从程序的entry到strcpy找到一条路径。(只有我们能够控制strcpy的参数的时候)

为了找到正确的路径,angr需要去算一个密码参数,算这个密码,angr只要2s。脚本可能看起来很多,但那是因为有很多注释。

二进制程序链接:https://github.com/angr/angr-doc/tree/master/examples/strcpy_find/strcpy_test

脚本链接:https://github.com/angr/angr-doc/tree/master/examples/strcpy_find/solve.py

首先用ida分析下目标二进制看看。反编译main函数。可以看到如果参数小于2,会进入func3函数。否则是比较一下密码,如果密码正确则执行func函数。

image-20210629143839810

再看看func3函数,可以知道是类似打印帮助信息的功能。

image-20210629145433991

接着看看func函数。会发现这里有个strcpy函数,并且strcpy的源操作数由一个函数参数a1控制。而这个a1在main函数里则是argv[2],也就是用户可以控制的输入。如果输入一个很长的字符串,那么就会发生栈溢出。

image-20210629145512813

接下来要分析的angr脚本,就是要去找到能够触发这个漏洞的输入。运行脚本,可以知道password的值。

image-20210629145942648

然后,运行目标二进制,并把之前脚本得到的密码用作输入,可以发现出现了段错误。

image-20210629150118317

接下来就来分析脚本是如何找到可以执行到漏洞的输入。完整脚本见angr-doc的github:https://github.com/angr/angr-doc/tree/master/examples/strcpy_find/solve.py

首先是一个helper 函数,这是用来从符号表找到函数的地址。

def getFuncAddress( funcName, plt=None ):
        found = [
            addr for addr,func in cfg.kb.functions.items()
            if funcName == func.name and (plt is None or func.is_plt == plt)
            ]
        if len( found ) > 0:
            print("Found "+funcName+"'s address at "+hex(found[0])+"!")
            return found[0]
        else:
            raise Exception("No address found for function : "+funcName)

获取某个字节的数据,好像这个脚本并没有用到这个函数,可以不管。

def get_byte(s, i):
        pos = s.size() // 8 - 1 - i
        return s[pos * 8 + 7 : pos * 8]

加载二进制,不需要加载额外的库

project = angr.Project("strcpy_test", load_options={'auto_load_libs':False})

构造CFG图,然后我们可以从符号表获取去函数地址。设置fail_fast选项为True来最小化这个过程需要的时间。

cfg = project.analyses.CFG(fail_fast=True)

获取strcpy以及bad函数的地址

addrStrcpy = getFuncAddress('strcpy', plt=True)
addrBadFunc = getFuncAddress('func3')

创建命令行参数列表,并且将程序名加入

argv = [project.filename]   #argv[0]

为密码buffer添加一个符号化变量,用来后续求解

sym_arg_size = 40   #max number of bytes we'll try to solve for

使用8*sym_arg_size来作为符号变量的size参数,是因为他是bit不是bytes。

sym_arg = claripy.BVS('sym_arg', 8*sym_arg_size)
argv.append(sym_arg)    #argv[1]

添加信息buffer,后续执行到strcpy漏洞点的时候,可以用这个值来确认,是否到达漏洞点了。

argv.append("HAHAHAHA") # argv[2]

初始化程序的entry_state

state = project.factory.entry_state(args=argv)

基于entry state创建个simulation manager

sm = project.factory.simulation_manager(state)

当我们能够控制src buffer的时候,我们想要找到一条路径到strcpy,所以我们需要一个check函数,来把路径作为参数。

也许你会想知道,我们该做什么才能让angr去找到我们的目标地址的路径。因为我们把find=参数用这个check函数代替了。

在检查其他条件满足前,先检查p.state.ip.args[0](当前的指令指针),确保我们在我们想要的目标路径上。

check函数的逻辑:

  • 检查指令指针是否在strcpy的地址,是的话,就继续
  • 从内存加载rsi的值,加载出来的是BV对象,所以需要使用solver的eval转换成python的字符串(byte类型)。
  • 字符串.encode()可以把字符串变成byte类型。
  • 如果strcpy的源参数和我们argv是一样的,说明这个漏洞实锤了,也就返回True。
 def check(state):
        if (state.ip.args[0] == addrStrcpy):    # Ensure that we're at strcpy
            '''
             By looking at the disassembly, I've found that the pointer to the
             source buffer given to strcpy() is kept in RSI.  Here, we dereference
             the pointer in RSI and grab 8 bytes (len("HAHAHAHA")) from that buffer.
            '''
            BV_strCpySrc = state.memory.load( state.regs.rsi, len(argv[2]) )
            '''
             Now that we have the contents of the source buffer in the form of a bit
             vector, we grab its string representation using the current state's
             solver engine's function "eval" with cast_to set to str so we get a python string.
            '''
            strCpySrc = state.solver.eval( BV_strCpySrc , cast_to=bytes )
            '''
             Now we simply return True (found path) if we've found a path to strcpy
             where we control the source buffer, or False (keep looking for paths) if we
             don't control the source buffer
            '''
            return True if argv[2].encode() in strCpySrc else False
        else:
            '''
             If we aren't in the strcpy function, we need to tell angr to keep looking
             for new paths.
            '''
            return False

使用explore接口去找到满足check function的路径。如果为find或者avoid指定一个元组/列表/集合,它就会将其翻译成地址给find/avoid。如果是给一个函数,他会把状态传递给这个函数,然后看函数返回的是True还是False。就像上面的check函数。

这里,我们告诉explore去找到满足check方法的路径,并且避免任何以addrBadfunc结尾的路径。

sm = sm.explore(find=check, avoid=(addrBadFunc,))

found = sm.found

从找到的路径里提取出password的具体值。如果你把这个密码放在程序的第一个参数里,你就应该能够去strcpy任何字符串到目标buffer中去。如果字符串太大的话,就可以导致段错误。

if len(found) > 0:    #   Make sure we found a path before giving the solution
        found = sm.found[0]
        result = found.solver.eval(argv[1], cast_to=bytes)
        try:
            result = result[:result.index(b'\0')]
        except ValueError:
            pass
    else:   # Aww somehow we didn't find a path.  Time to work on that check() function!
        result = "Couldn't find any paths which satisfied our conditions."
    return result

然后是测试函数。

def test():
    output = main()
    target = b"Totally not the password..."
    assert output[:len(target)] == target

main函数输出password

if __name__ == "__main__":
    print('The password is "%s"' % main())

最后总结下这个angr脚本的流程。首先导入二进制,构建project对象。由project对象生成CFG。接着利用CFG里的函数管理器,找到strcpy的地址。确定符号执行探索的终点和需要避免的状态。

另外一边,将要求解的参数符号化,同时利用符号化的参数初始化entry_state。然后用entry_state实例化一个simulation manager对象sm。

simulation manager对象有了起点状态,终点以及avoid之后,就可以explore下个状态。每次到一个状态的时候,都要过一遍check函数,检查状态是否到了终点。

最后,如果sm.found有结果,就求解符号变量。求出来的值,就是能够到达漏洞点的输入。

image-20210629155451332

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破落之实

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值