CS:APP - Attack Lab
Introduction to Computer Systems I (H) @ Fudan University, fall 2019.
实验简介
附件说明
ctarget:Level 1 ~ 3 的攻击目标,未开启栈溢出保护,可以自己写可执行代码。rtarget:Level 4 ~ 5 的攻击目标,开启了栈溢出保护,需要使用 ROP(Return-Oriented Programming)来实现注入。hex2raw:辅助工具,用于将十六进制数转换为对应字符(串)。farm.c:ROP 中 gadget 的来源。cookie.txt:记录了cookie的值,解题过程中会用到。
实验报告
准备工作
0.1 反汇编 ctarget 和 rtarget
使用 objdump 反汇编 ctarget 和 rtarget 程序,并将输出重定向到 ctarget.asm 和 rtarget.asm。
1 2 | |
0.2 如何输入用于注入的字符串
用于注入的字符串中有很多不可见字符不能直接用键盘输入,因此这里需要借助本 Lab 提供的辅助工具 hex2raw 来实现这些不可见字符的输入。
0.2.1 hex2raw 使用方法
- 输入文件
exploit.txt的内容:以空格隔开的 2 位一组的十六进制数 - 输出文件
exploit_raw.txt的内容:还原出的字符串
编辑输入文件 exploit.txt:
1 | |
1 | |
使用 hex2raw 还原字符串:
1 | |
查看输出文件 exploit_raw.txt:
1 | |
1 | |
0.2.2 输入字符串
以 ctarget 程序为例:
1 | |
输出信息:
1 2 3 | |
Level 1: Code Injection
1.1 本关目标
执行函数 touch1。
1.2 本关解答
exploit.txt 中的内容:
1 2 3 4 5 6 | |
1.3 解题思路
输入一个长字符串,使函数 test 在调用函数 getbuf 时发生缓冲区溢出,利用溢出部分修改函数的返回地址,从而使函数 getbuf 不返回到函数 test,而是返回到目标函数 touch1。
1.4 解题过程
1.4.0 总览
讲义中给出了函数 test 和 getbuf 的源代码:
1 2 3 4 5 | |
1 2 3 4 5 | |
在函数 getbuf 中,函数 Gets 将读取我们输入的字符串,并保存在缓冲区 buf 中。可见,这里存在缓冲区溢出漏洞。
1.4.1 观察函数 getbuf
在 ctarget.asm 中找到函数 getbuf 对应的汇编语句:
1 2 3 4 5 6 7 8 9 | |
4017a8: sub $0x28,%rsp 对应 C 语言代码中的 char buf[BUFFER_SIZE],可见 BUFFER_SIZE 的值即为 40。
因此,当我们输入的字符串长度超过 40 时,就将造成缓冲区溢出。
1.4.2 查看函数 touch1 的地址
在 ctarget.asm 中找到函数 touch1 对应的汇编语句:
1 2 | |
可见函数 touch1 的地址为 0x4017c0。
1.4.3 ATTACK
函数 getbuf 在栈中没有保存寄存器,也没有其他局部变量。因此由栈帧的结构可知,当发生缓冲区溢出时,溢出部分将直接覆盖调用者栈帧中的返回地址。
构造用于注入的字符串:
- 输入任意 40 个字符填满缓冲区;
- 输入作为地址的 8 个字符导致溢出,溢出部分将返回地址修改为
touch1的地址0x4017c0。注意地址的字节顺序应当按照小端序(little endian),即低位字节保存在低地址,高位字节保存在高地址。
编辑输入文件 exploit.txt:
1 2 3 4 5 6 | |
在 ctarget 程序中输入该字符串:
1 | |
输出信息:
1 2 3 4 5 6 7 8 | |
1.4.4 运行结果

Level 2: Code Injection
2.1 本关目标
执行函数 touch2,并传入 cookie 的值作为参数 val。
2.2 本关解答
exploit.txt 中的内容:
1 2 3 4 5 6 | |
2.3 解题思路
同样,还是利用函数 getbuf 的缓冲区溢出漏洞。只是在返回前需要额外一步操作:将 cookie 的值传给 %rdi 寄存器(第 1 个参数)。我们可以自行构造汇编代码来实现这一点。
那么如何执行我们构造的代码呢?我们可以将这段代码放在缓冲区内,然后利用缓冲区溢出部分修改函数的返回地址,使函数 getbuf 返回到这段代码的起始地址。之后再写一段用于返回到目标函数 touch2 的返回语句即可。
2.4 解题过程
2.4.0 总览
讲义中给出了函数 touch2 的源代码:
1 2 3 4 5 6 7 8 9 10 11 | |
可见需要传入 cookie 的值作为参数 val。
2.4.1 查看函数 touch2 的地址
在 ctarget.asm 中找到函数 touch2 对应的汇编语句:
1 2 | |
可见函数 touch2 的地址为 0x4017ec。
2.4.2 查看缓冲区的起始地址
使用 gdb 调试 ctarget。
1 | |
1 | |
在函数 getbuf 的入口处设置一个断点。
1 | |
运行,到断点处暂停,执行一步 4017a8: sub $0x28,%rsp,查看此时 %rsp 的值。
1 2 3 | |
输出信息:
1 | |
可见此时 %rsp 的值为 0x5561dc78,这就是缓冲区的起始地址。
2.4.3 构造汇编代码
这段汇编代码需要实现的功能:
- 将
cookie的值传给%rdi寄存器(第 1 个参数); - 返回到目标函数
touch2。
其中 cookie 的值由附件 cookie.txt 给出,为 0x59b997fa,函数 touch2 的地址由 2.4.1 节可知为 0x4017ec。
由此构造汇编代码:
1 2 3 | |
因为 ret 时将弹栈,并将弹出的值传给 %rip,这里 push $0x4017ec 的作用就是将函数 touch2 的地址先压入栈中。由于栈后进先出(LIFO)的特点,这样弹栈时这个地址就会被当作函数的返回地址。
2.4.4 将汇编代码转换为机器码
将这段汇编代码保存在 level2.s 文件中。
使用 gcc 编译得到目标代码 level2.o:
1 | |
使用 objdump 反汇编 level2.o:
1 | |
输出信息:
1 2 3 4 5 6 7 8 9 | |
这样就得到了对应的机器码:
1 2 3 | |
2.4.5 ATTACK
构造用于注入的字符串:
- 输入这段机器码(13 个字符),其起始地址就是缓冲区的起始地址;
- 输入任意 27 个字符填满缓冲区的剩余部分;
- 输入作为地址的 8 个字符导致溢出,溢出部分将返回地址修改为缓冲区的起始地址
5561dc78。
编辑输入文件 exploit.txt:
1 2 3 4 5 6 | |
在 ctarget 程序中输入该字符串:
1 | |
输出信息:
1 2 3 4 5 6 7 8 | |
2.4.6 运行结果

Level 3: Code Injection
3.1 本关目标
执行函数 touch3,并传入表示 cookie 的值的字符串作为参数 sval。
3.2 本关解答
exploit.txt 中的内容:
1 2 3 4 5 6 7 8 | |
3.3 解题思路
与 Level 2 类似,区别在于这次我们需要构造一个字符串,而不是直接传一个整数。需要注意字符串的保存位置。
3.4 解题过程
3.4.0 总览
讲义中给出了函数 hexmatch 和 touch3 的源代码:
1 2 3 4 5 6 7 8 | |
1 2 3 4 5 6 7 8 9 10 11 | |
可见需要传入表示 cookie 的值的字符串作为参数 sval。同时函数 hexmatch 的设计使得直接获取用于检验的字符串较为困难,因此我们需要自行构造这个字符串。
3.4.1 查看函数 touch3 的地址
在 ctarget.asm 中找到函数 touch3 对应的汇编语句:
1 2 | |
可见函数 touch3 的地址为 0x4018fa。
3.4.2 构造表示 cookie 的值的字符串
已知 cookie 的值为 0x59b997fa,我是懒得去对照 ASCII 码表了,直接交给程序处理。编写代码如下:
1 2 3 4 5 6 7 | |
传入字符串 59b997fa(已去除 0x 前缀),输出结果:
1 | |
即为需要构造的字符串的十六进制数表示。
3.4.3 字符串的保存位置
需注意,该字符串不能保存在函数 getbuf 的缓冲区中,即函数返回地址在栈中保存位置的下方。否则返回到函数 touch3 后,在进行字符串比较前,一系列压栈操作将使缓冲区里的内容被新写入的数据覆盖。如下所示:
1 2 3 4 | |
1 2 3 4 5 | |
因此,字符串应当保存在函数返回地址在栈中保存位置的上方,也就是函数 test 的栈帧中。方便起见,不妨保存在返回地址保存位置的下一个地址,即缓冲区的起始地址 0x5561dc78 加上偏移量 48 后得到的地址 0x5561dca8。
3.4.4 构造汇编代码
这段汇编代码需要实现的功能:
- 将这个字符串的地址传给
%rdi寄存器(第 1 个参数); - 返回到目标函数
touch3。
其中,字符串的地址由 3.4.3 节可知为 0x5561dca8,函数 touch3 的地址由 3.4.1 节可知为 0x4018fa。
类似 2.4.3 节,构造汇编代码:
1 2 3 | |
3.4.5 将汇编代码转换为机器码
类似 2.4.4 节,得到对应的机器码:
1 2 3 | |
3.4.6 ATTACK
构造用于注入的字符串:
- 输入这段机器码(13 个字符);
- 输入任意 27 个字符填满缓冲区的剩余部分;
- 输入作为地址的 8 个字符导致溢出,溢出部分将返回地址修改为缓冲区的起始地址
5561dc78; - 输入表示
cookie的值的字符串。
编辑输入文件 exploit.txt:
1 2 3 4 5 6 7 8 | |
在 ctarget 程序中输入该字符串:
1 | |
输出信息:
1 2 3 4 5 6 7 8 | |
3.4.7 运行结果

Level 4: Return-Oriented Programming
4.1 本关目标
同 Level 2。区别在于 rtarget 程序开启了地址随机化(Address Space Layout Randomization, ASLR)和栈不可执行机制(No-eXecute, NX),但同时也新增了辅助用的 gadget farm。这次我们需要进行 ROP 攻击,利用源程序已有的代码来构造我们需要的指令序列。
4.2 本关解答
exploit.txt 中的内容:
1 2 3 4 5 6 7 8 9 | |
4.3 解题思路
同 2.3 节,目标是将 cookie 的值传给 %rdi 寄存器,并最终返回到目标函数 touch2。
由于没有指令可以将一个特定的立即数直接传给某个寄存器,这里我们可以将这个立即数先保存在栈中,然后利用 popq 指令弹栈传给某个寄存器,此后再利用 movq、movl 等指令在寄存器间互相传递。
为了使指令能够连成一个序列,我们需要每句指令后都紧接着一句 ret 指令(对应机器码 c3)。先提前将下一条指令的地址保存在栈中,然后利用 ret 指令弹栈传给 %rip,从而实现跳转。
需注意,由题目要求,只允许跳转到以下位置:
- 函数
touch1、touch2、touch3的地址 - 注入代码的任意位置
- gadget farm 中的某一个 gadget(即
start_farm和end_farm之间的地址)
4.4 解题过程
4.4.0 总览
原本的汇编代码:
1 2 3 | |
我们逐句进行改写。
4.4.1 第 1 句指令
1 | |
由 4.3 节的分析,我们可以将 0x59b997fa 先保存在栈中,然后利用 popq 指令弹栈传给某个寄存器,此后再利用 movq、movl 等指令传给 %rdi 寄存器。
4.4.1.1 第 1 个 gadget
注意到 gadget farm 中函数 getval_280 对应的汇编语句:
1 2 3 | |
其中机器码 58 90 c3 对应汇编指令:
1 2 | |
该指令的地址为 0x4019cc。
4.4.1.2 第 2 个 gadget
注意到 gadget farm 中函数 addval_273 对应的汇编语句:
1 2 3 | |
其中机器码 48 89 c7 c3 对应汇编指令:
1 2 | |
该指令的地址为 0x4019a2。
4.4.1.3 整合
于是可改写第 1 句指令为:
1 2 3 4 | |
在栈中需要保存的内容为:
1 2 3 | |
分别对应:
- 第 1 个 gadget 中指令的地址
0x4019cc cookie的值0x59b997fa- 第 2 个 gadget 中指令的地址
0x4019a2
4.4.2 第 2 ~ 3 句指令
1 2 | |
这次就不需要压栈了,直接将函数 touch2 的地址保存在栈中即可,上一条指令结尾的 ret 指令将使函数返回到目标函数 touch2。
在栈中需要保存的内容为:
1 | |
表示函数 touch2 的地址 0x4017ec。
4.4.3 ATTACK
构造用于注入的字符串:
- 输入任意 40 个字符填满缓冲区;
- 输入由以上分析可知在栈中需要保存的内容。
编辑输入文件 exploit.txt:
1 2 3 4 5 6 7 8 9 | |
在 rtarget 程序中输入该字符串:
1 | |
输出信息:
1 2 3 4 5 6 7 8 | |
4.4.4 运行结果

Level 5: Return-Oriented Programming
5.1 本关目标
同 Level 3。区别在于这次我们需要进行 ROP 攻击。
5.2 本关解答
exploit.txt 中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
5.3 解题思路
同 3.3 节,目标是构造表示 cookie 的值的字符串,将其地址传给 %rdi 寄存器,并最终返回到目标函数 touch3。
解题思路整体类似 4.3 节。
需注意,由于 rtarget 程序开启了地址随机化,不能直接得到字符串的地址,因此需要利用 movq、movl 等指令将 %rsp 的值传给某个寄存器,以获得栈的起始地址。之后再为这个地址加上一个偏移量 offset,从而得到字符串的实际地址。
为了不影响指令间的跳转,字符串应当保存在所有栈中保存内容的最后。
5.4 解题过程
5.4.0 总览
原本的汇编代码:
1 2 3 | |
我们逐句进行改写。
5.4.1 第 1 句指令
1 | |
由 5.3 节的分析,我们不能直接得到字符串的地址,因此需要利用 movq、movl 等指令将 %rsp 的值传给某个寄存器。
当然,此时这个寄存器保存的值并不是字符串的地址。于是我们需要为这个地址加上一个偏移量 offset,从而得到字符串的实际地址。但字符串将保存在所有栈中保存内容的最后,因此 offset 的值我们最后才能得到。
5.4.1.1 第 3 个 gadget
注意到 gadget farm 中函数 addval_190 对应的汇编语句:
1 2 3 | |
其中机器码 48 89 e0 c3 对应汇编指令:
1 2 | |
该指令的地址为 0x401a06。
5.4.1.2 第 2 个 gadget
由 4.4.1.2 节,第 2 个 gadget 中有汇编指令:
1 2 | |
该指令的地址为 0x4019a2。
5.4.1.3 第 4 个 gadget
惊奇地注意到 gadget farm 中函数 add_xy 对应的汇编语句(竟然还有这种函数):
1 2 3 | |
其对应汇编指令:
1 2 | |
该指令的地址为 0x4019d6。
其作用是将 %rdi 和 %rsi 的值之和传给 %rax 寄存器。目前 %rsp 的值保存在 %rdi 寄存器中,因此我们需要想办法把偏移量 offset 的值传到 %rsi 寄存器,从而我们就能利用这个 gadget 中的指令得到字符串的实际地址。
类似 4.4.1 节,我们可以将 offset 的值先保存在栈中,然后利用 popq 指令弹栈传给某个寄存器,此后再利用 movq、movl 等指令传到 %rsi 寄存器。
5.4.1.4 第 1 个 gadget
由 4.4.1.1 节,第 1 个 gadget 中有汇编指令:
1 2 | |
该指令的地址为 0x4019cc。
现在我们的目标就转化为利用 movq、movl 等指令将 %rax 的值传到 %rsi 寄存器。遗憾的是,gadget farm 中并不存在直接将 %rax 的值传给 %rsi 寄存器的汇编指令,因此需要进行多次传递。
5.4.1.5 第 5 个 gadget
注意到 gadget farm 中函数 addval_436 对应的汇编语句:
1 2 3 | |
其中机器码 89 ce 90 90 c3 对应汇编指令:
1 2 | |
该指令的地址为 0x401a13。
5.4.1.6 第 6 个 gadget
注意到 gadget farm 中函数 getval_159 对应的汇编语句:
1 2 3 | |
其中机器码 89 d1 38 c9 c3 对应汇编指令:
1 2 3 | |
该指令的地址为 0x401a34。这里 cmpb %cl,%cl 相当于一条无用指令。
5.4.1.7 第 7 个 gadget
注意到 gadget farm 中函数 addval_487 对应的汇编语句:
1 2 3 | |
其中机器码 89 c2 84 c0 c3 对应汇编指令:
1 2 3 | |
该指令的地址为 0x401a42。这里 testb %al,%al 相当于一条无用指令。
至此,我们可以将 %rax 的值传到 %rsi 寄存器了(%eax-> %edx -> %ecx -> %esi)。
5.4.1.8 整合
于是可改写第 1 句指令为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
在栈中需要保存的内容为:
1 2 3 4 5 6 7 8 9 | |
分别对应:
- 第 1 个 gadget 中指令的地址
0x4019cc - 偏移量
offset的值(目前先用0xff占位) - 第 7 个 gadget 中指令的地址
0x401a42 - 第 6 个 gadget 中指令的地址
0x401a34 - 第 5 个 gadget 中指令的地址
0x401a13(%rsi寄存器就位) - 第 3 个 gadget 中指令的地址
0x401a06 - 第 2 个 gadget 中指令的地址
0x4019a2(%rdi寄存器就位) - 第 4 个 gadget 中指令的地址
0x4019d6(得到字符串地址) - 第 2 个 gadget 中指令的地址
0x4019a2(传给%rdi寄存器)
5.4.2 第 2 ~ 3 句指令
1 2 | |
同 4.4.2 节,直接将函数 touch3 的地址保存在栈中即可,上一条指令结尾的 ret 指令将使函数返回到目标函数 touch3。
在栈中需要保存的内容为:
1 | |
表示函数 touch3 的地址 0x4018fa。
5.4.3 确定偏移量的值
构造用于注入的字符串(初步):
- 输入任意 40 个字符填满缓冲区;
- 输入由以上分析可知在栈中需要保存的内容;
- 输入表示
cookie的值的字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
可见,第 11 行跳转完后得到了 %rsp 的值(此时 %rsp 指向第 12 行),第 16 行是字符串的地址,因此偏移量 offset 的值为 32(即 0x20)。
将第 7 行的占位符修改为 offset 的实际值 0x20:
1 2 3 | |
5.4.4 ATTACK
编辑输入文件 exploit.txt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
在 rtarget 程序中输入该字符串:
1 | |
输出信息:
1 2 3 4 5 6 7 8 | |
5.4.5 运行结果

测试环境
- Ubuntu 18.04.3 LTS (GNU/Linux 5.0.0-32-generic x86_64)
- GCC 7.4.0
- GDB 8.1.0