我正在研究一些与安全有关的事情,现在我正在玩我自己的堆栈。我正在做的事应该非常简单,我甚至都没有尝试执行堆栈,只是为了表明我可以控制64位系统上的指令指针。我已经关闭了所有我能够使用它的保护机制(NX-bit,ASLR,也用-fno-stack-protector -z execstack编译)。 我没有那么多64位汇编的经验,在花了一些时间搜索和试验自己后,我想知道是否有人可以解释我遇到的问题。
我有一个程序(下面的源代码),它只是将一个字符串复制到一个没有边界检查的堆栈驻留缓冲区。但是,当我用一系列0x41覆盖时,我希望看到RIP设置为0x4141414141414141,而我发现我的RBP设置为此值。我确实遇到了分段错误,但是在执行RET指令时RIP不会更新为此(非法)值,即使RSP设置为合法值也是如此。我甚至在GDB中验证了在RET指令之前有一个可读存储器,其中包含一系列0x41的RSP。
我的印象是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位上做了什么?简而言之,我在这里失踪了什么?我在编译时试图省略编译器参数,看它是否有任何区别,它似乎没有任何区别。
答案 0 :(得分:6)
这两条指令完全符合您的期望。您已使用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来展示正确的东西。您应该尝试使用程序文本段中的有效地址覆盖返回值,您可能不会看到这种奇怪的行为。
答案 1 :(得分: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情况一样(当然,这会导致崩溃的程序。)
答案 2 :(得分:2)
“kch”和“import os.boom.headshot”给出的答案并不完全正确。
实际发生的是,RET指令要弹出到RIP的堆栈(0x4141414141414141)上的值包含一个处理器的“非规范”地址范围内的地址。这会导致CPU生成一般保护故障(GPF)中断,而不是内核预检查生成的故障。 GPF反过来会在RIP实际更新之前触发内核报告分段错误,这就是您在GDB中看到的内容。
大多数现代CPU仅提供48位地址范围,该地址范围分为较高和较低的一半,分别占用地址范围0x0000000000000000至0x00007FFFFFFFFFFF和0xFFFF800000000至0xFFFFFFFFFFFFFFFF。有关详细信息,请参阅this wikipedia link。
如果地址超出了非规范范围(0x00008FFFFFFFFFFF到0xFFFF7FFFFFFFFFFF),则RIP将按预期更新。当然,如果新地址由于任何其他原因(即在进程的地址范围之外)无效,则内核可能会生成后续错误。