ROP chains on ARM64
Last updated
Last updated
So we have already seen buffer overflows in ARM64, Let's now write rop chains on ARM64.
This is similar to that of the ROP chains we did in ARM32. if you understood ROP chains in the ARM32 chapter. This would be very easy for you. if not, please revise the "Introduction to ROP chains" chapter. I will not be explaining this step by step because I have already covered that. This is exactly similar. The only change is that we have new ARM64 assembly instructions.
So let's start.
So let's take the vulnerable binary from before (previous chapter) and compile it by removing the "-z execstack" flag.
(ps: I removed the system call from the source code)
gcc bof.c -fno-stack-protector -o bof
Also, make sure ASLR is off.
Let's just execute the binary.
So the binary is running fine. So let's find the gadgets using ropper.
For this, I copied the "libc" library from the emulated environment to the host using bashupload just like before.
Now, let's fire up ropper and search for the gadgets.
Just like in the previous chapter we are trying to execute the system function with "/bin/sh" as the argument so that we get a shell.
So we need the exact gadgets that can help us.
We need to find the gadgets that load values from the stack and copy them to certain registers. For this, we can explore MOV, LDR, and LDP gadgets.
After searching for the time. I found some interesting gadgets. I will list them below.
From this, I looked for an LDP gadget that can load the addresses of the system function and the "/bin/sh" string.
Then I found a very suitable LDP gadget.
So the gadget is
0x000b56d4: ldp x21, x30, [sp, #0x10]; ldp x19, x20, [sp], #0x20; ret;
So we have already discussed what this instruction does. it will load two values from the stack to the corresponding registers.
In short, if we execute the first instruction
ldp x21, x30, [sp, #0x10];
x21 = [sp + 16]
x30 = [sp + 16 + 8]
sp remains unchanged
if we execute the second instruction
ldp x19, x20, [sp], #0x20;
x19 = [sp]
x20 = [sp + 8]
sp = sp + 0x20
And when 'ret' is encountered it will return its control back to the address at x30.
But we know according to the calling convention of ARM64, the arguments passed to a function are through registers x0 to x7.
So the first and only argument of our system function (address of the '/bin/sh' string) should be loaded into r0.for that, I looked for 'MOV' instructions that can copy the values from the registers that we loaded using LDP instruction.
And after some digging, I found a useful gadget.
As you can see here, there's no 'ret' in this instruction. but still, we can use that because the blr instruction is branching to the x20 register which can be controlled using our first gadget.
Always try to make use of gadgets to their maximum capability rather than adding new gadgets. This will make our ROP chain easier and cleaner.
So let me talk you through the exploit plan.
Firstly let's get the address of the system function, libc library, and "/bin/sh" string.
Let's load the binary into gdb, put a breakpoint at the main, and run the program.
When the breakpoint is hit. Obtain the system address using the 'print' or '' disass' command.
So the address of the system is 0x0000ffffb7ec9898.
Now use the vmmap command to get the base address of the libc library.
So libc address = 0x0000ffffb7e8c000
And finally, we need to get the address of the '/bin/sh' string, we can use 'grep' for that.
grep /bin/sh
Address of /bin/sh = 0xffffb7fa2ac8
Now let's think of the best way to place the addresses in our gadgets.
So in the first gadget
ldp x21, x30, [sp, #0x10]; ldp x19, x20, [sp], #0x20; ret;
x21 should contain the address of the "/bin/sh" string because it will be copied to r0 in the second gadget.
Similarly, x30 should be loaded with the address of the next gadget. So, when 'ret' is hit it will return its control back to the address in the x30.
And x20 should contain the address of the system because it's the only way to call the system function in our gadgets. The 'blr' instruction will be used to call the system function. The return of the first gadget will be used to point to the second gadget.
In short,
x21 = address(system)
x20 = address("/bin/sh") will be moved to x0 (using the second gadget : mov x0,x21)
x30 = address(gadget_two)
Now let's start writing our script.
nano ropchain.py
Firstly, let's fill up our buffer to overflow the pc.
Let's now add the address of the system, the"/bin/sh" string, and libc using the struct module.
We have already seen how to use the struct module in the previous ROP chain chapter.
But in this case, it's 64-bit so instead of "<L" we need to use "Q".
Let's add our gadgets now.
Now we have to add some junk characters because in the first gadget it's loading values from [sp + 16] and [sp + 16 + 8].
So we need to check first So I printed out the junk and gadget_one to add that to our exploit.
Now I used gdbserver and gef to debug this.
I opened two tabs and ssh'ed into both tabs and created a remote session using gdbserver.
Now let's debug this by putting a breakpoint at the ret instruction at the end of the main function.
As we can see, we need to fill the stack with junk characters to control the region with our addresses so that it can be loaded into the registers.
Let's find out how many characters we need to add to reach this region.
So the value in 0x0000fffffffff4c0 will be loaded into x21 and the value in 0x0000fffffffff4c8 will be loaded into x30.
Let's use examine the stack using the command (x).
Let's add 32 junk characters to fill these 4 blocks and add the address of the "/bin/sh" string and also add 8 junk characters to fill the register x30 for now.
At the end, we will replace the junk characters with the address of the gadget_two in x30.
After executing the script and debugging using gdb
As we expected we filled x21 with the address of the "/bin/sh" string and x30 with 8 'A' s.
As for the next instruction (ldp x19, x20, [sp], #32). it will load values from [sp] and [sp + 8] into x19 and x20.
So the values (8 A's) from 0x0000fffffffff4b0 and 0x0000fffffffff4b8 will be loaded into x19 and x20.
0x0000fffffffff4b0 can be filled with junk because the x19 register is not useful to us.
0x0000fffffffff4b8 should be filled with the address of the string (is loaded into the x20 register).
So if we execute this, x19 and x20 will be filled with 8 'A' s.
As expected, x19 and x20 are filled with "A"s.
Let's find where 0x0000fffffffff4b0 and 0x0000fffffffff4b8 are in the stack using the examine command.
Now we found where 0x0000fffffffff4b0 and 0x0000fffffffff4b8 lie.
So let's rewrite our exploit.
Don't forget to add the address of the second gadget into x30.
Now let's see the layout of our exploit in our stack.
Let's run this in gdb with the help of gdbserver.
If you are confused about finding the offset of the registers from the stack memory you can use a pattern offset generator.
https://zerosum0x0.blogspot.com/2016/11/overflow-exploit-pattern-generator.html
Let's finally run our exploit outside gdb for the first time.
If everything is properly aligned we will get a shell.
user@ubuntu1604-aarch64:~$ (python ropchain.py ; cat ) | ./bof
So finally we got our beautiful shell using ROP chains.
Let me show you another rop chain that I made a while ago.
This is very messy but still works.
So that brings the end of our ROP Chain journey.