《CSAPP》是指计算机系统基础课程的经典教材《Computer Systems: A Programmer's Perspective》,由Randal E. Bryant和David R. O'Hallaron编写。该书的主要目标是帮助深入理解计算机系统的工作原理,包括硬件和软件的相互关系,其涵盖了计算机体系结构、汇编语言、操作系统、计算机网络等主题,旨在培养学生系统级编程和分析的能力。
target1实验通常与CS:APP书中的“Buffer Overflow Attack”相关。这个实验旨在教授计算机系统的安全性,防止攻击者定位攻击和锻炼使用金丝雀防护,特别是关于缓冲区溢出漏洞的理解和利用。在这个实验中,尝试利用缓冲区溢出漏洞来修改程序的执行流程,从而实现未授权的操作,比如执行恶意代码或获取系统权限。要求深入了解程序内存布局、堆栈和函数调用等概念,并学会利用输入缓冲区溢出漏洞来修改程序行为,这有助于理解系统安全中的一些基本原则和漏洞。
实验准备阶段:首先需要使用ubuntu联网环境跳转到链接下载实验所需的attacklab:attacklab源文件
下载target1压缩包并输入
tar –xvf target1.tar
进行解压缩,进入该目录所有文件如下所示:
当前提供材料包含一个攻击实验室实例的材料:
1.ctarget 带有代码注入漏洞的Linux二进制文件。用于作业的第1-3阶段。 2.rtarget 带有面向返回编程漏洞的Linux二进制文件。用于作业的第4-5阶段。 3.cookie.txt 包含此实验室实例所需的4字节签名的文本文件。(通过一些Phase需要用到的字符串) 4.farm.c rtarget实例中出现的gadget场的源代码。您可以编译(使用标志-Og)并反汇编它来查找gadget。 5.hex2raw 生成字节序列的实用程序。参见实验讲义中的文档。(Lab提供给我们的把16进制数转二进制字符串的程序)
在终端处输入命令
tar -xvf target1.tar
将压缩包解压如下:
图3-2
实验过程阶段:
使用
objdump -d ctarget > ctarget.asm
objdump -d rtarget > rtarget.asm
对ctarget以及rtarget进行反汇编,得到ctarget.asm和rtarget.asm。
在官方文档的目标程序给出,CTARGET和RTARGET都从标准输入读取字符串。它们使用下面定义的函数getbuf来执行此操作:
函数Gets类似于标准库函数gets—它从标准输入中(从缓冲区)读取字符串 (以’ \n '或文件结束符结束) 并将其(连同空结束符)存储在指定的目的地。即空格/Tab/回车可以写入数组文本文件,不算作字符元素, 不占字节,直到文件结束, 如果是命令行输入的话,直到回车结束(区别getchar ():是在输入缓冲区顺序读入一个字符 (包括空格、回车和 Tab)结束,scanf:空格/Tab/回车都当作结束。函数Gets()无法确定它们的目标缓冲区是否足够大,以存储它们读取的字符串。它们只是复制字节序列,可能会超出在目的地分配的存储边界(缓冲区溢出)对应汇编代码:
因为Ctarget就是让我们通过缓冲区溢出来达到实验目的,所以可以推断sub $0x28,%rsp的40个字节数就等于输入字符串的最大空间,如果大于40个字节,则发生缓冲区溢出(超过40个字节的部分作为函数返回地址,如果不是确切对应指令的地址,则会误入未知区域,报错:
Type string:Ouch!: You caused a segmentation fault!段错误,可能访问了未知额内存)
对于Level 1,您将不会注入新代码。相反,您的漏洞利用字符串将重定向程序以执行现有过程。函数getbuf由具有以下C代码的函数测试在CTARGET中调用:
当getbuf执行其返回语句(getbuf的第5行)时,程序通常会在函数测试中恢复执行(在该函数的第5行里)。我们想改变这种行为。在文件ctarget中,有一个函数touch1的代码,具有以下C表示:
任务是让CTARGET在getbuf执行其return语句时执行touch1的代码,而不是返回测试。请注意,您的漏洞利用字符串也可能损坏与此阶段没有直接关系的堆栈部分,但这不会导致问题,因为touch1会导致程序直接退出。
在输入了字符串后,需要经过touch1 函数部分(而不是执行test的返回语句),即缓冲区需要溢出,如果缓冲区不溢出,则在运行test函数后就结束了,不会经过touch1
touch1对应的汇编代码:
原理:一个函数在调用另外一个函数时,首先需要把下一条指令位置在栈上保存下来,然后再为另外一个函数提供新空间,当另一个函数结束时%rsp回到这个保存的位置 (与没有溢出的区别是,被调用的函数溢出时返回地址被改写)跳转到touch1的条件:
test()调用getbuf(),而getbuf()函数可以造成溢出,可以 溢出到存放返回地址的内存(touch1),并且可以把返回地址改写(改写为touch1的地址,若不溢出,则getbuf()的返回地址为test,即继续执行调用getbuf()后的下一条语句)。我们只要把touch1()函数的起始地址写入进getbuf()函数的返回地址,就可以完成。即test()->getbuf()->…->getbuf()->test()变成test()->getbuf()->…->getbuf()->touch1()。
当缓冲区输入达到40个字符时,再输入的字符如果是 0x4017c0(touch1的第一条指令位置),就会把之前函数保存的位置覆盖掉,那么当Gets函数结束后,就会跳转到touch1。
发生溢出的原因为输入的字符串中包括了写返回地址的字符串,所以大于40个字节;
不溢出的条件 为只输入40个字节的字符串,不写返回地址
但是经检验:当只输入40个字节,不写返回地址时:原本应该Type String:No exploit.Getbuf returned 0x%x\n,即继续执行调用getbuf()后的下一条printf语句,但是却得到Type String:Oops!: You executed an illegal instruction,原因尚未可知(猜想可能与本实验的设计有关,因为工具hex2raw是把16进制数转二进制字符串的程序,可能需要输入有效16进制数。
输入44个字节,显示错误:
解决:任意输入40个16进制数(相当未知额内存,不对应具体指令)和0x4017c0 (小端法),hex2rax将输入的16进制数转换为字符串,修改level1.txt文本如下:
输入./hex2raw < Hex1.txt | ./ctarget -q进行验证如下所示:
设计此级别的漏洞利用字符串所需的所有信息都可以通过检查反汇编版本的CTARGET来确定。使用objdump-d可以获得这个经过分解的版本。其想法是定位touch1起始地址的字节表示,以便getbuf代码末尾的ret指令将控制权转移到touch1。
第2阶段涉及注入少量代码作为漏洞利用字符串的一部分。
在文件ctarget中,存在用于具有以下C表示的函数touch2的代码:
任务是让CTARGET执行touch2的代码,而不是返回测试,且输入的字符串要与Cookie文件中的字符串相匹配。
根据文件中的信息,得知首先需要把字符串送到寄存器%rdi中,再进入touch2函数,即可通过Level2
所以,指令应该包括以下两个部分:
1.把字符串cookie mov进%rdi中 2.进入touch2
在vim中,编写 InjectCode1.s文件,编写汇编之后再反汇编,将得到的指令的机器码写入最后运行的十六进制(字符串)文件中即可。
查找函数touch2的地址为4017ec:
新建一个anwer1.s文件,输入内容如下:
其中,movq 0x59b997fa,%rdi是为了将cookie字符串存到%rdi中,pushq 0x4017ec是为了将touch2的地址压入栈中,而retq指令是为了将栈中值弹出,然后跳转到该地址。
创建完成后如下所示
输入命令:
gcc -c anwer1.s
会生成机器码文件anwer1.o, 输入命令:
objdump -d anwer1.o > anwer1.txt
进行反汇编,得到anwer1.o和anwer1.txt:
进入anwer1.txt得到反汇编结果如下:
其中,汇编指令对应的机器码如下:
48 c7 c7 fa 97 b9 59
68 ec 17 40 00
c3
接下来需要找到40个字符 开栈的位置(即调用getbuf()函数数据压入栈后栈顶指针%rsp的值),让getbuf()返回到这片代码区域(touch2的地址即最终返回地址)
查看函数getbuf的汇编代码:
起始地址为4017a8,所以需要在4017a8处设置断点(即还没运行4017a8),查看栈顶指针寄存器%rsp的值。
输入命令调试:
gdb ctarget
设置断点:
b *0x4017a8
r -q运行, p $rsp查看%rsp的值
得到%rsp此时为 0x5561dca0,根据开栈40字节推算:0x5561dca0-0x28 = 0x5561dc78,即40个字节的开栈位置为0x5561dc78,然后把操作指令放到该位置,让getbuf()返回到这片代码区域(touch2的地址即最终返回地址)。
输入命令:
vim lever2.txt
创建内容如下:
输入命令:
./hex2raw < level2.txt | ./ctarget -q
涉及代码注入攻击,但传递一个字符串作为参数。
在文件ctarget中,有用于函数hexmatch和touch3的代码,具有以下C表示:
任务是让CTARGET执行touch3的代码,而不是返回测试。必须让它看起来像touch3,就好像已经传递了cookie的字符串表示作为参数。
由文档可知,操作指令应包括:
1.让%rdi指向字符串cookie的起始地址。 2.跳转到touch3函数
字符串cookie应该用字节表示,输入命令man ascii查看所需字符的字节表示,0x59b997fa对应16进制为:35 39 62 39 39 37 66 61(不显示0x)。
文档中的advice中需要注意的是:
翻译为:当调用函数hexmatch和strncmp时,它们会将数据推送到堆栈上,从而覆盖内存中保存getbuf使用的缓冲区的部分。因此,您需要小心放置cookie的字符串表示的位置。
说明在Test3中会push数据进入堆栈,所以需要注意cookie字符串的存放位置,因为覆盖了保存getbuf使用的缓冲区的内存部分,所以可以不考虑把cookie字符串放到40个字符的堆栈里面,那40个字符用来存放命令后填满即可。
所以cookie字符串可以考虑放到get的栈帧中,即越过40个字符的上方,因为不再返回了,所以那部分就不会被触碰到
将cookie字符串存放在栈顶+8字节的位置(即cookie字符串的起始地址),再把这个 cookie字符串的起始地址存放进%rdi中。
在Level 2中已经得到栈顶指针%rsp的初始值为0x5561dca0,所以cookie字符串的起始地址为0x5561dca0;并查看,touch3函数的地址为4018fa。
新建anwer2.txt文件,内容如下所示:
其中mov 0x5561dxa8,%rdi是为了将cookie字符串存到%rdi中,pushq 0x4017fa是为了将touch2的地址压入栈中,而retq指令是为了将栈中值弹出,然后跳转到该地址。
创建完成后如下所示
输入命令:gcc -c anwer2.s会生成机器码文件anwer2.o, 输入命令:objdump -d anwer2.o > anwer2.txt进行反汇编。
得到anwer2.o和anwer2.txt
进入anwer2.txt得到反汇编结果如下:
其中,汇编指令对应的机器码如下:
0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
7: 68 fa 17 40 00 pushq $0x4017fa
c: c3 retq
接下来需要找到40个字符 开栈的位置(即调用getbuf()函数数据压入栈后栈顶指针%rsp的值),让getbuf()返回到这片代码区域(touch2的地址即最终返回地址)。
补充满40个字节,加上getbuf()的返回地址0x5561dc78和cookie字符串的十六进制值,新建level3.txt建立内容如下:
输入命令进行验证:./hex2raw < level3.txt | ./ctarget -q,显示结果为PASS:
解决完level1-level3后,进入到第二部分:面向返回的编程。对程序RTARGET执行代码注入攻击比CTARGET要困难得多,因为它使用两种技术来阻止这种攻击:
•使用随机化,以便堆栈位置在不同的运行中不同。这使得无法确定注入的代码将位于何处。 •将保存堆栈的内存部分标记为不可执行,因此即使将程序计数器设置为注入代码的开头,程序也会因分段错误而失败。
通过执行现有代码,而不是注入新代码,在程序中完成有用的事情。这种最通用的形式被称为面向返回编程(ROP)[1,2]。ROP的策略是识别现有程序中的字节序列,该序列由一条或多条指令组成,后面跟着指令ret.这样的段被称为gadget. 即Part II和PartI I的区别是:这里用栈随机性和禁止栈中使用命令:栈随机性导致栈的位置不再固定,也导致我们不能像Part I一样,运行命令直接用栈中的确切位置就返回;禁止栈中使用命令为如果我们的命令是在栈中的,即%rip(程序计数器)指向栈,则会报错(段错误)。
该图表示需要设置要执行的gadget序列,字节值0xc3对ret指令进行编码。说明了如何设置堆栈以执行一系列n个gadget。图中,堆栈包含一系列gadget地址。每个gadget都由一系列指令字节组成,最后一个字节是0xc3,用于编码ret指令。当程序从该配置开始执行ret指令时,它将启动一系列gadget执行,每个gadget末尾的ret指令会导致程序跳到下一个gadget的开头。gadget可以使用与编译器生成的汇编语言语句相对应的代码,尤其是函数末尾的代码。在实践中,可能有一些这种形式的有用gadget,但不足以实现许多重要的操作。例如,编译后的函数不太可能在返回之前将popq%rdi作为其最后一条指令。幸运的是,对于面向字节的指令集,如x86-64,通常可以通过从指令字节序列的其他部分提取模式来找到gadget。
例如,rtarget的一个版本包含为以下C函数生成的代码:
这个功能对攻击系统有用的可能性似乎很小。但是,这个函数的反汇编机器代码显示了一个有趣的字节序列:
字节序列48 89 c7对指令movq%rax,%rdi进行编码。此序列后面是字节值c3,它对ret指令进行编码。函数从地址0x400f15开始,序列从函数的第四个字节开始。因此,此代码包含一个gadget,其起始地址为0x400f18,它将把寄存器%rax中的64位值复制到寄存器%rdi。
RTARGET代码包含许多类似于上面显示的setval_210函数的函数,这些函数位于称为gadget farm的区域中(注意: 重要提示:gadget farm由rtarget副本中的函数start_farm和end_farm划分,不要试图从程序代码的其他部分构造gadget)。工作将是在gadget farm中识别有用的gadget,并使用这些gadget执行类似于第2阶段和第3阶段的攻击。
这一关需要完成的部分还是touch2,只不过是rtarget部分。对于第4阶段,将重复第2阶段的攻击,但使用gadget farm中的gadget对程序RTARGET进行攻击。可以使用由以下指令类型组成的gadget构建解决方案,并且只使用前八个x86-64寄存器(%rax–%rdi)。
1.movq:将数据从一个位置复制到另一个位置。 2.popq:把数据弹出栈。 3.ret:此指令由单字节0xc3编码。 4.nop:此指令(发音为“no-op”,是“no-operation”的缩写)由单字节0x90编码。它唯一的作用是使程序计数器增加1。
在第一部分的Level 2中提到解法为:
定位需要注入的函数touch2的地址的字节表示,以便在getbuf的代码末尾的ret指令将控制权传递给它。第一个参数是在寄存器%rdi中传递的。注入的代码应该先将cookie保存在寄存器%rdi中,然后在使用ret指令将控制权传递给touch2。所以首先需要popq %rdi,把cookie存放到%rdi中,然后再利用retq返回到touch2。
为了完成上面的过程,需要查找工具部分机器码能不能直接提供相应的功能,如果不行,则需要分几个步骤来完成。
在反汇编文件rtarget.asm中查看farm部分汇编代码:
通过搜索48 89 (mov指令),还有对于popq对应的机器码,其中 0xc3 = retq ,0x90 = nop, 找到了两个对这个实验有用的指令且有效的指令(不唯一):popq %rax 58 0x4019a7和movq %rax,%rdi 0x4019c3
新建anwer3.s文件,内容如下所示:
popq %rax (%rax = 0x59b997fa) (location: 0x4019ab) retq (jmp location: 0x4019c5) movq %rax,%rdi (location: 0x4019c5) retq (jmp location: touch2 4017ec)
其中popq %rax (%rax = 0x59b997fa) (location: 0x4019ab)是为了将栈元素存放到%rax中,而retq (jmp location: 0x4019c5)指令是为了将栈中值弹出,然后跳转到地址0x4019c5;movq %rax,%rdi (location: 0x4019c5)是为了将%rax数据取出存放至%rdi,而retq (jmp location: touch2 4017ec)指令是为了将栈中值弹出,然后跳转到地址touch2 4017ec。
新建level4.txt建立内容如下:
命令进行验证:./hex2raw < level4.txt | ./rtarget -q,显示结果为PASS(需要注意的是,这里的指向件应该为rtarget而非ctarget,否则显示仍然为Fail):
阶段5要求对RTARGET进行ROP攻击,以使用指向cookie的字符串表示的指针调用函数touch3。
在第二阶段和第三阶段,已经解决了让一个程序执行自行设计的机器代码。如果CTARGET是一个网络服务器,则可以将自己的代码注入到远处的机器中。第四阶段绕过了现代系统用来阻止缓冲区溢出攻击的两个主要设备。虽然没有注入自己的代码,但可以注入一种通过将现有代码序列拼接在一起来操作的程序类型。这一阶段要求对 RTARGET 进行 ROP 攻击,以使用指向 cookie 字符串表示的指针调用函数 touch3。
要解决阶段5,可以在rtarget中由函数start_farm和end_farm划分的代码区域中使用小工具。除了在阶段4中使用的小工具,这个扩展的场还包括不同的movl指令的编码。这部分场中的字节序列也包含2字节指令,它们作为nops函数,也就是说,它们不改变任何寄存器或内存值。包括指令如andb %al,%al,它们对一些寄存器的低阶字节进行操作,但不改变它们的值。
因为有了栈随机性,我们不能指定指针确切位置了,但是可以通过 相对位置 + 栈顶的位置,先获取到栈顶的位置,然后加上我们放置距离栈顶的相对位置,得到cookie字符串起始地址放置的位置,推导如下:
1.需要 获取栈顶的位置,查看Source = %rsp的mov指令机器码,查找与之相关联的寄存器。 2.发现movq %rsp,%rax可用,则先找movq %rax,%rdi,因为最终需要%rdi 来存放 字符串的指针开始地址,因为lea命令可以起到add和mov的作用,lea (%rdi,%rsi,1),%rax 中%rsi可以作 相对位置偏移的量,这个偏移量由最终栈顶离字符串的相对偏移位置来确定,%rdi里面存%rsp刚开始的栈顶位置,由上一步movq %rsp,%rax movq %rax,%rdi得到,最后的偏移位置在%rax中,最后只需要movq %rax,%rdi即可达到目的。 3.需要把偏移量放到%rsi中,因为没有pop %rsi,只能逆向思维,找与mov %rsi相关的指令。 4.在farm部分只找到movl %ecx,$esi ,需要把%eax与%ecx联系起来,因为只有%rax可以pop(可以指定值),所以逆向寻找movl %ecx,得到逆向顺序:%eax -> %edx -> %ecx -> %esi。
新建anwer4.s文件,内容如下所示:
movq %rsp,%rax (location:0x401a06)
movq %rax,%rdi (location: 0x4019c5)
popq %rax (location:0x4019cc)
movq %eax,%edx (location:0x4019dd)
movq %edx,%ecx (location: 0x401a69)
movq %ecx,%esi (location:0x401a13)
lea %rdi + %rsi* 1, %rax (location:0x4019d6)
movq %rax,%rdi (location:0x4019c5)
retq (jmp location: touch3 4018fa)
其中movq %rsp,%rax (location:0x401a06)是为了将%rsp元素存放到%rax中,接着使用movq %rax,%rdi (location:0x4019c5)将%rax数据存放到%rdi中,利用popq %rax (location:0x4019cc) 将栈元素存放到%rax中,接着连着使用movq 是为了将%eax数据依此存放到%edx、%ecx和%esi。
关于lea指令在intel开发手册上的官方解释见下。
官方解释Load Effective Address,即装入有效地址的意思,它的操作数就是地址;因此lea %rdi + %rsi* 1, %rax (location:0x4019d6)含义是将%rdi与%rsi存储的数据相加,并将结果存放到%rax中。而movq %rax,%rdi (location:0x4019c5)指令是为了将%rax值存放到%rdi,最后的retq (jmp location: touch3 4018fa)指令是为了将栈中值弹出,然后跳转到地址touch3 4018fa。
新建level5.txt建立内容如下:
输入命令进行验证:
./hex2raw < level5.txt | ./rtarget -q
显示结果为PASS(需要注意的是,这里的指向文件应该为rtarget而非ctarget,否则显示仍然为Fail):
由于实验通关过程中是分阶段的,故展示通关过程中所需的创建文件如下:
Attack Lab实验让我学会了如何使用金丝雀进行防护,掌握了如何在程序中插入金丝雀,并且学会了如何利用金丝雀来防护程序免受攻击。通过这些操作,可以更好地保护计算机系统的安全,避免被攻击者利用漏洞进行攻击。作为安全机制之一的金丝雀可以在程序中插入一些随机值,从而防止攻击者通过定位攻击来破坏程序。
通过完成实验,我学会了如何分析和解决程序中的安全漏洞。这些能力对于我今后的计算机安全学习和工作都有很大的帮助。此外还学习了很多其他的知识,例如栈溢出攻击的原理和防御方法,如何分析程序中的汇编代码等。同时也让我认识到了计算机安全的重要性,如果系统存在安全漏洞,则攻击者可以利用这些漏洞进行攻击,甚至可以窃取敏感信息、破坏系统等。