Intuit Help System Protocol URL Heap Corruption and Memory Leak
Derek Soeder
ds.adv.pub@gmail.com
Reported to security@intuit.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
AFFECTED VENDOR
---------------
Intuit, Inc.
AFFECTED ENVIRONMENTS
---------------------
QuickBooks 2009 through QuickBooks 2012, in conjunction with Microsoft
Internet Explorer
UNAFFECTED ENVIRONMENTS
-----------------------
Unknown: other products, versions, and Web browsers have not been tested
IMPACT
------
The vulnerability described in this document can potentially be
exploited by malicious HTML and/or Javascript to execute arbitrary
code as the user viewing the malicious content.
VULNERABILITY DETAILS
---------------------
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:
intu-help-qb#:path/archive.zip::file.ext?...
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).
EXPLOITATION
------------
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.
For the sake of completeness, the problem of crafting the source data
deserves mention. The URL passed to HelpAsyncPluggableProtocol.dll is
a null-terminated string which cannot contain embedded nulls--any
attempt to include a null character simply terminates the URL that
HelpAsyncPluggableProtocol.dll receives. Other than nulls and
unmatched surrogates (0xD800..0xDBFF not followed by 0xDC00..0xDFFF,
or the latter not preceded by the former), the URL can be constructed
to contain any desired characters, using either "\uXXXX" sequences or
the "unescape" function in conjunction with "%uXXXX" encodings in
Javascript. However, because the decoding loop becomes indefinite
only after a "%<nul>" or "%X<nul>" sequence is encountered (which must
happen at the very end of the original URL, because nulls cannot be
embedded), and because the overflowable buffer is allocated to the
same apparent size as the original URL string, the fact is that the
overflow data cannot be included in the URL--the process heap must be
manipulated so that the URL string passed to
HelpAsyncPluggableProtocol.dll is followed shortly by the overflow
data. The overflow data need not be immediately adjacent--additional
"%XX" sequences can be placed within the URL to increase the distance
before the end of the destination buffer where post-URL data begins
filling the buffer.
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.
MITIGATION
----------
* Disable the Intuit Help System protocol
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.
CONCLUSION
----------
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.
GREETINGS
---------
www.ridgewayis.com
www.ftmband.com