BCC #1 -- UaF in Chromium and MiraclePtr
"BCC" stands for "Browser Contribution/Crash Club" :)
I've been interested in UaF in Chromium recently. Although I've not found any UaF bugs yet (😭), would like to leave something for myself :)
famous pattern of UaF
There is a famous implementation pattern leading to UaF, "RenderFrameHostImpl UAF". This term was introduced on Microsoft Security blog in 2021.
In summary, a raw pointer to RenderFrameHost (a correspondent to RenderFrame in renderer processes) is taken as an arg by a callback, and it's been already freed when the callback is fired due to removal of iframe.
It's intuitive that RenderFrameHost has the same lifetime as the corresponding tab or iframe. So it's relatively easy to trigger UaF with a raw pointer to RenderFrameHost only if you find a vulnerable code, i.e. all you have to do is just removing an iframe on the right time.
According to the post, mojo (IPC interface in Chromium) object sometimes has to refer to RenderFrameHost in order to get state or object tied to the render process. For example, regarding the vuln explained in the post, calling getJavaObject needed a reference to RenderFrameHost. In these cases, "RenderFrameHostImpl UaF" could be happened.
In the RenderFrameHost header file (content/public/browser/render_frame_host.h), you can see that Chromium developers basically should use not RenderFrameHost, but GlobalRenderFrameHostToken. If implemented in such way, the code will be safe.
// Returns the GlobalRenderFrameHostToken for this frame. Embedders should
// store this instead of a raw RenderFrameHost pointer.
virtual GlobalRenderFrameHostToken GetGlobalFrameToken() const = 0;
FYI, getJavaObject might be necessary to call mobile native methods. In the above case, they are needed to receive SMS. The belows may be nice references.
- https://github.blog/security/vulnerability-research/the-fugitive-in-java-escaping-to-java-to-escape-the-chrome-sandbox/
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/jni_zero/README.md
expand the pattern
For a beginner of UaF in Chromium like me, this pattern has some ease of approach. It's simple. "Roughly speaking, there is a raw pointer to tab/iframe and it's used later. So let's close the tab/iframe. That's it."
Of course, 4 years have passed since that post was published. Easy bugs seem to have already been gathered. At least, I've not found yet :(
Then, I thought the pattern can be expanded. All I need instead of a raw pointer to RenderFrameHost is a raw pointer to something that can be freed by interaction with browser UI.
And there is a good example :)
alternative to RenderFrameHost
CVE-2022-2852 is a nice example here. The title is "Security: heap-use-after-free in AccountSelectionBubbleView::OnAccountImageFetched". Let's see it.
background
This bug is related to fetching img for FedCM account select dialog UI.
FedCM account select dialog UI. You can try this on FedCM DEMO site: https://fedcm-rp-demo.glitch.me/
Those who haven't seen the dialog, please check the above mov.
You can see the following. (Note: I can use these terms wrongly)
- When a FedCM provider provides multiple accounts, you can see a dialog for choose your account.
- After clicking your account, the dialog changes and you can see some "loading UI".
Obviously, the browser has to fetch icon images in order to display this dialog. This is the point.
use after free
In the pre-patch implementation, fetched images are put in unique_ptrs which are owned by each "row" of the dialog.
So, if it takes a bit long time to fetch images and the fetch is completed AFTER you choose your account, the rows are already vanished when the browser got images. This means the rows' unique_ptrs for images are already freed when they are used. This is the trick.
FYI: how it's solved
For future reference, let's see the patch a bit.
It's interesting that the post-patched implementation completely relies memory safety on BindOnce implementation. Please look at the following part.
// OnImageFetched() is a member of AccountImageView so that the callback
// is cancelled in the case that AccountImageView is destroyed prior to
// the callback returning.
image_fetcher.FetchImage(account.picture,
base::BindOnce(&AccountImageView::OnImageFetched,
weak_ptr_factory_.GetWeakPtr(),
base::UTF8ToUTF16(account.name)),
std::move(params));
Who secures what the comment says, the callback is cancelled in the case that AccountImageView is destroyed, is BindOnce.
BindOnce creates a callback and it's canceled (strictly speaking, handled as no-op) when the bounded class instance is already destroyed (fyi: the second arg, weak_ptr, bounds the callback to the class instance).
This article is really helpful to understand BindOnce.
found another example, but...
found a candidate!
Like this issue, the path to free memory by interaction with UI is not limited to RenderFrameHost. Actually, after looking for this pattern, I found one :)
DesktopMediaPickerDialogView::DesktopMediaPickerDialogView( /* a lot of args*/) {
// a lot of calc
RegisterDeleteDelegateCallback(base::BindOnce(
[](DesktopMediaPickerDialogView* dialog) {
// If the dialog is being closed then notify the parent about it.
if (dialog->parent_) {
dialog->parent_->NotifyDialogResult(DesktopMediaID());
}
},
this)); // here!!!
// a lot of calc
}
void WidgetDelegate::RegisterDeleteDelegateCallback(
base::OnceClosure callback) {
delete_delegate_callbacks_.emplace_back(std::move(callback));
}
this refers to DesktopMediaPickerDialogView. Yes, it sounds like connected to UI.
Digging into this class...
- DesktopMediaPickerDialogView: "Dialog view used for DesktopMediaPickerImpl." (chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.h)
- DesktopMediaPickerImpl: "Implementation of DesktopMediaPicker for Views." (chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.h)
- DesktopMediaPicker: "Base class for desktop media picker UI. It's used by Desktop Media API, and by ARC to let user choose a desktop media source." (chrome/browser/media/webrtc/desktop_media_picker.h)
- It seems to be used around webRTC and Desktop sharing dialog.
- DesktopMediaPicker: "Base class for desktop media picker UI. It's used by Desktop Media API, and by ARC to let user choose a desktop media source." (chrome/browser/media/webrtc/desktop_media_picker.h)
- DesktopMediaPickerImpl: "Implementation of DesktopMediaPicker for Views." (chrome/browser/ui/views/desktop_capture/desktop_media_picker_views.h)
So, when using desktop media picker UI in webRTC or something, DesktopMediaPickerDialogView is crated and delete_delegate_callbacks_ gets a callback using it later. This class is completely related to browser UI.
The callbacks in delete_delegate_callbacks_ are consumed in the followings.
- ProfilePickerView::Clear()
- MessagePopupCollection::ClosePopupItem(PopupItem& item)
- Widget::HandleWidgetDestroyed()
After running Chromium and logging it, I grasped that closing a webRTC screen-sharing dialog fires callbacks in delete_delegate_callbacks_ via Widget::HandleWidgetDestroyed().
Here, what is Widget? Let's see the doc.
Views need an underlying canvas to paint onto. This has to be supplied by the operating system, usually by creating a native drawing surface of some kind. Views calls these widgets. A Widget is the bridge between a tree of Views and a native window of some sort, which Views calls a native widget.
https://chromium.googlesource.com/chromium/src/+/master/docs/ui/views/overview.md#widgets
Anyway, all I have to do is open webRTC desktop sharing dialog, and then destroy DesktopMediaPickerDialogView BEFORE Widget::HandleWidgetDestroyed() is called.
but...
After playing with Chrome to realize UaF, I had got no clue. The log shows no clues. (So I didn't come to use ASan build.)
Then, walking around the code, I noticed that, first of all, DesktopMediaPickerDialogView (my target) is created by void DesktopMediaPickerImpl::Show and put it to its member var, _dialog. In its definition, you can see two important points...!
class DesktopMediaPickerImpl : public DesktopMediaPicker {
/* etc... */
// The |dialog_| is owned by the corresponding views::Widget instance.
// When DesktopMediaPickerImpl is destroyed the |dialog_| is destroyed
// asynchronously by closing the widget.
raw_ptr<DesktopMediaPickerDialogView> dialog_;
/* etc... */
}
- It seems that the developer has already taken a lot of care here.
- What is raw_ptr? Why not DesktopMediaPickerDialogView* though we like asterisk?
As for 1, I found no proof if |dialog_| is really "owned by the corresponding views::Widget instance".
But what is more critical for bug hunters is 2 :)
MiraclePtr
What is it?
raw_ptr is so-called MiraclePtr. Those who are interested in browser security might know this name.
I think MiraclePtr aka raw_ptr aka BackupRefPtr is a nice explainer.
Basically we can "think of it as a raw C++ pointer".
Before telling you what raw_ptr<T> is, we'd like you to follow one simple rule: think of it as a raw C++ pointer. ...
...
- Don't assume that moving clears the pointer (it may or may not).
- The owner of the memory must free it when the time is right, don't assume raw_ptr<T> will free it for you (it won't). Unlike std::unique_ptr<T>, base::scoped_refptr<T>, etc., it does not manage ownership or lifetime of an allocated object.
What is the difference is what it does to dangling pointer. It quarantines and poisons freed memory.
it can prevent a significant percentage of Use-after-Free (UaF) bugs from being exploitable. It achieves this by quarantining the freed memory as long as any dangling raw_ptr<T> pointing to it exists, and poisoning it (with 0xEF..EF pattern).
This sounds great. According to Google Security Blog, they rewrote "more than 15,000 raw pointers" in 2022.
We successfully rewrote more than 15,000 raw pointers in the Chrome codebase into raw_ptr<T>, then enabled BackupRefPtr for the browser process on Windows and Android (both 64 bit and 32 bit) in Chrome 102 Stable.
https://security.googleblog.com/2022/09/use-after-freedom-miracleptr.html
The commit is here. Actually, DesktopMediaPickerDialogView was also wrapped by this commit😂
You can see the phenomenal result in their blog in 2024, after 2 yrs since then.
MiraclePtr effectively mitigated 57% of these use-after-free vulnerabilities in privileged processes, exceeding our initial estimate of 50%.
https://security.googleblog.com/2024/01/miracleptr-protecting-users-from-use.html
According to the post, Unretained pointers are also handled in this campaign.
implication for bug bounty
MiraclePtr is in fact a great mitigation for UaF. And the bounty program clearly states UaF is out of scope if it's already guarded by MiraclePtr.
Beginning in Chrome 118, MiraclePtr was enabled in non-renderer processes across all active release channels of Chrome. MiraclePtr has been enabled in Fuchsia beginning in 128. As of Chrome 128, MiraclePtr-protected bugs are no longer considered security issues and are not in scope for VRP rewards.
...
A vulnerability is protected by MiraclePtr only when the test case triggering it results in a status of "MiraclePtr Status: PROTECTED" when reproduced in an ASAN build. ... If, during the course of triage and investigation, the bug is deemed to be BRP protected, it will be downgraded to a bug and handled as a functional issue and would not be eligible for a VRP reward.
https://bughunters.google.com/about/rules/chrome-friends/5745167867576320/chrome-vulnerability-reward-program-rules
But there are still chance.
According to the doc, MiraclePtr is "enabled by default in all **non-**Renderer processes" and on all the platforms other than "iOS" and "Linux CastOS (Nest hardware)".
Also it seems not to be applied modules such as V8, Blink etc.
So, Valid UaF could be around the followings.
- renderer process
- iOS and Linux CastOS specific codes
- modules like V8, Blink etc.
Also, according to the aforementioned post, 43% of UaF found in privileged processes was not covered.
Google Security Blog, "MiraclePtr: protecting users from use-after-free vulnerabilities on more platforms"
It could be also a nice idea to dig into these categories.
Lessons from this journey
- About Chromium
- The digest of MiraclePtr mechanism and its scope
- The definitions of Widget and View
- The relationship between important classes and concepts
- BrowserContext is pretty much profile.
- A tab is built on WebContents. But WebContents is not always a tab.
- RenderFrameHost is pretty much tab/iframe. I don't confirm if it affects the correspondence whether the iframe is cross-origin or not to its parent.
- source: Anatomy of the browser 201 (Chrome University 2019), How Chromium Displays Web Pages
- About Chromium Bug Bounty
- It seems better to search UaF in the aforementioned areas where MiraclePtr is not placed in order to get bounty for UaF. Or "bypassing" it and getting $100,000 sounds a good idea...?? :)
- It's better to check whatever unusual. What if I overlooked the definition of raw_ptr
- How to use CodeQL (I didn't introduce it in this post because I had found no exciting results...!)