Project Zero

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

2022 0-day In-the-Wild Exploitation…so far

30 Červen, 2022 - 15:00
ul.lst-kix_68660h7uawx-0{list-style-type:none}ul.lst-kix_68660h7uawx-1{list-style-type:none}ul.lst-kix_68660h7uawx-2{list-style-type:none}ul.lst-kix_68660h7uawx-3{list-style-type:none}ul.lst-kix_68660h7uawx-4{list-style-type:none}ul.lst-kix_68660h7uawx-5{list-style-type:none}ul.lst-kix_68660h7uawx-6{list-style-type:none}.lst-kix_68660h7uawx-7>li:before{content:"\0025cb "}ul.lst-kix_68660h7uawx-7{list-style-type:none}ul.lst-kix_68660h7uawx-8{list-style-type:none}.lst-kix_68660h7uawx-0>li:before{content:"\0025cf "}.lst-kix_68660h7uawx-6>li:before{content:"\0025cf "}.lst-kix_68660h7uawx-8>li:before{content:"\0025a0 "}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}.lst-kix_68660h7uawx-3>li:before{content:"\0025cf "}.lst-kix_68660h7uawx-2>li:before{content:"\0025a0 "}.lst-kix_68660h7uawx-4>li:before{content:"\0025cb "}.lst-kix_68660h7uawx-1>li:before{content:"\0025cb "}.lst-kix_68660h7uawx-5>li:before{content:"\0025a0 "}ol{margin:0;padding:0}table td,table th{padding:0}.HZWKYxoXby-c2{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:156.8pt;border-top-color:#000000;border-bottom-style:solid}.HZWKYxoXby-c9{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.2pt;border-top-color:#000000;border-bottom-style:solid}.HZWKYxoXby-c3{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}.HZWKYxoXby-c19{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:8pt;font-family:"Arial";font-style:normal}.HZWKYxoXby-c15{color:#000000;font-weight:700;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.HZWKYxoXby-c5{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.HZWKYxoXby-c4{padding-top:0pt;padding-bottom:0pt;line-height:1.5;orphans:2;widows:2;text-align:left;height:11pt}.HZWKYxoXby-c6{padding-top:0pt;padding-bottom:0pt;line-height:1.5;orphans:2;widows:2;text-align:left}.HZWKYxoXby-c12{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial"}.HZWKYxoXby-c17{border-spacing:0;border-collapse:collapse;margin-right:auto}.HZWKYxoXby-c0{padding-top:0pt;padding-bottom:0pt;line-height:1.0;text-align:left}.HZWKYxoXby-c1{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.HZWKYxoXby-c14{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.HZWKYxoXby-c18{margin-left:36pt;padding-left:0pt}.HZWKYxoXby-c8{color:inherit;text-decoration:inherit}.HZWKYxoXby-c13{padding:0;margin:0}.HZWKYxoXby-c21{font-weight:700}.HZWKYxoXby-c7{height:0pt}.HZWKYxoXby-c20{margin-left:36pt}.HZWKYxoXby-c11{height:11pt}.HZWKYxoXby-c16{margin-left:72pt}.HZWKYxoXby-c10{font-style:italic}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.5;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.5;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.5;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.5;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.5;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.5;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.5;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.5;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

Posted by Maddie Stone, Google Project Zero

This blog post is an overview of a talk, “ 0-day In-the-Wild Exploitation in 2022…so far”, that I gave at the FIRST conference in June 2022. The slides are available here.

For the last three years, we’ve published annual year-in-review reports of 0-days found exploited in the wild. The most recent of these reports is the 2021 Year in Review report, which we published just a few months ago in April. While we plan to stick with that annual cadence, we’re publishing a little bonus report today looking at the in-the-wild 0-days detected and disclosed in the first half of 2022.        

As of June 15, 2022, there have been 18 0-days detected and disclosed as exploited in-the-wild in 2022. When we analyzed those 0-days, we found that at least nine of the 0-days are variants of previously patched vulnerabilities. At least half of the 0-days we’ve seen in the first six months of 2022 could have been prevented with more comprehensive patching and regression tests. On top of that, four of the 2022 0-days are variants of 2021 in-the-wild 0-days. Just 12 months from the original in-the-wild 0-day being patched, attackers came back with a variant of the original bug.  


2022 ITW 0-day


Windows win32k


CVE-2021-1732 (2021 itw)

iOS IOMobileFrameBuffer


CVE-2021-30983 (2021 itw)


CVE-2022-30190 (“Follina”)

CVE-2021-40444 (2021 itw)

Chromium property access interceptors


CVE-2016-5128 CVE-2021-30551 (2021 itw) CVE-2022-1232 (Addresses incomplete CVE-2022-1096 fix)

Chromium v8




CVE-2022-22620 (“Zombie”)

Bug was originally fixed in 2013, patch was regressed in 2016

Google Pixel


* While this CVE says 2021, the bug was patched and disclosed in 2022

Linux same bug in a different subsystem

Atlassian Confluence




CVE-2022-26925 (“PetitPotam”)

CVE-2021-36942 (Patch regressed)

So, what does this mean?

When people think of 0-day exploits, they often think that these exploits are so technologically advanced that there’s no hope to catch and prevent them. The data paints a different picture. At least half of the 0-days we’ve seen so far this year are closely related to bugs we’ve seen before. Our conclusion and findings in the 2020 year-in-review report were very similar.

Many of the 2022 in-the-wild 0-days are due to the previous vulnerability not being fully patched. In the case of the Windows win32k and the Chromium property access interceptor bugs, the execution flow that the proof-of-concept exploits took were patched, but the root cause issue was not addressed: attackers were able to come back and trigger the original vulnerability through a different path. And in the case of the WebKit and Windows PetitPotam issues, the original vulnerability had previously been patched, but at some point regressed so that attackers could exploit the same vulnerability again. In the iOS IOMobileFrameBuffer bug, a buffer overflow was addressed by checking that a size was less than a certain number, but it didn’t check a minimum bound on that size. For more detailed explanations of three of the 0-days and how they relate to their variants, please see the slides from the talk.

When 0-day exploits are detected in-the-wild, it’s the failure case for an attacker. It’s a gift for us security defenders to learn as much as we can and take actions to ensure that that vector can’t be used again. The goal is to force attackers to start from scratch each time we detect one of their exploits: they’re forced to discover a whole new vulnerability, they have to invest the time in learning and analyzing a new attack surface, they must develop a brand new exploitation method. To do that effectively, we need correct and comprehensive fixes.

This is not to minimize the challenges faced by security teams responsible for responding to vulnerability reports. As we said in our 2020 year in review report:

Being able to correctly and comprehensively patch isn't just flicking a switch: it requires investment, prioritization, and planning. It also requires developing a patching process that balances both protecting users quickly and ensuring it is comprehensive, which can at times be in tension. While we expect that none of this will come as a surprise to security teams in an organization, this analysis is a good reminder that there is still more work to be done.

Exactly what investments are likely required depends on each unique situation, but we see some common themes around staffing/resourcing, incentive structures, process maturity, automation/testing, release cadence, and partnerships.


Practically, some of the following efforts can help ensure bugs are correctly and comprehensively fixed. Project Zero plans to continue to help with the following efforts, but we hope and encourage platform security teams and other independent security researchers to invest in these types of analyses as well:

  • Root cause analysis

Understanding the underlying vulnerability that is being exploited. Also tries to understand how that vulnerability may have been introduced. Performing a root cause analysis can help ensure that a fix is addressing the underlying vulnerability and not just breaking the proof-of-concept. Root cause analysis is generally a pre-requisite for successful variant and patch analysis.

  • Variant analysis

Looking for other vulnerabilities similar to the reported vulnerability. This can involve looking for the same bug pattern elsewhere, more thoroughly auditing the component that contained the vulnerability, modifying fuzzers to understand why they didn’t find the vulnerability previously, etc. Most researchers find more than one vulnerability at the same time. By finding and fixing the related variants, attackers are not able to simply “plug and play” with a new vulnerability once the original is patched.

  • Patch analysis

Analyzing the proposed (or released) patch for completeness compared to the root cause vulnerability. I encourage vendors to share how they plan to address the vulnerability with the vulnerability reporter early so the reporter can analyze whether the patch comprehensively addresses the root cause of the vulnerability, alongside the vendor’s own internal analysis.

  • Exploit technique analysis

Understanding the primitive gained from the vulnerability and how it’s being used. While it’s generally industry-standard to patch vulnerabilities, mitigating exploit techniques doesn’t happen as frequently. While not every exploit technique will always be able to be mitigated, the hope is that it will become the default rather than the exception. Exploit samples will need to be shared more readily in order for vendors and security researchers to be able to perform exploit technique analysis.

Transparently sharing these analyses helps the industry as a whole as well. We publish our analyses at this repository. We encourage vendors and others to publish theirs as well. This allows developers and security professionals to better understand what the attackers already know about these bugs, which hopefully leads to even better solutions and security overall.  

Kategorie: Hacking & Security

The curious tale of a fake

23 Červen, 2022 - 18:01
@import url('');ol{margin:0;padding:0}table td,table th{padding:0}.HovzfPYjjR-c13{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}.HovzfPYjjR-c34{padding-top:0pt;padding-bottom:3pt;line-height:1.5;page-break-after:avoid;text-align:left}.HovzfPYjjR-c15{padding-top:18pt;padding-bottom:6pt;line-height:1.5;page-break-after:avoid;text-align:left}.HovzfPYjjR-c17{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.HovzfPYjjR-c31{padding-top:0pt;padding-bottom:0pt;line-height:1.5;text-align:center}.HovzfPYjjR-c6{color:#000000;font-weight:400;font-size:11pt;font-family:"Courier New"}.HovzfPYjjR-c9{font-size:10pt;font-family:Consolas,"Courier New";color:#9c27b0;font-weight:400}.HovzfPYjjR-c0{font-size:10pt;font-family:Consolas,"Courier New";color:#000000;font-weight:400}.HovzfPYjjR-c30{font-size:10pt;font-family:Consolas,"Courier New";color:#455a64;font-weight:400}.HovzfPYjjR-c20{border-spacing:0;border-collapse:collapse;margin-right:auto}.HovzfPYjjR-c3{padding-top:0pt;padding-bottom:0pt;line-height:1.5;text-align:left}.HovzfPYjjR-c14{font-size:10pt;font-family:Consolas,"Courier New";color:#3367d6;font-weight:400}.HovzfPYjjR-c16{color:#000000;font-weight:400;font-size:16pt;font-family:"Arial"}.HovzfPYjjR-c5{color:#000000;font-weight:400;font-size:11pt;font-family:"Arial"}.HovzfPYjjR-c27{color:#000000;font-weight:700;font-size:11pt;font-family:"Arial"}.HovzfPYjjR-c23{font-size:10pt;font-family:Consolas,"Courier New";color:#c53929;font-weight:700}.HovzfPYjjR-c22{font-size:10pt;font-family:Consolas,"Courier New";color:#0f9d58;font-weight:400}.HovzfPYjjR-c1{font-size:10pt;font-family:Consolas,"Courier New";color:#616161;font-weight:400}.HovzfPYjjR-c4{font-size:10pt;font-family:Consolas,"Courier New";color:#c53929;font-weight:400}.HovzfPYjjR-c24{padding-top:0pt;padding-bottom:0pt;line-height:1.5;text-align:right}.HovzfPYjjR-c21{color:#000000;font-weight:400;font-size:26pt;font-family:"Arial"}.HovzfPYjjR-c2{text-decoration:none;vertical-align:baseline;font-style:normal}.HovzfPYjjR-c33{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.HovzfPYjjR-c32{margin-left:22.5pt;text-indent:36pt}.HovzfPYjjR-c19{color:inherit;text-decoration:inherit}.HovzfPYjjR-c26{font-weight:700;font-family:"Courier New"}.HovzfPYjjR-c7{font-weight:400;font-family:"Courier New"}.HovzfPYjjR-c28{text-decoration:none;vertical-align:baseline}.HovzfPYjjR-c8{orphans:2;widows:2}.HovzfPYjjR-c29{font-weight:700}.HovzfPYjjR-c18{height:0pt}.HovzfPYjjR-c25{font-style:italic}.HovzfPYjjR-c11{background-color:#00ff00}.HovzfPYjjR-c12{margin-right:-45pt}.HovzfPYjjR-c10{height:11pt}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.5;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.5;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.5;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.5;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.5;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.5;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.5;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.5;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

Posted by Ian Beer, Google Project Zero

NOTE: This issue was CVE-2021-30983 was fixed in iOS 15.2 in December 2021. 

Towards the end of 2021 Google's Threat Analysis Group (TAG) shared an iPhone app with me:

App splash screen showing the Vodafone carrier logo and the text "My Vodafone" (not the legitimate Vodadone app)

Although this looks like the real My Vodafone carrier app available in the App Store, it didn't come from the App Store and is not the real application from Vodafone. TAG suspects that a target receives a link to this app in an SMS, after the attacker asks the carrier to disable the target's mobile data connection. The SMS claims that in order to restore mobile data connectivity, the target must install the carrier app and includes a link to download and install this fake app.

This sideloading works because the app is signed with an enterprise certificate, which can be purchased for $299 via the Apple Enterprise developer program. This program allows an eligible enterprise to obtain an Apple-signed embedded.mobileprovision file with the ProvisionsAllDevices key set. An app signed with the developer certificate embedded within that mobileprovision file can be sideloaded on any iPhone, bypassing Apple's App Store review process. While we understand that the Enterprise developer program is designed for companies to push "trusted apps" to their staff's iOS devices, in this case, it appears that it was being used to sideload this fake carrier app.

In collaboration with Project Zero, TAG has published an additional post with more details around the targeting and the actor. The rest of this blogpost is dedicated to the technical analysis of the app and the exploits contained therein.

App structure

The app is broken up into multiple frameworks. InjectionKit.framework is a generic privilege escalation exploit wrapper, exposing the primitives you'd expect (kernel memory access, entitlement injection, amfid bypasses) as well as higher-level operations like app installation, file creation and so on.

Agent.framework is partially obfuscated but, as the name suggests, seems to be a basic agent able to find and exfiltrate interesting files from the device like the Whatsapp messages database.

Six privilege escalation exploits are bundled with this app. Five are well-known, publicly available N-day exploits for older iOS versions. The sixth is not like those others at all.

This blog post is the story of the last exploit and the month-long journey to understand it.

Something's missing? Or am I missing something?

Although all the exploits were different, five of them shared a common high-level structure. An initial phase where the kernel heap was manipulated to control object placement. Then the triggering of a kernel vulnerability followed by well-known steps to turn that into something useful, perhaps by disclosing kernel memory then building an arbitrary kernel memory write primitive.

The sixth exploit didn't have anything like that.

Perhaps it could be triggering a kernel logic bug like Linuz Henze's Fugu14 exploit, or a very bad memory safety issue which gave fairly direct kernel memory access. But neither of those seemed very plausible either. It looked, quite simply, like an iOS kernel exploit from a decade ago, except one which was first quite carefully checking that it was only running on an iPhone 12 or 13.

It contained log messages like:

  printf("Failed to prepare fake vtable: 0x%08x", ret);

which seemed to happen far earlier than the exploit could possibly have defeated mitigations like KASLR and PAC.

Shortly after that was this log message:

  printf("Waiting for R/W primitives...");

Why would you need to wait?

Then shortly after that:

  printf("Memory read/write and callfunc primitives ready!");

Up to that point the exploit made only four IOConnectCallMethod calls and there were no other obvious attempts at heap manipulation. But there was another log message which started to shed some light:

  printf("Unexpected data read from DCP: 0x%08x", v49);


In October 2021 Adam Donenfeld tweeted this:

DCP is the "Display Co-Processor" which ships with iPhone 12 and above and all M1 Macs.

There's little public information about the DCP; the most comprehensive comes from the Asahi linux project which is porting linux to M1 Macs. In their August 2021 and September 2021 updates they discussed their DCP reverse-engineering efforts and the open-source DCP client written by @alyssarzg. Asahi describe the DCP like this:

On most mobile SoCs, the display controller is just a piece of hardware with simple registers. While this is true on the M1 as well, Apple decided to give it a twist. They added a coprocessor to the display engine (called DCP), which runs its own firmware (initialized by the system bootloader), and moved most of the display driver into the coprocessor. But instead of doing it at a natural driver boundary… they took half of their macOS C++ driver, moved it into the DCP, and created a remote procedure call interface so that each half can call methods on C++ objects on the other CPU!

The Asahi linux project reverse-engineered the API to talk to the DCP but they are restricted to using Apple's DCP firmware (loaded by iBoot) - they can't use a custom DCP firmware. Consequently their documentation is limited to the DCP RPC API with few details of the DCP internals.

Setting the stage

Before diving into DCP internals it's worth stepping back a little. What even is a co-processor in a modern, highly integrated SoC (System-on-a-Chip) and what might the consequences of compromising it be?

Years ago a co-processor would likely have been a physically separate chip. Nowadays a large number of these co-processors are integrated along with their interconnects directly onto a single die, even if they remain fairly independent systems. We can see in this M1 die shot from Tech Insights that the CPU cores in the middle and right hand side take up only around 10% of the die:

M1 die-shot from with possible location of DCP added

Companies like SystemPlus perform very thorough analysis of these dies. Based on their analysis the DCP is likely the rectangular region indicated on this M1 die. It takes up around the same amount of space as the four high-efficiency cores seen in the centre, though it seems to be mostly SRAM.

With just this low-resolution image it's not really possible to say much more about the functionality or capabilities of the DCP and what level of system access it has. To answer those questions we'll need to take a look at the firmware.

My kingdom for a .dSYM!

The first step is to get the DCP firmware image. iPhones (and now M1 macs) use .ipsw files for system images. An .ipsw is really just a .zip archive and the Firmware/ folder in the extracted .zip contains all the firmware for the co-processors, modems etc. The DCP firmware is this file:


The im4p in this case is just a 25 byte header which we can discard:

  $ dd if=iphone13dcp.im4p of=iphone13dcp bs=25 skip=1

  $ file iphone13dcp

  iphone13dcp: Mach-O 64-bit preload executable arm64

It's a Mach-O! Running nm -a to list all symbols shows that the binary has been fully stripped:

  $ nm -a iphone13dcp

  iphone13dcp: no symbols

Function names make understanding code significantly easier. From looking at the handful of strings in the exploit some of them looked like they might be referencing symbols in a DCP firmware image ("M3_CA_ResponseLUT read: 0x%08x" for example) so I thought perhaps there might be a DCP firmware image where the symbols hadn't been stripped.

Since the firmware images are distributed as .zip files and Apple's servers support range requests with a bit of python and the partialzip tool we can relatively easily and quickly get every beta and release DCP firmware. I checked over 300 distinct images; every single one was stripped.

Guess we'll have to do this the hard way!

Day 1; Instruction 1

$ otool -h raw_fw/iphone13dcp


Mach header

magic      cputype   cpusubtype caps filetype ncmds sizeofcmds flags

0xfeedfacf 0x100000C 0          0x00 5        5     2240       0x00000001

That cputype is plain arm64 (ArmV8) without pointer authentication support. The binary is fairly large (3.7MB) and IDA's autoanalysis detects over 7000 functions.

With any brand new binary I usually start with a brief look through the function names and the strings. The binary is stripped so there are no function name symbols but there are plenty of C++ function names as strings:

The cross-references to those strings look like this:




    "%s: capture buffer exhausted, aborting capture\n",

    "void IOMFB::UPBlock_ALSS::send_data(uint64_t, uint32_t)");

This is almost certainly a logging macro which expands __FILE__, __LINE__ and __PRETTY_FUNCTION__. This allows us to start renaming functions and finding vtable pointers.

Object structure

From the Asahi linux blog posts we know that the DCP is using an Apple-proprietary RTOS called RTKit for which there is very little public information. There are some strings in the binary with the exact version:

ADD  X8, X8, #aLocalIphone13d@PAGEOFF ; "local-iphone13dcp.release"

ADD  X9, X9, #aRtkitIos182640@PAGEOFF ; "RTKit_iOS-1826.40.9.debug"

The code appears to be predominantly C++. There appear to be multiple C++ object hierarchies; those involved with this vulnerability look a bit like IOKit C++ objects. Their common base class looks like this:

struct __cppobj RTKIT_RC_RTTI_BASE


  RTKIT_RC_RTTI_BASE_vtbl *__vftable /*VFT*/;

  uint32_t refcnt;

  uint32_t typeid;


(These structure definitions are in the format IDA uses for C++-like objects)

The RTKit base class has a vtable pointer, a reference count and a four-byte Run Time Type Information (RTTI) field - a 4-byte ASCII identifier like BLHA, WOLO, MMAP, UNPI, OSST, OSBO and so on. These identifiers look a bit cryptic but they're quite descriptive once you figure them out (and I'll describe the relevant ones as we encounter them.)

The base type has the following associated vtable:

struct /*VFT*/ RTKIT_RC_RTTI_BASE_vtbl


  void (__cdecl *take_ref)(RTKIT_RC_RTTI_BASE *this);

  void (__cdecl *drop_ref)(RTKIT_RC_RTTI_BASE *this);

  void (__cdecl *take_global_type_ref)(RTKIT_RC_RTTI_BASE *this);

  void (__cdecl *drop_global_type_ref)(RTKIT_RC_RTTI_BASE *this);

  void (__cdecl *getClassName)(RTKIT_RC_RTTI_BASE *this);

  void (__cdecl *dtor_a)(RTKIT_RC_RTTI_BASE *this);

  void (__cdecl *unk)(RTKIT_RC_RTTI_BASE *this);


Exploit flow

The exploit running in the app starts by opening an IOKit user client for the AppleCLCD2 service. AppleCLCD seems to be the application processor of IOMobileFrameBuffer and AppleCLCD2 the DCP version.

The exploit only calls 3 different external method selectors on the AppleCLCD2 user client: 68, 78 and 79.

The one with the largest and most interesting-looking input is 78, which corresponds to this user client method in the kernel driver:



  IOMobileFramebufferUserClient *this,

  void *reference,

  IOExternalMethodArguments *args)


  const unsigned __int64 *extra_args;

  u8 *structureInput;

  structureInput = args->structureInput;

  if ( structureInput && args->scalarInputCount >= 2 )


    if ( args->scalarInputCount == 2 )

      extra_args = 0LL;


      extra_args = args->scalarInput + 2;

    return this->framebuffer_ap->set_block_dcp(





             args->scalarInputCount - 2,



  } else {

    return 0xE00002C2;



this unpacks the IOConnectCallMethod arguments and passes them to:


        IOMobileFramebufferAP *this,

        task *task,

        int first_scalar_input,

        int second_scalar_input,

        const unsigned __int64 *pointer_to_remaining_scalar_inputs,

        unsigned int scalar_input_count_minus_2,

        const unsigned __int8 *struct_input,

        unsigned __int64 struct_input_size)

This method uses some autogenerated code to serialise the external method arguments into a buffer like this:



  struct task* task

  u64 scalar_input_0

  u64 scalar_input_1

  u64[] remaining_scalar_inputs

  u64 cntExtraScalars

  u8[] structInput

  u64 CntStructInput


which is then passed to UnifiedPipeline2::rpc along with a 4-byte ASCII method identifier ('A435' here):







UnifiedPipeline2::rpc calls DCPLink::rpc which calls AppleDCPLinkService::rpc to perform one more level of serialisation which packs the method identifier and a "stream identifier" together with the arg_struct shown above.

AppleDCPLinkService::rpc then calls rpc_caller_gated to allocate space in a shared memory buffer, copy the buffer into there then signal to the DCP that a message is available.

Effectively the implementation of the IOMobileFramebuffer user client has been moved on to the DCP and the external method interface is now a proxy shim, via shared memory, to the actual implementations of the external methods which run on the DCP.

Exploit flow: the other side

The next challenge is to find where the messages start being processed on the DCP. Looking through the log strings there's a function which is clearly called ​​rpc_callee_gated - quite likely that's the receive side of the function rpc_caller_gated we saw earlier.

rpc_callee_gated unpacks the wire format then has an enormous switch statement which maps all the 4-letter RPC codes to function pointers:

      switch ( rpc_id )


        case 'A000':

          goto LABEL_146;

        case 'A001':

          handler_fptr = callback_handler_A001;


        case 'A002':

          handler_fptr = callback_handler_A002;


        case 'A003':

          handler_fptr = callback_handler_A003;


        case 'A004':

          handler_fptr = callback_handler_A004;


        case 'A005':

          handler_fptr = callback_handler_A005;


At the the bottom of this switch statement is the invocation of the callback handler:

ret = handler_fptr(






in_struct_ptr points to a copy of the serialised IOConnectCallMethod arguments we saw being serialized earlier on the application processor:



  struct task* task

  u64 scalar_input_0

  u64 scalar_input_1

  u64[] remaining_scalar_inputs

  u32 cntExtraScalars

  u8[] structInput

  u64 cntStructInput


The callback unpacks that buffer and calls a C++ virtual function:

unsigned int


  u8* meta,

  void *args,

  uint32_t args_size,

  void *out_struct_ptr,

  uint32_t out_struct_size


  int64 instance_id;

  uint64_t instance;

  int err;

  int retval;

  unsigned int result;

  instance_id = meta->instance_id;

  instance =


  if ( !instance ) {


      "IOMFB: %s: no instance for instance ID: %u\n",

      "static T *IOMFB::InstanceTracker::instance"

        "(IOMFB::InstanceTracker::tracked_entity_t, uint32_t)"

        " [T = IOMobileFramebuffer]",



  err = (instance-16)->vtable_0x378( // virtual call









  retval = convert_error(err);

  result = 0;

  *(_DWORD *)out_struct_ptr = retval;

  return result;


The challenge here is to figure out where that virtual call goes. The object is being looked up in a global table based on the instance id. We can't just set a breakpoint and whilst emulating the firmware is probably possible that would likely be a long project in itself. I took a hackier approach: we know that the vtable needs to be at least 0x380 bytes large so just go through all those vtables, decompile them and see if the prototypes look reasonable!

There's one clear match in the vtable for the UNPI type:


        UNPI* this,

        struct task* caller_task_ptr,

        unsigned int first_scalar_input,

        int second_scalar_input,

        int *remaining_scalar_inputs,

        uint32_t cnt_remaining_scalar_inputs,

        uint8_t *structure_input_buffer,

        uint64_t structure_input_size)

Here's my reversed implementation of UNPI::set_block


        UNPI* this,

        struct task* caller_task_ptr,

        unsigned int first_scalar_input,

        int second_scalar_input,

        int *remaining_scalar_inputs,

        uint32_t cnt_remaining_scalar_inputs,

        uint8_t *structure_input_buffer,

        uint64_t structure_input_size)


  struct block_handler_holder *holder;

  struct metadispatcher metadisp;

  if ( second_scalar_input )

    return 0x80000001LL;

  holder = this->UPPipeDCP_H13P->block_handler_holders;

  if ( !holder )

    return 0x8000000BLL;

  metadisp.address_of_some_zerofill_static_buffer = &unk_3B8D18;

  metadisp.handlers_holder = holder;

  metadisp.structure_input_buffer = structure_input_buffer;

  metadisp.structure_input_size = structure_input_size;

  metadisp.remaining_scalar_inputs = remaining_scalar_inputs;

  metadisp.cnt_remaining_sclar_input = cnt_remaining_scalar_inputs;

  metadisp.some_flags = 0x40000000LL;

  metadisp.dispatcher_fptr = a_dispatcher;

  metadisp.offset_of_something_which_looks_serialization_related = &off_1C1308;

  return metadispatch(holder, first_scalar_input, 1, caller_task_ptr, structure_input_buffer, &metadisp, 0);


This method wraps up the arguments into another structure I've called metadispatcher:

struct __attribute__((aligned(8))) metadispatcher


 uint64_t address_of_some_zerofill_static_buffer;

 uint64_t some_flags;

 __int64 (__fastcall *dispatcher_fptr)(struct metadispatcher *, struct BlockHandler *, __int64, _QWORD);

 uint64_t offset_of_something_which_looks_serialization_related;

 struct block_handler_holder *handlers_holder;

 uint64_t structure_input_buffer;

 uint64_t structure_input_size;

 uint64_t remaining_scalar_inputs;

 uint32_t cnt_remaining_sclar_input;


That metadispatcher object is then passed to this method:

return metadispatch(holder, first_scalar_input, 1, caller_task_ptr, structure_input_buffer, &metadisp, 0);

In there we reach this code:

  block_type_handler = lookup_a_handler_for_block_type_and_subtype(


                         first_scalar_input, // block_type

                         a3);                // subtype

The exploit calls this set_block external method twice, passing two different values for first_scalar_input, 7 and 19. Here we can see that those correspond to looking up two different block handler objects here.

The lookup function searches a linked list of block handler structures; the head of the list is stored at offset 0x1448 in the UPPipeDCP_H13P object and registered dynamically by a method I've named add_handler_for_block_type:

add_handler_for_block_type(struct block_handler_holder *handler_list,

                           struct BlockHandler *handler)

The logging code tells us that this is in a file called IOMFBBlockManager.cpp. IDA finds 44 cross-references to this method, indicating that there are probably that many different block handlers. The structure of each registered block handler is something like this:

struct __cppobj BlockHandler : RTKIT_RC_RTTI_BASE


  uint64_t field_16;

  struct handler_inner_types_entry *inner_types_array;

  uint32_t n_inner_types_array_entries;

  uint32_t field_36;

  uint8_t can_run_without_commandgate;

  uint32_t block_type;

  uint64_t list_link;

  uint64_t list_other_link;

  uint32_t some_other_type_field;

  uint32_t some_other_type_field2;

  uint32_t expected_structure_io_size;

  uint32_t field_76;

  uint64_t getBlock_Impl;

  uint64_t setBlock_Impl;

  uint64_t field_96;

  uint64_t back_ptr_to_UPPipeDCP_H13P;


The RTTI type is BLHA (BLock HAndler.)

For example, here's the codepath which builds and registers block handler type 24:

BLHA_24 = (struct BlockHandler *)CXXnew(112LL);

BLHA_24->__vftable = (BlockHandler_vtbl *)BLHA_super_vtable;

BLHA_24->block_type = 24;

BLHA_24->refcnt = 1;

BLHA_24->can_run_without_commandgate = 0;

BLHA_24->some_other_type_field = 0LL;

BLHA_24->expected_structure_io_size = 0xD20;

typeid = typeid_BLHA();

BLHA_24->typeid = typeid;

modify_typeid_ref(typeid, 1);

BLHA_24->__vftable = vtable_BLHA_subclass_type_24;

BLHA_24->inner_types_array = 0LL;

BLHA_24->n_inner_types_array_entries = 0;

BLHA_24->getBlock_Impl = BLHA_24_getBlock_Impl;

BLHA_24->setBlock_Impl = BLHA_24_setBlock_Impl;

BLHA_24->field_96 = 0LL;

BLHA_24->back_ptr_to_UPPipeDCP_H13P = a1;

add_handler_for_block_type(list_holder, BLHA_24);

Each block handler optionally has getBlock_Impl and setBlock_Impl function pointers which appear to implement the actual setting and getting operations.

We can go through all the callsites which add block handlers; tell IDA the type of the arguments and name all the getBlock and setBlock implementations:

You can perhaps see where this is going: that's looking like really quite a lot of reachable attack surface! Each of those setBlock_Impl functions is reachable by passing a different value for the first scalar argument to IOConnectCallMethod 78.

There's a little bit more reversing though to figure out how exactly to get controlled bytes to those setBlock_Impl functions:

Memory Mapping

The raw "block" input to each of those setBlock_Impl methods isn't passed inline in the IOConnectCallMethod structure input. There's another level of indirection: each individual block handler structure has an array of supported "subtypes" which contains metadata detailing where to find the (userspace) pointer to that subtype's input data in the IOConnectCallMethod structure input. The first dword in the structure input is the id of this subtype - in this case for the block handler type 19 the metadata array has a single entry:

<2, 0, 0x5F8, 0x600>

The first value (2) is the subtype id and 0x5f8 and 0x600 tell the DCP from what offset in the structure input data to read a pointer and size from. The DCP then requests a memory mapping from the AP for that memory from the calling task:

return wrap_MemoryDescriptor::withAddressRange(

  *(void*)(structure_input_buffer + addr_offset),

  *(unsigned int *)(structure_input_buffer + size_offset),


We saw earlier that the AP sends the DCP the struct task pointer of the calling task; when the DCP requests a memory mapping from a user task it sends those raw task struct pointers back to the AP such that the kernel can perform the mapping from the correct task. The memory mapping is abstracted as an MDES object on the DCP side; the implementation of the mapping involves the DCP making an RPC to the AP:

make_link_call('D453', &req, 0x20, &resp, 0x14);

which corresponds to a call to this method on the AP side:

IOMFB::MemDescRelay::withAddressRange(unsigned long long, unsigned long long, unsigned int, task*, unsigned long*, unsigned long long*)

The DCP calls ::prepare and ::map on the returned MDES object (exactly like an IOMemoryDescriptor object in IOKit), gets the mapped pointer and size to pass via a few final levels of indirection to the block handler:






where the dispatcher_fptr looks like this:


        struct metadispatcher *disp,

        struct BlockHandler *block_handler,

        __int64 controlled_ptr,

        unsigned int controlled_size)


  return block_handler->BlockHandler_setBlock(










You can see here just how useful it is to keep making structure definitions while reversing; there are so many levels of indirection that it's pretty much impossible to keep it all in your head.

BlockHandler_setBlock is a virtual method on BLHA. This is the implementation for BLHA 19:


  struct BlockHandler *this,

  void *structure_input_buffer,

  int64 structure_input_size,

  int64 *remaining_scalar_inputs,

  unsigned int cnt_remaining_scalar_inputs,

  struct CommandGate *gate,

  void* mapped_mdesc_ptr,

  unsigned int mapped_mdesc_length)

This uses a Command Gate (GATI) object (like a call gate in IOKit to serialise calls) to finally get close to actually calling the setBlock_Impl function.

We need to reverse the gate_context structure to follow the controlled data through the gate:

struct __attribute__((aligned(8))) gate_context


 struct BlockHandler *the_target_this;

 uint64_t structure_input_buffer;

 void *remaining_scalar_inputs;

 uint32_t cnt_remaining_scalar_inputs;

 uint32_t field_28;

 uint64_t controlled_ptr;

 uint32_t controlled_length;


The callgate object uses that context object to finally call the BLHA setBlock handler:


  struct UnifiedPipeline *parent_pipeline,

  struct gate_context *context,

  int64 a3,

  int64 a4,

  int64 a5)


  return context->the_target_this->setBlock_Impl)(









And finally we've made it through the whole callstack following the controlled data from IOConnectCallMethod in userspace on the AP to the setBlock_Impl methods on the DCP!

The prototype of the setBlock_Impl methods looks like this:

setBlock_Impl(struct UPPipeDCP_H13P *pipe_parent,

              void *structure_input_buffer,

              int *remaining_scalar_inputs,

              int cnt_remaining_scalar_inputs,

              void* ptr_via_memdesc,

              unsigned int len_of_memdesc_mapped_buf)

The exploit calls two setBlock_Impl methods; 7 and 19. 7 is fairly simple and seems to just be used to put controlled data in a known location. 19 is the buggy one. From the log strings we can tell that block type 19 handler is implemented in a file called UniformityCompensator.cpp.

Uniformity Compensation is a way to correct for inconsistencies in brightness and colour reproduction across a display panel. Block type 19 sets and gets a data structure containing this correction information. The setBlock_Impl method calls UniformityCompensator::set and reaches the following code snippet where controlled_size is a fully-controlled u32 value read from the structure input and indirect_buffer_ptr points to the mapped buffer, the contents of which are also controlled:

uint8_t* pages = compensator->inline_buffer; // +0x24

for (int pg_cnt = 0; pg_cnt < 3; pg_cnt++) {

  uint8_t* this_page = pages;

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

    memcpy(this_page, indirect_buffer_ptr, 4 * controlled_size);

    indirect_buffer_ptr += 4 * controlled_size;

    this_page += 0x100;


  pages += 0x4000;


There's a distinct lack of bounds checking on controlled_size. Based on the structure of the code it looks like it should be restricted to be less than or equal to 64 (as that would result in the input being completely copied to the output buffer.) The compensator->inline_buffer buffer is inline in the compensator object. The structure of the code makes it look that that buffer is probably 0xc000 (three 16k pages) large. To verify this we need to find the allocation site of this compensator object.

It's read from the pipe_parent object and we know that at this point pipe_parent is a UPPipeDCP_H13P object.

There's only one write to that field, here in UPPipeDCP_H13P::setup_tunables_base_target:

compensator = CXXnew(0xC608LL);


this->compensator = compensator;

The compensator object is a 0xc608 byte allocation; the 0xc000 sized buffer starts at offset 0x24 so the allocation has enough space for 0xc608-0x24=0xC5E4 bytes before corrupting neighbouring objects.

The structure input provided by the exploit for the block handler 19 setBlock call looks like this:

struct_input_for_block_handler_19[0x5F4] = 70; // controlled_size

struct_input_for_block_handler_19[0x5F8] = address;

struct_input_for_block_handler_19[0x600] = a_size;

This leads to a value of 70 (0x46) for controlled_size in the UniformityCompensator::set snippet shown earlier. (0x5f8 and 0x600 correspond to the offsets we saw earlier in the subtype's table:  <2, 0, 0x5F8, 0x600>)

The inner loop increments the destination pointer by 0x100 each iteration so 0x46 loop iterations will write 0x4618 bytes.

The outer loop writes to three subsequent 0x4000 byte blocks so the third (final) iteration starts writing at 0x24 + 0x8000 and writes a total of 0x4618 bytes, meaning the object would need to be 0xC63C bytes; but we can see that it's only 0xc608, meaning that it will overflow the allocation size by 0x34 bytes. The RTKit malloc implementation looks like it adds 8 bytes of metadata to each allocation so the next object starts at 0xc610.

How much input is consumed? The input is fully consumed with no "rewinding" so it's 3*0x46*0x46*4 = 0xe5b0 bytes. Working backwards from the end of that buffer we know that the final 0x34 bytes of it go off the end of the 0xc608 allocation which means +0xe57c in the input buffer will be the first byte which corrupts the 8 metadata bytes and +0x8584 will be the first byte to corrupt the next object:

This matches up exactly with the overflow object which the exploit builds:

  v24 = address + 0xE584;

  v25 = *(_DWORD *)&v54[48];

  v26 = *(_OWORD *)&v54[32];

  v27 = *(_OWORD *)&v54[16];

  *(_OWORD *)(address + 0xE584) = *(_OWORD *)v54;

  *(_OWORD *)(v24 + 16) = v27;

  *(_OWORD *)(v24 + 32) = v26;

  *(_DWORD *)(v24 + 48) = v25;

The destination object seems to be allocated very early and the DCP RTKit environment appears to be very deterministic with no ASLR. Almost certainly they are attempting to corrupt a neighbouring C++ object with a fake vtable pointer.

Unfortunately for our analysis the trail goes cold here and we can't fully recreate the rest of the exploit. The bytes for the fake DCP C++ object are read from a file in the app's temporary directory (base64 encoded inside a JSON file under the exploit_struct_offsets key) and I don't have a copy of that file. But based on the flow of the rest of the exploit it's pretty clear what happens next:

sudo make me a DART mapping

The DCP, like other coprocessors on iPhone, sits behind a DART (Device Address Resolution Table.) This is like an SMMU (IOMMU in the x86 world) which forces an extra layer of physical address lookup between the DCP and physical memory. DART was covered in great detail in Gal Beniamini's Over The Air - Vol. 2, Pt. 3 blog post.

The DCP clearly needs to access lots of buffers owned by userspace tasks as well as memory managed by the kernel. To do this the DCP makes RPC calls back to the AP which modifies the DART entries accordingly. This appears to be exactly what the DCP exploit does: the D45X family of DCP->AP RPC methods appear to expose an interface for requesting arbitrary physical as well as virtual addresses to be mapped into the DCP DART.

The fake C++ object is most likely a stub which makes such calls on behalf of the exploit, allowing the exploit to read and write kernel memory.


Segmentation and isolation are in general a positive thing when it comes to security. However, splitting up an existing system into separate, intercommunicating parts can end up exposing unexpected code in unexpected ways.

We've had discussions within Project Zero about whether this DCP vulnerability is interesting at all. After all, if the UniformityCompensator code was going to be running on the Application Processors anyway then the Display Co-Processor didn't really introduce or cause this bug.

Whilst that's true, it's also the case that the DCP certainly made exploitation of this bug significantly easier and more reliable than it would have been on the AP. Apple has invested heavily in memory corruption mitigations over the last few years, so moving an attack surface from a "mitigation heavy" environment to a "mitigation light" one is a regression in that sense.

Another perspective is that the DCP just isn't isolated enough; perhaps the intention was to try to isolate the code on the DCP such that even if it's compromised it's limited in the effect it could have on the entire system. For example, there might be models where the DCP to AP RPC interface is much more restricted.

But again there's a tradeoff: the more restrictive the RPC API, the more the DCP code has to be refactored - a significant investment. Currently, the codebase relies on being able to map arbitrary memory and the API involves passing userspace pointers back and forth.

I've discussed in recent posts how attackers tend to be ahead of the curve. As the curve slowly shifts towards memory corruption exploitation getting more expensive, attackers are likely shifting too. We saw that in the logic-bug sandbox escape used by NSO and we see that here in this memory-corruption-based privilege escalation that side-stepped kernel mitigations by corrupting memory on a co-processor instead. Both are quite likely to continue working in some form in a post-memory tagging world. Both reveal the stunning depth of attack surface available to the motivated attacker. And both show that defensive security research still has a lot of work to do.

Kategorie: Hacking & Security

An Autopsy on a Zombie In-the-Wild 0-day

14 Červen, 2022 - 18:00
@import url('');.lst-kix_vnkr79j0brd5-6>li:before{content:"\0025cf "}.lst-kix_vnkr79j0brd5-8>li:before{content:"\0025a0 "}ol.lst-kix_rpn0sahs9m0k-5.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-5 0}.lst-kix_rpn0sahs9m0k-1>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-1}.lst-kix_vnkr79j0brd5-5>li:before{content:"\0025a0 "}.lst-kix_vnkr79j0brd5-2>li:before{content:"\0025a0 "}.lst-kix_vnkr79j0brd5-4>li:before{content:"\0025cb "}ol.lst-kix_rpn0sahs9m0k-8.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-8 0}.lst-kix_vnkr79j0brd5-3>li:before{content:"\0025cf "}.lst-kix_vnkr79j0brd5-0>li:before{content:"\0025cf "}.lst-kix_vnkr79j0brd5-1>li:before{content:"\0025cb "}ol.lst-kix_omx22nj2js1z-1.start{counter-reset:lst-ctn-kix_omx22nj2js1z-1 0}.lst-kix_omx22nj2js1z-0>li{counter-increment:lst-ctn-kix_omx22nj2js1z-0}ul.lst-kix_vnkr79j0brd5-4{list-style-type:none}ul.lst-kix_vnkr79j0brd5-3{list-style-type:none}ul.lst-kix_vnkr79j0brd5-2{list-style-type:none}ul.lst-kix_vnkr79j0brd5-1{list-style-type:none}.lst-kix_omx22nj2js1z-1>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-1,lower-latin) ". "}ul.lst-kix_vnkr79j0brd5-8{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-0.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-0 0}ul.lst-kix_vnkr79j0brd5-7{list-style-type:none}.lst-kix_omx22nj2js1z-0>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-0,decimal) ". "}ul.lst-kix_vnkr79j0brd5-6{list-style-type:none}ul.lst-kix_vnkr79j0brd5-5{list-style-type:none}ul.lst-kix_vnkr79j0brd5-0{list-style-type:none}.lst-kix_vnkr79j0brd5-7>li:before{content:"\0025cb "}.lst-kix_omx22nj2js1z-2>li{counter-increment:lst-ctn-kix_omx22nj2js1z-2}.lst-kix_omx22nj2js1z-8>li{counter-increment:lst-ctn-kix_omx22nj2js1z-8}.lst-kix_3vloefecb731-0>li:before{content:"\0025cf "}ol.lst-kix_omx22nj2js1z-0.start{counter-reset:lst-ctn-kix_omx22nj2js1z-0 0}ul.lst-kix_ziaggw1wsf95-3{list-style-type:none}ul.lst-kix_ziaggw1wsf95-4{list-style-type:none}ol.lst-kix_omx22nj2js1z-7.start{counter-reset:lst-ctn-kix_omx22nj2js1z-7 0}ul.lst-kix_ziaggw1wsf95-5{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-3.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-3 0}ul.lst-kix_ziaggw1wsf95-6{list-style-type:none}ul.lst-kix_ziaggw1wsf95-0{list-style-type:none}ul.lst-kix_ziaggw1wsf95-1{list-style-type:none}ul.lst-kix_ziaggw1wsf95-2{list-style-type:none}ul.lst-kix_3vloefecb731-0{list-style-type:none}.lst-kix_3vloefecb731-5>li:before{content:"\0025a0 "}.lst-kix_3vloefecb731-4>li:before{content:"\0025cb "}.lst-kix_3vloefecb731-1>li:before{content:"\0025cb "}ul.lst-kix_3vloefecb731-8{list-style-type:none}ul.lst-kix_3vloefecb731-7{list-style-type:none}ul.lst-kix_3vloefecb731-6{list-style-type:none}ul.lst-kix_3vloefecb731-5{list-style-type:none}.lst-kix_3vloefecb731-3>li:before{content:"\0025cf "}ul.lst-kix_3vloefecb731-4{list-style-type:none}ul.lst-kix_3vloefecb731-3{list-style-type:none}.lst-kix_3vloefecb731-2>li:before{content:"\0025a0 "}ul.lst-kix_3vloefecb731-2{list-style-type:none}ul.lst-kix_3vloefecb731-1{list-style-type:none}.lst-kix_rpn0sahs9m0k-8>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-8}ol.lst-kix_omx22nj2js1z-6.start{counter-reset:lst-ctn-kix_omx22nj2js1z-6 0}.lst-kix_rpn0sahs9m0k-2>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-2}.lst-kix_rpn0sahs9m0k-5>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-5}.lst-kix_3vloefecb731-7>li:before{content:"\0025cb "}.lst-kix_3vloefecb731-6>li:before{content:"\0025cf "}.lst-kix_3vloefecb731-8>li:before{content:"\0025a0 "}ol.lst-kix_rpn0sahs9m0k-2.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-2 0}.lst-kix_omx22nj2js1z-5>li{counter-increment:lst-ctn-kix_omx22nj2js1z-5}ol.lst-kix_rpn0sahs9m0k-1.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-1 0}.lst-kix_omx22nj2js1z-4>li{counter-increment:lst-ctn-kix_omx22nj2js1z-4}.lst-kix_rpn0sahs9m0k-6>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-6}ol.lst-kix_rpn0sahs9m0k-4.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-4 0}ol.lst-kix_omx22nj2js1z-8.start{counter-reset:lst-ctn-kix_omx22nj2js1z-8 0}ol.lst-kix_omx22nj2js1z-2.start{counter-reset:lst-ctn-kix_omx22nj2js1z-2 0}ul.lst-kix_ziaggw1wsf95-7{list-style-type:none}ul.lst-kix_ziaggw1wsf95-8{list-style-type:none}.lst-kix_omx22nj2js1z-3>li{counter-increment:lst-ctn-kix_omx22nj2js1z-3}ol.lst-kix_rpn0sahs9m0k-7.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-7 0}.lst-kix_omx22nj2js1z-6>li{counter-increment:lst-ctn-kix_omx22nj2js1z-6}.lst-kix_rpn0sahs9m0k-4>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-4}.lst-kix_rpn0sahs9m0k-7>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-7}ol.lst-kix_omx22nj2js1z-5.start{counter-reset:lst-ctn-kix_omx22nj2js1z-5 0}.lst-kix_rpn0sahs9m0k-5>li:before{content:"(" counter(lst-ctn-kix_rpn0sahs9m0k-5,lower-roman) ") "}.lst-kix_rpn0sahs9m0k-6>li:before{content:"" counter(lst-ctn-kix_rpn0sahs9m0k-6,decimal) ". "}.lst-kix_rpn0sahs9m0k-4>li:before{content:"(" counter(lst-ctn-kix_rpn0sahs9m0k-4,lower-latin) ") "}.lst-kix_rpn0sahs9m0k-8>li:before{content:"" counter(lst-ctn-kix_rpn0sahs9m0k-8,lower-roman) ". "}.lst-kix_rpn0sahs9m0k-1>li:before{content:"" counter(lst-ctn-kix_rpn0sahs9m0k-1,lower-latin) ") "}.lst-kix_rpn0sahs9m0k-2>li:before{content:"" counter(lst-ctn-kix_rpn0sahs9m0k-2,lower-roman) ") "}.lst-kix_rpn0sahs9m0k-3>li:before{content:"(" counter(lst-ctn-kix_rpn0sahs9m0k-3,decimal) ") "}.lst-kix_omx22nj2js1z-2>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-2,lower-roman) ". "}.lst-kix_rpn0sahs9m0k-3>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-3}.lst-kix_omx22nj2js1z-3>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-3,decimal) ". "}ol.lst-kix_omx22nj2js1z-3.start{counter-reset:lst-ctn-kix_omx22nj2js1z-3 0}ol.lst-kix_rpn0sahs9m0k-7{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-8{list-style-type:none}.lst-kix_omx22nj2js1z-5>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-5,lower-roman) ". "}.lst-kix_ziaggw1wsf95-0>li:before{content:"\0025cf "}.lst-kix_rpn0sahs9m0k-0>li:before{content:"" counter(lst-ctn-kix_rpn0sahs9m0k-0,decimal) ") "}.lst-kix_omx22nj2js1z-4>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-4,lower-latin) ". "}.lst-kix_omx22nj2js1z-7>li{counter-increment:lst-ctn-kix_omx22nj2js1z-7}.lst-kix_omx22nj2js1z-7>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-7,lower-latin) ". "}ol.lst-kix_rpn0sahs9m0k-1{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-2{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-0{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-5{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-6{list-style-type:none}.lst-kix_omx22nj2js1z-6>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-6,decimal) ". "}ol.lst-kix_rpn0sahs9m0k-3{list-style-type:none}ol.lst-kix_rpn0sahs9m0k-6.start{counter-reset:lst-ctn-kix_rpn0sahs9m0k-6 0}.lst-kix_omx22nj2js1z-1>li{counter-increment:lst-ctn-kix_omx22nj2js1z-1}ol.lst-kix_rpn0sahs9m0k-4{list-style-type:none}.lst-kix_ziaggw1wsf95-6>li:before{content:"\0025cf "}ol.lst-kix_omx22nj2js1z-0{list-style-type:none}.lst-kix_ziaggw1wsf95-5>li:before{content:"\0025a0 "}.lst-kix_ziaggw1wsf95-7>li:before{content:"\0025cb "}.lst-kix_ziaggw1wsf95-4>li:before{content:"\0025cb "}.lst-kix_ziaggw1wsf95-8>li:before{content:"\0025a0 "}.lst-kix_omx22nj2js1z-8>li:before{content:"" counter(lst-ctn-kix_omx22nj2js1z-8,lower-roman) ". "}ol.lst-kix_omx22nj2js1z-6{list-style-type:none}.lst-kix_ziaggw1wsf95-2>li:before{content:"\0025a0 "}ol.lst-kix_omx22nj2js1z-5{list-style-type:none}ol.lst-kix_omx22nj2js1z-4.start{counter-reset:lst-ctn-kix_omx22nj2js1z-4 0}ol.lst-kix_omx22nj2js1z-8{list-style-type:none}.lst-kix_ziaggw1wsf95-1>li:before{content:"\0025cb "}.lst-kix_ziaggw1wsf95-3>li:before{content:"\0025cf "}ol.lst-kix_omx22nj2js1z-7{list-style-type:none}ol.lst-kix_omx22nj2js1z-2{list-style-type:none}ol.lst-kix_omx22nj2js1z-1{list-style-type:none}{margin-left:-18pt;white-space:nowrap;display:inline-block;min-width:18pt}ol.lst-kix_omx22nj2js1z-4{list-style-type:none}ol.lst-kix_omx22nj2js1z-3{list-style-type:none}.lst-kix_rpn0sahs9m0k-0>li{counter-increment:lst-ctn-kix_rpn0sahs9m0k-0}.lst-kix_rpn0sahs9m0k-7>li:before{content:"" counter(lst-ctn-kix_rpn0sahs9m0k-7,lower-latin) ". "}ol{margin:0;padding:0}table td,table th{padding:0}.ZslSztaQWN-c45{border-right-style:solid;padding-top:1.7pt;border-top-width:0pt;border-right-width:0pt;padding-left:8.5pt;padding-bottom:4.2pt;line-height:1.5;border-left-width:0pt;border-top-style:solid;border-left-style:solid;border-bottom-width:0pt;border-bottom-style:solid;text-align:left;padding-right:0pt}.ZslSztaQWN-c9{border-right-style:solid;padding:0pt 4pt 0pt 4pt;border-bottom-color:#999988;border-top-width:1pt;border-right-width:1pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#00aa00;border-left-width:1pt;border-top-style:solid;background-color:#eeeedd;border-left-style:solid;border-bottom-width:1pt;width:30pt;border-top-color:#999988;border-bottom-style:solid}.ZslSztaQWN-c49{border-right-style:solid;padding:1pt 2pt 1pt 2pt;border-bottom-color:#00aa00;border-top-width:0pt;border-right-width:1pt;border-left-color:#00aa00;vertical-align:top;border-right-color:#00aa00;border-left-width:1pt;border-top-style:solid;background-color:#ddffdd;border-left-style:solid;border-bottom-width:1pt;width:421.5pt;border-top-color:#cc0000;border-bottom-style:solid}.ZslSztaQWN-c38{border-right-style:solid;padding:0pt 2pt 0pt 2pt;border-bottom-color:#999988;border-top-width:1pt;border-right-width:1pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#d7d7d7;border-left-width:1pt;border-top-style:solid;background-color:#eeeeee;border-left-style:solid;border-bottom-width:1pt;width:30pt;border-top-color:#d7d7d7;border-bottom-style:solid}.ZslSztaQWN-c30{border-right-style:solid;padding:0pt 2pt 0pt 2pt;border-bottom-color:#999988;border-top-width:1pt;border-right-width:1pt;border-left-color:#999999;vertical-align:top;border-right-color:#d7d7d7;border-left-width:0pt;border-top-style:solid;background-color:#eeeeee;border-left-style:solid;border-bottom-width:1pt;width:27pt;border-top-color:#d7d7d7;border-bottom-style:solid}.ZslSztaQWN-c44{border-right-style:solid;padding:1pt 2pt 1pt 2pt;border-bottom-color:#cc0000;border-top-width:0pt;border-right-width:0pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#ffffff;border-left-style:solid;border-bottom-width:1pt;width:421.5pt;border-top-color:#000000;border-bottom-style:solid}.ZslSztaQWN-c36{border-right-style:solid;padding:0pt 4pt 0pt 4pt;border-bottom-color:#999988;border-top-width:1pt;border-right-width:1pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#cc0000;border-left-width:1pt;border-top-style:solid;background-color:#eeeedd;border-left-style:solid;border-bottom-width:1pt;width:30pt;border-top-color:#999988;border-bottom-style:solid}.ZslSztaQWN-c51{border-right-style:solid;padding:1pt 2pt 1pt 2pt;border-bottom-color:#000000;border-top-width:1pt;border-right-width:0pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#000000;border-left-width:1pt;border-top-style:solid;background-color:#ffffff;border-left-style:solid;border-bottom-width:0pt;width:421.5pt;border-top-color:#d7d7d7;border-bottom-style:solid}.ZslSztaQWN-c13{border-right-style:solid;padding:0pt 4pt 0pt 4pt;border-bottom-color:#999988;border-top-width:1pt;border-right-width:1pt;border-left-color:#888866;vertical-align:top;border-right-color:#d7d7d7;border-left-width:0pt;border-top-style:solid;background-color:#eeeedd;border-left-style:solid;border-bottom-width:1pt;width:27pt;border-top-color:#999988;border-bottom-style:solid}.ZslSztaQWN-c48{border-right-style:solid;padding:0pt 4pt 0pt 4pt;border-bottom-color:#999988;border-top-width:1pt;border-right-width:1pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#d7d7d7;border-left-width:1pt;border-top-style:solid;background-color:#eeeedd;border-left-style:solid;border-bottom-width:1pt;width:30pt;border-top-color:#999988;border-bottom-style:solid}.ZslSztaQWN-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}.ZslSztaQWN-c50{border-right-style:solid;padding:1pt 2pt 1pt 2pt;border-bottom-color:#d7d7d7;border-top-width:1pt;border-right-width:1pt;border-left-color:#d7d7d7;vertical-align:top;border-right-color:#d7d7d7;border-left-width:1pt;border-top-style:solid;background-color:#f7f7f7;border-left-style:solid;border-bottom-width:1pt;width:421.5pt;border-top-color:#d7d7d7;border-bottom-style:solid}.ZslSztaQWN-c34{border-right-style:solid;padding:1pt 2pt 1pt 2pt;border-bottom-color:#cc0000;border-top-width:1pt;border-right-width:1pt;border-left-color:#cc0000;vertical-align:top;border-right-color:#cc0000;border-left-width:1pt;border-top-style:solid;background-color:#ffdddd;border-left-style:solid;border-bottom-width:0pt;width:421.5pt;border-top-color:#cc0000;border-bottom-style:solid}.ZslSztaQWN-c18{padding-top:0pt;padding-bottom:0pt;line-height:1.5;orphans:2;widows:2;text-align:center;height:11pt}.ZslSztaQWN-c11{padding-top:0pt;padding-bottom:0pt;line-height:1.5;orphans:2;widows:2;text-align:left;height:11pt}.ZslSztaQWN-c5{color:#000000;font-weight:400;text-decoration:none;vertical-align:baseline;font-size:11pt;font-family:"Arial";font-style:normal}.ZslSztaQWN-c31{padding-top:20pt;padding-bottom:6pt;line-height:1.5;page-break-after:avoid;text-align:left}.ZslSztaQWN-c52{padding-top:6pt;padding-bottom:0pt;line-height:1.45;text-align:left}.ZslSztaQWN-c46{font-size:10pt;font-family:Consolas,"Courier New";color:#9c27b0;font-weight:400}.ZslSztaQWN-c24{padding-top:0pt;padding-bottom:0pt;line-height:1.5;text-align:right}.ZslSztaQWN-c8{color:#000000;font-weight:400;font-size:20pt;font-family:"Arial"}.ZslSztaQWN-c25{color:#24292f;font-weight:400;font-size:10pt;font-family:Consolas,"Courier New"}.ZslSztaQWN-c12{padding-top:0pt;padding-bottom:0pt;line-height:1.5;text-align:left}.ZslSztaQWN-c17{font-size:10pt;font-family:Consolas,"Courier New";color:#0f9d58;font-weight:400}.ZslSztaQWN-c0{font-size:10pt;font-family:Consolas,"Courier New";color:#000000;font-weight:400}.ZslSztaQWN-c32{font-size:10pt;font-family:Consolas,"Courier New";color:#455a64;font-weight:400}.ZslSztaQWN-c21{color:#000000;font-weight:400;font-size:7pt;font-family:"Verdana"}.ZslSztaQWN-c1{font-size:10pt;font-family:Consolas,"Courier New";color:#3367d6;font-weight:400}.ZslSztaQWN-c33{font-size:7pt;font-family:"Verdana";color:#888866;font-weight:400}.ZslSztaQWN-c16{text-decoration-skip-ink:none;-webkit-text-decoration-skip:none;color:#1155cc;text-decoration:underline}.ZslSztaQWN-c3{font-size:10pt;font-family:Consolas,"Courier New";color:#c53929;font-weight:400}.ZslSztaQWN-c27{border-spacing:0;border-collapse:collapse;margin-right:auto}.ZslSztaQWN-c42{color:#333333;font-weight:700;font-size:11pt;font-family:"Arial"}.ZslSztaQWN-c2{font-size:10pt;font-family:Consolas,"Courier New";color:#616161;font-weight:400}.ZslSztaQWN-c28{font-size:7pt;font-family:"Courier New";font-weight:400}.ZslSztaQWN-c43{background-color:#ffffff;max-width:468pt;padding:72pt 72pt 72pt 72pt}.ZslSztaQWN-c4{font-family:Consolas,"Courier New";color:#0d904f;font-weight:400}.ZslSztaQWN-c6{text-decoration:none;vertical-align:baseline;font-style:normal}.ZslSztaQWN-c19{orphans:2;widows:2}.ZslSztaQWN-c29{color:#1155cc;font-weight:700}.ZslSztaQWN-c23{margin-left:36pt;padding-left:0pt}.ZslSztaQWN-c47{font-weight:400;font-family:Consolas,"Courier New"}.ZslSztaQWN-c14{color:inherit;text-decoration:inherit}.ZslSztaQWN-c22{padding:0;margin:0}.ZslSztaQWN-c26{margin-left:72pt;padding-left:0pt}.ZslSztaQWN-c20{height:11pt}.ZslSztaQWN-c15{height:0pt}.ZslSztaQWN-c37{background-color:#99ee99}.ZslSztaQWN-c40{height:11.2pt}.ZslSztaQWN-c35{height:12pt}.ZslSztaQWN-c7{height:12.8pt}.ZslSztaQWN-c39{background-color:#ee9999}.ZslSztaQWN-c41{margin-left:36pt}.title{padding-top:0pt;color:#000000;font-size:26pt;padding-bottom:3pt;font-family:"Arial";line-height:1.5;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.5;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.5;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.5;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.5;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.5;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.5;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.5;page-break-after:avoid;font-style:italic;orphans:2;widows:2;text-align:left}

Posted by Maddie Stone, Google Project Zero

Whenever there’s a new in-the-wild 0-day disclosed, I’m very interested in understanding the root cause of the bug. This allows us to then understand if it was fully fixed, look for variants, and brainstorm new mitigations. This blog is the story of a “zombie” Safari 0-day and how it came back from the dead to be disclosed as exploited in-the-wild in 2022. CVE-2022-22620 was initially fixed in 2013, reintroduced in 2016, and then disclosed as exploited in-the-wild in 2022. If you’re interested in the full root cause analysis for CVE-2022-22620, we’ve published it here.

In the 2020 Year in Review of 0-days exploited in the wild, I wrote how 25% of all 0-days detected and disclosed as exploited in-the-wild in 2020 were variants of previously disclosed vulnerabilities. Almost halfway through 2022 and it seems like we’re seeing a similar trend. Attackers don’t need novel bugs to effectively exploit users with 0-days, but instead can use vulnerabilities closely related to previously disclosed ones. This blog focuses on just one example from this year because it’s a little bit different from other variants that we’ve discussed before. Most variants we’ve discussed previously exist due to incomplete patching. But in this case, the variant was completely patched when the vulnerability was initially reported in 2013. However, the variant was reintroduced 3 years later during large refactoring efforts. The vulnerability then continued to exist for 5 years until it was fixed as an in-the-wild 0-day in January 2022.

Getting Started

In the case of CVE-2022-22620 I had two pieces of information to help me figure out the vulnerability: the patch (thanks to Apple for sharing with me!) and the description from the security bulletin stating that the vulnerability is a use-after-free. The primary change in the patch was to change the type of the second argument (stateObject) to the function FrameLoader::loadInSameDocument from a raw pointer, SerializedScriptValue* to a reference-counted pointer, RefPtr<SerializedScriptValue>.




// This does the same kind of work that didOpenURL does, except it relies on the fact



// that a higher level already checked that the URLs match and the scrolling is the right thing to do.



void FrameLoader::loadInSameDocument(const URL& url, SerializedScriptValue* stateObject, bool isNewNavigation)



void FrameLoader::loadInSameDocument(URL url, RefPtr<SerializedScriptValue> stateObject, bool isNewNavigation)

Whenever I’m doing a root cause analysis on a browser in-the-wild 0-day, along with studying the code, I also usually search through commit history and bug trackers to see if I can find anything related. I do this to try and understand when the bug was introduced, but also to try and save time. (There’s a lot of 0-days to be studied! &#x1f600;)

The Previous Life

In the case of CVE-2022-22620, I was scrolling through the git blame view of FrameLoader.cpp. Specifically I was looking at the definition of loadInSameDocument. When looking at the git blame for this line prior to our patch, it’s a very interesting commit. The commit was actually changing the stateObject argument from a reference-counted pointer, PassRefPtr<SerializedScriptValue>, to a raw pointer, SerializedScriptValue*. This change from December 2016 introduced CVE-2022-22620. The Changelog even states:

(WebCore::FrameLoader::loadInSameDocument): Take a raw pointer for the

serialized script value state object. No one was passing ownership.

But pass it along to statePopped as a Ref since we need to pass ownership

of the null value, at least for now.

Now I was intrigued and wanted to track down the previous commit that had changed the stateObject argument to PassRefPtr<SerializedScriptValue>. I was in luck and only had to go back in the history two more steps. There was a commit from 2013 that changed the stateObject argument from the raw pointer, SerializedScriptValue*, to a reference-counted pointer, PassRefPtr<SerializedScriptValue>. This commit from February 2013 was doing the same thing that our commit in 2022 was doing. The commit was titled “Use-after-free in SerializedScriptValue::deserialize” and included a good description of how that use-after-free worked.

The commit also included a test:

Added a test that demonstrated a crash due to use-after-free

of SerializedScriptValue.

Test: fast/history/replacestate-nocrash.html

The trigger from this test is:

Object.prototype.__defineSetter__("foo",function(){history.replaceState("", "")});

history.replaceState({foo:1,zzz:"a".repeat(1<<22)}, "");


My hope was that the test would crash the vulnerable version of WebKit and I’d be done with my root cause analysis and could move on to the next bug. Unfortunately, it didn’t crash.

The commit description included the comment to check out a Chromium bug. (During this time Chromium still used the WebKit rendering engine. Chromium forked the Blink rendering engine in April 2013.) I saw that my now Project Zero teammate, Sergei Glazunov, originally reported the Chromium bug back in 2013, so I asked him for the details.

The use-after-free from 2013 (no CVE was assigned) was a bug in the implementation of the History API. This API allows access to (and modification of) a stack of the pages visited in the current frame, and these page states are stored as a SerializedScriptValue. The History API exposes a getter for state, and a method replaceState which allows overwriting the "most recent" history entry.

The bug was that in the implementation of the getter for state, SerializedScriptValue::deserialize was called on the current "most recent" history entry value without increasing its reference count. As SerializedScriptValue::deserialize could trigger a callback into user JavaScript, the callback could call replaceState to drop the only reference to the history entry value by replacing it with a new value. When the callback returned, the rest of SerializedScriptValue::deserialize ran with a free'd this pointer.

In order to fix this bug, it appears that the developers decided to change every caller of SerializedScriptValue::deserialize to increase the reference count on the stateObject by changing the argument types from a raw pointer to PassRefPtr<SerializedScriptValue>.  While the originally reported trigger called deserialize on the stateObject through the V8History::stateAccessorGetter function, the developers’ fix also caught and patched the path to deserialize through loadInSameDocument.

The timeline of the changes impacting the stateObject is:

  • HistoryItem.m_stateObject is type RefPtr<SerializedScriptValue>
  • HistoryItem::stateObject() returns SerializedScriptValue*
  • FrameLoader::loadInSameDocument takes stateObject argument as SerializedScriptValue*
  • HistoryItem::stateObject returns a PassRefPtr<SerializedScriptValue>
  • FrameLoader::loadInSameDocument takes stateObject argument as PassRefPtr<SerializedScriptValue>
  • HistoryItem::stateObject returns RefPtr instead of PassRefPtr
  • HistoryItem::stateObject() is changed to return raw pointer instead of RefPtr
  • FrameLoader::loadInSameDocument changed to take stateObject as a raw pointer instead of PassRefPtr<SerializedScriptValue>
  • FrameLoader::loadInSameDocument changed to take stateObject as a RefPtr<SerializedScriptValue>

The Autopsy

When we look at the timeline of changes for FrameLoader::loadInSameDocument it seems that the bug was re-introduced in December 2016 due to refactoring. The question is, why did the patch author think that loadInSameDocument would not need to hold a reference. From the December 2016 commit ChangeLog: Take a raw pointer for the serialized script value state object. No one was passing ownership.

My assessment is that it’s due to the October 2016 changes in HistoryItem:stateObject. When the author was evaluating the refactoring changes needed in the dom directory in December 2016, it would have appeared that the only calls to loadInSameDocument passed in either a null value or the result of stateObject() which as of October 2016 now passed a raw SerializedScriptValue* pointer. When looking at those two options for the type of an argument, then it’s potentially understandable that the developer thought that loadInSameDocument did not need to share ownership of stateObject.

So why then was HistoryItem::stateObject’s return value changed from a RefPtr to a raw pointer in October 2016? That I’m struggling to find an explanation for.

According to the description, the patch in October 2016 was intended to “Replace all uses of ExceptionCodeWithMessage with WebCore::Exception”. However when we look at the ChangeLog it seems that the author decided to also do some (seemingly unrelated) refactoring to HistoryItem. These are some of the only changes in the commit whose descriptions aren’t related to exceptions. As an outsider looking at the commits, it seems that the developer by chance thought they’d do a little “clean-up” while working through the required refactoring on the exceptions. If this was simply an additional ad-hoc step while in the code, rather than the goal of the commit, it seems plausible that the developer and reviewers may not have further traced the full lifetime of HistoryItem::stateObject.

While the change to HistoryItem in October 2016 was not sufficient to introduce the bug, it seems that that change likely contributed to the developer in December 2016 thinking that loadInSameDocument didn’t need to increase the reference count on the stateObject.

Both the October 2016 and the December 2016 commits were very large. The commit in October changed 40 files with 900 additions and 1225 deletions. The commit in December changed 95 files with 1336 additions and 1325 deletions. It seems untenable for any developers or reviewers to understand the security implications of each change in those commits in detail, especially since they’re related to lifetime semantics.

The Zombie

We’ve now tracked down the evolution of changes to fix the 2013 vulnerability…and then revert those fixes… so I got back to identifying the 2022 bug. It’s the same bug, but triggered through a different path. That’s why the 2013 test case wasn’t crashing the version of WebKit that should have been vulnerable to CVE-2022-22620:

  1. The 2013 test case triggers the bug through the V8History::stateAccessorAndGetter path instead of FrameLoader::loadInSameDocument, and
  2. As a part of Sergei’s 2013 bug report there were additional hardening measures put into place that prevented user-code callbacks being processed during deserialization. 

Therefore we needed to figure out how to call loadInSameDocument and instead of using the deserialization to trigger a JavaScript callback, we needed to find another event in the loadInSameDocument function that would trigger the callback to user JavaScript.

To quickly figure out how to call loadInSameDocument, I modified the WebKit source code to trigger a test failure if loadInSameDocument was ever called and then ran all the tests in the fast/history directory. There were 5 out of the 80 tests that called loadInSameDocument:

The tests history-back-forward-within-subframe-hash.html and fast/history/history-traversal-is-asynchronous.html were the most helpful. We can trigger the call to loadInSameDocument by setting the history stack with an object whose location is the same page, but includes a hash. We then call history.back() to go back to that state that includes the URL with the hash. loadInSamePage is responsible for scrolling to that location.

history.pushState("state1", "", location + "#foo");

history.pushState("state2", ""); // current state

history.back(); //goes back to state1, triggering loadInSameDocument


Now that I knew how to call loadInSameDocument, I teamed up with Sergei to identify how we could get user code execution sometime during the loadInSameDocument function, but prior to the call to statePopped (FrameLoader.cpp#1158):

m_frame.document()->statePopped(stateObject ? Ref<SerializedScriptValue> { *stateObject } : SerializedScriptValue::nullValue());

The callback to user code would have to occur prior to the call to statePopped because stateObject was cast to a reference there and thus would now be reference-counted. We assumed that this would be the place where the “freed” object was “used”.

If you go down the rabbit hole of the calls made in loadInSameDocument, we find that there is a path to the blur event being dispatched. We could have also used a tool like CodeQL to see if there was a path from loadInSameDocument to dispatchEvent, but in this case we just used manual auditing. The call tree to the blur event is:







            dispatchWindowEvent(Event::create(eventNames().blurEvent, Event::CanBubble::No, Event::IsCancelable::No));

The blur event fires on an element whenever focus is moved from that element to another element. In our case loadInSameDocument is triggered when we need to scroll to a new location within the current page. If we’re scrolling and therefore changing focus to a new element, the blur event is fired on the element that previously had the focus.

The last piece for our trigger is to free the stateObject in the onblur event handler. To do that we call replaceState, which overwrites the current history state with a new object. This causes the final reference to be dropped on the stateObject and it’s therefore free’d. loadInSameDocument still uses the free’d stateObject in its call to statePopped.

input = document.body.appendChild(document.createElement("input"));

a = document.body.appendChild(document.createElement("a")); = "foo";

history.pushState("state1", "", location + "#foo");

history.pushState("state2", "");

setTimeout(() => {


        input.onblur = () => history.replaceState("state3", "");

        setTimeout(() => history.back(), 1000);

}, 1000);

In both the 2013 and 2022 cases, the root vulnerability is that the stateObject is not correctly reference-counted. In 2013, the developers did a great job of patching all the different paths to trigger the vulnerability, not just the one in the submitted proof-of-concept. This meant that they had also killed the vulnerability in loadInSameDocument. The refactoring in December 2016 then revived the vulnerability to enable it to be exploited in-the-wild and re-patched in 2022.


Usually when we talk about variants, they exist due to incomplete patches: the vendor doesn’t correctly and completely fix the reported vulnerability. However, for CVE-2022-22620 the vulnerability was correctly and completely fixed in 2013. Its fix was just regressed in 2016 during refactoring. We don’t know how long an attacker was exploiting this vulnerability in-the-wild, but we do know that the vulnerability existed (again) for 5 years: December 2016 until January 2022.

There’s no easy answer for what should have been done differently. The developers responding to the initial bug report in 2013 followed a lot of best-practices:

  • Patched all paths to trigger the vulnerability, not just the one in the proof-of-concept. This meant that they patched the variant that would become CVE-2022-22620.
  • Submitted a test case with the patch.
  • Detailed commit messages explaining the vulnerability and how they were fixing it.
  • Additional hardening measures during deserialization.

As an offensive security research team, we can make assumptions about what we believe to be the core challenges facing modern software development teams: legacy code, short reviewer turn-around expectations, refactoring and security efforts are generally under-appreciated and under-rewarded, and lack of memory safety mitigations. Developers and security teams need time to review patches, especially for security issues, and rewarding these efforts, will make a difference. It also will save the vendor resources in the long run. In this case, 9 years after a vulnerability was initially triaged, patched, tested, and released, the whole process had to be duplicated again, but this time under the pressure of in-the-wild exploitation.

While this case study was a 0-day in Safari/WebKit, this is not an issue unique to Safari. Already in 2022, we’ve seen in-the-wild 0-days that are variants of previously disclosed bugs targeting Chromium, Windows, Pixel, and iOS as well. It’s a good reminder that as defenders we all need to stay vigilant in reviewing and auditing code and patches.

Kategorie: Hacking & Security