在64位堆栈驻留缓冲区溢出
我正在研究一些与安全有关的事情,现在我正在玩我自己的堆栈。 我所做的应该是非常简单的,我甚至不试图执行堆栈,只是为了表明我可以控制64位系统上的指令指针。 我已经关闭了我所知道的所有保护机制,只是为了能够使用它(NX-bit,ASLR,还使用-fno-stack-protector -z execstack编译)。 我在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]
我认为这只是在执行这条指令之前和之后观察所有寄存器的内容。 虽然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 %pn",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 <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'`
我在内存中搜索返回地址0x00400610(我从查看main的反汇编中找到)。
(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位上做了什么? 总之,我在这里错过了什么? 编译时我试图忽略编译器参数,看看它是否有任何区别,但似乎没有任何区别。
  这两条指令正在做你期望他们做的事。  你用0x41覆盖了以前的堆栈帧,所以当你点击leaveq ,你是这样做的: 
mov rsp, rbp
pop rpb
  现在rsp指向rbp之前的位置。  但是,您已覆盖该区域的内存,因此当您执行pop rbp ,硬件基本上正在执行此操作 
mov rbp, [rsp]
add rsp,1
  但[rsp]现在有0x41 。  所以这就是为什么你看到rbp得到充分的价值。 
  至于为什么rip没有像你期望的那样被设置,这是因为ret将rip设置为0x41 ,然后在取指令上产生异常(页面错误)。  在这种情况下,我不会依赖GDB来展示正确的东西。  您应该尝试用程序文本段中的有效地址覆盖返回值,您可能不会看到这种奇怪的行为。 
在x32上发生EIP 0×41414141崩溃的原因是,当程序弹出之前保存的EIP值离开堆栈并返回EIP时,CPU会尝试执行存储器地址0x 41414141处的指令,这会导致段错误。 (它必须在执行课程之前获取页面)
现在,在x64执行过程中,当程序弹出之前保存的RIP值返回到RIP寄存器时,内核会尝试执行内存地址0x 4141414141414141处的指令。 首先,由于规范形式寻址,任何虚拟地址的第48到63位必须是第47位的复制(类似于符号扩展),否则处理器将引发异常。 如果这不是问题 - 由于最大用户空间地址是0x00007FFFFFFFFFF,内核在调用页面错误处理程序之前会执行额外的检查。
回顾一下,在x32体系结构中,地址是在没有任何“验证”的情况下传递给页面错误处理程序的,该页面错误处理程序试图加载触发内核发送程序segfault的页面,但是x64并没有得到这么多。
测试它,使用0×0000414141414141覆盖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将按预期进行更新。 当然,如果新地址由于任何其他原因(即在进程的地址范围之外)无效,内核可能会产生后续故障。
链接地址: http://www.djcxy.com/p/87129.html