64-bit resident buffer overflow?

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.

+4
source share
3 answers

These two instructions do exactly what you expect from them. You have overwritten the previous stack stack of 0x41 , so when you press leaveq , you do this:

 mov rsp, rbp pop rpb 

Now rsp indicates where rbp done this before. However, you have overwritten this memory area, so when you execute pop rbp , the hardware essentially does this

 mov rbp, [rsp] add rsp,1 

But [rsp] now has 0x41 . This is why you see that rbp populated with this value.

Why is rip not set as you expect, because ret sets rip to 0x41 , and then throws an exception (page error) in the command selection. I would not rely on GDB to show the right thing in this case. You should try to rewrite the return value with a valid address in the text segment of the program, and you probably won't see this strange behavior.

+6
source

The reason you get a 0 Γ— 41414141 EIP failure on x32 is because when a program pops a previously saved EIP value from the stack and back to EIP, the CPU then tries to execute a command at memory address 0 Γ— 41414141, which causes segfault. (he must get the page before completing the course)

Now, at runtime x64, when the program pulls the previously saved RIP value back to the RIP register, the kernel then tries to execute instructions at the memory address 0 Γ— 4141414141414141. First, due to access to the canonical form, bits 48 through 63 of any virtual addresses must be copies of bit 47 (in a manner close to character expansion), or the processor throws an exception. If this is not a problem, the kernel performs additional checks before calling the page error handler, since the maximum address of the user space is 0x00007FFFFFFFFFF.

To remind you that in the x32 architecture, the address is passed without any β€œcheck” to the page error handler, which tries to load the page that launches the kernel to send the segfault program, but x64 does not achieve this.

Check this, rewrite RIP from 0 Γ— 0000414141414141, and you will see that the expected value is placed in RIP, because the preliminary frames are through the kernel pass and then the page error handler is called, like the x32 case (which, of course, causes the program to crash) .

+2
source

The answers given by "kch" and "import os.boom.headshot" are not entirely correct.

What actually happens is that the value on the stack (0x414141414141414141), which must be written to RIP with the RET command, contains an address that is in the "non-canonical" range of processor addresses. This causes the CPU to generate a general protection interrupt (GPF) rather than an error caused by a preliminary kernel scan. The GPF, in turn, starts the kernel to report segmentation failure before the RIP is actually updated, and this is what you see in GDB.

Most modern processors only provide a 48-bit address range that is split between the higher and lower half, which occupy address ranges from 0x0000000000000000 to 0x00007FFFFFFFFFFF and 0xFFFF800000000 to 0xFFFFFFFFFFFFFFFF, respectively. For more information, see this link on Wikipedia .

If the address was outside the non-canonical range (0x00008FFFFFFFFFFFFF to 0xFFFF7FFFFFFFFFFF), then the RIP would be updated as expected. Of course, a subsequent error could be generated by the kernel if the new address was invalid for any other reason (that is, outside the process address range).

+2
source

All Articles