■ ■ ■ ■ ■ ■
0day-RCAs/2022/CVE-2022-22620.md
| 1 | + | # CVE-2022-22620: Use-after-free in Safari |
| 2 | + | *Maddie Stone* |
| 3 | + | |
| 4 | + | ## The Basics |
| 5 | + | |
| 6 | + | **Disclosure or Patch Date:** 10 February 2022 |
| 7 | + | |
| 8 | + | **Product:** Apple Safari/WebKit |
| 9 | + | |
| 10 | + | **Advisory:** https://support.apple.com/en-us/HT213093 |
| 11 | + | |
| 12 | + | **Affected Versions:** Safari 15.3, iOS 15.3, macOS 12.2 and earlier |
| 13 | + | |
| 14 | + | **First Patched Version:** Safari 15.3 (v. 16612.4.9.1.8 and 15612.4.9.1.8), iOS 15.3.1, macOS 12.2.1 |
| 15 | + | |
| 16 | + | **Issue/Bug Report:** https://bugs.webkit.org/show_bug.cgi?id=235551 |
| 17 | + | |
| 18 | + | **Patch CL:** https://github.com/WebKit/WebKit/commit/486816dc355c19f1de1b8056f85d0bbf7084dd6e |
| 19 | + | |
| 20 | + | **Bug-Introducing CL:** https://github.com/WebKit/WebKit/commit/aa31b6b4d09b09acdf1cec11f2f7f35bd362dd0e |
| 21 | + | |
| 22 | + | **Reporter(s):** Anonymous |
| 23 | + | |
| 24 | + | ## The Code |
| 25 | + | |
| 26 | + | **Proof-of-concept:** |
| 27 | + | |
| 28 | + | ```javascript |
| 29 | + | input = document.body.appendChild(document.createElement("input")); |
| 30 | + | |
| 31 | + | foo = document.body.appendChild(document.createElement("a")); |
| 32 | + | foo.id = "foo"; |
| 33 | + | |
| 34 | + | // Go to state1 when history.back is called |
| 35 | + | // The URL needs to be <currentPage+hash> to trigger loadInSameDocument during the call to back() |
| 36 | + | // Since the foo's element id="foo", focus will change to that element |
| 37 | + | history.pushState("state1", "", location + "#foo"); |
| 38 | + | |
| 39 | + | // Current state = state2 |
| 40 | + | history.pushState("state2", ""); |
| 41 | + | |
| 42 | + | setTimeout(() => { |
| 43 | + | |
| 44 | + | // Set the focus on the input element. |
| 45 | + | // During the call to back() the focus will change to the foo element |
| 46 | + | // and therefore triggering the blur event on the input element |
| 47 | + | input.focus(); |
| 48 | + | input.onblur = () => history.replaceState("state3", ""); |
| 49 | + | setTimeout(() => history.back(), 1000); |
| 50 | + | }, 1000); |
| 51 | + | ```` |
| 52 | + | |
| 53 | + | **Exploit sample:** N/A |
| 54 | + | |
| 55 | + | **Did you have access to the exploit sample when doing the analysis?** No |
| 56 | + | |
| 57 | + | ## The Vulnerability |
| 58 | + | |
| 59 | + | **Bug class:** Use-after-free |
| 60 | + | |
| 61 | + | **Vulnerability details:** |
| 62 | + | |
| 63 | + | The History 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. |
| 64 | + | |
| 65 | + | The bug is that `FrameLoader::loadInSameDocument` takes the `state` as an argument (`stateObject`), but doesn't increase its reference count. Only a `HistoryItem` object holds a reference to the `stateObject`. `loadInSameDocument` can trigger a callback into user JavaScript through the `onblur` event. The user's callback can call `replaceState` to replace the `HistoryItem`'s `state` with a new object, therefore dropping the only reference to the `stateObject`. When the callback returns, `loadInSameDocument` will still use this free'd object in its call to `statePopped`, leading to the use-after-free. |
| 66 | + | |
| 67 | + | When `loadInSameDocument` is called it changes the focus to the element its scrolling to. If we set the focus on a different element prior to `loadInSameDocument` running, the [`blur` event](https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event) will be fired on that element. Then we can free the `stateObject` by calling `replaceState` in the `onblur` event handler. |
| 68 | + | |
| 69 | + | **Patch analysis:** |
| 70 | + | |
| 71 | + | The patch changes the `stateObject` argument to `loadInSameDocument` from a raw pointer, `SerializedScriptValue*`, to a reference-counted pointer, `RefPtr<SerializedScriptValue>`, so that `loadInSameDocument` now increments the reference count on the object. |
| 72 | + | |
| 73 | + | **Thoughts on how this vuln might have been found _(fuzzing, code auditing, variant analysis, etc.)_:** |
| 74 | + | |
| 75 | + | It seems reasonable that the vulnerability could have been found through watching the commits and seeing the initial fix from 2013 reverted in 2016, code auditing, or fuzzing. Fuzzing seems *slightly* less likely due to needing to support "navigation" which many fuzzers explicitly try to exclude. |
| 76 | + | |
| 77 | + | **(Historical/present/future) context of bug:** |
| 78 | + | |
| 79 | + | This bug was actually reported and initially fixed in 2013. In 2016 the fix was regressed during (it seems) refactoring. A full write-up is available [here](https://googleprojectzero.blogspot.com/2022/06/an-autopsy-on-zombie-in-wild-0-day.html). |
| 80 | + | |
| 81 | + | ## The Exploit |
| 82 | + | |
| 83 | + | (The terms *exploit primitive*, *exploit strategy*, *exploit technique*, and *exploit flow* are [defined here](https://googleprojectzero.blogspot.com/2020/06/a-survey-of-recent-ios-kernel-exploits.html).) |
| 84 | + | |
| 85 | + | **Exploit strategy (or strategies):** N/A, no access to exploit sample |
| 86 | + | |
| 87 | + | **Exploit flow:** |
| 88 | + | |
| 89 | + | **Known cases of the same exploit flow:** |
| 90 | + | |
| 91 | + | **Part of an exploit chain?** |
| 92 | + | |
| 93 | + | ## The Next Steps |
| 94 | + | |
| 95 | + | ### Variant analysis |
| 96 | + | |
| 97 | + | **Areas/approach for variant analysis (and why):** |
| 98 | + | |
| 99 | + | * Look for any commits where a reference-counted pointer was changed to a raw pointer. |
| 100 | + | * Update fuzzer so that it could find this bug and therefore also hopefully provide coverage for other similar bugs too. |
| 101 | + | |
| 102 | + | **Found variants:** N/A |
| 103 | + | |
| 104 | + | ### Structural improvements |
| 105 | + | |
| 106 | + | What are structural improvements such as ways to kill the bug class, prevent the introduction of this vulnerability, mitigate the exploit flow, make this type of vulnerability harder to exploit, etc.? |
| 107 | + | |
| 108 | + | **Ideas to kill the bug class:** |
| 109 | + | |
| 110 | + | By default, arguments to functions should be reference-counted. Raw pointers should only be used in rare exceptions. |
| 111 | + | |
| 112 | + | **Ideas to mitigate the exploit flow:** N/A |
| 113 | + | |
| 114 | + | **Other potential improvements:** |
| 115 | + | |
| 116 | + | * The bug was killed in 2013 and re-introduced in 2016. It seems that this likely occured due to the large issues affecting most software dev 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. Steps towards any of these would likely make a difference. |
| 117 | + | * The two commits that reverted the 2013 fix were very, very large commits: 40 and 94 files changed. While some large commits may include exclusively no-ops, these commits included many changes affecting lifetime semantics. This seems like it would make it very difficult for any developer or reviewer to be able to truly audit and understand the security impacts of all the changes being made. |
| 118 | + | |
| 119 | + | ### 0-day detection methods |
| 120 | + | |
| 121 | + | What are potential detection methods for similar 0-days? Meaning are there any ideas of how this exploit or similar exploits could be detected **as a 0-day**? |
| 122 | + | |
| 123 | + | The code to trigger this vulnerability is pretty generic. I think any detection would have to be around exploitation method, which we couldn't analyze in this case since we didn't have access to the exploit sample. |
| 124 | + | |
| 125 | + | ## Other References |
| 126 | + | |
| 127 | + | * ["An Autopsy on a Zombie In-the-Wild 0-day"](https://googleprojectzero.blogspot.com/2022/06/an-autopsy-on-zombie-in-wild-0-day.html) Project Zero blog post |