I learn some things related to security, and now I play with my own stack. What I am doing should be very trivial, I am not even trying to execute the stack, just to show that I can get control of the pointer on my 64-bit system. I disabled all the protection mechanisms that I know of in order to be able to play with it (NX-bit, ASLR, as well as compilation with -fno-stack-protector -z execstack). I donβt have so much experience working with 64-bit builds, and after spending some time searching and experimenting, I wonder if anyone can shed light on the problem that I am having.
I have a program (source code below) that simply copies a string to the resident buffer of the stack without checking the bounds. However, when I overwrite the 0x41 series, I expect the RIP to be set to 0x414141414141414141, instead, I found that my RBP gets this value. I get a segmentation error, but the RIP is not updated to this (illegal) value when executing the RET instruction, even if the legal value is set for RSP. I even checked in GDB that there is readable memory containing the 0x41 series in RSP just before the RET instruction.
I got the impression that the LEAVE instruction did:
MOV (E) SP, (E) BP
POP (E) BP
However, in the 64-bit instruction, the LEAVEQ command seems to do (similarly):
MOV RBP, QWORD PTR [RSP]
I think this is done simply by observing the contents of all the registers before and after executing this instruction. LEAVEQ seems to be just a context-sensitive RET command name (which makes it a GDB disassembler), since it is still only 0xC9.
And the RET team seems to be doing something with the RBP register, perhaps playing it out? I got the impression that RET (looks like):
MOV RIP, QWORD PTR [RSP]
However, as I already mentioned, this seems to play out RBP, I think it happens because I get a segmentation error when no other register contains an invalid value.
Source code of the program:
#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; }
The for loop only serves to fill the legal part of the 0x42 buffer, which makes it easy to see where it is in the debugger until it overflows.
The following is a snippet of debugging:
(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
I crashed just before calling strcpy (), but after the buffer was full 0x42.
(gdb) break *0x00000000004005e1
The program is executed with argument 650 0x41 as an argument, this should be a lot to overwrite the return address on the stack.
(gdb) run `perl -e 'print "A"x650'`
I am looking for memory for the return address 0x00400610 (which I found looking at the disassembly of the main).
(gdb) find $rsp, +1024, 0x00400610 0x7fffffffda98 1 pattern found.
I scan the memory using x / 200x and get a good overview, which I missed here because of its size, but I can clearly see 0x42, which indicates the legal size of the buffer and the return address.
0x7fffffffda90: 0xffffdab0 0x00007fff 0x00400610 0x00000000
New breakpoint immediately after strcpy ():
(gdb) break *0x00000000004005e9 (gdb) set disassemble-next-line on (gdb) si 19 } => 0x00000000004005ee <vulnerable+130>: c9 leave 0x00000000004005ef <vulnerable+131>: c3 ret (gdb) ir 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) ir 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) ir 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>
I verify that the return address has been rewritten and I should have expected RIP to get this address:
(gdb) x/4x 0x7fffffffda90 0x7fffffffda90: 0x41414141 0x41414141 0x41414141 0x41414141 (gdb) x/4x $rsp 0x7fffffffda98: 0x41414141 0x41414141 0x41414141 0x41414141
However, the RIP is explicitly:
rip 0x4005ef 0x4005ef <vulnerable+131>
Why hasn't the RIP updated as I expect? What does LEAVEQ and RETQ do at the 64-bit level? In short, what am I missing here? I tried to omit the compiler arguments at compile time to make sure that it has any meaning, but it does not matter.