在64位堆栈驻留缓冲区溢出?

我正在研究一些安全相关的事情,现在我正在玩我自己的堆栈。 我所做的应该是非常微不足道的,我甚至不试图执行堆栈,只是为了表明我可以控制64位系统上的指令指针。 我已经closures了所有的保护机制,只是为了能够使用它(NX-bit,ASLR,还用-fno-stack-protector -z execstack编译)。 我在64位汇编方面没有太多的经验,花了一些时间寻找和尝试自己,我想知道是否有人能够解决我遇到的问题。

我有一个程序(源代码如下),它只是将一个string复制到一个堆栈驻留缓冲区,没有边界检查。 但是,当我覆盖了一系列的0x41我期待看到RIP被设置为0x4141414141414141,而我发现我的RBP被设置为这个值。 我确实遇到了分段错误,但即使将RSP设置为合法值,RIP在执行RET指令时也不会更新为此(非法)值。 我甚至在GDB中validation过,在RET指令之前,在RSP上有一系列包含0x41的可读存储器。

我在LEAVE指令的印象之下:

MOV(E)SP,(E)BP

POP(E)BP

然而,在64位上,“LEAVEQ”指令似乎做了(类似于):

MOV RBP,QWORD PTR [RSP]

我认为这只是在执行这条指令之前和之后观察所有寄存器的内容。 LEAVEQ似乎只是RET指令的一个依赖于上下文的名字(GDB的反汇编给出的),因为它仍然只是一个0xC9。

而且RET指令似乎是用RBP寄存器做些事情的,可能会解引用它? 我的印象是RET所做的(类似于):

MOV RIP,QWORD PTR [RSP]

但是就像我刚才提到的那样,它似乎取消了RBP的引用,我正在考虑这样做,因为当没有其他寄存器似乎包含非法值时,我得到了分段错误。

程序的源代码:

#include <stdio.h> #include <string.h> int vuln_function(int argc,char *argv[]) { char buffer[512]; for(int i = 0; i < 512; i++) { buffer[i] = 0x42; } printf("The buffer is at %p\n",buffer); if(argc > 1) { strcpy(buffer,argv[1]); } return 0; } int main(int argc,char *argv[]) { vuln_function(argc,argv); return 0; } 

for循环只是用0x42填充缓冲区的合法部分,这使得在debugging器中很容易看到溢出之前的位置。

debugging会话的摘录如下:

 (gdb) disas vulnerable Dump of assembler code for function vulnerable: 0x000000000040056c <+0>: push rbp 0x000000000040056d <+1>: mov rbp,rsp 0x0000000000400570 <+4>: sub rsp,0x220 0x0000000000400577 <+11>: mov DWORD PTR [rbp-0x214],edi 0x000000000040057d <+17>: mov QWORD PTR [rbp-0x220],rsi 0x0000000000400584 <+24>: mov DWORD PTR [rbp-0x4],0x0 0x000000000040058b <+31>: jmp 0x40059e <vulnerable+50> 0x000000000040058d <+33>: mov eax,DWORD PTR [rbp-0x4] 0x0000000000400590 <+36>: cdqe 0x0000000000400592 <+38>: mov BYTE PTR [rbp+rax*1-0x210],0x42 0x000000000040059a <+46>: add DWORD PTR [rbp-0x4],0x1 0x000000000040059e <+50>: cmp DWORD PTR [rbp-0x4],0x1ff 0x00000000004005a5 <+57>: jle 0x40058d <vulnerable+33> 0x00000000004005a7 <+59>: lea rax,[rbp-0x210] 0x00000000004005ae <+66>: mov rsi,rax 0x00000000004005b1 <+69>: mov edi,0x40070c 0x00000000004005b6 <+74>: mov eax,0x0 0x00000000004005bb <+79>: call 0x4003d8 <printf@plt> 0x00000000004005c0 <+84>: cmp DWORD PTR [rbp-0x214],0x1 0x00000000004005c7 <+91>: jle 0x4005e9 <vulnerable+125> 0x00000000004005c9 <+93>: mov rax,QWORD PTR [rbp-0x220] 0x00000000004005d0 <+100>: add rax,0x8 0x00000000004005d4 <+104>: mov rdx,QWORD PTR [rax] 0x00000000004005d7 <+107>: lea rax,[rbp-0x210] 0x00000000004005de <+114>: mov rsi,rdx 0x00000000004005e1 <+117>: mov rdi,rax 0x00000000004005e4 <+120>: call 0x4003f8 <strcpy@plt> 0x00000000004005e9 <+125>: mov eax,0x0 0x00000000004005ee <+130>: leave 0x00000000004005ef <+131>: ret 

我在strcpy()的调用之前断开,但在缓冲区填充0x42之后。

 (gdb) break *0x00000000004005e1 

程序以650 0x41作为参数执行,这应该足以覆盖堆栈上的返回地址。

 (gdb) run `perl -e 'print "A"x650'` 

我search内存的返回地址0x00400610(我发现从主反汇编中find)。

 (gdb) find $rsp, +1024, 0x00400610 0x7fffffffda98 1 pattern found. 

我用x / 200x检查内存,并得到一个很好的概述,因为它的大小,我在这里省略了,但是我可以清楚地看到0x42表示缓冲区的合法大小和返回地址。

 0x7fffffffda90: 0xffffdab0 0x00007fff 0x00400610 0x00000000 

strcpy()之后的新断点:

 (gdb) break *0x00000000004005e9 (gdb) set disassemble-next-line on (gdb) si 19 } => 0x00000000004005ee <vulnerable+130>: c9 leave 0x00000000004005ef <vulnerable+131>: c3 ret (gdb) ir rax 0x0 0 rbx 0x0 0 rcx 0x4141414141414141 4702111234474983745 rdx 0x414141 4276545 rsi 0x7fffffffe17a 140737488347514 rdi 0x7fffffffdb00 140737488345856 rbp 0x7fffffffda90 0x7fffffffda90 rsp 0x7fffffffd870 0x7fffffffd870 r8 0x1 1 r9 0x270 624 r10 0x6 6 r11 0x7ffff7b9fff0 140737349550064 r12 0x400410 4195344 r13 0x7fffffffdb90 140737488346000 r14 0x0 0 r15 0x0 0 rip 0x4005ee 0x4005ee <vulnerable+130> 0x00000000004005ee <vulnerable+130>: c9 leave => 0x00000000004005ef <vulnerable+131>: c3 ret (gdb) ir rax 0x0 0 rbx 0x0 0 rcx 0x4141414141414141 4702111234474983745 rdx 0x414141 4276545 rsi 0x7fffffffe17a 140737488347514 rdi 0x7fffffffdb00 140737488345856 rbp 0x4141414141414141 0x4141414141414141 rsp 0x7fffffffda98 0x7fffffffda98 r8 0x1 1 r9 0x270 624 r10 0x6 6 r11 0x7ffff7b9fff0 140737349550064 r12 0x400410 4195344 r13 0x7fffffffdb90 140737488346000 r14 0x0 0 r15 0x0 0 rip 0x4005ef 0x4005ef <vulnerable+131> (gdb) si Program received signal SIGSEGV, Segmentation fault. 0x00000000004005ee <vulnerable+130>: c9 leave => 0x00000000004005ef <vulnerable+131>: c3 ret (gdb) ir rax 0x0 0 rbx 0x0 0 rcx 0x4141414141414141 4702111234474983745 rdx 0x414141 4276545 rsi 0x7fffffffe17a 140737488347514 rdi 0x7fffffffdb00 140737488345856 rbp 0x4141414141414141 0x4141414141414141 rsp 0x7fffffffda98 0x7fffffffda98 r8 0x1 1 r9 0x270 624 r10 0x6 6 r11 0x7ffff7b9fff0 140737349550064 r12 0x400410 4195344 r13 0x7fffffffdb90 140737488346000 r14 0x0 0 r15 0x0 0 rip 0x4005ef 0x4005ef <vulnerable+131> 

我validation返回地址已被覆盖,我应该看到RIP被设置为这个地址:

 (gdb) x/4x 0x7fffffffda90 0x7fffffffda90: 0x41414141 0x41414141 0x41414141 0x41414141 (gdb) x/4x $rsp 0x7fffffffda98: 0x41414141 0x41414141 0x41414141 0x41414141 

然而RIP很明显:

 rip 0x4005ef 0x4005ef <vulnerable+131> 

为什么RIP没有得到更新,因为我期待? LEAVEQ和RETQ在64位上做了什么? 总之,我在这里错过了什么? 编译时我试图省略编译器参数,看看它是否有任何区别,似乎没有任何区别。

Solutions Collecting From Web of "在64位堆栈驻留缓冲区溢出?"

这两条指令正在做你期望他们做的事情。 你用0x41覆盖了以前的堆栈帧,所以当你点击leaveq ,你是这样做的:

 mov rsp, rbp pop rpb 

现在rsp指向之前的rbp 。 但是,您已经覆盖了该区域的内存,所以当您执行pop rbp ,硬件实质上是这样做的

 mov rbp, [rsp] add rsp,1 

[rsp]现在有0x41 。 所以这就是为什么你看到rbp得到充分的价值。

至于为什么rip没有像你期望的那样设置,这是因为retrip设置为0x41 ,然后在取指令上产生一个异常(页面错误)。 在这种情况下,我不会依靠GDB来展示正确的东西。 您应该尝试用程序文本段中的有效地址覆盖返回值,您可能不会看到这种奇怪的行为。

在x32上发生EIP 0×41414141崩溃的原因是,当程序从堆栈中弹出之前保存的EIP值并返回到EIP时,CPU将尝试执行内存地址0x 414141上的指令,这会导致段错误。 (它必须在执行课程之前取得页面)

现在,在执行x64程序时,程序将先前保存的RIP值弹回到RIP寄存器中,然后内核尝试执行内存地址为0×4141414141414141的指令。 首先,由于规范形式的寻址,任何虚拟地址的第48到63位必须是第47位的复制(类似于符号扩展),否则处理器将引发异常。 如果这不是问题 – 内核会在调用页面错误处理程序之前进行额外的检查,因为最大用户空间地址是0x00007FFFFFFFFFF。

回顾一下,在x32体系结构中,地址是在没有任何“验证”的情况下传递给页面错误处理程序的,该页面错误处理程序尝试加载触发内核发送程序段错误的页面,但是x64并没有得到如此的结果。

测试一下,用0x0000414141414141覆盖RIP,你会看到期望的值被放置在RIP中,因为内核预先检查通过,然后页面错误处理程序被调用,就像x32的情况(当然这会导致程序崩溃)。

由“kch”和“import os.boom.headshot”给出的答案是不完全正确的。

实际发生的事情是,要被RET指令弹出到RIP中的栈(0x4141414141414141)的值包含处理器的“非规范”地址范围内的地址。 这会导致CPU产生一般保护错误(GPF)中断,而不是由内核预先检查产生的错误。 在实际更新RIP之前,GPF会触发内核报告分段错误,这就是您在GDB中看到的内容。

大多数现代的CPU只提供一个48位的地址范围,它分别占据地址范围0x0000000000000000到0x00007FFFFFFFFFFF和0xFFFF800000000到0xFFFFFFFFFFFFFFFF的高一半和低一半。 请参阅此维基百科链接了解更多信息。

如果地址超出了非规范范围(0x00008FFFFFFFFFFF到0xFFFF7FFFFFFFFFFF),RIP将按照预期进行更新。 当然,如果新地址由于任何其他原因(即在进程的地址范围之外)是无效的,内核可能已经产生了后续故障。