armgdbbuffer-overflowarm64

ARM64 Buffer Overflow-Cannot overwrite $pc


This is the source code.

#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

Assembly code for main

0x0000000000400604 <+0>:    stp x29, x30, [sp, #-96]!
0x0000000000400608 <+4>:    mov x29, sp
0x000000000040060c <+8>:    str w0, [sp, #28]
0x0000000000400610 <+12>:   str x1, [sp, #16]
0x0000000000400614 <+16>:   add x0, sp, #0x20
0x0000000000400618 <+20>:   bl  0x4004d0 <gets@plt>
0x000000000040061c <+24>:   mov w0, #0x0                    // #0
0x0000000000400620 <+28>:   ldp x29, x30, [sp], #96
0x0000000000400624 <+32>:   ret

Assembly code for win

0x00000000004005e4 <+0>:    stp x29, x30, [sp, #-16]!
0x00000000004005e8 <+4>:    mov x29, sp
0x00000000004005ec <+8>:    adrp    x0, 0x400000
0x00000000004005f0 <+12>:   add x0, x0, #0x6e0
0x00000000004005f4 <+16>:   bl  0x4004c0 <puts@plt>
0x00000000004005f8 <+20>:   nop
0x00000000004005fc <+24>:   ldp x29, x30, [sp], #16
0x0000000000400600 <+28>:   ret

The source code is from protostar-stack4. As far as I know, you have to overwrite $pc to run the win function. So I tried to overwrite $pc, but I can't. I put a breakpoint at main+32 and ran it with 100*a, but neither $pc nor $x30 has been overwritten. What should I do to overwrite $pc? Am I even on the right path? Please help.

$x0  : 0x0               
$x1  : 0x0000fffff7fb1290  →  0x0000000000000000
$x2  : 0xfbad2288        
$x3  : 0x0000fffff7fae8d0  →  0x00000000fbad2288
$x4  : 0x6161616161616161 ("aaaaaaaa"?)
$x5  : 0x0000fffffffff384  →  0x0000000000000000
$x6  : 0x6161616161616161 ("aaaaaaaa"?)
$x7  : 0x6161616161616161 ("aaaaaaaa"?)
$x8  : 0x6161616161616161 ("aaaaaaaa"?)
$x9  : 0x6161616161616161 ("aaaaaaaa"?)
$x10 : 0x6161616161616161 ("aaaaaaaa"?)
$x11 : 0x6161616161616161 ("aaaaaaaa"?)
$x12 : 0x6161616161616161 ("aaaaaaaa"?)
$x13 : 0x6161616161616161 ("aaaaaaaa"?)
$x14 : 0x6161616161616161 ("aaaaaaaa"?)
$x15 : 0x6161616161616161 ("aaaaaaaa"?)
$x16 : 0x1               
$x17 : 0x6161616161616161 ("aaaaaaaa"?)
$x18 : 0x0               
$x19 : 0x0000000000400630  →  <__libc_csu_init+0> stp x29,  x30,  [sp,  #-64]!
$x20 : 0x0               
$x21 : 0x00000000004004e0  →  <_start+0> mov x29,  #0x0                     // #0
$x22 : 0x0               
$x23 : 0x0               
$x24 : 0x0               
$x25 : 0x0               
$x26 : 0x0               
$x27 : 0x0               
$x28 : 0x0               
$x29 : 0x0000fffffffff360  →  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$x30 : 0x0000fffff7e62218  →  <__libc_start_main+232> bl 0xfffff7e77d00 <__GI_exit>
$sp  : 0x0000fffffffff360  →  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$pc  : 0x0000000000400624  →  <main+32> ret 
$cpsr: [negative ZERO CARRY overflow interrupt fast]
$fpsr: 0x0               
$fpcr: 0x0               

What should I do?


Solution

  • As you can see from the code, the compiler has placed the return address below the buffer on the stack, so it is impossible for you to overwrite it, no matter how many bytes you write.

    Specifically, stp x29, x30, [sp, #-96]! is pre-decrement, so it stores x29 at the new address of [sp], and x30, which contains the return address, at [sp, 8]. The buffer, on the other hand, is at [sp, 32] (note the add x0, sp, 0x20). This is typical for ARM64; the pre-decrement addressing mode makes it convenient to store the return address at the bottom of the stack frame, below all the function's local variables.

    What you may be able to overwrite instead is the return address saved by whatever function called main (somewhere in the C startup code, e.g. glibc's __libc_start_main()). That would only happen when that other function returns to its own caller, so you have stopped the program too soon.

    Unfortunately for you, on many systems, that function does not return at all; it calls exit() instead. That's what glibc's __libc_start_main does.. As such, unless I am missing something, this kind of buffer overflow will not work on such a system when gets is called from within main. If you want to play with it, try writing a different program where it's called from some subroutine:

    void other_func(void) {
        char buf[64];
        gets(buf);
    }
    
    int main(void) {
        other_func();
        return 0;
    }
    

    Now your overflow won't overwrite the return address stored by other_func() (which is again below the buffer), but it can overwrite the return address stored by main() (which is further up the stack). You'll get control, not when other_func() returns, but when main() returns. (Even so, that's after its ret instruction executes; placing a breakpoint on the ret instruction from main is still too soon.)

    It looks like this exercise was intended for x86, where the return address is normally at the top of the stack frame, since it's saved by the call instruction which executes before the stack frame is set up. On an x86 system your overflow would indeed overwrite the return address saved by main, and give you control over the program counter.