我正在研究一些与安全性相关的东西,现在我正在玩自己的堆栈。我正在做的事情应该是微不足道的,我什至没有试图执行堆栈,只是为了表明我可以控制我的64位系统上的指令指针。我已经关闭了所有我知道的保护机制,以便能够使用它(NX位,ASLR,还可以使用- fno-stack-protector -z execstack进行编译)。我在64位汇编方面没有太多经验,花了一些时间进行搜索和实验后,我想知道是否有人可以阐明我遇到的问题。
我有一个程序(下面的源代码),可以简单地将字符串复制到堆栈驻留缓冲区中,而无需进行边界检查。但是,当我用一系列0x41覆盖时,我期望看到RIP设置为0x4141414141414141,相反,我发现我的RBP设置为该值。我确实遇到了分段错误,但是即使将RSP设置为合法值,RIP也不会在执行RET指令时更新为该(非法)值。我什至在GDB中验证了在RET指令之前的RSP处有一个包含一系列0x41的可读存储器。
我给人的印象是LEAVE指令可以做到:
MOV(E)SP,(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(通过查看主程序的反汇编找到了该地址)。
(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,您将执行以下操作:
0x41
leaveq
mov rsp, rbp pop rpb
现在rsp指向rbp以前的操作。但是,您已经覆盖了该内存区域,因此当您执行时pop rbp,硬件实际上就是在这样做
rsp
rbp
pop rbp
mov rbp, [rsp] add rsp,1
但是[rsp]现在有了0x41。因此,这就是为什么您看到rbp被该值填充的原因。
[rsp]
至于为什么rip没有像您期望的那样进行设置,那是因为ret将设置rip为0x41,然后在指令提取时生成异常(页面错误)。在这种情况下,我不会依靠GDB来显示正确的内容。您应该尝试使用程序文本段中的有效地址覆盖返回值,并且您可能不会看到这种奇怪的行为。
rip
ret