2012-07-16 63 views
4

我正在学习一些安全相关的东西,现在我正在玩我自己的堆栈。我所做的应该是非常简单的,我甚至不试图执行堆栈,只是为了表明我可以控制64位系统上的指令指针。我已经关闭了我所知道的所有保护机制,只是为了能够使用它(NX-bit,ASLR,还使用-fno-stack-protector -z execstack编译)。 我对64位程序集没有那么多的经验,花了一些时间搜索和试验自己之后,我想知道是否有人能够解释我遇到的问题。在64位堆栈驻留缓冲区溢出?

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

我的印象是,leave指令做了下:

MOV(E)SP,(E)BP

POP(E)BP

但是在64位,在“ LEAVEQ”指令似乎做(类似):

MOV RBP,QWORD PTR [RSP]

我想它确实这只是从之前遵守所有寄存器的内容d执行此指令后。虽然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的缓冲,这使得它很容易在它的调试器看到法律的一部分,溢出前。

摘录调试会话如下:

(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 <[email protected]> 
    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 <[email protected]> 
    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'` 

我搜索返回地址0x00400610内存(这是我从看的主拆卸找到)。

(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) i r 
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) i r 
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) i r 
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> 

我确认返回地址已被覆盖,我应该有希望看到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位上做了什么?总之,我在这里错过了什么?编译时我试图忽略编译器参数,看看它是否有任何区别,但似乎没有任何区别。

回答

6

这两条指令正在做你期望他们做的事情。已覆盖前一个堆栈帧与0x41的,所以当你打leaveq,你这样做:

mov rsp, rbp 
pop rpb 

现在rsp点到rbp以前那样。然而,已覆盖的内存区域,所以当你做pop rbp,硬件基本上是这样

mov rbp, [rsp] 
add rsp,1 

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

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

+0

我在想知道leaveq如何“知道”释放先前分配的堆栈空间时发现了您的答案。你的解释清楚了。 SP更改为BP,删除任何堆栈分配,然后弹出一个新的BP,它在该函数启动时被弹出的位置。谢谢。 – suprjami 2014-09-13 15:05:42

2

您在x32上遇到EIP 0×41414141崩溃的原因是因为当程序弹出之前保存的EIP值离开堆栈并返回EIP时,CPU会尝试执行内存地址0x 41414141处的指令,导致段错误。 (它必须在执行过程之前获取页面)

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

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

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

2

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

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

大多数现代的CPU只提供一个48位的地址范围,它分别占据地址范围0x0000000000000000到0x00007FFFFFFFFFFF和0xFFFF800000000到0xFFFFFFFFFFFFFFFF的上半部和下半部。有关更多信息,请参阅this wikipedia link

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