Angr实例分析——strcpy_find续

本博客由闲散白帽子胖胖鹏鹏胖胖鹏潜力所写,仅仅作为个人技术交流分享,不得用做商业用途。转载请注明出处,未经许可禁止将本博客内所有内容转载、商用。      

      上周看到Angr代码有些迷茫,因为自己的毕业设计需要使用到Angr所以才开了Angr源码分析这个坑,但是呢,我发现读源码的+官方API的方式学习效率太低了,思来想去还是从工程实际入手可能会快一点。后续更新可能没有之前那么快了(因为毕设的内容不好公开写到博客里,毕竟我要毕业嘛),但是我一旦找到想和大家分享的内容,会第一时间更新到博客里的。同时可能会穿插着找工作时候的算法题和代码(马上就要经历招聘的考验了)。

      我们这次接着谈一谈strcpy_find这个实例。前文我们用Angr对strcpy_find函数中的密码进行了求解,不知道大家再看程序代码的时候有没有注意到一点,在func函数中,如下图,是存在着缓冲区溢出漏洞的。


这里的a1就是我们传进来的字符串,没有经过任何长度检查,直接传给了strcpy。那么,同时,dest是栈上的变量,如果a1的长度大于dest在栈上预留的长度,那么将造成缓冲区溢出。我的目标就是让Angr能够检测出这个缓冲区溢出。

0x00 不成熟的尝试

      缓冲区溢出的根本问题,就是想一个小的内存空间(比如栈)中拷贝了一个大的数据(超长字符串)。这里涉及到两个变量,栈的大小和字符串的大小。字符串的大小是我们能够控制的,但是,请不要忽略程序进行的检查。我们首先来看栈上的大小,以strcpy_find函数为例,我们检查他的汇编代码。


rbp为栈底指针,4005F6处,rax=rbp - 0x30,而前面除了rbp没有其他参数入栈,所以我们知道这段缓冲区大小为48(0x30)字节,我们如果想影响程序的运行,就要覆盖返回地址,那么这个长度就是48(缓冲区长度)+8(rbp)+8(返回地址)= 64字节,但实际上超过56字节之后,返回地址被破坏,程序运行将出错。

      那这个长度是否能够通过Angr获得呢?答案是可以的,我们还是用上一篇博客里面的代码,当我们运行到strcpy的时候,符号执行停止,此时我们使用 

BOF_MinLength = sm.found[0].regs.rax - sm.found[0].regs.bp + 8

我们就获得了触发缓冲区的最小长度。

      接下来,我们需要获得程序运行过程中,用户输入字符串的约束条件。当然,在这个程序中我们是没有任何约束的。我自己写了一个程序,在func中增加了一个约束条件,同时我觉得密码有些繁琐,就将密码修改成了“sss”。代码就放一下。

#include <stdio.h>
#include <string.h>

int func(char *str){
    char dest[10];
    if (strlen(str) > 30)
        return printf("Your Message: too long \n");
    strcpy(dest,str);
    return printf("Your Message: %s\n",dest);
}

int func3(char *argv[]){
    return printf("Usage: %s <password> <message_to_store> \nNote: You can only post a custom message if you give the right password! \n",*argv);
}

int main(int argc, char *argv[]){
    char psswd[] = "sss";
    char ch;
    int len;
    char *wrong = "Wrong password";
    if (argc <= 2){
        func3(argv);
    }
    else{
        if (!strcmp(argv[1],psswd))
            func(argv[2]);
        else
            return 0;
            //func(wrong);
    }
    return 0;
}

      在我的代码里面,我将缓冲区设置成了10字节,并且当message长度超过30时,程序将退出。那么根据我们的经验,字符串长度超过18的时候就会溢出,18小于30。所以虽然对字符串长度进行了限制,我们还是能够触发缓冲区溢出的。依旧使用前面的代码,但是将argv[2]替换成符号变量,之后对其求解。代码如下:

import angr
import claripy
    
def main():
    proj = angr.Project('x',load_options={'auto_load_libs':False})

    addrStrcpy = 0x400500L
    addrFunc3 = 0x4006e3L

    argv = []
    argv.append(proj.filename)
    
    passwd_arg_size = 40
    passwd_arg = claripy.BVS('passwd_arg',8*passwd_arg_size)
    argv.append(passwd_arg)

    # use symbolic varaible as 3rd argument
    #third_arg = claripy.BVS('str',8*passwd_arg_size)
    third_arg = claripy.BVS('str',8*passwd_arg_size)
    argv.append(third_arg)
    #third_arg = "hird_argthird_argthird_argthird_argthird_argthird_argthird_argthird_arg"
   # argv.append(third_arg)

    state = proj.factory.entry_state(args=argv)
    sm = proj.factory.simulation_manager(state)

    def successState(state):
        if (state.ip.args[0] == addrStrcpy):
            return True

    sm.explore(find=successState, avoid=(addrFunc3,))
    found = sm.found

    if ( len(found) > 0):
        found = sm.found[0]
        result = found.solver.eval(argv[1], cast_to=str)
        try:
            result = result[:result.index('\0')]
        except ValueError:
            pass
    else:
        result = "couldn't find any satisfaied state"
    return result


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

代码里面没有管argv[2],我们手动进行求解。

长度还是40,看起来好像不满足我们的约束条件呢,反复测试+思考良久,还是无果。我们先来看看约束条件是不是对的。

使用命令提取约束条件

state.solver.constraines
 [<Bool (((if (passwd_arg_0_320[319:312] == 0) then 0x0 else (if (passwd_arg_0_320[311:304] == 0) then 0x1 else (if (passwd_arg_0_320[303:296] == 0) then 0x2 else (if (passwd_arg_0_320[295:288] == 0) then 0x3 else (if (passwd_arg_0_320[287:280] == 0) then 0x4 else (if (passwd_arg_0_320[279:272] == 0) then 0x5 else (if (passwd_arg_0_320[271:264] == 0) then 0x6 else (if (passwd_arg_0_320[263:256] == 0) then 0x7 else (if (passwd_arg_0_320[255:248] == 0) then 0x8 else (if (passwd_arg_0_320[247:240] == 0) then 0x9 else (if (passwd_arg_0_320[239:232] == 0) then 0xa else (if (passwd_arg_0_320[231:224] == 0) then 0xb else (if (passwd_arg_0_320[223:216] == 0) then 0xc else (if (passwd_arg_0_320[215:208] == 0) then 0xd else (if (passwd_arg_0_320[207:200] == 0) then 0xe else (if (passwd_arg_0_320[199:192] == 0) then 0xf else (if (passwd_arg_0_320[191:184] == 0) then 0x10 else (if (passwd_arg_0_320[183:176] == 0) then 0x11 else (if (passwd_arg_0_320[175:168] == 0) then 0x12 else (if (passwd_arg_0_320[167:160] == 0) then 0x13 else (if (passwd_arg_0_320[159:152] == 0) then 0x14 else (if (passwd_arg_0_320[151:144] == 0) then 0x15 else (if (passwd_arg_0_320[143:136] == 0) then 0x16 else (if (passwd_arg_0_320[135:128] == 0) then 0x17 else (if (passwd_arg_0_320[127:120] == 0) then 0x18 else (if (passwd_arg_0_320[119:112] == 0) then 0x19 else (if (passwd_arg_0_320[111:104] == 0) then 0x1a else (if (passwd_arg_0_320[103:96] == 0) then 0x1b else (if (passwd_arg_0_320[95:88] == 0) then 0x1c else (if (passwd_arg_0_320[87:80] == 0) then 0x1d else (if (passwd_arg_0_320[79:72] == 0) then 0x1e else (if (passwd_arg_0_320[71:64] == 0) then 0x1f else (if (passwd_arg_0_320[63:56] == 0) then 0x20 else (if (passwd_arg_0_320[55:48] == 0) then 0x21 else (if (passwd_arg_0_320[47:40] == 0) then 0x22 else (if (passwd_arg_0_320[39:32] == 0) then 0x23 else (if (passwd_arg_0_320[31:24] == 0) then 0x24 else (if (passwd_arg_0_320[23:16] == 0) then 0x25 else (if (passwd_arg_0_320[15:8] == 0) then 0x26 else (if (passwd_arg_0_320[7:0] == 0) then 0x27 else 0x28)))))))))))))))))))))))))))))))))))))))) + 0x7fffffffffeff8a) - 0x7fffffffffeff8a) == strlen_9_64>,
 <Bool And((0x3 == strlen_9_64), ((passwd_arg_0_320[319:312] == 115) || (strlen_9_64 < 0x0)), ((passwd_arg_0_320[311:304] == 115) || (strlen_9_64 < 0x1)), ((passwd_arg_0_320[303:296] == 115) || (strlen_9_64 < 0x2)), (strncmp_ret_10_64 == 0x0)) || (!And((0x3 == strlen_9_64), ((passwd_arg_0_320[319:312] == 115) || (strlen_9_64 < 0x0)), ((passwd_arg_0_320[311:304] == 115) || (strlen_9_64 < 0x1)), ((passwd_arg_0_320[303:296] == 115) || (strlen_9_64 < 0x2))) && (strncmp_ret_10_64 == 0x1))>,
 
 <Bool strncmp_ret_10_64[31:0] == 0x0>,
 
 <Bool (((if (str_1_320[319:312] == 0) then 0x0 else (if (str_1_320[311:304] == 0) then 0x1 else (if (str_1_320[303:296] == 0) then 0x2 else (if (str_1_320[295:288] == 0) then 0x3 else (if (str_1_320[287:280] == 0) then 0x4 else (if (str_1_320[279:272] == 0) then 0x5 else (if (str_1_320[271:264] == 0) then 0x6 else (if (str_1_320[263:256] == 0) then 0x7 else (if (str_1_320[255:248] == 0) then 0x8 else (if (str_1_320[247:240] == 0) then 0x9 else (if (str_1_320[239:232] == 0) then 0xa else (if (str_1_320[231:224] == 0) then 0xb else (if (str_1_320[223:216] == 0) then 0xc else (if (str_1_320[215:208] == 0) then 0xd else (if (str_1_320[207:200] == 0) then 0xe else (if (str_1_320[199:192] == 0) then 0xf else (if (str_1_320[191:184] == 0) then 0x10 else (if (str_1_320[183:176] == 0) then 0x11 else (if (str_1_320[175:168] == 0) then 0x12 else (if (str_1_320[167:160] == 0) then 0x13 else (if (str_1_320[159:152] == 0) then 0x14 else (if (str_1_320[151:144] == 0) then 0x15 else (if (str_1_320[143:136] == 0) then 0x16 else (if (str_1_320[135:128] == 0) then 0x17 else (if (str_1_320[127:120] == 0) then 0x18 else (if (str_1_320[119:112] == 0) then 0x19 else (if (str_1_320[111:104] == 0) then 0x1a else (if (str_1_320[103:96] == 0) then 0x1b else (if (str_1_320[95:88] == 0) then 0x1c else (if (str_1_320[87:80] == 0) then 0x1d else (if (str_1_320[79:72] == 0) then 0x1e else (if (str_1_320[71:64] == 0) then 0x1f else (if (str_1_320[63:56] == 0) then 0x20 else (if (str_1_320[55:48] == 0) then 0x21 else (if (str_1_320[47:40] == 0) then 0x22 else (if (str_1_320[39:32] == 0) then 0x23 else (if (str_1_320[31:24] == 0) then 0x24 else (if (str_1_320[23:16] == 0) then 0x25 else (if (str_1_320[15:8] == 0) then 0x26 else (if (str_1_320[7:0] == 0) then 0x27 else 0x28)))))))))))))))))))))))))))))))))))))))) + 0x7fffffffffeffb3) - 0x7fffffffffeffb3) == strlen_12_64>,
 <Bool strlen_12_64 <= 0x1e>]

条件贼拉长,我们关注后面两个条件,浓缩一下

 <Bool ((if (str_1_320[319:312] == 0) then 0x0 else ...+ 0x7fffffffffeffb3) - 0x7fffffffffeffb3) == strlen_12_64>
 <Bool strlen_12_64 <= 0x1e>
那么我们的输入是啥呢,BVS初始的时候里面都是0,所以if (str_1_320[319:312] == 0) 是成立的,那么这个括号里面的值就是0x0,这个方程就变成了  0x0 + 0x7fffffffffeffb3 - 0x7fffffffffeffb3 == strlen_12_64 = 0,底下的那个方程检查strlen_12_64 是否小于30。字符串长度为0,这就是我们得到40个0x00的原因。

再使用别的eval函数测试,eval_atleast会列举出至少n个可行的解。


我一口气生成了10个解,注意0x00的位置,我用红圈圈出来了。因为我们使用cast_to=str将解输出成字符串,而我们设置的BVS长度是固定的,所以我们实际解的长度应该以\0的位置为准(字符串结束符),这样一来我们就得到了长度为15、0、15、15的字符串,每个字符串的内容不一样,从约束条件(长度<30)来看,确实是符合约束的。为了验证,我将第三个参数替换成了50字节长度的固定字符串,运行之后sm.found 为空[]。再次验证。 

      如此看来,Angr是没有问题的,可是我们问题大了。我想要通过Angr动态执行,直接检测出缓冲区溢出的想法好像不太可行了。因为Angr会生成长度最小的可行解,而我们需要长度最大的可行解,以验证是否存在缓冲区溢出。如果采用穷举方法的话,别忘了Angr用的是bitvector,10个字节是80个bit,也就是2^80种答案,别想了。这里我可能需要看下求解引擎,暂时不知道支不支持这种求解方式。

0x01 来自Angr作者的解答

      我后来就这个程序问题问了问Angr作者。作者的答复是这样的。

"to clarify: detecting buffer overflow is very difficult in binaries, because oftentimes you know nothing about buffers, esp. their sizes when we are talking about finding buffer overflows, we are usually referring to finding a side effect of buffer overflow, i.e., return addresses on the stack being overwritten in angr, if you want to find such side effects during symbolic execution, you can set siminspect breakpoint on a state with a condition being, for example, the return address on the stack is overwritten by a symbolic value."

"presumably buffer1 is allocated before buffer2, so when you overflow buffer1 you overflow into buffer2, not onto the saved IP."

      这两段话的意思就是说在二进制文件里面检查缓冲区溢出漏洞是很困难的,因为不知道esp和栈上的情况。但是可以通过一些副作用推测缓冲区溢出了,比如返回地址被Angr的符号值所改变,可以通过增加siminspect断点的方式检查。同时缓冲区溢出发生时,有可能只是溢出到相邻的变量中,并不一定会溢出到返回地址。

0x02 一些想法

      经过以上的分析呢,一个缓冲区溢出的约束条件其实分为两部分,第一部分就是文件中提供的条件,第二部分是缓冲区长度(这个条件需要我们自己分析)。我知道了Angr求解的时候往往给出的都是实际值,而这个实际值是满足第一部分约束条件的。我们进行缓冲区溢出的目标是寻找到满足第一部分约束,但不满足第二部分约束的值,我想这部分符号执行应该很难完成吧。但是我还没想好具体使用什么方法,路还很长。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值