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函数,并传入参数。

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

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

case1: issue-1303614

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

Root Cause

让我们看一下代码,这里注册了一个名为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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 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

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

1
2
3
4
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

unique_ptr double init

case1: CVE-2022-2859

https://chromium.googlesource.com/chromium/src/+/08b5eaecf33165cda178517fa4ba070d1f598e16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

void MultidevicePhoneHubHandler::RegisterMessages() {
web_ui()->RegisterDeprecatedMessageCallback(
"setFakePhoneHubManagerEnabled",
base::BindRepeating(
**&MultidevicePhoneHubHandler::HandleEnableFakePhoneHubManager**,
base::Unretained(this)));
...
...
void MultidevicePhoneHubHandler::HandleEnableFakePhoneHubManager(
const base::ListValue* args) {
AllowJavascript();
const auto& list = args->GetListDeprecated();
CHECK(!list.empty());
**const bool enabled = list[0].GetBool();**
if (enabled) {
**EnableFakePhoneHubManager();**
return;
}
EnableRealPhoneHubManager();
}
...
...
void MultidevicePhoneHubHandler::EnableRealPhoneHubManager() {
// If no FakePhoneHubManager is active, return early. This ensures that we
// don't unnecessarily re-initialize the Phone Hub UI.
if (!fake_phone_hub_manager_)
return;

PA_LOG(VERBOSE) << "Setting real Phone Hub Manager";
Profile* profile = Profile::FromWebUI(web_ui());
auto* phone_hub_manager =
phonehub::PhoneHubManagerFactory::GetForProfile(profile);
ash::SystemTray::Get()->SetPhoneHubManager(phone_hub_manager);

RemoveObservers();
fake_phone_hub_manager_.reset();
}
...
void MultidevicePhoneHubHandler::EnableFakePhoneHubManager() {
DCHECK(!fake_phone_hub_manager_);
PA_LOG(VERBOSE) << "Setting fake Phone Hub Manager";
**fake_phone_hub_manager_ = std::make_unique<phonehub::FakePhoneHubManager>();** //--->[0]
ash::SystemTray::Get()->SetPhoneHubManager(**fake_phone_hub_manager_.get()**); // ---->[1]
AddObservers();
}

void PhoneHubUiController::SetPhoneHubManager(
phonehub::PhoneHubManager* phone_hub_manager) {
if (phone_hub_manager == phone_hub_manager_)
return;

**CleanUpPhoneHubManager();** //---->[2]

**phone_hub_manager_ = phone_hub_manager;** // ---->[1]
if (phone_hub_manager_) {
phone_hub_manager_->GetFeatureStatusProvider()->AddObserver(this);
phone_hub_manager_->GetOnboardingUiTracker()->AddObserver(this);
phone_hub_manager_->GetPhoneModel()->AddObserver(this);
}

UpdateUiState(GetUiStateFromPhoneHubManager());
}

void PhoneHubUiController::CleanUpPhoneHubManager() {
if (!phone_hub_manager_)
return;

**phone_hub_manager_->GetFeatureStatusProvider**()->RemoveObserver(this); //---->[2]
phone_hub_manager_->GetOnboardingUiTracker()->RemoveObserver(this);
phone_hub_manager_->GetPhoneModel()->RemoveObserver(this);
}

[0] 当我们调用两次EnableFakePhoneHubManager, fake_phone_hub_manager_字段将会被初始化两次, 又由于fake_phone_hub_manager_是一个unique_ptr,所以前一次创建的FakePhoneHubManager将会被后一次创建释放掉。

[1] 但是第一次创建的fake_phone_hub_manager_的raw ptr还保存在PhoneHubUiController的phone_hub_manager_字段里

[2] 这将导致第二次调用EnableFakePhoneHubManager的时候,沿着EnableFakePhoneHubManager->SetPhoneHubManager->CleanUpPhoneHubManager 路径,再次使用到前一次被保存到phone_hub_manager_里的被释放的FakePhoneHubManager,造成UAF。

cross-thread calback race

case1: CVE-2022-1311

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

https://chromium.googlesource.com/chromium/src.git/+/HEAD/docs/threading_and_tasks.md

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

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

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

  • 一个 main thread
    • 在 Browser 进程中 (BrowserThread::UI):用于更新 UI
    • 在 Render 进程中:运行Blink
  • 一个 io thread
    • 在 Browser 进程中(BrowserThread::IO): 用于处理 IPC 消息以及网络请求
    • 在 Render 进程中:用于处理IPC消息
  • 一些使用 base::Tread 创建的,有特殊用途的线程(可能存在)
  • 一些在使用线程池时产生的线程(可能存在)
  1. CVE-2022-1311
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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会调度该回调以执行其操作。

这个漏洞就是这么产生的,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=1320624

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

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

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

Listener no check destroyed

case1: issue-1315102

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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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=1305068

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

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