type
date
slug
summary
status
tags
category
password
Last edited time
Apr 25, 2025 10:09 AM
icon
系WHU2021级《系统级程序设计》实验3,节选于CSAPP Lab。CMU实验指导原文PDF

引言

1. 实验步骤

1.1 第一步:获取文件

在远程桌面中用浏览器访问网页:http://172.16.2.207:15513或者双击桌面上的Attack Lab Download Page快捷方式,输入你的学号和email地址,得到targetXXXX.tar文件。解压targetXXXX.tar文件(tar -xvf targetXXXX.tar)得到一个目录./targetXXXX,其中包含如下文件:
  • README.txt:描述本目录内容的文件。
  • ctarget:一个容易遭受code-injection攻击的可执行程序。
  • rtarget:一个容易遭受return-oriented-programming攻击的可执行程序。
  • cookie.txt:一个8位的十六进制码,是你的唯一标识符,用于验证身份。
  • farm.c:你的目标“gadget farm”的源代码,在产生return-oriented programming攻击时会用到。
  • hex2raw:一个生成攻击字符串的工具。

1.2 要点说明

要在我们提供的实验平台上完成该实验,我们不保证在其他平台上作出的结果能在我们的验证平台上成功执行。
你的解答不能绕开程序中的验证代码。也就是说,ret指令使用的攻击字符串中注入的地址必须是一下几种之一:
  • 函数touch1,touch2或touch3的地址
  • 你注入的代码的地址
  • gadget farm中gadget的地址
只能从文件rtarget中地址范围在函数start_farm和end_farm之间的地址构造gadget。

1.3 提交成绩

由于不同班要求不同,不用点击“提交评测”,以scoreboard分数为准
务必在所有实验内容完成之后(三关共五阶段),点击“提交评测”提交成绩到希冀平台!否则系统无法记录分数。

2. 目标程序

CTARGET和RTARGET都是用getbuf函数从标准输入读入字符串,getbuf函数定义如下:
函数Gets类似于标准库函数gets,从标准输入读入一个字符串(以\n或者end-of-file结束),将字符串(带null结束符)存储在指定的目的地址
从这段代码可以看出,目标地址是数组buf,声明为BUFFER_SIZE个字节。BUFFER_SIZE是一个编译时常量,在你的target程序生成时就具体确定了。
 
函数Gets()gets()都无法确定目标缓冲区是否够大,能够存储下读入的字符串。它们都只会简单地拷贝字节序列,可能会超出目标地址处分配的存储空间的边界。
如果用户输入和getbuf读入的字符串足够短,getbuf会返回1,如下执行示例所示:
notion image
如果你输入的字符串很长,就会出错:
notion image
(注意cookie的值会每个人有所不同。)
 
RTARGET程序有类似的行为。正如错误消息提示的那样,超出缓冲区大小通常会导致程序状态被破坏,引起内存访问错误。你的任务是巧妙的设计输入给CTARGET和RTARGET的字符串,让它们做些更有趣的事情。这样的字符串称为攻击(exploit)字符串。
CTARGET和RTARGET有这样一些命令行参数:
  • -h:输出可能的命令行参数列表
  • -q:不向打分服务器发送结果
  • -i FILE:输入来自于文件FILE而不是标准输入
一般来说,你的攻击字符串包含的字节值并不都对应着能够打印出来的字符的ASCII值,因此建议使用文件输入,在文件中直接写字节值。
HEX2RAW程序的使用见附录A。
⚠️要点说明:
  • 你的攻击字符串不能包含字节值0x0a,这是换行符(’\n’)的ASCII代码。Gets遇到这个字节时会认为你意在结束该字符串。
  • HEX2RAW要求输入的十六进制值必须是两位的,值与值之间以一个或多个空白分隔。如果你想得到一个十六进制值为0的字节,必须输入00。要得到字0xdeadbeef,必须向HEX2RAW输入“ef be ad de”(注意顺序相反是因为使用的是小端法字节序)。
本实验分为五个阶段,CTARGET的三个使用的是CI(code-injection),RTARGET的两个阶段使用的是ROP(return-oriented-programming),如图1所示。
阶段
程序
关数
方法
函数
分数
1
CTARGET
1
CI
touch1
10
2
CTARGET
2
CI
touch2
20
3
CTARGET
3
CI
touch3
20
4
RTARGET
2
ROP
touch2
35
5
RTARGET
3
ROP
touch3
15
图1. attack lab阶段小结

3. 实验内容第一部分:代码注入攻击

三个阶段,你的攻击字符串会攻击CTARGET程序。程序被设置成栈的位置每次执行都一样,这样一来栈上的数据就可以等效于可执行代码。这使得程序更容易遭受包含可执行代码字节编码的攻击字符串的攻击。

3.1 第一关

在这一关中,你不用注入新的代码,你的攻击字符串要指引程序去执行一个已经存在的函数。
CTARGET中函数test调用了函数getbuftest的代码如下:
getbuf执行返回语句时(getbuf的第5行),按照规则,程序会继续执行test函数中的语句,而我们想改变这个行为。
在文件ctarget中,函数touch1的代码如下:
你的任务是让CTARGET在getbuf执行返回语句后执行touch1的代码。注意,你的攻击字符串可以破坏栈中不直接和本阶段相关的部分,这不会造成问题,因为touch1会使得程序直接退出。
⚠️要点说明:
  • 设计本阶段的攻击字符串所需的信息都从检查CTARGET的反汇编代码中获得。用objdump -d进行反汇编。
  • 主要思路是找到touch1的起始地址的字节表示的位置,使得getbuf结尾处的ret指令会将控制转移到touch1。
  • 注意字节顺序。
  • 可能需要用GDB单步跟踪调试getbuf的最后几条指令,确保它按照你期望的方式工作。
  • buf在getbuf栈帧中的位置取决于编译时常数BUFFER_SIZE的值,以及GCC使用的分配策略。你需要检查反汇编带来来确定它的位置。

3.2 第二关

第二关中,你需要在攻击字符串中注入少量代码。
在ctarget文件中,函数touch2的代码如下:
你的任务是使CTARGET执行touch2的代码而不是返回到test。在这个例子中,你必须让touch2以为它收到的参数是你的cookie。
💡建议:
  • 需要确定你注入代码的地址的字节表示的位置,使getbuf代码最后的ret指令会将控制转移到那里。
  • 注意,函数的第一个参数是放在寄存器%rdi中的。
  • 你注入的代码必须将寄存器的值设定为你的cookie,然后利用ret指令将控制转移到touch2的第一条指令。
  • 不要在攻击代码中使用jmp或call指令。所有的控制转移都要使用ret指令,即使实际上你并不是要从一个函数调用返回。
  • 参见附录B学习如何生成指令序列的字节级表示。

3.3 第三关

第三阶段还是代码注入攻击,但是是要传递字符串作为参数。
ctarget文件中函数hexmatch和touch3的C代码如下:
你的任务是让CTARGET执行touch3而不要返回到test。要使touch3以为你传递你的cookie的字符串表示作为它的参数。
💡建议:
  • 你的攻击字符串中要包含你的cookie的字符串表示。这个字符串由8个十六进制数字组成(顺序是从最高位到最低位),开头没有“0x”。
  • 注意,C中的字符串表示是一个字节序列,最后跟一个值为0的字节。“man ascii”能够找到你需要的字符的字节表示。
  • 你的注入代码应该将寄存器%rdi设置为攻击字符串的地址。
  • 调用hexmatch和strncmp函数时,会将数据压入栈中,覆盖getbuf使用的缓冲区的内存,你需要很小心把你的cookie字符串表示放在哪里。

4. 实验内容第二部分:面向返回的编程

对程序RTARGET进行代码注入攻击要难一些,它采用了以下两种技术对抗攻击:
  • 采用了随机化,每次运行栈的位置都不同。所以无法决定你的注入代码应该放在哪里。
  • 将保存栈的内存区域设置为不可执行,所以即使你能把注入的代码的起始地址放到程序计数器中,程序也会报段错误失败。
幸运的是,聪明的人们设计了一些策略,通过执行现有程序中的代码来做他们期望的事情,而不是注入新的代码。这种方法称为面向返回的编程(ROP)
例如,rtarget可能包含如下代码:
这个函数不太可能会攻击到一个系统,但是这段代码反汇编出来的机器代码是:
字节序列48 49 c7是指令movq %rax, %rdi的编码。图2A展示了一些有用的movq指令的编码。你的RTARGET的攻击代码由一组类似于setval_210的函数组成,我们称为gadget farm。你的工作是从gadget farm中挑选出有用的gadget执行类似于前述第二关和第三关的攻击。
⚠️要点说明:
  • 函数start_farm和end_farm之间的所有函数构成了你的gadget farm。不要用程序代码中的其他部分作为你的gadget。

4.1 第二关

在第四阶段,你将重复第二阶段的攻击,不过要使用gadget farm里的gadget来攻击RTARGET程序。你的答案只使用如下指令类型的gadget,也只能使用前八个x86-64寄存器(%rax-%rdi)。
  • movq:代码如图2A所示。
  • popq;代码如图2B所示。
  • ret:该指令编码为0xc3。
  • nop:该指令编码为0x90。
💡建议:
  • 只能用两个gadget来实现该次攻击。
  • 如果一个gadget使用了popq指令,那么它会从栈中弹出数据。这样一来,你的攻击代码能既包含gadget的地址也包含数据。
notion image
图2. 指令的字节编码。所有的值均为十六进制。

4.2 第三关

阶段五要求你对RTARGET程序进行ROP攻击,用指向你的cookie字符串的指针,使程序调用touch3函数。
这一关,允许你使用函数start_farm和end_farm之间的所有gadget。除了第四阶段允许的那些指令,还允许使用movl指令(如图2C所示),以及图2D中的2字节指令,它们可以作为有功能的nop,不改变任何寄存器或内存的值,例如,andb %al, %al,这些指令对寄存器的低位字节做操作,但是不改变它们的值。
💡提示:
  • 官方答案需要8个gadgets。

附录A HEX2RAW的使用

HEX2RAW的输入是一个十六进制格式的字符串,用两个十六进制数字表示一个字节值。例如,字符串“012345”,必须输入“30 31 32 33 34 35 00”。十六进制字符之间以空白符(空格或新行)分隔。
可以把攻击字符串存入文件中,例如exploit.txt,以下列几种方式调用:
  1.  
        这种方法也可以结合gdb使用:
        这种方法也可以和gdb一起使用。

    附录B 生成字节代码

    假设编写一个汇编文件example.s,代码如下:
    可以汇编和反汇编文件:
    生成的example.d包含如下内容:
    由此可以推出这段代码的字节序列:
    68 ef cd ab 00 48 83 c0 11 89 c2
    可以通过HEX2RAW生成目标程序的输入字符串。也可以手动修改example.d的代码,得到下面的内容:
    这也是合法的HEX2RAW的输入。

    概述

    基本上跟着Instruction一步步走就好
    在解题之前,先将CTARGETRTARGET进行反汇编——执行objdump -d target > target_disas命令。
    📌
    通过测试发现,输入的字符串会转化为对应的ASCII码,从栈顶开始一个个存进去。 但并不是所有的字节值都对应着能够打印出来的字符的ASCII值,因此使用文件输入——在文件exploit.txt中直接写字节值,通过HEX2RAW生成作为CTARGET输入的exploit-raw.txt
     
    在获取汇编指令对应的字节值时,执行以下命令:
    之后打开exploit_command.d文件即可,注意获得的字节值应直接放入exploit.txt,不需要调转顺序

    Part I : Code Injection Attacks

    在第一部分,攻击字符串会攻击CTARGET程序,而程序的每次执行栈都在同一个位置,于是栈上的数据可等效于可执行代码。这使得程序更容易遭受包含可执行代码字节编码的攻击字符串的攻击。
    📌
    基本操作:在调用函数Gets时打断点,观察读取前后的栈结构变化

    Phase1 Level 1

    任务

    CTARGET中函数test调用了函数getbuf,在getbuf返回之后,原本会继续执行函数test剩下的打印语句,但我们需要让其转而执行函数touch1
    相关的代码如下:

    思路

    找到touch1的起始地址的字节表示的位置,使得getbuf结尾处的ret指令将控制转移到touch1
    具体而言,由于函数Gets不会检查输入长度,我们有机会通过“输入溢出”来改写栈中意外的地方,而在这里“意外的地方”就是getbuf函数返回时的跳转地址,而究竟怎么写需要根据buf[BUFFER_SIZE]的地址(这是函数Get接收输入并写入的地方)。
    查看CTARGET的反汇编文件,可以找到这三个函数的汇编代码:
    getbuf
    touch1
    test
    可知,touch1函数的起始地址为0x401778,而调用Gets前分配的栈空间大小为56个字节。
    接下来,通过gdb查看调用Gets前的栈空间:
    notion image
    其中0x4018eatestgetbuf的返回地址,也是我们希望更改的地方。
    在这里,我定位返回地址的方法是——从前往后数7*8个字节,发现最后8个字节在0x55654938处,那么0x55654940的8个字节必然是返回地址,其值为0x4018ea

    栈结构

    getbuf函数执行ret返回后的栈结构如下:
    notion image
    其中蓝色底的格子为函数getbuf返回地址
    💡
    返回地址应该存在调用者test()栈桢的顶层

    解决

    因此,攻击字符串的结果为:
    查看调用Gets后的栈空间
    notion image
    这里我们的攻击字符串破坏了栈中不直接和本阶段相关的部分,即0x5558600,但并不会造成问题,因为touch1会使得程序直接退出。
    执行后任务完成。
    notion image

    Phase 2 Level 2

    任务

    Level 1类似,我们需要让程序执行touch2而不是返回test
    但这次我们需要在攻击字符串中加入少许代码,使得调用touch2时参数为我的cookie(ungisned类型)。
    touch2(C语言)
    touch2(反汇编)

    思路

    根据level1中查看的栈情况,我们可以在buf数组(即栈中的空白区)写入攻击代码,再通过“输入溢出“,修改函数跳转后的地址为攻击代码的首地址。
    其中,攻击代码也需要一次ret跳转,于是需要在攻击代码中压栈touch2的首地址。
    已知函数第一个参数存放在寄存器%rdi,而题目要求任何跳转都只能使用ret指令。
    查看CTARGET文件,可知touch2的首地址为0x4017a4
    于是编写攻击代码如下:
    💡
    注意,指令ret <=> pop %rip,跳转的指令地址是需要从栈中弹出来的,需要在进行ret刚好处在栈顶,在这里需要进行pushq操作,而不能简单地在追加到攻击字符串的后面。
    错误的攻击代码和字符串:

    栈结构

    getbuf函数执行ret返回后的栈结构如下:
    notion image
    注意,&(touch2)是执行攻击代码后压栈的,之后rsp将下移8bytes。
    具体而言,getbuf函数返回后将跳转0x55654908执行攻击代码,将&(touch2)压栈;之后执行ret命令,栈弹出&(touch2),跳转执行函数touch2
    💡
    需要注意,如果地址中包含有字节值0x0a,函数Gets读取到后会识别为换行符,导致后面的字符串全都丢失; 为了解决这个问题,我们需要将其转化为两个步骤——先赋值0x9,再通过指令inc加一得到0xa,如:

    解决

    将攻击代码写入文件two.s,之后执行以下命令:
    可以得到攻击代码的字节值:
    notion image
    于是攻击字符串为:
    查看调用Gets前后的栈结构对比如下:
    notion image
    notion image
    紧跟着往下执行,可以看到程序成功跳转到了攻击代码的首地址,且成功更改寄存器%rdi的值
    notion image
    继续执行,任务完成。
    notion image

    Phase 3 Level 3

    任务

    同样是代码注入攻击,要让程序执行touch3而不是返回到test,且执行touch3时传入的参数须为我的cookie(char*字符串类型)。
    hexmatch(C语言)
    touch3(C语言)
    touch3(反汇编)

    思路

    思路依旧是通过函数Gets向(getbuf的)栈中写入攻击代码,但由于参数需要是char*类型,我们还需要考虑字符串形式的cookie存在哪里。
    重新捋一下函数调用关系——
    1. test调用 getbuf
    1. 执行时,getbuf栈桢被修改,执行一段攻击代码,之后返回到touch3的首地址,回收栈桢;
    1. touch3调用hexmatch
    因此栈中总共的栈桢变化为:test+getbuf(修改)→ test+hexmatch
     
    hexmatch栈桢中,局部变量s的地址是随机的,而且还调用了函数strncmp,将cookie存在原先的getbuf栈桢中会有被覆盖的风险;故考虑cookie存入父函数test的栈桢中
     
    让我们先看看test的栈桢,将断点打在调用getbuf之前,可知此时栈指针为0x55654948、指向了一段可用的8字节空间,刚好能够用来存cookie
    notion image
    可知touch3的首地址为0x401878,于是攻击代码如下:
    又可知cookie0x2ded9dc0)的ASCII码字节值为:32 64 65 64 39 64 63 30

    栈结构

    getbuf函数执行ret返回后的栈结构如下:
    notion image
    注意,&(touch3)是执行攻击代码后才压栈的,压栈后rsp将下移8字节。
    具体而言,getbuf函数执行ret后弹出返回地址0x55654908(攻击代码的首地址)并跳转执行攻击代码,在执行pushq时将touch3的首地址压栈,最后执行ret将其弹出,跳转执行函数touch3

    解决

    查看攻击代码的字节值:
    notion image
    于是可构建攻击字符串:
    运行,任务完成。
    notion image

    Part II : Return-Oriented Programming

    第二部分将攻击程序RTARGET,但对其代码注入要难一些,它采用了以下两种技术对抗攻击:
    • 栈随机化,每次运行栈的位置都不同,因而无法决定你的攻击代码代码应该注入哪里。
    • 将保存栈的内存区域设置为不可执行,所以即使你能把注入的代码的起始地址放到程序计数器中,程序也会报段错误失败。
    幸运的是,聪明的人们设计了一些策略,通过执行现有程序中的代码来做他们期望的事情,而不是注入新的代码。这种方法称为面向返回的编程(ROP)
    The strategy with ROP is to identify byte sequences within an existing program that consist of one or more instructions followed by the instruction ret. 在已有的程序中找到特定的以ret结尾的指令序列,称这样的代码段为gadget 把要用到部分的地址压入栈中,每次ret后又会取出一个新的gadget,进而形成一个程序链。
    notion image
    举例而言,rtarget可能包含下面这个函数:
    这个函数本无害,但其末尾的字节序列48 49 c7是指令movq %rax, %rdi的编码。
    因此,我们可以通过将执行的地址定在0x400f18而不是0x400f15,来让程序执行命令movq %rax, %rdi
     
    由此,我们的攻击代码将由一系列这样的函数组成,称为gadget farm
    gadget farm函数组 (RTARGET文件中)
    我们的工作是从gadget farm中挑选出有用的gadget,执行类似于前述第二关和第三关的攻击。
    函数start_farmend_farm之间的所有函数构成了你的gadget farm。 不要用程序代码中的其他部分作为你的gadget。
    同时,给出了一些指令的字节值:
    notion image

    Phase 4 Level 2

    任务

    重复Phase 2 Level 2的攻击——让程序执行touch2而不是返回test,并使调用touch2时参数为我的cookie(ungisned类型)——但需要使用gadget farm里的gadget来攻击RTARGET程序。
    答案只使用如下指令类型的gadget,也只能使用前八个x86-64寄存器(%rax-%rdi)。
    • movq:代码如前图所示。
    • popq:代码如前图所示。
    • ret:该指令编码为0xc3
    • nop:该指令编码为0x90
    💡注意:
    • 只能用两个gadget来实现该次攻击。
    • 如果一个gadget使用了popq指令,那么它会从栈中弹出数据。这样一来,你的攻击代码能既包含gadget的地址也包含数据。

    思路

    Level 2中,攻击代码为:
    但在这里,我们显然无法进行立即数的赋值,只能通过字符串攻击将cookie存入栈中,然后通过popq操作将其弹出并赋值给指定寄存器。
    因此,猜测目标攻击代码主体大概为:
    查询gadget farm函数组,发现没有对应于popq %rdi的字节值,于是需要找另一个寄存器来当中介。
    经过一番查找,发现%rax适合当中介——函数组内具有popqmovq指令对应的字节值、且后面紧跟着ret指令:
    题目有提示,0x90对应的是空指令nop,因此可忽略
    于是攻击代码为:

    栈结构

    根据gadget farm函数组中找到的两个函数,可知攻击代码的地址分别为0x4019230x40191c
    getbuf函数执行ret返回后的栈结构如下:
    notion image
    具体而言,getbuf函数返回后将跳转0x401923执行popq命令,于是栈指针rsp上移并将cookie弹出;之后执行ret命令,栈弹出地址0x40191c并跳转,执行movq命令后ret,栈弹出&(touch2),跳转执行函数touch2
    其实test函数的栈桢并没有这么长,在Phase 3 Level 3 中我们已经查看过了,它的栈桢只有16字节——8字节的可用空间和8字节的返回地址(其值为0x401df3); 这里只是方便起见,用test笼统地称呼高位栈的父函数们,包括最后一个任务,“test函数”的栈桢会更长(罪过罪过)。

    解决

    可得攻击字符串:
    执行后完成任务。
    notion image

    Phase 5 Level 3

    CMU实验指导原文PDF中,这里放了一段劝退文案:
    Before you take on the Phase 5, pause to consider what you have accomplished so far. In Phases 2 and 3, you caused a program to execute machine code of your own design. If CTARGET had been a network server, you could have injected your own code into a distant machine. In Phase 4, you circumvented two of the main devices modern systems use to thwart buffer overflow attacks. Although you did not inject your own code, you were able inject a type of program that operates by stitching together sequences of existing code. You have also gotten 95/100 points for the lab. That’s a good score. If you have other pressing obligations consider stopping right now. Phase 5 requires you to do an ROP attack on RTARGET to invoke function touch3 with a pointer to a string representation of your cookie. That may not seem significantly more difficult than using an ROP attack to invoke touch2, except that we have made it so. Moreover, Phase 5 counts for only 5 points, which is not a true measure of the effort it will require. Think of it as more an extra credit problem for those who want to go beyond the normal expectations for the course.
    我不管我就是要做

    任务

    RTARGET程序进行ROP攻击,重复Phase 3 Level 3的效果——使程序调用touch3函数,且参数为指向cookie字符串的指针,
    这一关,允许你使用函数start_farm和end_farm之间的所有gadget。
    除了第四阶段允许的那些指令,还允许使用movl指令,以及D中的2字节指令(如前图所示),它们可以作为有功能的nop,不改变任何寄存器或内存的值。(例如,andb %al, %al,这些指令对寄存器的低位字节做操作,但是不改变它们的值)
    💡提示:
    • 官方答案需要8个gadgets。

    思路

    Phase 3 Level 3 中的攻击代码为:
    但由于RTARGET程序的栈是随机化的,无法直接指定存入栈中的cookie字符串的地址,只能通过栈指针rsp加上一个偏移量来访问
    因此,我们必然使用到lea命令,但题目并未给出lea命令的字节值,查阅gadget farm函数组,发现有一个现成的gadget可以用:
    此外,也可以发现movq %rsp,xxxgadget farm函数组中存在的字节值:(最终确定目标寄存器为%rax
    而用于访问cookie的偏移量也需要存入栈中,通过pop弹出使用,于是目标攻击代码主体大概为:
    查阅gadget farm函数组,发现攻击代码竟然可以全部实现、不用拐弯抹角,那就开心进行吧。

    栈结构1

    需要注意,偏移量的确定需要考虑执行movq %rsp,%rax命令时的栈指针地址。(其为0x55654948
    getbuf函数执行ret返回后的栈结构如下:
    notion image

    解决1

    攻击字符串如下:
    执行,任务完成。(才怪)
    notion image

    粗心咯!

    但是!打开Scoreboard发现成绩被标注”Invalid”,看来必须要使用整整8gadget才行。
    notion image
    另外在检查的时候发现,地址0x4021f2对应的函数<rio_read>并不属于gadget farm,实际上gadget farm中只含一个弹栈指令——popq %rax。(决定单开一个文件来检索,省的搜到其他地方白高兴了😭)
    查阅gadget farm函数组(省略尝试过程),巧用movl指令,将攻击代码变为:

    栈结构2

    getbuf函数执行ret返回后的栈结构如下:
    notion image

    解决2

    于是攻击字符串为:
    执行,任务完成。
    notion image
    notion image
     
    随笔|我能怎么办Linux davfs2搭载Webdav协议网盘
    • Twikoo
    • Giscus
    Antony_Zhang
    Antony_Zhang
    理想与泥土 Blood in mud
    公告
    type
    date
    slug
    summary
    status
    tags
    category
    password
    Last edited time
    Oct 20, 2024 10:22 AM
    icon
    添加评论功能!
    其中Giscus需要Github登录,Twikoo需要用户名和邮箱
    🧎
    小站破破烂烂 劳烦客人们常用善用 “刷新键”