技术前瞻|WebUI: The easiest attack surface in Chromes

admin 2023年1月29日18:53:14评论9 views字数 13044阅读43分28秒阅读模式

技术前瞻|WebUI: The easiest attack surface in Chromes

"WebUI "是一个术语,用于宽泛地描述用网络技术(即HTML、CSS、JavaScript)实现的Chrome浏览器的部分UI。

Chromium中的WebUI的例子。

  • Settings (chrome://settings)
  • History (chrome://history)
  • Downloads (chrome://downloads)

关于webui具体怎么工作在这里将不展开,请参考官方文档详细阅读,本文将重点介绍webui中常见的几类漏洞模式。

https://chromium.googlesource.com/chromium/src/+/master/docs/webui_explainer.md

find but no check end

我们将以一个简单的漏洞模式来学习webui的数据流传递。

具体的说就是每个WebUI都会注册很多WebUIMessageHandler,而每个Handler上又会注册多个Message Callback,每个Message Callback都有一个对应的Message Name,可以通过这个Message Name来调用到对应的webui函数,并传入参数。

具体来说就是形如以下调用:

chrome.send("recordNavigation",[1337,0]);

case1: issue-1303614

由于该漏洞代码只存在于chromium dev,不存在发行版中,所以没有CVE,只有对应的issue编号。

Root Cause

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

让我们看一下代码,这里注册了一个名为recordNavigation的Message Callback,它将对应调用到HandleRecordNavigation函数,并处理传入的参数。

它将对传入的参数列表依次调用ConvertToNavigationView,将其强制转换为NavigationView类型的枚举值,分别得到from_view和to_view。

但由于这里并没有检查传入的参数是否小于NavigationView类型能处理的最大值,注意这里仅仅只有一个debug check,这个debug check在release发行版里是不存在的,所以可以试做没有检查。

这将导致在EmitScreenOpenDuration函数处理cast之后得到的from_view的时候, 触发一个堆溢出。

这里它将对kOpenDurationMetrics列表进行find,但是由于没有检查传入的参数是否小于NavigationView类型能处理的最大值,所以它将find不到。

我们知道在c++里,find如果找不到,迭代器iter将指向end,这其实代表的是指向容器的最后一个元素的下一个

而这里同样也没有检查find找不到的情况,也就是没有检查iter是否指向end,就直接解引用了。它同样也是使用了一个Debug Check,但这其实是无用的。

所以对iter解引用将直接越界,造成buffer overflow。

// content::WebUIMessageHandler:
void DiagnosticsMetricsMessageHandler::RegisterMessages() {
  DCHECK(web_ui());

  web_ui()->RegisterMessageCallback(
      kRecordNavigation, //----->"recordNavigation"
      base::BindRepeating(
          &DiagnosticsMetricsMessageHandler::HandleRecordNavigation,
          base::Unretained(this)));
}

enum class NavigationView {
  kSystem = 0,
  kConnectivity = 1,
  kInput = 2,
  kMaxValue = kInput,
};

// Converts base::Value<int> to NavigationView based on enum values.
NavigationView ConvertToNavigationView(const base::Value& value) {
 DCHECK(value.is_int());
  DCHECK_LE(value.GetInt(), static_cast<int>(NavigationView::kMaxValue));
  **return static_cast<NavigationView>(value.GetInt());**
}

// Message Handlers:
void DiagnosticsMetricsMessageHandler::HandleRecordNavigation(
    const base::Value::List& args)
 
{
  DCHECK_EQ(2u, args.size());
  DCHECK_NE(args[0], args[1]);

  **const NavigationView from_view = ConvertToNavigationView(args[0]);**
  const NavigationView to_view = ConvertToNavigationView(args[1]);
  const base::Time updated_start_time = base::Time::Now();

  // Recordable navigation event occurred.
  **EmitScreenOpenDuration(from_view, updated_start_time - navigation_started_);**

  // `current_view_` updated to recorded `to_view` and reset timer.
  current_view_ = to_view;
  navigation_started_ = updated_start_time;
}

void EmitScreenOpenDuration(const NavigationView screen,
                            const base::TimeDelta& time_elapsed)
 
{
  // Map of screens within Diagnostics app to matching duration metric name.
  constexpr auto kOpenDurationMetrics =
      base::MakeFixedFlatMap<NavigationView, base::StringPiece>({
          {NavigationView::kConnectivity,
           "ChromeOS.DiagnosticsUi.Connectivity.OpenDuration"},
          {NavigationView::kInput, "ChromeOS.DiagnosticsUi.Input.OpenDuration"},
          {NavigationView::kSystem,
           "ChromeOS.DiagnosticsUi.System.OpenDuration"},
      });

  **auto* iter = kOpenDurationMetrics.find(screen);**
  DCHECK(iter != kOpenDurationMetrics.end());

  base::UmaHistogramLongTimes100(std::string(iter->second), time_elapsed);
}

poc

browsing chrome://diagnostics and open devtools

execute chrome.send("recordNavigation",[1337,0]); in console.

patch

补丁就是加上了我刚刚提到的没有加的检查。

auto* iter = kOpenDurationMetrics.find(screen);
-  DCHECK(iter != kOpenDurationMetrics.end());
+  if (iter == kOpenDurationMetrics.end())
+    return;

other case

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

cross-thread calback race

case1: CVE-2022-1311

Root Cause

  • https://bugs.chromium.org/p/chromium/issues/detail?id=1310717
  • https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/threading_and_tasks.md

技术前瞻|WebUI: The easiest attack surface in Chromes


Chrome将运行UI并管理所有网页和插件进程的主进程称为“浏览器进程”或“浏览器”,而每个网页都运行在一个单独的进程里,这个进程称为渲染进程。

鉴于渲染进程在单独的进程中运行,所以Chrome有机会通过沙箱限制其对系统资源的访问,所有渲染器对网络和文件资源的访问都通过IPC来通知浏览器进程来完成。

在一个进程中,往往有如下几种线程:

  • 一个 main thread
    • 在 Browser 进程中 (BrowserThread::UI):用于更新 UI
    • 在 Render 进程中:运行Blink
  • 一个 io thread
    • 在 Browser 进程中(BrowserThread::IO): 用于处理 IPC 消息以及网络请求
    • 在 Render 进程中:用于处理IPC消息
  • 一些使用 base::Tread 创建的,有特殊用途的线程(可能存在)
  • 一些在使用线程池时产生的线程(可能存在)
void CrostiniUpgrader::Backup(const ContainerId& container_id,
                              bool show_file_chooser,
                              content::WebContents* web_contents)
 
{
  if (show_file_chooser) {
    CrostiniExportImport::GetForProfile(profile_)->ExportContainer(
        container_id, web_contents, MakeFactory());
    return;
  }
  base::FilePath default_path =
      CrostiniExportImport::GetForProfile(profile_)->GetDefaultBackupPath();
  **base::ThreadPool::PostTaskAndReplyWithResult**(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&base::PathExists, default_path),
      base::BindOnce(&CrostiniUpgrader::OnBackupPathChecked,
                     weak_ptr_factory_.GetWeakPtr(), container_id, **web_contents**,
                     default_path));
}

我介绍一个我挖掘的漏洞,首先我们要知道Chrome线程内部是怎么实现任务的同步的,其实是通过派发一个回调给一个处理线程的MessageLoop,然后MessageLoop会调度该回调以执行其操作。

技术前瞻|WebUI: The easiest attack surface in Chromes


技术前瞻|WebUI: The easiest attack surface in Chromes


这个漏洞就是这么产生的,ThreadPool::PostTaskAndReplyWithResult是UI线程向线程池里的线程发送一个PathExists函数的回调,然后线程池会检查backup路径是否存在,然后当线程池执行完任务PathExists,它会向UI线程发送一个OnRestorePathChecked函数的回调,一个回调其实和一个闭包是相似的,它会包括一个函数指针和它使用的函数参数。

在这个过程中就可能产生条件竞争。因为OnRestorePathChecked的参数里包括了一个原始指针web_contents,这样的指针是没有被保护的,所以如果我们在线程池里正在执行PathExists的同时,我们在UI线程这边通过关闭网页把web_contents释放掉,从而当OnRestorePathChecked被发回到UI线程执行的时候,此时web_contents已经被释放掉了,解引用它的指针就会触发UAF。

other case

https://bugs.chromium.org/p/chromium/issues/detail?id=1320624https://bugs.chromium.org/p/chromium/issues/detail?id=1322744https://bugs.chromium.org/p/chromium/issues/detail?id=1311701https://bugs.chromium.org/p/chromium/issues/detail?id=1304145

Listener no check destroyed

case1: issue-1315102

Root Cause

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

SupportToolMessageHandler::HandleStartDataExport 会创建一个 select_file_dialog_ [1] 并显示一个 SelectFileDialog对话框。

当 [1] 被调用时,this 原始指针被传递给ui::SelectFileDialog::Create ,并且传递的this 原始指针被保存在listener_ [2] 中。

当用户选择一个文件夹时,listener_->FileSelected(paths[0], index, params); [3]被调用来处理用户的文件夹选择。

但是,SupportToolMessageHandler::~SupportToolMessageHandler [4] 是默认析构函数,不会调 select_file_dialog_->ListenerDestroyed(); 将listener_ 置为nullptr。

如果用户在 SupportToolMessageHandler 被释放后选择了一个文件夹(即 listener_ 被释放),UAF 将在 [3] 中触发。

因此,我们可以构建以下 UAF 链:

  1. 通过chrome.send调用SupportToolMessageHandler::HandleStartDataExport
  2. 通过关闭webui网页来释放SupportToolMessageHandler
  3. 在SelectFileDialog里选择一个文件,在[3]中触发UAF。
scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
void SupportToolMessageHandler::HandleStartDataExport(
    const base::Value::List& args)
 
{
  CHECK_EQ(1U, args.size());
  const base::Value::List* pii_items = args[0].GetIfList();
  DCHECK(pii_items);

  selected_pii_to_keep_ = GetSelectedPIIToKeep(pii_items);

  AllowJavascript();
  content::WebContents* web_contents = web_ui()->GetWebContents();
  gfx::NativeWindow owning_window =
      web_contents ? web_contents->GetTopLevelNativeWindow()
                   : gfx::kNullNativeWindow;
  select_file_dialog_ = ui::SelectFileDialog::Create(
      this,
      std::make_unique<ChromeSelectFilePolicy>(web_ui()->GetWebContents()));

  select_file_dialog_->SelectFile(
      ui::SelectFileDialog::SELECT_SAVEAS_FILE,
      /*title=*/std::u16string(),
      /*default_path=*/
      GetDefaultFileToExport(handler_->GetCaseID(), data_collection_time_),
      /*file_types=*/nullptr,
      /*file_type_index=*/0,
      /*default_extension=*/base::FilePath::StringType(), owning_window,
      /*params=*/nullptr);
}

void SupportToolMessageHandler::FileSelected(const base::FilePath& path,
                                             int index,
                                             void* params)
 
{
  FireWebUIListener("support-data-export-started");
  select_file_dialog_.reset();
  this->handler_->ExportCollectedData(
      std::move(selected_pii_to_keep_), path,
      base::BindOnce(&SupportToolMessageHandler::OnDataExportDone,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SupportToolMessageHandler::FileSelectionCanceled(void* params) {
  selected_pii_to_keep_.clear();
  select_file_dialog_.reset();
}

// Checks `errors` and fires WebUIListener with the error message or the
// exported path according to the returned errors.
// type DataExportResult = {
//  success: boolean,
//  path: string,
//  error: string,
// }
void SupportToolMessageHandler::OnDataExportDone(
    base::FilePath path,
    std::set<SupportToolError> errors)
 
{
  data_path_ = path;
  base::Value::Dict data_export_result;
  const auto& export_error = std::find_if(
      errors.begin(), errors.end(), [](const SupportToolError& error) {
        return (error.error_code == SupportToolErrorCode::kDataExportError);
      });
  if (export_error == errors.end()) {
    data_export_result.Set("success"true);
    std::string displayed_path = data_path_.AsUTF8Unsafe();
#if BUILDFLAG(IS_CHROMEOS_ASH)
    displayed_path = file_manager::util::GetPathDisplayTextForSettings(
        Profile::FromWebUI(web_ui()), displayed_path);
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)
    data_export_result.Set("path", displayed_path);
    data_export_result.Set("error"std::string());
  } else {
    // If a data export error is found in the returned set of errors, send the
    // error message to UI with empty string as path since it means the export
    // operation has failed.
    data_export_result.Set("success"false);
    data_export_result.Set("path"std::string());
    data_export_result.Set("error", export_error->error_message);
  }
  FireWebUIListener("data-export-completed",
                    base::Value(std::move(data_export_result)));
}

void SupportToolMessageHandler::HandleShowExportedDataInFolder(
    const base::Value::List& args)
 
{
  platform_util::ShowItemInFolder(Profile::FromWebUI(web_ui()), data_path_);
}

////////////////////////////////////////////////////////////////////////////////
//
// SupportToolUI
//
////////////////////////////////////////////////////////////////////////////////

SupportToolUI::SupportToolUI(content::WebUI* web_ui) : WebUIController(web_ui) {
  web_ui->AddMessageHandler(std::make_unique<SupportToolMessageHandler>());

  // Set up the chrome://support-tool/ source.
  Profile* profile = Profile::FromWebUI(web_ui);
  content::WebUIDataSource::Add(
      profile, CreateSupportToolHTMLSource(web_ui->GetWebContents()->GetURL()));
}

SupportToolUI::~SupportToolUI() = default;
  **select_file_dialog_ = ui::SelectFileDialog::Create(
      this,** //----------> [1]
      **std::make_unique<ChromeSelectFilePolicy>(web_ui()->GetWebContents()));**

  select_file_dialog_->SelectFile(
      ui::SelectFileDialog::SELECT_SAVEAS_FILE,
      /*title=*/std::u16string(),
      /*default_path=*/
      GetDefaultFileToExport(handler_->GetCaseID(), data_collection_time_),
      /*file_types=*/nullptr,
      /*file_type_index=*/0,
      /*default_extension=*/base::FilePath::StringType(), owning_window,
      /*params=*/nullptr);
}

// The listener to be notified of selection completion.
raw_ptr<Listener> listener_;
SelectFileDialog::SelectFileDialog(Listener* listener,
                                   std::unique_ptr<ui::SelectFilePolicy> policy)
    : **listener_(listener)**, select_file_policy_(std::move(policy)) { // [2]
  DCHECK(listener_);
}

void SelectFileDialogImpl::OnSelectFileExecuted(
    Type type,
    std::unique_ptr<RunState> run_state,
    void* params,
    const std::vector<base::FilePath>& paths,
    int index)
 
{
  if (listener_) {
    // The paths vector is empty when the user cancels the dialog.
    if (paths.empty()) {
      listener_->FileSelectionCanceled(params);
    } else {
      switch (type) {
        case SELECT_FOLDER:
        case SELECT_UPLOAD_FOLDER:
        case SELECT_EXISTING_FOLDER:
        case SELECT_SAVEAS_FILE:
        case SELECT_OPEN_FILE:
          DCHECK_EQ(paths.size(), 1u);
          listener_->FileSelected(paths[0], index, params); // [3]
          break;
        case SELECT_OPEN_MULTI_FILE:
          listener_->MultiFilesSelected(paths, params);
          break;
        case SELECT_NONE:
          NOTREACHED();
      }
    }
  }

  EndRun(std::move(run_state));
}

~SupportToolMessageHandler() override = default// [4]

Patch

补丁就是让select_file_dialog观察webui(也就是它的listerner)的生命周期,以及避免double show。

SupportToolMessageHandler::~SupportToolMessageHandler() {
  if (select_file_dialog_) {
    select_file_dialog_->ListenerDestroyed();
  }
}

...

void SupportToolMessageHandler::HandleStartDataExport(
    const base::Value::List& args)
 
{
  CHECK_EQ(1U, args.size());
  const base::Value::List* pii_items = args[0].GetIfList();
  DCHECK(pii_items);
  // Early return if the select file dialog is already active.
  if (select_file_dialog_)
    return;

  selected_pii_to_keep_ = GetSelectedPIIToKeep(pii_items);

other case

https://bugs.chromium.org/p/chromium/issues/detail?id=1305068https://bugs.chromium.org/p/chromium/issues/detail?id=1306391https://bugs.chromium.org/p/chromium/issues/detail?id=1304884

-End-

技术前瞻|WebUI: The easiest attack surface in Chromes


原文始发于微信公众号(360漏洞研究院):技术前瞻|WebUI: The easiest attack surface in Chromes

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年1月29日18:53:14
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   技术前瞻|WebUI: The easiest attack surface in Chromeshttps://cn-sec.com/archives/1391624.html

发表评论

匿名网友 填写信息