一、实验步骤
1.1 第一步:获取文件
在远程桌面中用浏览器访问网页:http://172.16.2.207:15513,输入你的学号和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指令使用的攻击字符串中注入的地址必须是一下几种之一:
n 函数touch1,touch2或touch3的地址
n 你注入的代码的地址
n gadget farm中gadget的地址
Ø 只能从文件rtarget中地址范围在函数start_farm和end_farm之间的地址构造gadget。
二、目标程序
CTARGET和RTARGET都是用getbuf函数从标准输入读入字符串,getbuf函数定义如下:
1 unsigned getbuf()
2 {
3 char buf[BUFFER_SIZE];
4 Gets(buf);
5 return 1;
6 }
函数Gets类似于标准库函数gets,从标准输入读入一个字符串(以’\n’或者end-of-file结束),将字符串(带null结束符)存储在指定的目的地址。从这段代码可以看出,目标地址是数组buf,声明为BUFFER_SIZE个字节。BUFFER_SIZE是一个编译时常量,在你的target程序生成时就具体确定了。
函数Gets()和gets()都无法确定目标缓冲区是否够大,能够存储下读入的字符串。它们都只会简单地拷贝字节序列,可能会超出目标地址处分配的存储空间的边界。
如果用户输入和getbuf读入的字符串足够短,getbuf会返回1,如下执行示例所示:
如果你输入的字符串很长,就会出错:
(注意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.1. 第一关
在这一关中,你不用注入新的代码,你的攻击字符串要指引程序去执行一个已经存在的函数。
CTARGET中函数test调用了函数getbuf,test的代码如下:
1 void test()
2 {
3 int val;
4 val = getbuf();
5 printf("No exploit. Getbuf returned 0x%x\n", val);
6 }
getbuf执行返回语句时(getbuf的第5行),按照规则,程序会继续执行test函数中的语句,而我们想改变这个行为。在文件ctarget中,函数touch1的代码如下:
1 void touch1()
2 {
3 vlevel = 1; /* Part of validation protocol */
4 printf("Touch1!: You called touch1()\n");
5 validate(1);
6 exit(0);
7 }
你的任务是让CTARGET在getbuf执行返回语句后执行touch1的代码。注意,你的攻击字符串可以破坏栈中不直接和本阶段相关的部分,这不会造成问题,因为touch1会使得程序直接退出。
要点说明:
· 设计本阶段的攻击字符串所需的信息都从检查CTARGET的反汇编代码中获得。用objdump -d进行反汇编。
· 主要思路是找到touch1的起始地址的字节表示的位置,使得getbuf结尾处的ret指令会将控制转移到touch1。
· 注意字节顺序。
· 可能需要用GDB单步跟踪调试getbuf的最后几条指令,确保它按照你期望的方式工作。
· buf在getbuf栈帧中的位置取决于编译时常数BUFFER_SIZE的值,以及GCC使用的分配策略。你需要检查反汇编带来来确定它的位置。
Test的汇编代码如下:
Sub $0x8,%rsp表明开辟了8字节的栈空间
4018e8地址处调用了getbuf函数
将getbuf反编译后代码如下,
Sub $0x28,%rsp表明开辟了40字节的栈空间
因此此时的栈帧结构为:
看前面的任务,我们要让getbuf执行返回语句后,执行touch1的代码
也就是返回地址处应该改为touch1的值
查看touch1的地址为 0x40177b
因此解决办法即,让输入占满getbuf开辟的40字节栈空间,并写入touch1的地址值。
因此本题的答案如上图
执行,成功过了第一关
3.2. 第二关
第二关中,你需要在攻击字符串中注入少量代码。
在ctarget文件中,函数touch2的代码如下:
1 void touch2(unsigned val)
2 {
3 vlevel = 2; /* Part of validation protocol */
4 if (val == cookie) {
5 printf("Touch2!: You called touch2(0x%.8x)\n", val);
6 validate(2);
7 } else {
8 printf("Misfire: You called touch2(0x%.8x)\n", val);
9 fail(2);
10 }
11 exit(0);
12 }
你的任务是使CTARGET执行touch2的代码而不是返回到test。在这个例子中,你必须让touch2以为它收到的参数是你的cookie。
建议:
· 需要确定你注入代码的地址的字节表示的位置,使getbuf代码最后的ret指令会将控制转移到那里。
· 注意,函数的第一个参数是放在寄存器%rdi中的。
· 你注入的代码必须将寄存器的值设定为你的cookie,然后利用ret指令将控制转移到touch2的第一条指令。
· 不要在攻击代码中使用jmp或call指令。所有的控制转移都要使用ret指令,即使实际上你并不是要从一个函数调用返回。
· 参见附录B学习如何生成指令序列的字节级表示。
本关的任务相比第一关,多了一些操作。不仅需要修改返回地址调用touch2函数,还需要将我们的cookie值作为参数传递给touch2
题目给出建议,不要在攻击代码中使用jmp和call指令,所有转移使用ret指令,因此我们的输入应该在栈中保存目标代码的地址,再利用ret进行跳转。
借用另一篇关于attacklab的实验报告的内容,关于ret指令有如下解释:
因此,本题的主要思路如下:
编写注入代码,跳转注入代码的地址并执行。
注入代码的作用如下:
首先肯定是通过字符串输入,将返回地址设置成touch2函数的地址
其次是设置第一个参数寄存器的值,将cookie传入调用touch2
经过查看,我的cookie值为:
0x479a8e36
Touch2代码的地址为:
0x4017a7
所以本题应该注入的代码内容为:
之后应该确认存放注入代码的地址,在test调用完getbuf之后能够跳转到注入代码的存放位置,去执行我们的注入代码
我们发现,getbuf函数开辟的40字节的栈空间可以用于存放我们的注入代码。
因此我们利用gdb在getbuf分配完栈帧后
打断点并查看栈指针的位置
因此,getbuf函数分配完空间以后,栈指针位置为0x5567fc18
这个地方是我们注入代码保存的位置,应该将返回地址修改成这个值
所以我们希望将栈结构修改成这样
本题的解决办法就此明晰,首先,通过将返回地址设置为注入代码的存放地址,执行注入代码。注入代码中,将cookie作为第一个参数寄存器的值,并将touch2的地址压入栈中,最后ret将touch2地址值取出并执行touch2
知道了解决的全过程,就要开始写我们的输入内容了。
首先,要将我们的注入汇编代码转化成16进制的字节级表示形式。
我的做法是,将汇编代码保存在c2.s文件中
使用如下指令:
这样会生成一个c2.o的文件,将其反编译
打开c2.d,得到我们注入代码的字节级表示
这样就可以着手写输入了,最终的输入为:
成功,顺利通过第二关
3.3. 第三关
第三阶段还是代码注入攻击,但是是要传递字符串作为参数。
ctarget文件中函数hexmatch和touch3的C代码如下:
1 int hexmatch(unsigned val, char *sval)
2 {
3 char cbuf[110];
4 /* Make position of check string unpredictable */
5 char *s = cbuf + random() % 100;
6 sprintf(s, "%.8x", val);
7 return strncmp(sval, s, 9) == 0;
8 }
9
10 void touch3(char *sval)
11 {
12 vlevel = 3; /* Part of validation protocol */
13 if (hexmatch(cookie, sval)) {
14 printf("Touch3!: You called touch3("%s")\n", sval);
15 validate(3);
16 } else {
17 printf("Misfire: You called touch3("%s")\n", sval);
18 fail(3);
19 }
20 exit(0);
21 }
你的任务是让CTARGET执行touch3而不要返回到test。要使touch3以为你传递你的cookie的字符串表示作为它的参数。
建议:
· 你的攻击字符串中要包含你的cookie的字符串表示。这个字符串由8个十六进制数字组成(顺序是从最高位到最低位),开头没有“0x”。
· 注意,C中的字符串表示是一个字节序列,最后跟一个值为0的字节。“man ascii”能够找到你需要的字符的字节表示。
· 你的注入代码应该将寄存器%rdi设置为攻击字符串的地址。
· 调用hexmatch和strncmp函数时,会将数据压入栈中,覆盖getbuf使用的缓冲区的内存,你需要很小心把你的cookie字符串表示放在哪里。
分析完第三关的要求,发现本题与第二关十分类似
区别在于,本题要求传入字符串,且字符串放置的位置需要考虑
首先将我们的cookie转化为字符串
我的cookie值是0x479a8e36
转化为字符串后成为: 34 37 39 61 38 65 33 36
接下来,分析题目建议中一个比较奇怪的点
这段话相当于把调用这两个函数对我们输入产生的限制告诉我们,我们现在不能将cookie放在getbuf的栈空间中了,一旦cookie被覆盖,那么就不能在调用touch3时比较出正确的结果。
不放在getbuf中,就很自然的想到可以放在test的栈空间中
若要放在test的栈空间中,就需要知道当test分配栈空间时,栈指针的位置:
得知栈指针的位置为0x5567fc48
这个就是我们字符串存放的位置,即调用touch3应该传入参数的位置
因此,本题的栈结构应该是这样的
注入代码应该为:
字节级表示为:
因此最终我们的输入为:
运行,成功过关
四. 实验内容第二部分:面向返回的编程
对程序RTARGET进行代码注入攻击要难一些,它采用了以下两种技术对抗攻击:
· 采用了随机化,每次运行栈的位置都不同。所以无法决定你的注入代码应该放在哪里。
· 将保存栈的内存区域设置为不可执行,所以即使你能把注入的代码的起始地址放到程序计数器中,程序也会报段错误失败。
幸运的是,聪明的人们设计了一些策略,通过执行现有程序中的代码来做他们期望的事情,而不是注入新的代码。这种方法称为面向返回的编程(ROP)。
例如,rtarget可能包含如下代码:
void setval_210(unsigned *p)
{
*p = 3347663060U;
}
这个函数不太可能会攻击到一个系统,但是这段代码反汇编出来的机器代码是:
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq
字节序列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的地址也包含数据。
图2. 指令的字节编码。所有的值均为十六进制。
本题的任务与第二关相同,需要我们修改返回地址,调用touch2函数
第二关注入代码为:
但我们很容易地发现,我们很难获取到一个含有立即数的gadget代码
因此只能思考其他的办法。
查看第四关建议中的一条,
我们得到线索:只需要把数据和我们的popq指令编码一同输入进去就可以了
因此,我们可以通过将cookie数据和以下指令配合,
pop %rdi
ret
将我们的cookie放入第一个参数寄存器%rdi中,这样实现了参数的赋值
值得注意的是,题目有如是要求:
我们在提供的gadget farm中找不到可以直接表示pop %rdi ret的代码片段
因此,我们可以使用类似拼积木一样的方法,利用两个gadget完成任务
Popq %rax
Ret
Movq %rax,%rdi
Ret
此时我们希望的栈结构为:
Getbuf函数返回后,执行我们插入的gadget1
它会将我们的cookie值弹出,存入%rax寄存器中后ret
接下来执行我们的gadget2,
它会将我们的cookie值从%rax转移到%rdi中,ret
Gadget2执行后的ret将弹出touch2函数的地址值
调用touch2函数
在写我们的输入内容之前,首先查表,找到我们用到的gadget
Gadget1:popq %rax
Ret
地址值为0x40193a
Gadget2:movq %rax,%redi
Ret
因此最终结果为:
成功通过第四关:
4.2. 第五关
阶段五要求你对RTARGET程序进行ROP攻击,用指向你的cookie字符串的指针,使程序调用touch3函数。
这一关,允许你使用函数start_farm和end_farm之间的所有gadget。除了第四阶段允许的那些指令,还允许使用movl指令(如图2C所示),以及图2D中的2字节指令,它们可以作为有功能的nop,不改变任何寄存器或内存的值,例如,andb %al, %al,这些指令对寄存器的低位字节做操作,但是不改变它们的值。
提示:
· 官方答案需要8个gadgets。
本题要我们传入cookie并调用touch3函数
第三关时,我们要注入的代码为:
现在需要利用gadget来进行代替。
值得注意的是,题目告诉我们官方答案用了8个gadgets
因此我们拼gadgets积木的时候,需要留意使用gadgets的个数
其中,0x5567fc48是在原来第三关中,cookie的地址
现在在本题中,栈的位置是随机的,我们这时候若要将cookie放在栈中,则没有办法通过绝对的地址访问到cookie,因此可以采取偏移量的计算方法,用相对地址访问。
计算相对地址需要使用%rsp寄存器保存的地址,因此想办法用gadget进行获取
由于要进行地址计算,而attacklab中给出的字节形式表格的指令只有mov,pop,ret和nop等指令,并没有lea用于计算
查看寻找gadget的gadget farm,我们发现,有唯一加法函数:
因此,我们只能使用这个函数进行计算,计算的操作数有%rdi,%rsi。结果放在%rax中
这肯定是8个gadgets中的一个,还要找7个
而计算的结果地址放在了%rax中,我们最终调用函数时,结果应当在%rdi第一个寄存器中
因此需要
Movq %rax,%rdi
Ret
这个gadget也是必须的,还差寻找6个gadget
由于计算的操作数为栈指针位置和偏移量,操作数寄存器分别为%rdi和%rsi
所以,我们应当将栈指针位置取出,放到%rdi寄存器中
将偏移量取出,放到%rsi(%esi)寄存器中
注意,%rsi和%esi指向同一个寄存器,只不过使用的位数不同
偏移量使用32位表示即可,所以使用%esi来表示
首先,获取栈指针位置
需要使用movq %rsp,X取出寄存器的值
由于%rdi最终存放栈指针的位置参与加法运算,最理想的状态便是:
Movq %rsp,%rdi
查表可知,此时字节表示应该为:48 89 e7
但是在gadget farm中并没有找到满足条件的字节代码
因此,只能通过多次movq达到我们的目的
先考虑movq两次,达到我们的目的
将所有movq %rsp,A的字节表示列出
再将所有movq A,%rdi的字节表示列出
再此过程省略,得到了两条可以满足条件的gadget:
Movq %rsp,%rax
Ret
///
Movq %rax,%rdi
Ret
此时使用最少gadget达到将栈指针的位置放到了%rdi中
这两个gadget也是我们需要的8个gadget之二,还需要找4个gadget
计算相对地址的另一个操作数是%rsi,保存着地址偏移值
所以,地址偏移值是我们自己手动输入的,需要使用
popq %rax
ret
将其取出,它在%rax(%eax寄存器中)
这个gadget也是我们需要的8个gadget之一,还需要找3个gadget
后续的任务就是将%eax的值存放到%esi中,参加加法运算
- 首先看是否可以一次mov达到目的
查看gadget farm中是否有
mov %eax,%esi
ret
结果是没有
- 再看是否可以使用两次mov达到目的
查看gadget farm中是否有
mov %eax,A
ret
Mov A,%esi
Ret
结果也没有
- 不要气馁,再看看是否可以使用3次mov达到目的
查看gadget farm中是否有
Mov %eax,A
Ret
/
Mov A,B
Ret
/
Mov B,%esi
Ret
寻找的过程省略
最终能够找到这样的指令,
Mov %eax,%ecx
Ret
//
Mov %ecx,%edx
Ret
//
Mov %edx,%esi
Ret
这三条gadget是我们需要的,目前已经找到了所有的gadget啦
将找到的gadget进行排序,并标号①~⑧
将栈结构画出来,使用标号表示gadget的具体内容
在栈空间示意图中,
计算得cookie的偏移量,相对于栈指针位置,
Cookie前面有9条指令,9*8=72=0x48
因此偏移量应该即0x48
最终得到的答案为:
运行,顺利解决第五关
附录A HEX2RAW的使用
HEX2RAW的输入是一个十六进制格式的字符串,用两个十六进制数字表示一个字节值。例如,字符串“012345”,必须输入“30 31 32 33 34 35 00”。十六进制字符之间以空白符(空格或新行)分隔。
可以把攻击字符串存入文件中,例如exploit.txt,以下列几种方式调用:
unix> *cat exploit.txt | ./hex2raw | ./ctarget*
unix> *./hex2raw < exploit.txt > exploit-raw.txt*
unix> ./ctarget < exploit-raw.txt
这种方法也可以结合gdb使用。
unix> gdb ctarget
(gdb) run < exploit-raw.txt
unix> *./hex2raw < exploit.txt > exploit-raw.txt*
unix> ./ctarget -i exploit-raw.txt
这种方法也可以和gdb一起使用。
附录B 生成字节代码
假设编写一个汇编文件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
可以汇编和反汇编文件:
unix> gcc -c example.s
unix> 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
由此可以推出这段代码的字节序列:
68 ef cd ab 00 48 83 c0 11 89 c2
可以通过HEX2RAW生成目标程序的输入字符串。也可以手动修改example.d的代码,得到下面的内容:
68 ef cd ab 00 /* pushq $0xabcdef */
48 83 c0 11 /* add $0x11,%rax */
89 c2 /* mov %eax,%edx */
这也是合法的HEX2RAW的输入。
版权归原作者 猪猡猪猡猪 所有, 如有侵权,请联系我们删除。