BraveBrowser AdBlock Out-of-Bounds Read off by One Byte Vulnerability

10 Oct 2019

 

CVE

CVE-2019-16078

CVSS SCORE

6.5 (AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N)

XID

XL-19-032

AFFECTED VENDORS

Brave browser – https://brave.com

AFFECTED SYSTEMS

Ad Block component - Brave Browser Android versions 1.0.77 - 1.0.94

VULNERABILITY SUMMARY

 

 

 

 

 

 

 

Brave Browser implements a built-in AdBlock component that can parse
Adblock Plus filters (e.g. EasyList). The parser is implemented from
Brave in native C++ code and was found to be vulnerable to an
out-of-bounds (OOB) read of 1 byte.

Exploiting this vulnerability might allow an adversary to read memory
from Chrome's privileged process since the AdBlock initialization is
executed from the main process before delegating to sandboxed workers.
This means one could use this vulnerability to perform information
disclosure and chain this with other vulnerabilities to achieve code
execution.

TECHNICAL DETAILS

 

 

 

 

The vulnerability was identified via fuzz testing of a standalone
AdBlock parser implementation and was reproduced in AdBlock component as
shipped with Brave version 1.0.77 (and the crash has been reproduced on
1.0.94).

The crash was initially triaged with Clang's Address Sanitizer (ASan),
which produced the following report.

 
=================================================================
==18358==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60300026bc4e at pc 0x56292117bff8
bp 0x7ffd1f8661c0 sp 0x7ffd1f8661b0
READ of size 1 at 0x60300026bc4e thread T0
#0 0x56292117bff7 in Filter::matches(
char const*, int, FilterOption, char const*, BloomFilter*, char const*, int) ../../../filter.cc:642
#1 0x56292116588c in isHostAnchoredHashSetMiss(
char const*, int, HashSet<Filter>*, char const*, int, FilterOption, char const*, Filter**) ../../../ad_block_client.cc:730
#2 0x562921166163 in AdBlockClient::matches(
char const*, FilterOption, char const*) ../../../ad_block_client.cc:802
#3 0x56292115d826 in
operator() ../../../main.cc:52
#4 0x56292115ea01 in for_each<__gnu_cxx::__normal_iterator<const std::__cxx11::basic_string<char>*, std::vector<std::__cxx11::basic_string<char> > >, checkForClient(AdBlockClient*, char const*, const std::vector<std::__cxx11::basic_string<
char> >&)::<lambda(
const string&)> > /usr/include/c++/7/bits/stl_algo.h:3884
#5 0x56292115dac9 in checkForClient(AdBlockClient*,
char const*, std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<
char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<
char> > > > const&) ../../../main.cc:50
#6 0x56292115e2db in main ../../../main.cc:118
#7 0x7fe8505a4b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)
#8 0x56292115cfe9 in _start (/home/dmuser/Developer/ad-block/build/out/Default/sample
+0x143fe9)0x60300026bc4e is located 0 bytes to the right of 30-byte region [0x60300026bc30,0x60300026bc4e)
allocated by thread T0 here:
#0 0x7fe850ff5618 in
operator
new[](unsigned
long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0618)
#1 0x5629211782e4 in Filter::Filter(Filter
const&) ../../../filter.cc:107
#2 0x562921173448 in HashSet<Filter>::Add(Filter
const&, bool) ../../../node_modules/hashset-cpp/./hash_set.h:57
#3 0x562921162c31 in parseFilter(
char const*, char const*, Filter*, BloomFilter*, BloomFilter*, HashSet<Filter>*, HashSet<Filter>*, HashSet<CosmeticFilter>*, bool) ../../../ad_block_client.cc:455
#4 0x56292116b8a3 in AdBlockClient::parse(char const*, bool) ../../../ad_block_client.cc:1351
#5 0x56292115dff6 in main ../../../main.cc:105
#6 0x7fe8505a4b96 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21b96)SUMMARY:
AddressSanitizer: heap-buffer-overflow ../../../filter.cc:642 in Filter::matches(
Char const*, int, FilterOption, char const*, BloomFilter*, char const*, int)
Shadow bytes around the buggy address:
0x0c0680045730: fa fa 00 00 00 fa fa fa 00 00 04 fa fa fa fd fd
0x0c0680045740: fd fa fa fa fd fd fd fa fa fa 00 00 03 fa fa fa
0x0c0680045750: 00 00 03 fa fa fa 00 00 03 fa fa fa 00 00 00 06
0x0c0680045760: fa fa fd fd fd fa fa fa fd fd fd fa fa fa 00 00
0x0c0680045770: 04 fa fa fa 00 00 04 fa fa fa fd fd fd fd fa fa
=>0x0c0680045780: fd fd fd fd fa fa 00 00 00[06]fa fa 00 00 00 06
0x0c0680045790: fa fa 00 00 06 fa fa fa fd fd fd fa fa fa fd fd
0x0c06800457a0: fd fa fa fa fd fd fd fa fa fa 00 00 02 fa fa fa
0x0c06800457b0: 00 00 02 fa fa fa fd fd fd fd fa fa fd fd fd fd
0x0c06800457c0: fa fa 00 00 00 03 fa fa 00 00 00 03 fa fa fd fd
0x0c06800457d0: fd fa fa fa fd fd fd fa fa fa 00 00 03 fa fa fa
Shadow
byte legend (one shadow
byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after
return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==18358==ABORTING
 

 

The top frame indicates an OOB read at line 642, with 1 byte being read
beyond the boundaries of the buffer heap-allocated buffer.

 

 
File: ad-block/filter.cc
547:bool Filter::matches(const char *input, int inputLen,
548: FilterOption contextOption, const char *contextDomain,
549: BloomFilter *inputBloomFilter, const char *inputHost, int inputHostLen) {
550: if (!matchesOptions(input, contextOption, contextDomain)) {

[..]

618:
619: // Wildcard match comparison
620: const char *filterPartStart = data;
621: const char *filterPartEnd = getNextPos(data, '*', data + dataLen);
622: int index = 0;
623: while (filterPartStart != filterPartEnd || *filterPartStart == '*') {
624: int filterPartLen = static_cast<int>(filterPartEnd - filterPartStart);
625:
626: if (inputBloomFilter) {
627: for (int i = 1; i < filterPartLen && filterPartEnd -
628: filterPartStart - i >= 2; i++) {
629: if (!isSeparatorChar(*(filterPartStart + i - 1)) &&
630: !isSeparatorChar(*(filterPartStart + i)) &&
631: !inputBloomFilter->exists(filterPartStart + i - 1, 2)) {
632: return false;
633: }
634: }
635: }
636:
637: int newIndex = indexOfFilter(input + index, inputLen - index,
638: filterPartStart, filterPartEnd);
639: if (newIndex == -1) {
640: return false;
641: }
642: newIndex += index;
643:
 

 

Filter::matches is invoked from AdBlockClient::isHostAnchoredHashSetMiss
(line 730), as indicated in the first frame of the stack trace.

 

 
File: ad-block/ad_block_client.cc

688 bool isHostAnchoredHashSetMiss(const char *input, int inputLen,
689 HashSet<Filter> *hashSet,
690 const char *inputHost,
691 int inputHostLen,
692 FilterOption contextOption,
693 const char *contextDomain,
694 Filter **foundFilter = nullptr) {
695 if (!hashSet) {
696 return false;
697 }
698
699 const char *start = inputHost + inputHostLen;
700 // Skip past the TLD
701 while (start != inputHost) {
702 start--;
703 if (*(start) == '.') {
704 break;
705 }
706 }

[..]

724 Filter *filter = hashSet->Find(Filter(start,
725 static_cast<int>(inputHost + inputHostLen - start), nullptr,
726 start, inputHostLen));
727 if (!filter) {
728 return true;
729 }
730 bool result = !filter->matches(input, inputLen, contextOption, contextDomain);
731 if (!result && foundFilter) {
732 *foundFilter = filter;
733 }
734 return result;
735 }
 

 

The next frame 2, we see that the
AdBlockClient::isHostAnchoredHashSetMiss is invoked from
AdBlockClient::matches (See like 802).

 

 
File: ad-block/ad_block_client.cc
737 bool AdBlockClient::matches(const char *input, FilterOption contextOption,
738 const char *contextDomain) {
739 int inputLen = static_cast<int>(strlen(input));
740
741 if (!isBlockableProtocol(input, inputLen)) {
742 return false;
743 }
744
745 int inputHostLen;
746 const char *inputHost = getUrlHost(input, &inputHostLen);
747

[..]

797 bool bloomFilterMiss = false;
798 bool hostAnchoredHashSetMiss = false;
799 if (!hasMatch) {
800 bloomFilterMiss = bloomFilter
801 && !bloomFilter->substringExists(input, AdBlockClient::kFingerprintSize);
802 hostAnchoredHashSetMiss = isHostAnchoredHashSetMiss(input, inputLen,
803 hostAnchoredHashSet, inputHost, inputHostLen,
804 contextOption, contextDomain);
805 if (bloomFilterMiss && hostAnchoredHashSetMiss) {
806 if (bloomFilterMiss) {
807 numBloomFilterSaves++;
808 }
809 if (hostAnchoredHashSetMiss) {
810 numHashSetSaves++;
811 }
812 return false;
813 }
814
815 hasMatch = !hostAnchoredHashSetMiss;
816 }
 

 

Now tracing the place within Brave where the AdBlock payload (as
downloaded over the Internet from Brave endpoints) is parsed, the
following is identified. AdBlockClient::matches is called from
BlockersWorker::shouldAdBlockUrl function (in line 398) passing it the
file contents downloaded in previous steps.

 

 

File: chrome/browser/net/blockers/blockers_worker.cc

383 bool BlockersWorker::shouldAdBlockUrl(const std::string& tab_url, const std::string& url,
384 unsigned int resource_type, bool isAdBlockRegionalEnabled) {
385 if (!isAdBlockerInitialized()) {
386 return false;
387 }
388
389 // Skip check for sync requests
390 if (tab_url == "file:///android_asset/") {
391 return false;
392 }
393
394 std::string base_host = GURL(tab_url).host();
395
396 FilterOption currentOption = ResourceTypeToFilterOption((content::ResourceType)resource_type);
397
398 if (adblock_parser_->matches(url.c_str(), currentOption, base_host.c_str())) {
399 return true;
400 }
401
402 // Check regional ad block
403 if (!isAdBlockRegionalEnabled || !isAdBlockerRegionalInitialized()) {
404 return false;
405 }
406 for (size_t i = 0; i < adblock_regional_parsers_.size(); i++) {
407 if (adblock_regional_parsers_[i]->matches(url.c_str(), currentOption, base_host.c_str())) {
408 return true;
409 }
410 }
411 //
412
413 return false;
414 }

 

 

BlockersWorker::shouldAdBlockUrl is invoked from
ChromeNetworkDelegate::set_blockers_worker (line 648). The
ChromeNetworkDelegate family of callbacks is the central point from
within the chrome code to add hooks into the network stack that executed
from the main process.

 

 

File: chrome/browser/net/chrome_network_delegate.cc

310 void ChromeNetworkDelegate::set_blockers_worker(
311 std::shared_ptr<net::blockers::BlockersWorker> blockers_worker) {
312 blockers_worker_ = blockers_worker;
313 }

[..]

647 if (ctx->needPerformAdBlock) {
648 if (blockers_worker_->shouldAdBlockUrl(
649 ctx->firstparty_host,
650 request->url().spec(),
651 (unsigned int)ctx->info->GetResourceType(),
652 ctx->isAdBlockRegionalEnabled)) {
653 ctx->block = true;
654 ctx->adsBlocked++;
655 }
656 }

PROOF OF CONCEPT

 

 

As a proof-of-concept, the malformed file generated from the fuzzer was
provided as input to the Brave application (served over the web via a
Man-in-The-Middle attack). Upon visiting any URL the main process of the
Brave Browser crashed.

POC Attachment

 

Payload: oob_read_getFingerprint.payload (2.61 MB)
ASAN Dump: oob_read_getFingerprint.asan (3 kB)

TIMELINE

 

 

 

19/06/2019 – Notified Vendor

15/08/2019 - Brave browser Android v1.2.0 released which resolves this issue

10/10/2019 - xen1thLabs Public disclosure

SOLUTION

 

Upgrade to the latest version of Brave browser Android

 

CREDIT

xen1thLabs - Software Labs

 

 

Resources