0% found this document useful (0 votes)
76 views55 pages

An - Effective - Way - of - Bug - Hunting - Chrome 02564

The document discusses using fuzzing to find similar vulnerabilities to a known one in Chrome. It describes using variant analysis to find bugs related to improper management of RenderFrameHost lifetime. The analysis focuses on cases where a RenderFrameHost pointer is stored in a member variable and used after the object is destroyed. CodeQL queries are used to find bugs matching this pattern, identifying several issues including one addressed as CVE-2020-6461. The document advocates mutating known patterns of issues, like unexpected object releases, to find new vulnerabilities.

Uploaded by

John
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
76 views55 pages

An - Effective - Way - of - Bug - Hunting - Chrome 02564

The document discusses using fuzzing to find similar vulnerabilities to a known one in Chrome. It describes using variant analysis to find bugs related to improper management of RenderFrameHost lifetime. The analysis focuses on cases where a RenderFrameHost pointer is stored in a member variable and used after the object is destroyed. CodeQL queries are used to find bugs matching this pattern, identifying several issues including one addressed as CVE-2020-6461. The document advocates mutating known patterns of issues, like unexpected object releases, to find new vulnerabilities.

Uploaded by

John
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 55

Put in one bug and pop out more:

An effective way of bug hunting in Chrome

Rong Jian, Leecraso, Guang Gong


Alpha Lab, 360 Internet Security Center

#BHUSA @BlackHatEvents
About 360 Alpha Lab

360 Alpha Lab

 More than 400 vulnerabilities acknowledged by top vendors


 Won the highest reward
- in the history of the ASR program in 2017
- in the history of Google VRP in 2019
 Successful pwner of several Pwn2Own and Tianfu Cup events
Introduction
Variant Analysis
Find similar vulnerabilities based on a known one

 Manual code audit


 Static analysis tools
 Fuzzing as a "seed"
Variant Analysis
Find similar vulnerabilities based on a known one

 Manual code audit


 Static analysis tools
 Fuzzing as a "seed"

memset
Data Flow memmove

memcpy(dst, src, len) memcpy(dst, src, len)


The Target : Chrome
 Chrome has a multi-process architecture
- Focusing on the code runs in Browser process
- Not sandboxed
 CodeQL
- A great analysis tool
- Compiles code to a snapshot database and
- Can run queries against it for program analysis
RenderFrameHost
Issues
RenderFrameHost (RFH)
 Lives in the browser process
 Provides a communication conduit with a frame
in the render process
 Destroyed when the frame is closed

Render process Browser process


<html> RenderFrameHost (main frame)
<iframe title="My iframe"></iframe>
RenderFrameHost (iframe)
</html>
How to Access a RFH ?
 Store a GlobalFrameRoutingId and
using RenderFrameHost::FromID() to retrieve it back
 Hold a raw pointer to RFH
An Example
Chrome Issue 1062091

 InstalledAppProviderImpl provides installed app


information related to the origin of the
requesting page

1 class InstalledAppProviderImpl :
2 public blink::mojom::InstalledAppProvider
3 {
4 // ...
5 private:
6 RenderFrameHost* render_frame_host_;
7 };
An Example
Chrome Issue 1062091

 InstalledAppProviderImpl outlives RFH


 UAF occurs after RFH deconstruction

1 void InstalledAppProviderImpl::Create(
2 RenderFrameHost* host,
3 mojo::PendingReceiver<blink::mojom::InstalledAppProvider>
receiver) {
4 mojo::MakeSelfOwnedReceiver(
5 std::make_unique<InstalledAppProviderImpl>(host),
6 std::move(receiver));
7 }
Finding Bug Variants
Candidates
 Strore RFH as raw pointer in a member variable
Finding Bug Variants
Reduce false positives

WebContentsObserver
 Class can get notified of page events by inheriting it
 Give a chance to clean up when RFH is going away

FrameServiceBase
 Wrapper class of WebContentsObserver
 Work the same way
Finding Bug Variants
Candidates
 Strore RFH as raw pointer in a member variable
 Not a subclass of FrameServiceBase
 Not a subclass of WebContentsObserver or the
RenderFrameDeleted method is not implemented
Finding Bug Variants
CodeQL query
Finding Bug Variants

RawClipboardHostImpl
Reported as issue 1117348
ERROR RETURN ISSUES
RenderFrameHost lifetime issue is a too common.

The way how to mutate the pattern is important.

ERROR RETURN ISSUES

=> found 14 vulnerabilities and got 5 CVEs


Example - CVE-2020-6461

1 void BlobRegistryImpl::BlobUnderConstruction::TransportComplete(
2 …
3 if (context()->registry().HasEntry(uuid())) {
4 if (result == BlobStatus::DONE)
5 context()->NotifyTransportComplete(uuid()); std::map blobs_under_construction_.erase
6 else
7 context()->CancelBuildingBlob(uuid(), result);
8 } delete this
9 if (BlobStatusIsBadIPC(result)) {
10 std::move(bad_message_callback_)
11 .Run("Received invalid data while transporting blob");
12 }
13 …
Example - crbug/1065704

1 void WebSocket::ReadAndSendFromDataPipe() {
2 …
3 const size_t size_to_send =
4 std::min(static_cast<uint64_t>(readable_size), data_frame.data_length);
5 auto data_to_pass = base::MakeRefCounted<net::IOBuffer>(size_to_send);
6 const bool is_final = (size_to_send == data_frame.data_length); FailChannel
7 memcpy(data_to_pass->data(), buffer, size_to_send);
8 channel_->SendFrame(is_final, MessageTypeToOpCode(data_frame.type),
9 std::move(data_to_pass), size_to_send);
std::set connections_.erase
10
11 const MojoResult end_result = readable_->EndReadData(size_to_send);
12 DCHECK_EQ(end_result, MOJO_RESULT_OK);
13 … delete this
14 }
class A {
void A::Func(){
Root cause :

During the code execution of a class instance, Foo(); map.erase

calling other function which could cause the …


mem_var_->DoSth();
destruction of this instance.
}
The UAF will occur if any member variable or }

member function is accessed after that.


1 ...
2 key_map.getType().stripType() instanceof ManagedMapType and
3 key_map.getType().stripType().(ManagedMapType).getManagedType() =
map_field.getManagedType() and
4 reset_func = map_field.getAManagedReset().getEnclosingFunction() and
5 reach*(ext_func, reset_func) and
6 member_f1.getDeclaringType() = map_field.getManagedType() and
7 fc.getTarget() = ext_func and
8 fc.getEnclosingFunction() = member_f1 and
9
10 (member_V.getAnAccess() = ex and
11 ex.getEnclosingFunction() = member_f1)
12 or
13 (member_fc.getTarget() = member_f2 and
14 member_fc.getEnclosingFunction() = member_f1) and
15 ...
1 void PasswordProtectionRequest::OnWhitelistCheckDone(bool match_whitelist) {
2 DCHECK(CurrentlyOnThread(ThreadID::UI));
CVE-2021-21115 3 if (match_whitelist) {
4 if (password_protection_service_->CanSendSamplePing()) {
5 FillRequestProto(/*is_sampled_ping=*/true);
6 }
7 Finish(RequestOutcome::MATCHED_WHITELIST, nullptr);
8 } else {
9 StartTimeout();
10 CheckCachedVerdicts();
11 }
12}
delete the request instance
1 void PasswordProtectionRequest::SendRequest() {
2 DCHECK(CurrentlyOnThread(ThreadID::UI));
3
4 web_ui_token_ =
5 WebUIInfoSingleton::GetInstance()->AddToPGPings(*request_proto_); std::set pending_requests_.erase
6
7 std::string serialized_request;
8 if (!request_proto_->SerializeToString(&serialized_request)) {
9 Finish(RequestOutcome::REQUEST_MALFORMED, nullptr);
10 return;
11 }
12 …
13}
how to "mutate"

Releasing objects in some unexpected conditional branches is prone to be vulnerable.

Focus on lifetime management of the object referenced by a smart pointer.


1 ...
2 and fc.getTarget() = vuln_func
3 and fc.getAnArgument() = pass
4 and vuln_func = para.getFunction()
5 and move.getEnclosingFunction() = vuln_func
6 and move.getTarget() instanceof StdMove
7 and move.getAnArgument() = para.getAnAccess()
8 and ret.getEnclosingFunction() = vuln_func
9 and not dominates(move,ret)
10 ...
1 void DistillCurrentPageAndView(content::WebContents* old_web_contents) {
2 …
crbug.com/1150328 3 std::unique_ptr<content::WebContents> old_web_contents_owned =
4 CoreTabHelper::FromWebContents(old_web_contents)
5 ->SwapWebContents(std::move(new_web_contents), false, false);
6
7 std::unique_ptr<SourcePageHandleWebContents> source_page_handle(
8 new SourcePageHandleWebContents(old_web_contents_owned.release(), true));
9
10 MaybeStartDistillation(std::move(source_page_handle));
11
12 #if !defined(OS_ANDROID)
13 dom_distiller::UMAHelper::LogTimeOnDistillablePage(old_web_contents);
14 #endif
15 }
1 void MaybeStartDistillation(
2 std::unique_ptr<SourcePageHandleWebContents> source_page_handle) {
3 const GURL& last_committed_url =
4 source_page_handle->web_contents()->GetLastCommittedURL();
5 if (!dom_distiller::url_utils::IsUrlDistillable(last_committed_url))
6 return;
7 …
8 std::unique_ptr<DistillerPage> distiller_page =
9 dom_distiller_service->CreateDefaultDistillerPageWithHandle(
10 std::move(source_page_handle));
11 …
12 }
The return conditions of most results is hard to be met

How to further mutate the pattern?


1 void TabStrip::TabDragContextImpl::ContinueDrag(views::View* view, const
ui::LocatedEvent& event) {
Found a special case 2 if (drag_controller_.get() &&
3 drag_controller_->event_source() == EventSourceFromEvent(event)) {
4 gfx::Point screen_location(event.location());
5 views::View::ConvertPointToScreen(view, &screen_location);
6 drag_controller_->Drag(screen_location);
7 }
8
9 // Note: |drag_controller| can be set to null during the drag above.
10 if (drag_controller_ && drag_controller_->group())
11 tab_strip_->UpdateTabGroupVisuals(*drag_controller_->group());
12 }

In Linux, Drag will eventually call X11WholeScreenMoveLoop::RunMoveLoop, which will run a


nested message loop

1 bool X11WholeScreenMoveLoop::RunMoveLoop(
2 …
3 in_move_loop_ = true;
4 canceled_ = false;
5 base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
6 quit_closure_ = run_loop.QuitClosure();
7 run_loop.Run();
11 …
12 }
Nested message loop
Create a new message loop

1 bool X11WholeScreenMoveLoop::RunMoveLoop(
2 … Destroy instances in new loop
3 in_move_loop_ = true;
4 canceled_ = false;
5 base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed); Exit the new loop
6 quit_closure_ = run_loop.QuitClosure();
7 run_loop.Run();
11 …
12 } Back to the blocking
context and continue code
execution
Save the context and create a new message loop in the current thread
1 void TabStrip::TabDragContextImpl::ContinueDrag(views::View* view, const
ui::LocatedEvent& event) {
Back to the special case 2 if (drag_controller_.get() &&
3 drag_controller_->event_source() == EventSourceFromEvent(event)) {
4 gfx::Point screen_location(event.location());
5 views::View::ConvertPointToScreen(view, &screen_location);
6 drag_controller_->Drag(screen_location);
7 }
8
9 // Note: |drag_controller| can be set to null during the drag above.
10 if (drag_controller_ && drag_controller_->group())
11 tab_strip_->UpdateTabGroupVisuals(*drag_controller_->group());
back to the context 12 }

destroy TabStrip::TabDragContextImpl in new message loop

1 bool X11WholeScreenMoveLoop::RunMoveLoop(
2 …
3 in_move_loop_ = true;
4 canceled_ = false;
5 base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
6 quit_closure_ = run_loop.QuitClosure();
CVE-2020-16004 7 run_loop.Run();
11 …
12 }
Nested message loop Results

8 UAF bugs about the Linux X11 clipboard

2 UAF issues related to the messageBox

a series of issues in the ozone clipboard


1 void ChromePasswordManagerClient::OnPaste() {
2 …
crbug.com/1161147 3 if (!used_crosapi_workaround) {
4 ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread();
5 ui::DataTransferEndpoint data_dst = ui::DataTransferEndpoint(
6 ui::EndpointType::kDefault, /*notify_if_restricted=*/false);
7 clipboard->ReadText(ui::ClipboardBuffer::kCopyPaste, &data_dst, &text);
8 }
9 was_on_paste_called_ = true;
10 password_reuse_detection_manager_.OnPaste(std::move(text));
back to the context 11 }

destroy ChromePasswordManagerClient in new message loop

1 void SelectionRequestor::BlockTillSelectionNotifyForRequest(Request* request) {


2 …
3 base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
4 request->quit_closure = run_loop.QuitClosure();
5 run_loop.Run()
6 …
7 }
WeakPtr
Optimization
WeakPtr
 Chromium implements its own version of weak_ptr
 Widely used in the codebase
 Null-test before accessing the underlying object

1 class Foo { ... };


2 WeakPtr<Foo> foo;
3 if (foo) // null-test for validation
4 foo->method();
What if there is no null-test ?
Null pointer dereference ?
1 template <typename T>
2 class WeakPtr : public internal::WeakPtrBase {
3 // ...
4 T* operator->() const {
5 DCHECK(ref_.IsValid());
6 return get();
7 }
8
9 T* get() const {
10 return ref_.IsValid() ? reinterpret_cast<T*>(ptr_) : nullptr;
11 }
12
13 }
class Bar {
public:
virtual void increase() { count_++; }
private:
int count_ = 0;
};
Just like what WeakPtr does
class Foo {
public:
Foo() {
inner_ = new Bar();
is_valid_ = true;
}
Bar* get() { return is_valid_ ? inner_ : nullptr; }
void invalidate() { is_valid_ = false; }
private:
Bar* inner_;
bool is_valid_;
};  get( ) should return null after invalidation
int main(){
Foo* foo = new Foo();  Virtual function call on nullptr should crash the process
foo->invalidate();
for(int i = 0; i < 2; i++)
foo->get()->increase();
cout << "Not crash" << endl;
return 0;
}
WeakPtr Optimization
Null pointer dereference

 Compiler chooses to fold the branch


 Convert a null pointer dereference crash to an
exploitable UAF bug
Finding Bug Variants
Candidates
 WeakPtr as class member variable
 Being accessed without any null-test
Finding Bug Variants
Step 1 : Find function calls like foo->method( )

CodeQL C++ Code


1 weak_ptr.getType().getName()
2 .matches("%WeakPtr<%>%") foo->method();
2 and fc.getQualifier()
5 = weak_ptr.getAnAccess().(Expr)
3 and fc.getTarget().getName()
4 .matches("%operator->%")
Finding Bug Variants
No related to
Step 2 : There is no null-test before accessing WeakPtr
the WeakPtr

CodeQL C++ Code


1 weak_ptr.getType().getName()
2 .matches("%WeakPtr<%>%") if(condition) {
2 and fc.getQualifier() //...
5 = weak_ptr.getAnAccess().(Expr) return;
3 and fc.getTarget().getName() }
4 .matches("%operator->%") foo->method();
5 not exists(IfStmt if_stmt |
6 isWeakPtrCheck(if_stmt, weak_ptr)
7 and dominates(if_stmt.getCondition(), fc)
8 )
Results
About 363 results, analysis part of them...

 CVE-2020-15996 , CVE-2020-16014 , CVE-2020-16016


 Fixed in CL https://crrev.com/816701
 We exploited one of them to escape Chrome
sandbox in TianFu Cup 2020
THE EOP VULNERABILITY
Prior Knowledge

Mojo IPC

Legacy IPC

control message

route message
Prior Knowledge

Ppapi

• By using the existing connection

• PP_Instance ID <-> Connection

• All PP_Instance ID is stored in a global


map in renderer process

• call ppb_thunk function with PP_Instance


The Bug

std::unique_ptr <T> sptr_x;

raw_x_ = x.get();
weakptr
sptr_x.reset();

raw_x_->DoSomething();
Stores in map |resources_|

get as a raw pointer

wrapped as a weakptr

Keeped in file_system_host_
The Bug
could be passed in from the renderer-side

1 void PpapiHost::OnHostMsgResourceCreated(const proxy::ResourceMessageCallParams& params,


2 …
3 DCHECK(resource_host->pp_resource());
4 resources_[params.pp_resource()] = std::move(resource_host);
5 }

typedef std::map<PP_Resource, std::unique_ptr<ResourceHost>> ResourceMap;

ResourceMap resources_;

base::WeakPtr<PepperFileSystemBrowserHost> file_system_host_
Exploit
Exploit

Need to construct the structure to meet the constraints on this path and hijack the control flow.

No inter-process randomization on Windows.

Only need to leak the heap address


Exploit

Leak the heap address

Use SharedBuffers as Mark Brand[*]?

32GB limited!

[*] https://googleprojectzero.blogspot.com/2019/04/virtually-unlimited-memory-escaping.html
Exploit
1 void PepperFileIOHost::SendFileOpenReply(
2 …
3 if (pp_error == PP_OK) {
4 state_manager_.SetOpenSucceed();
5 // A non-zero resource id signals the plugin side to check quota.
6 if (check_quota_)
Reply 4 bytes to renderer process 7 quota_file_system = file_system_host_->pp_resource();
8 }
9 reply_context.params.set_result(pp_error);
10 host()->SendReply(
11 reply_context,
12 PpapiPluginMsg_FileIO_OpenReply(quota_file_system, max_written_offset_));
13 state_manager_.SetOperationFinished();
14 }
Exploit
Demo Video
CONCLUSION

Some background

Some bug and pattern, how to find bug variants

RenderFrameHost lifetime issues

Error Return pattern

WeakPtr Optimization

The exploit
THANKS!

You might also like