Intuit Help System Protocol URL Heap Corruption and Memory Leak
Derek Soeder firstname.lastname@example.org
Reported to email@example.com on March 15, 2012; vendor did not respond. Reported to CERT on March 22, 2012; vendor did not respond. Responsible disclosure failed with error code 10060.
Published: March 30, 2012
QuickBooks 2009 through QuickBooks 2012, in conjunction with Microsoft Internet Explorer
Unknown: other products, versions, and Web browsers have not been tested
The Intuit Help System Async Pluggable Protocol ("intu-help-qb5:" in QuickBooks 2012), implemented in HelpAsyncPluggableProtocol.dll, is intended to provide access to ZIP-archived content stored on the local file system for use in displaying QuickBooks' help pages. A URL takes the following form:
Here, '#' is a digit specific to the installed version of QuickBooks, currently from 1 to 5; "path/archive.zip" is the path and file name of a ZIP archive; "file.ext" is a non-empty file to retrieve from the archive; and the ellipsis represents any number of optional query field name-value pairs which may follow the question mark. Note that if the "::" or "?" delimiter is missing, the process in which HelpAsyncPluggableProtocol.dll is parsing the URL will crash on a null pointer read. Due to a number of unchecked wcscpy_s and wcscat_s calls targeting fixed-size buffers, most paths exceeding 260 characters--whether supplied explicitly or constructed internally--will also cause the process to terminate.
For reference, QuickBooks 2012 recognizes the following query field names:
qbexepath ocdpath img_pkg css_pkg js_pkg ocd_imgpkg ocd_csspkg ocd_jspkg fileref
Upon receiving a URL, HelpAsyncPluggableProtocol.dll first creates a URL-decoded copy in a separate heap buffer. Decoding is performed by a very simple loop that assumes each '%' character will be followed by two hexadecimal digits, which it converts to an integer using MSVCR90!wcstoul. Even if the loop encounters a '%' sequence in either of the last two characters of the URL, it will still skip ahead by the two characters after the '%', meaning it will skip over the null terminator. The loop does not perform length checking, and so it will continue to copy characters and decode '%' sequences even after the bounds of the source and destination buffers have been exceeded. The destination buffer (on the private heap) may therefore be overflowed with the data following the source buffer (on the process heap).
One formidable complication in exploiting the overflow is that, while the URLs passed to HelpAsyncPluggableProtocol.dll are wide character, null-terminated strings (not BSTRs) allocated on the default process heap, the buffers that HelpAsyncPluggableProtocol.dll allocates internally with MSVCR90!malloc reside on a private heap belonging to MSVCR90.DLL. Consequently, it is important to understand the sequence of allocations, frees, and virtual calls pertaining to this heap. The following log attempts to summarize the relevant operations: (Note that all variable-size string buffers are allocated to the exact size necessary to hold the source string and null terminator. All strings are wide character unless noted otherwise.)
A = new[0x1C] -- vtables at +0x00 and +0x04 A->pvtbl_0->pfn_0__QueryInterface(A, IID_IInternetProtocolInfo, x) A->pvtbl_0->pfn_34(A, 1) delete A B = new[0x24] -- vtables at +0x00, +0x08, and +0x0C B->pvtbl_0->pfn_0__QueryInterface(B, IID_IUnknown, x) B->pvtbl_0->pfn_0__QueryInterface(B, IID_IInternetProtocol, x) B->pvtbl_8->pfn_4__AddRef(B+8) B->pvtbl_0->pfn_8__Release(B) B->pvtbl_8->pfn_4__AddRef(B+8) B->pvtbl_8->pfn_8__Release(B+8) B->pvtbl_8->pfn_0__QueryInterface(B+8, IID_IInternetPriority, x) B->pvtbl_0->pfn_0__QueryInterface(B, IID_IWinInetCacheHints, x) B->pvtbl_0->pfn_0__QueryInterface(B, IID_IInternetProtocolEx, x) B->pvtbl_8->pfn_C__IInternetProtocolRoot__Start(B+8, URL, x, x, x, x) C = new[0x18] -- no vtables D = malloc(0x208) -- persistent buffer for "<qbexepath>\<img_pkg>" E = malloc(0x208) -- persistent buffer for "<ocdpath>\<ocd_imgpkg>" F = malloc(0x208) -- persistent buffer for "fileref" value G = malloc(0x200) -- persistent buffer for MIME type H = malloc(x) -- buffer for URL-decoded URL URL decoding occurs, potential buffer overflow I = malloc(x) -- buffer for scheme ("intu-help-qb#") J = malloc(x) -- buffer for archive path and file name K = malloc(x) -- buffer for inner file path and name L = malloc(x) -- buffer for query string M1 = malloc(x) -- buffer for "qbexepath" value, if present M2 = malloc(x) -- buffer for "ocdpath" value, if present M3 = malloc(x) -- buffer for "img_pkg" value, if present M4 = malloc(x) -- buffer for "css_pkg" value, if present M5 = malloc(x) -- buffer for "js_pkg" value, if present M6 = malloc(x) -- buffer for "ocd_imgpkg" value, if present M7 = malloc(x) -- buffer for "ocd_csspkg" value, if present M8 = malloc(x) -- buffer for "ocd_jspkg" value, if present M9 = malloc(x) -- buffer for "fileref" value, if present free(L) free(I) free(J) free(K) free(M1) free(M2) free(M3) free(M4) free(M5) free(M6) free(M7) free(M8) free(M9) N = malloc(0x200) O = malloc(0x208) P = malloc(0x208) Q = malloc(0x208) free(P) R = new[0x18] -- vtable at +0x00 S = malloc(x) -- buffer for wide character archive path and file name T = malloc(x) -- buffer for wide character inner file path and name U = malloc(x) -- buffer for multibyte archive path and file name V = malloc(x) -- buffer for multibyte inner file path and name ---- if archive was opened successfully ---- W = malloc(0x404) -- destination buffer for MSVCR90!fread X = malloc(0x1000) -- MSVCR90!fread internal buffer free(W) -- if MSVCR90!fopen failed Y = malloc(0x80) ---- if inner file was found in archive ---- Z = malloc(0x6C) AA = malloc(0x4000) -- decompression buffer BB = malloc(x) -- buffer for entire decompressed file free(AA) free(Z) ---- free(X) free(Y) ---- free(U) free(V) R->pvtbl_0->pfn_4(R) ---- if archive was opened successfully ---- B->pvtbl_8->pfn_24__IInternetProtocol__Read(B+8, x, x, x) R->pvtbl_0->pfn_8(R, x, x, x) B->pvtbl_8->pfn_24__IInternetProtocol__Read(B+8, x, x, x) R->pvtbl_0->pfn_0(R) free(S) free(T) free(BB) R->pvtbl_0->pfn_C(R, 1) delete R ---- B->pvtbl_8->pfn_18__IInternetProtocolRoot__Terminate(B+8, x) free(N) free(O) free(P) -- if not freed above free(Q) free(H) free(D) free(E) free(G) free(F) delete C B->pvtbl_0->pfn_8__Release(B) B->pvtbl_0->pfn_C(B, 1) ---- if archive was not opened ---- R->pvtbl_0->pfn_C(R, 1) free(S) free(T) delete R ---- delete B
The heap buffer labeled "H" is the buffer into which the original URL is decoded. As it is assumed that the decoded URL may be the same length as or shorter than the original URL, "H" is allocated with a size of (wcslen(URL) * 2 + 2).
The "M" allocations can be performed in any order and can be repeated, based on the sequence of field name-value pairs in the query string, but each "M" free occurs only once. Therefore, if the query string contains multiple instances of a particular recognized name-value pair, only the buffer allocated for the last instance will be freed; this allows an attacker to leak arbitrarily sized blocks of private heap memory, up to roughly 4KB per malicious URL requested.
To summarize the above: The overflowable buffer, as well as some targets of interest, are maintained on a private heap used exclusively by MSVCR90.DLL, typically allocated and freed in roughly last-in, first-out order. The most interesting targets are class instances containing vtable pointers. Allocation of the overflowable buffer occurs fairly early during URL processing and is followed immediately by the vulnerable URL-decoding operation, meaning a successful attack must 1) target whatever heap block header follows the overflowable buffer; 2) cause the overflowable buffer to be allocated in a hole that precedes a heap block of interest; and/or 3) attempt to use asynchronous activity in the process to cause a worthwhile allocation on the private heap, after the overflowable buffer is allocated but before the overflow occurs. Abuse of other libraries that link to the same version of MSVCR90.DLL and make use of its private heap is not explored here.
Heap header overwrites are of course complicated by heap protections in modern versions of Windows (as MSVCR90!malloc uses Windows' heap management functions), and asynchronous activity sounds like more trouble than it's worth for this particular vulnerability, so this examination will focus on arranging for the overflowable buffer to precede some target block, more specifically a class instance. As depicted above, the only class instance that exists at the time of the overflow and uses a vtable is the one labeled "B". Its size is 0x24 bytes, which Windows rounds up to a 0x28-byte allocation (in a 32-bit environment); for comparison, the smallest URL that triggers the overflow without ensuring process termination--the null-terminated, wide character string "intu-help-qb#:::?%"--is 0x26 bytes, which also rounds up to 0x28 bytes.
Unfortunately, however, "B" is the first block to be allocated (other than the transient "A" block) and the last to be freed during URL processing, and therefore it will typically occupy the first sufficient free block. This means "B" will typically reside at the same address each time it's allocated, unless some separate functionality can be used to obstruct that address. Although query string processing confers the ability to allocate and either free or leak arbitrarily sized blocks in any order, these operations still happen after "B" is allocated and before it is freed; assuming classical heap behavior, they cannot help us. While sophisticated heap manipulation based on special behaviors of the Windows heap cannot be ruled out, it appears that this vulnerability accidentally defies at least simple and obvious exploitation.
If a null character is encountered in post-URL data before the buffer is overflowed, it prevents the overflow but may present an opportunity for information leakage. To avoid crashing the process, the fully-decoded URL would still have to contain "::" and "?" delimiters, meaning either the copied post-URL data must contain any missing delimiters in proper order, or else it must be appended to the query string. The only field currently believed to be transmissible to the attacker is the archive path and file name field (which precedes the "::" delimiter), and unfortunately this field is passed to wcstombs_s before the archive is accessed, either stripping the string of most useful information or stymieing the attack entirely if conversion fails.
As a final note, the latest version of HelpAsyncPluggableProtocol.dll supports SafeSEH but not ASLR, although its default image base address of 0x10000000 increases its likelihood of being relocated due to collision, assuming that other images sharing that base address are not themselves being relocated by ASLR. The DLL remains loaded once an "intu-help-qb#" URL has been accessed.
Delete, rename, or restrict read access to the "HKEY_LOCAL_MACHINE\SOFTWARE\[Wow6432Node\]Classes\PROTOCOLS\Handler\intu-help-qb#" registry key (where '#' is a digit from 1 to 5 as of this writing), or delete, rename, or restrict execute access to the "HelpAsyncPluggableProtocol.dll" file in the QuickBooks installation directory, and then restart Internet Explorer and any application that uses it as an embedded Web browser. Note that disabling the protocol will prevent QuickBooks from displaying help pages.
This document describes a heap corruption vulnerability in Intuit QuickBooks which could potentially be exploited via an Internet Explorer-borne attack in order to execute arbitrary code as the current user (and with low integrity, in the case of modern versions of Internet Explorer on supporting operating systems). Successful exploitation of the vulnerability has been neither proven nor ruled out, although creation of a simple and straightforward exploit appears unlikely.