【问题标题】:Stack resident buffer overflow on 64-bit?64位堆栈驻留缓冲区溢出?
【发布时间】:2012-07-15 10:44:43
【问题描述】:

我正在研究一些与安全相关的东西,现在我正在玩我自己的堆栈。我所做的应该很简单,我什至没有尝试执行堆栈,只是为了表明我可以控制我的 64 位系统上的指令指针。我已经关闭了所有我知道的保护机制,只是为了能够使用它(NX-bit,ASLR,也使用 -fno-stack-protector -z execstack 编译)。 我在 64 位汇编方面没有太多经验,在花了一些时间搜索和试验自己之后,我想知道是否有人可以对我遇到的问题有所了解。

我有一个程序(下面的源代码),它只是将一个字符串复制到一个堆栈驻留缓冲区中,而不进行边界检查。但是,当我用一系列 0x41 覆盖时,我希望看到 RIP 设置为 0x4141414141414141,但我发现我的 RBP 设置为这个值。我确实遇到了分段错误,但 RIP 在执行 RET 指令时不会更新到这个(非法)值,即使 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 %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 <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 位上真正做了什么?简而言之,我在这里缺少什么?我试图在编译时省略编译器参数只是为了看看它是否有任何区别,它似乎没有任何区别。

【问题讨论】:

    标签: linux gdb 64-bit stack-overflow callstack


    【解决方案1】:

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

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

    大多数现代 CPU 仅提供 48 位地址范围,该地址范围分为上半部分和下半部分,分别占据地址范围 0x0000000000000000 到 0x00007FFFFFFFFFFF 和 0xFFFF800000000 到 0xFFFFFFFFFFFFFFFF。请参阅this wikipedia link 了解更多信息。

    如果地址超出了非规范范围(0x00008FFFFFFFFFFF 到 0xFFFF7FFFFFFFFFFF),那么 RIP 将按预期更新。当然,如果新地址由于任何其他原因(即超出进程的地址范围)无效,则内核可能会生成后续错误。

    【讨论】:

      【解决方案2】:

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

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

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

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

      【讨论】:

        【解决方案3】:

        这两条指令完全符合您的预期。您已经用0x41 覆盖了之前的堆栈帧,所以当您点击leaveq 时,您正在这样做:

        mov rsp, rbp
        pop rpb
        

        现在rsp 指向rbp 之前所做的位置。但是,您已经覆盖了该内存区域,因此当您执行pop rbp 时,硬件实际上是在执行此操作

        mov rbp, [rsp]
        add rsp,1
        

        但是[rsp] 现在有0x41。这就是为什么您会看到 rbp 被该值填充。

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

        【讨论】:

        • 我在想知道 leaveq 如何“知道”释放先前分配的堆栈空间时找到了您的答案。你的解释清楚了。 SP 更改为 BP,删除任何堆栈分配,然后从堆栈中弹出一个新的 BP,它在函数启动时已被弹出。谢谢。
        猜你喜欢
        • 1970-01-01
        • 2017-02-23
        • 1970-01-01
        • 1970-01-01
        • 2010-11-11
        • 1970-01-01
        • 2015-07-03
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多