A specially crafted HTTP response can allow a malicious web-page to trigger a out-of-bounds read vulnerability in Google Chrome. The data is read from the main process' memory.
Google Chrome up to, but not including, 31.
An attacker would need to get a target user to open a specially crafted web-page. Disabling JavaScript does not prevent an attacker from triggering the vulnerable code path, but may prevent exfiltration of information.
Since the affected code has not been changed since 2009, I assume this affects all versions of Chrome released in the last few years.
The HttpStreamParser
class
is used to send HTTP requests and receive HTTP responses. Its read_buf_
member is a buffer used to store HTTP response data received from the server.
Parts of the code are written under the assumption that the response currently
being parsed is always stored at the start of this buffer (as returned by
read_buf_->StartOfBuffer()
), other parts take into account that this may not
be the case (read_buf_->StartOfBuffer() + read_buf_unused_offset_
). In most
cases, responses are removed from the buffer once they have been parsed and any
superfluous data is moved to the beginning of the buffer, to be treated as part
of the next response. However, the code special cases HTTP 1xx
replies and
returns a result without removing the request from the buffer. This means that
the response to the next request will not be stored at the start of the buffer,
but after this HTTP 1xx
response and read_buf_unused_offset_
should be used
to find where it starts.
The code that special cases HTTP 1xx responses is:
if (end_of_header_offset == -1) {
<<<snip>>>
} else {
// Note where the headers stop.
read_buf_unused_offset_ = end_of_header_offset;
if (response_->headers->response_code() / 100 == 1) {
// After processing a 1xx response, the caller will ask for the next
// header, so reset state to support that. We don't just skip these
// completely because 1xx codes aren't acceptable when establishing a
// tunnel.
io_state_ = STATE_REQUEST_SENT;
response_header_start_offset_ = -1;
<<<Note: the code above does not remove the HTTP 1xx response from the
buffer.>>>
} else {
<<<Note: the code that follows either removes the response from the buffer
immediately, or expects it to be removed in a call to
ReadResponseBody later.>>>
<<<snip>>>
return result;
}
A look through the code has revealed one location where this can lead to a
security issue (also in DoReadHeadersComplete
). The code uses an offset from
the start of the buffer (rather than the start of the current responses) to
pass as an argument to a DoParseResponseHeaders
.
if (result == ERR_CONNECTION_CLOSED) {
<<<snip>>>
// Parse things as well as we can and let the caller decide what to do.
int end_offset;
if (response_header_start_offset_ >= 0) {
io_state_ = STATE_READ_BODY_COMPLETE;
end_offset = read_buf_->offset();
<<<Note: "end_offset" is relative to the start of the buffer>>>
} else {
io_state_ = STATE_BODY_PENDING;
end_offset = 0;
<<<Note: "end_offset" is relative to the start of the current response
i. e. start + read_buf_unused_offset_.>>>
}
int rv = DoParseResponseHeaders(end_offset);
<<<snip>>>
DoParseResponseHeaders
passes the argument unchanged to HttpUtil::
:
int HttpStreamParser:: DoParseResponseHeaders(int end_offset) {
scoped_refptr<HttpResponseHeaders> headers;
if (response_header_start_offset_ >= 0) {
headers = new HttpResponseHeaders(HttpUtil:: AssembleRawHeaders(
read_buf_->StartOfBuffer() + read_buf_unused_offset_, end_offset));
<<<snip>>>
The HttpUtil::
method
takes two arguments: a pointer to a buffer, and the length of the buffer. The
pointer is calculated correctly (in DoParseResponseHeaders
) and points to the
start of the current response. The length is the offset that was calculated
incorrectly in DoReadHeadersComplete
. If the current response is preceded by
a HTTP 1xx
response in the buffer, this length is larger than it should be:
the calculated value will be the correct length plus the size of the previous
HTTP 1xx
response (read_buf_unused_offset_
).
std::string HttpUtil:: AssembleRawHeaders(const char* input_begin,
int input_len) {
std::string raw_headers;
raw_headers. reserve(input_len);
const char* input_end = input_begin + input_len;
input_begin
was calculated as read_buf_->StartOfBuffer() + read_buf_unused_offset_
,input_len
was incorrectly calculated as len(headers) + read_buf_unused_offset_
,input_end
will be read_buf_->StartOfBuffer() + 2 * read_buf_unused_offset_ + len(headers)
input_end
is now beyond the end of the actual headers.
The code will continue to rely on this incorrect value to try to create a copy
of the headers, inadvertently making a copy of data that is not part of this
response and may not even be part of the read_buf_
buffer. This could cause
the code to copy data from memory that is stored immediately after read_buf_
into a string that represents the response headers. This string is passed to
the renderer process that made the request, allowing a web-page inside the
sandbox to read memory from the main process' heap.An ASCII diagram might be useful to illustrate what is going on:
read_buf_: "HTTP 100 Continue\r\n...HTTP XXX Current response\r\n...Unused..." read_buf_->StartOfBuffer() -----^ read_buf_->capacity() ----------[================================================================] read_buf_->offset() ------------[=======================================================] read_buf_unused_offset_ -------[=======================] DoReadHeadersComplete/DoParseResponseHeaders: end_offset ---------------------[=======================================================] AssembleRawHeaders: input_begin ---------------------------------------------^ input_len ----------------------------------------------[========================================###############] error in input_len value --------------------------------------------------------------[========###############] (== read_buf_unused_offset_) Memory read from the main process' heap ---------------------------------------------------------[##############]The below proof-of-concept consist of a server that hosts a simple web-page.
This web-page uses XMLHttpRequest to make requests to the server. The server
responds with a carefully crafted reply to exploit the vulnerability and leak
data from the main process' memory in the HTTP headers of the response. The
web-page then uses getAllResponseHeaders()
to read the leaked data, and posts
it to the server, which displays the memory. The PoC makes no attempt to
influence the layout of the main process' memory, so arbitrary data will be
shown and access violation may occur which crash Chrome. With the PoC loaded in
one tab, simply browsing the internet in another might show some leaked
information from the pages you visit.
The impact depends on what happens to be stored on the heap immediately
following the buffer. Since a web-page can influence the activities of the main
process (e.
There are little limits to the number of times an attacker can exploit this vulnerability, assuming the attacker can avoid triggering an access violation: if the buffer happens to be stored at the end of the heap, attempts to exploit this vulnerability could trigger an access violation/segmentation fault when the code attempts to read beyond the buffer from unallocated memory addresses.
I identified and tested two approaches to fixing this bug:
Fix the code where it relies on the response being stored at the start of the buffer.
This addresses the incorrect addressing of memory that causes this vulnerability in various parts of the code. The design to keep HTTP 1xx responses in the buffer remains unchanged.
Remove HTTP 1xx responses from the buffer.
There was inline documentation in the source that explained why HTTP 1xx responses were handled in a special way, but it didn't make much sense to me. This fix changes the design to no longer keep the HTTP 1xx response in the buffer. There is an added benefit to this fix in that it removes a potential DoS attack, where a server responds with many large HTTP 1xx replies, all of which are kept in memory and eventually cause an OOM crash in the main process.
The later fix was eventually implemented.
This report was generated using a predecessor of BugId, a Python script created to detect, analyze and id application bugs. Don't waste time manually analyzing issues and writing reports but try BugId out yourself today! You'll get even better reports than this one with the current version.id: chrome.dll!base:: StringTokenizerT<...>::QuickGetNext Arbitrary AVR(AD11CFDE) description: Security: Attempt to read from unallocated arbitrary memory (@0x08782000) in chrome. dll!base:: StringTokenizerT<...>::QuickGetNext note: The exception happens in the main process. Based on this information, this is expected to be a critical security issue!