CVE-2019-5786漏洞分析

漏洞公告

按照惯例,先上漏洞通告:72.0.3626.121版本之前的Google Chrome存在漏洞,漏洞源于Blink中的对象生存处理问题。远程攻击者可通过诱导用户点击精心制作的HTML页面来进行超出范围的内存访问,成功利用此漏洞的远程攻击者可执行任意代码(还需结合Win32k内核提权漏洞,从而绕过Chrome沙箱保护)。

补丁比对

注意到以下对FileReaderLoader::ArrayBufferResult函数的修复,这段补丁是72.0.3626.120版本到72.0.3626.121版本的改变中唯一一段看上去是针对Blink的安全修复。下面来分析一下,这个流程主要分为两个状态,finished_loading和unfinished_loading,两段代码只有一处不同。补丁前的代码中,无论是否加载完成,均返回DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());而补丁后的代码在未加载完成时会返回DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()));加载完成后返回DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())。


显然,未加载完成时的FileReaderLoader::ArrayBufferResult函数返回值成为本次研究的重点,从名称上来看,这个函数和File加载时ArrayBuffer的result相关。

补丁前:DOMArrayBuffer::Create(raw_data_->ToArrayBuffer())
补丁后:DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()))

首先分析ToArrayBuffer函数,它返回buffer_或buffer_->Slice(0, bytes_used_):

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_);
}

ArrayBuffer::Slice(int begin, int end)函数中又会调用SliceImpl(ClampIndex(begin),ClampIndex(end)),最终调用ArrayBuffer::Create(static_cast<const char*>(Data()) + begin, size),所以程序在finished_loading和unfinished_loading这两个状态查看ArrayBufferResult时都有可能调用ArrayBuffer::Create函数:

scoped_refptr<ArrayBuffer> ArrayBuffer::Slice(int begin, int end) const {
  return SliceImpl(ClampIndex(begin), ClampIndex(end));
}

scoped_refptr<ArrayBuffer> ArrayBuffer::SliceImpl(unsigned begin, unsigned end) const {
  size_t size = static_cast<size_t>(begin <= end ? end - begin : 0);
  return ArrayBuffer::Create(static_cast<const char*>(Data()) + begin, size);
}

再来看ArrayBuffer::Create函数,创建一个新的ArrayBuffer,然后将source复制进去。现在总结一下,修补后的FileReaderLoader::ArrayBufferResult函数会直接调用DOMArrayBuffer::Create(ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength())),而修补前的程序会在ArrayBufferBuilder::ToArrayBuffer函数中进行判断,如果buffer_->ByteLength() == bytes_used_就直接返回buffer_,否则返回buffer_->Slice(0, bytes_used_),最终还是会调用ArrayBuffer::Create(raw_data_->Data(), raw_data_->ByteLength()),所以问题应该出现在buffer_->ByteLength() == bytes_used_时:

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;
}

根据以下代码可知,buffer_属于scoped_refptr<ArrayBuffer>类型,至于buffer_->ByteLength()和bytes_used_是什么,就在调试中看吧,不然源码搜索太费劲:

static const int kDefaultBufferCapacity = 32768;

ArrayBufferBuilder::ArrayBufferBuilder()
    : bytes_used_(0), variable_capacity_(true) {
  buffer_ = ArrayBuffer::Create(kDefaultBufferCapacity, 1);
}

调试分析

FileReader对象
根据file_reader_loader文件名可推测这段代码会处理FileReader对象,在MDN中可以搜索该对象的详细使用方法

FileReader对象可以异步读取用户计算机上存储的文件(或原始数据缓冲区)的内容,使用FileBlob对象指定要读取的文件或数据。Blob对象代表不可变的原始数据的类似文件的对象。它们可以读取为文本或二进制数据,也可以转换为Readable Stream。 Blob可以表示不一定是JavaScript本机格式的数据。 File接口基于Blob,继承了Blob功能并将其扩展为支持用户系统上的文件。File提供有关文件的信息,并允许网页中的JavaScript访问其内容。

FileReader有一个readyState属性,记录其读取时的状态,分别为:EMPTY(还未加载)、LOADING(正在加载)、DONE(加载完成),对应了前面分析的 finished_loading 和 !finished_loading。

FileReader有一些内置事件,包括abort、error、load、loadend、loadstart、progress。可以为这些事件自定义处理函数,其中progress事件在读取数据时定期触发,我们可以注册progress事件的回调函数。如果在这时候去获取result,就会在未加载完成时进入FileReaderLoader::ArrayBufferResult函数。如果将要读取的数据的长度设置的稍微大一点,就会在加载的过程中多次回调这个函数。

FileReader.onprogress
A handler for the progress event. This event is triggered while reading a Blob content.
FileReader.onloadstart
A handler for the loadstart event. This event is triggered each time the reading is starting.

可以通过以上信息构造出一些代码,以供后续调试分析…

下面开始调试啦啦啦啦啦啦:
查找FileReaderLoader::ArrayBufferResult并下断点,由于在这个函数内部调用了ArrayBufferBuilder::ToArrayBuffer函数,可以单步跟踪过去,程序判断一些条件满足之后,就调用了WTF::ArrayBufferBuilder::ToArrayBuffer函数。

0:000> x chrome_child!*FileReaderLoader::ArrayBufferResult
61875480 chrome_child!blink::FileReaderLoader::ArrayBufferResult (void)

下面为WTF::ArrayBufferBuilder::ToArrayBuffer函数中的流程,从eax+8中取出值赋给ebx,值得关注的是0x4c4b400转化为十进制的值为80000000,为测试js中Blob对象字符串的长度,所以有理由怀疑buffer_->ByteLength()返回的是ArrayBuffer对象应有的长度。而后面的0x3f50000转化过来为66387968,说明已经读取了66387968字节,还没有完全加载完成,因而会调用ArrayBuffer::SliceImpl函数:

chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer:
61bd02d8 55              push    ebp
61bd02d9 89e5            mov     ebp,esp
61bd02db 53              push    ebx
61bd02dc 57              push    edi
61bd02dd 56              push    esi
61bd02de 8b5108          mov     edx,dword ptr [ecx+8]
61bd02e1 8b7508          mov     esi,dword ptr [ebp+8]
61bd02e4 8b4204          mov     eax,dword ptr [edx+4]
61bd02e7 85c0            test    eax,eax
61bd02e9 7405            je      chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x18 (61bd02f0)
61bd02eb 8b5808          mov     ebx,dword ptr [eax+8] ds:0023:04469ea8=04c4b400
61bd02ee eb02            jmp     chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x1a (61bd02f2)
0:000> 
eax=04469ea0 ebx=0022e838 ecx=04405090 edx=04404570 esi=0022e838 edi=04588000
eip=61bd02eb esp=0022e820 ebp=0022e82c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x13:
61bd02eb 8b5808          mov     ebx,dword ptr [eax+8] ds:0023:04469ea8=04c4b400

0:000> 
eax=04469ea0 ebx=04c4b400 ecx=04405090 edx=04404570 esi=0022e838 edi=04588000
eip=61bd02f2 esp=0022e820 ebp=0022e82c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x1a:
61bd02f2 8b39            mov     edi,dword ptr [ecx]  ds:0023:04405090=03f50000

调试WTF::ArrayBufferBuilder::ToArrayBuffer函数就会发现ArrayBuffer::SliceImpl函数的流程位于这个函数中(比较神奇,第一次调还以为被补丁了呢),跟踪多次发现程序都走了else流程,也就是buffer_->Slice(0, bytes_used_),返回的都是最终指向3e00000大小堆的不同blink::DOMArrayBuffer结构。

0:000> dd eax
2510f5a8  6284a020 00000000 04405260 00000000   chrome_child!blink::DOMArrayBuffer::`vftable'
2510f5b8  00000000 00000000 00000000 00000000
2510f5c8  00000000 00000000 00000000 00000000
2510f5d8  00000000 00000000 00000000 00000000
2510f5e8  00000000 00000000 00000000 00000000
2510f5f8  00000000 00000000 00000000 00000000
2510f608  00000000 00000000 00000000 00000000
2510f618  00000000 00000000 00000000 00000000
0:000> dd 4405260
04405260  00000001 0446a320 00000000 00000000
04405270  00000001 0446a340 00000000 00000000
04405280  00000001 0446a360 00000000 00000000
04405290  a0524004 00000000 00000000 00000000
044052a0  b0524004 00000000 00000000 00000000
044052b0  c0524004 00000000 00000000 00000000
044052c0  d0524004 00000000 00000000 00000000
044052d0  e0524004 00000000 00000000 00000000
0:000> dd 0446a320
0446a320  00000001 3ea04000 03e00000 605afe8e
0446a330  00000000 00000000 00000001 00000000
0446a340  00000001 50804000 03c00000 605afe8e
0446a350  00000000 00000000 00000001 00000000
0446a360  00000001 67204000 03d50000 605afe8e
0446a370  00000000 00000000 00000001 00000000
0446a380  a0a34604 00000000 00000000 5ee13660
0446a390  00000000 00000000 00000000 00000000
0:000> dd 3ea04000
3ea04000  41414141 41414141 41414141 41414141
3ea04010  41414141 41414141 41414141 41414141
3ea04020  41414141 41414141 41414141 41414141
3ea04030  41414141 41414141 41414141 41414141
3ea04040  41414141 41414141 41414141 41414141
3ea04050  41414141 41414141 41414141 41414141
3ea04060  41414141 41414141 41414141 41414141
3ea04070  41414141 41414141 41414141 41414141

0:000> dd eax
2510f5b8  6284a020 00000000 04405250 00000000  chrome_child!blink::DOMArrayBuffer::`vftable'
2510f5c8  00000000 00000000 00000000 00000000
2510f5d8  00000000 00000000 00000000 00000000
2510f5e8  00000000 00000000 00000000 00000000
2510f5f8  00000000 00000000 00000000 00000000
2510f608  00000000 00000000 00000000 00000000
2510f618  00000000 00000000 00000000 00000000
2510f628  00000000 00000000 00000000 00000000
0:000> dd 4405250
04405250  00000001 0446a300 00000000 00000000
04405260  00000001 0446a320 00000000 00000000
04405270  00000001 0446a340 00000000 00000000
04405280  00000001 0446a360 00000000 00000000
04405290  a0524004 00000000 00000000 00000000
044052a0  b0524004 00000000 00000000 00000000
044052b0  c0524004 00000000 00000000 00000000
044052c0  d0524004 00000000 00000000 00000000
0:000> dd 0446a300
0446a300  00000001 42a04000 03e00000 605afe8e
0446a310  00000000 00000000 00000001 00000000
0446a320  00000001 3ea04000 03e00000 605afe8e
0446a330  00000000 00000000 00000001 00000000
0446a340  00000001 50804000 03c00000 605afe8e
0446a350  00000000 00000000 00000001 00000000
0446a360  00000001 67204000 03d50000 605afe8e
0446a370  00000000 00000000 00000001 00000000
0:000> dd 42a04000
42a04000  41414141 41414141 41414141 41414141
42a04010  41414141 41414141 41414141 41414141
42a04020  41414141 41414141 41414141 41414141
42a04030  41414141 41414141 41414141 41414141
42a04040  41414141 41414141 41414141 41414141
42a04050  41414141 41414141 41414141 41414141
42a04060  41414141 41414141 41414141 41414141
42a04070  41414141 41414141 41414141 41414141

我们在if命中处下断点,看程序某一时刻是否会执行到这里,命中之后单步调试,发现程序使用edx的值覆写esi指向的内存,通过edx可引用到原始ArrayBuffer。

0:000> p
eax=04469ea0 ebx=04c4b400 ecx=04405090 edx=04404570 esi=0022e838 edi=04c4b400
eip=61bd02fa esp=0022e820 ebp=0022e82c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x22:
61bd02fa 8916            mov     dword ptr [esi],edx  ds:0023:0022e838=8aa66acd
0:000> dd esi
0022e838  8aa66acd 0022e83c 8aa66acd 255856d0
0022e848  0022e878 2510ee20 0022e868 612059da
0022e858  5e97584f 8aa66af5 255856d0 0022e878
0022e868  0022e898 60b50c4d 2510ee20 0022e878
0022e878  00000000 00000000 00000000 60b4d292
0022e888  8aa66a05 0344bf60 0344ced8 033e7328
0022e898  0022e8c0 5feb7239 0022e8a8 0022e8b8
0022e8a8  0022e8c8 0022e8dc 00000000 04295321
0:000> dd edx
04404570  00000001 04469ea0 00000000 00000000
04404580  00000006 00000000 00000001 00000007
04404590  00000002 00000000 00000000 00000000
044045a0  00000002 3f800000 ff000000 00000000
044045b0  00000002 00000000 00000000 00000000
044045c0  00000000 00000000 00000000 00000000
044045d0  044681e0 00000000 044046a0 00000000
044045e0  044681e0 25e01000 0001e000 00000000
0:000> dd 04469ea0
04469ea0  00000001 32404000 04c4b400 605afe8e
04469eb0  00000000 00000000 00000001 00000000
04469ec0  00000001 046101e0 00000000 00000000
04469ed0  25446ed0 00000049 00000000 00000000
04469ee0  033dacf8 41c00000 00000000 00000000
04469ef0  00000000 00000000 00000201 00000000
04469f00  00000001 00000000 00000000 04404fb0
04469f10  00000000 00000005 00000006 00000000
0:000> dd 32404000
32404000  41414141 41414141 41414141 41414141
32404010  41414141 41414141 41414141 41414141
32404020  41414141 41414141 41414141 41414141
32404030  41414141 41414141 41414141 41414141
32404040  41414141 41414141 41414141 41414141
32404050  41414141 41414141 41414141 41414141
32404060  41414141 41414141 41414141 41414141
32404070  41414141 41414141 41414141 41414141

后面单步调试,发现程序将该结构的引用加1,并将esi作为函数返回值,此时esi指针指向该结构体。这也证实了在完全加载的情况下,程序直接返回原缓冲区。如果可以获取两个以上对初始ArrayBuffer缓冲区的引用,释放其中一个,并试图通过其他引用访问,则会触发释放后重用。

0:000> 
eax=04469ea0 ebx=04c4b400 ecx=04405090 edx=04404570 esi=0022e838 edi=04c4b400
eip=61bd02fe esp=0022e820 ebp=0022e82c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x26:
61bd02fe ff02            inc     dword ptr [edx]      ds:0023:04404570=00000001
0:000> 
eax=04469ea0 ebx=04c4b400 ecx=04405090 edx=04404570 esi=0022e838 edi=04c4b400
eip=61bd0300 esp=0022e820 ebp=0022e82c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x28:
61bd0300 eb31            jmp     chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x5b (61bd0333)
0:000> 
eax=04469ea0 ebx=04c4b400 ecx=04405090 edx=04404570 esi=0022e838 edi=04c4b400
eip=61bd0333 esp=0022e820 ebp=0022e82c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x5b:
61bd0333 89f0            mov     eax,esi
0:000> dd edx
04404570  00000002 04469ea0 00000000 00000000
04404580  00000006 00000000 00000001 00000007
04404590  00000002 00000000 00000000 00000000
044045a0  00000002 3f800000 ff000000 00000000
044045b0  00000002 00000000 00000000 00000000
044045c0  00000000 00000000 00000000 00000000
044045d0  044681e0 00000000 044046a0 00000000
044045e0  044681e0 25e01000 0001e000 00000000
0:000> dd esi
0022e838  04404570 0022e83c 8aa66acd 255856d0
0022e848  0022e878 2510ee20 0022e868 612059da
0022e858  5e97584f 8aa66af5 255856d0 0022e878
0022e868  0022e898 60b50c4d 2510ee20 0022e878
0022e878  00000000 00000000 00000000 60b4d292
0022e888  8aa66a05 0344bf60 0344ced8 033e7328
0022e898  0022e8c0 5feb7239 0022e8a8 0022e8b8
0022e8a8  0022e8c8 0022e8dc 00000000 04295321

下面使用这篇文章中的POC进行调试,思路就是FileReader读取长度大的Blob对象,使其在progress阶段多次调用FileReaderLoader::ArrayBufferResult函数,总有一次会出现完全加载的情况,在完全加载之后又会回调onLoadEnd函数,再次调用FileReaderLoader::ArrayBufferResult函数,那么我们就得到原始ArrayBuffer的两个不同引用,然后再通过setUint32修改其中一个原始ArrayBuffer起始处的四字节,如果通过另一处引用发现这四字节改变了就证明存在漏洞,就继续后面的触发测试:

首先我们调试一下漏洞证明阶段,还是看FileReaderLoader::ArrayBufferResult函数,找到原始ArrayBuffer,然后下硬件断点:

0:000> g
Breakpoint 1 hit
eax=0406a7c0 ebx=05000000 ecx=04005410 edx=040054b0 esi=0022e888 edi=05000000
eip=5c8902f8 esp=0022e870 ebp=0022e87c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x20:
5c8902f8 85d2            test    edx,edx
0:000> p
eax=0406a7c0 ebx=05000000 ecx=04005410 edx=040054b0 esi=0022e888 edi=05000000
eip=5c8902fa esp=0022e870 ebp=0022e87c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!WTF::ArrayBufferBuilder::ToArrayBuffer+0x22:
5c8902fa 8916            mov     dword ptr [esi],edx  ds:0023:0022e888=c956a63e

0:000> dd edx
040054b0  00000001 0406a7c0 00000000 00000000
040054c0  30540004 0406a820 00000000 00000000
040054d0  10550004 0406a840 00000000 00000000
040054e0  00000001 0406a860 00000000 00000000
040054f0  00000001 0406a880 00000000 00000000
04005500  00000001 0406a8a0 00000000 00000000
04005510  c0540004 0406a8c0 00000000 00000000
04005520  30550004 00000000 00000000 00000000
0:000> dd 0406a7c0
0406a7c0  00000001 2f004000 05000000 5b26fe8e
0406a7d0  00000000 00000000 00000001 00000000
0406a7e0  20a70604 00000000 00000000 59ad3660
0406a7f0  00000000 00000000 00000000 00000000
0406a800  40a80604 62e04000 04d90000 5b26fe8e
0406a810  00000000 00000000 00000000 00000000
0406a820  e0a70604 34204000 04c00000 5b26fe8e
0406a830  00000000 00000000 00000000 00000000
0:000> dd 2f004000
2f004000  41414141 41414141 41414141 41414141
2f004010  41414141 41414141 41414141 41414141
2f004020  41414141 41414141 41414141 41414141
2f004030  41414141 41414141 41414141 41414141
2f004040  41414141 41414141 41414141 41414141
2f004050  41414141 41414141 41414141 41414141
2f004060  41414141 41414141 41414141 41414141
2f004070  41414141 41414141 41414141 41414141
ba r4 [2f004000]

程序断在Builtins_DataViewPrototypeSetUint32函数中,程序正逐字节将缓冲区前4字节赋值为0x61,对应了DataView(ab1).setUint32(0, flag, true)这句。

5ab95603 8b7df0          mov     edi,dword ptr [ebp-10h]
5ab95606 881407          mov     byte ptr [edi+eax],dl
5ab95609 8b55e8          mov     edx,dword ptr [ebp-18h]
5ab9560c 88540701        mov     byte ptr [edi+eax+1],dl
5ab95610 89f2            mov     edx,esi
5ab95612 88540702        mov     byte ptr [edi+eax+2],dl
5ab95616 89ca            mov     edx,ecx
5ab95618 88540703        mov     byte ptr [edi+eax+3],dl
5ab9561c 8b45ec          mov     eax,dword ptr [ebp-14h] ss:0023:0022e744=00000003

0:000> 
eax=00000000 ebx=032ba188 ecx=00000061 edx=00000061 esi=00000061 edi=2f004000
eip=5ab9561c esp=0022e730 ebp=0022e758 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!Builtins_DataViewPrototypeSetUint32+0x39c:
5ab9561c 8b45ec          mov     eax,dword ptr [ebp-14h] ss:0023:0022e744=00000003
0:000> dd ebp-18h l1
0022e740  00000061
0:000> dd 2f004000
2f004000  61616161 41414141 41414141 41414141
2f004010  41414141 41414141 41414141 41414141
2f004020  41414141 41414141 41414141 41414141
2f004030  41414141 41414141 41414141 41414141
2f004040  41414141 41414141 41414141 41414141
2f004050  41414141 41414141 41414141 41414141
2f004060  41414141 41414141 41414141 41414141
2f004070  41414141 41414141 41414141 41414141

然后程序又通过Builtins_DataViewPrototypeGetUint32函数获取缓冲区前4字节数据,根据POC可知,我们通过ab1的引用来设置缓冲区,然后通过ab2的引用来查看缓冲区是否改变,此时引用的缓冲区前四字节已经变成0x61616161,证明存在漏洞。

5ab91b03 0fb61406        movzx   edx,byte ptr [esi+eax]
5ab91b07 0fb67c0601      movzx   edi,byte ptr [esi+eax+1]   ds:0023:2f004001=61
5ab91b0c 8955f0          mov     dword ptr [ebp-10h],edx
5ab91b0f 0fb6540602      movzx   edx,byte ptr [esi+eax+2]
5ab91b14 0fb6440603      movzx   eax,byte ptr [esi+eax+3]

0:000> g
Breakpoint 2 hit
eax=00000000 ebx=032ba188 ecx=00000001 edx=00000061 esi=2f004000 edi=00000000
eip=5ab91b07 esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
chrome_child!Builtins_DataViewPrototypeGetUint32+0x2c7:
5ab91b07 0fb67c0601      movzx   edi,byte ptr [esi+eax+1]   ds:0023:2f004001=61
0:000> dd esi+eax
2f004000  61616161 41414141 41414141 41414141
2f004010  41414141 41414141 41414141 41414141
2f004020  41414141 41414141 41414141 41414141
2f004030  41414141 41414141 41414141 41414141
2f004040  41414141 41414141 41414141 41414141
2f004050  41414141 41414141 41414141 41414141
2f004060  41414141 41414141 41414141 41414141
2f004070  41414141 41414141 41414141 41414141

0:000> 
eax=00000061 ebx=032ba188 ecx=00000001 edx=00000061 esi=2f004000 edi=00000061
eip=5ab91b39 esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!Builtins_DataViewPrototypeGetUint32+0x2f9:
5ab91b39 c1e210          shl     edx,10h
0:000> 
eax=00000061 ebx=032ba188 ecx=00000001 edx=00610000 esi=2f004000 edi=00000061
eip=5ab91b3c esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!Builtins_DataViewPrototypeGetUint32+0x2fc:
5ab91b3c c1e018          shl     eax,18h
0:000> 
eax=61000000 ebx=032ba188 ecx=00000001 edx=00610000 esi=2f004000 edi=00000061
eip=5ab91b3f esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!Builtins_DataViewPrototypeGetUint32+0x2ff:
5ab91b3f c1e708          shl     edi,8
0:000> 
eax=61000000 ebx=032ba188 ecx=00000001 edx=00610000 esi=2f004000 edi=00006100
eip=5ab91b42 esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!Builtins_DataViewPrototypeGetUint32+0x302:
5ab91b42 0bd0            or      edx,eax
0:000> 
eax=61000000 ebx=032ba188 ecx=00000001 edx=61610000 esi=2f004000 edi=00006100
eip=5ab91b44 esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!Builtins_DataViewPrototypeGetUint32+0x304:
5ab91b44 0bfa            or      edi,edx
0:000> 
eax=61000000 ebx=032ba188 ecx=00000001 edx=61610000 esi=2f004000 edi=61616100
eip=5ab91b46 esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!Builtins_DataViewPrototypeGetUint32+0x306:
5ab91b46 8b55f0          mov     edx,dword ptr [ebp-10h] ss:0023:0022e74c=00000061
0:000> 
eax=61000000 ebx=032ba188 ecx=00000001 edx=00000061 esi=2f004000 edi=61616100
eip=5ab91b49 esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
chrome_child!Builtins_DataViewPrototypeGetUint32+0x309:
5ab91b49 0bd7            or      edx,edi
0:000> 
eax=61000000 ebx=032ba188 ecx=00000001 edx=61616161 esi=2f004000 edi=61616100
eip=5ab91b4b esp=0022e73c ebp=0022e75c iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!Builtins_DataViewPrototypeGetUint32+0x30b:
5ab91b4b 81faffffff3f    cmp     edx,3FFFFFFFh
0:000> ?edx
Evaluate expression: 1633771873 = 61616161

在 JavaScript 中,各线程之间通过 postMessage 实现数据的发送、通过 onmessage 回调函数实现消息的响应。线程之间的数据传递是通过复制(而不是共享)来实现的,如果要传递的对象实现了 Transferable 接口,那么可以实现数据的高效转移,即并不复制数据,而是通过直接转移所有权来实现传递。对于这种传递方式,因为直接转移了所有权,因此原有线程不再享有对象数据的访问权限。ArrayBuffer 就是以这样的方式转移的。

然后程序调用DedicatedWorker::postMessage函数最终释放这块缓冲区,当程序再次调用DataView(ab2).setUint32(4, 0x42424242, true)尝试访问已经释放的缓存区时,触发此漏洞。

0:000> g
Breakpoint 2 hit
eax=00000000 ebx=2f004000 ecx=00000000 edx=00000000 esi=5dc0938c edi=2f001020
eip=5b26fd96 esp=0022e1f8 ebp=0022e204 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
chrome_child!WTF::ArrayBufferContents::FreeMemory+0x5c:
5b26fd96 891f            mov     dword ptr [edi],ebx  ds:0023:2f001020=00000000
0:000> dps esp
0022e1f8  0406a7c0
0022e1fc  00000008
0022e200  04209540
0022e204  0022e21c
0022e208  5b26fe41 chrome_child!WTF::ArrayBufferContents::DataHolder::~DataHolder+0x3f [C:\b\c\b\win_clang\src\third_party\blink\renderer\platform\wtf\typed_arrays\array_buffer_contents.cc @ 147]
0022e20c  2f004000
0022e210  05000000
0022e214  00000000
0:000> kp
ChildEBP RetAddr  
0022e204 5b26fe41 chrome_child!WTF::ArrayBufferContents::FreeMemory(void * data = 0x2f004000, void * ptr = 0x2f004000, void * address = <Value unavailable error>, class base::subtle::SpinLock * _Mtx = <Value unavailable error>, int _Value = <Value unavailable error>, std::memory_order _Order = <Value unavailable error>, struct std::_Atomic_int * _Atom = <Value unavailable error>, unsigned long * _Tgt = <Value unavailable error>, unsigned int x = <Value unavailable error>)+0x5c [C:\b\c\b\win_clang\src\third_party\blink\renderer\platform\wtf\typed_arrays\array_buffer_contents.cc @ 126]
0022e21c 5b26fa6a chrome_child!WTF::ArrayBufferContents::DataHolder::~DataHolder(int64 diff = 0n0)+0x3f [C:\b\c\b\win_clang\src\third_party\blink\renderer\platform\wtf\typed_arrays\array_buffer_contents.cc @ 147]
0022e228 5b965627 chrome_child!scoped_refptr<WTF::ArrayBufferContents::DataHolder>::~scoped_refptr(class WTF::ArrayBufferContents::DataHolder * ptr = 0x0406a7c0, int _Value = <Value unavailable error>, std::memory_order _Order = <Value unavailable error>, struct std::_Atomic_int * _Atom = <Value unavailable error>, unsigned long * _Tgt = <Value unavailable error>, class WTF::ArrayBufferContents::DataHolder * x = <Value unavailable error>, void * p = <Value unavailable error>)+0x16 [C:\b\c\b\win_clang\src\base\memory\scoped_refptr.h @ 208]
0022e29c 5b96513a chrome_child!blink::SerializedScriptValue::TransferArrayBufferContents(class v8::Isolate * isolate = 0x032ba188, class blink::HeapVector<blink::Member<blink::DOMArrayBufferBase>,0> * array_buffers = 0x0022e2e4, class blink::ExceptionState * exception_state = 0x0022e59c, class WTF::ArrayBufferContents * buffer = <Value unavailable error>, unsigned int capacity = <Value unavailable error>, unsigned int size = <Value unavailable error>, unsigned int new_min_capacity = <Value unavailable error>, class WTF::ArrayBufferContents * begin = 0x04209540, class blink::Member<blink::DOMArrayBufferBase> * _First = <Value unavailable error>, class blink::Member<blink::DOMArrayBufferBase> * _Last = <Value unavailable error>, class WTF::String * string2 = 0x0022e230, class scoped_refptr<WTF::StringImpl> * r = 0x0022e230, class WTF::StringImpl * p = <Value unavailable error>, class WTF::StringImpl * ptr = <Value unavailable error>, class WTF::StringAppend<const char *,WTF::String> * string1 = <Value unavailable error>, class blink::DOMArrayBufferBase ** value = 0x0022e230, class blink::DOMArrayBufferBase * key = 0x00000000, class blink::DOMArrayBufferBase ** b = <Value unavailable error>, class blink::Member<blink::DOMArrayBufferBase> * a = <Value unavailable error>, unsigned int i = <Value unavailable error>, class WTF::Vector<WTF::ArrayBufferContents,1,WTF::PartitionAllocator> * other = 0x0022e230, class WTF::ArrayBufferContents * buffer_to_deallocate = <Value unavailable error>)+0x487 [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\core\v8\serialization\serialized_script_value.cc @ 678]
0022e2d0 598fc86b chrome_child!blink::SerializedScriptValue::TransferArrayBuffers(class v8::Isolate * isolate = 0x032ba188, class blink::HeapVector<blink::Member<blink::DOMArrayBufferBase>,0> * array_buffers = 0x0022e2e4, class blink::ExceptionState * exception_state = 0x0022e59c, class WTF::Vector<WTF::ArrayBufferContents,1,WTF::PartitionAllocator> * other = <Value unavailable error>, class WTF::ArrayBufferContents * begin = <Value unavailable error>, class WTF::ArrayBufferContents * buffer_to_deallocate = <Value unavailable error>)+0x2a [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\core\v8\serialization\serialized_script_value.cc @ 419]
0022e300 598f8a27 chrome_child!blink::V8ScriptValueSerializer::FinalizeTransfer(class blink::ExceptionState * exception_state = 0x0022e59c, class WTF::Vector<blink::Member<blink::DOMArrayBufferBase>,0,blink::HeapAllocator> * val = <Value unavailable error>)+0x5b [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\core\v8\serialization\v8_script_value_serializer.cc @ 157]
0022e354 598f85f3 chrome_child!blink::V8ScriptValueSerializer::Serialize(class v8::Local<v8::Value> value = class v8::Local<v8::Value>, class blink::ExceptionState * exception_state = 0x0022e59c, class blink::ExceptionState ** scoped_variable = <Value unavailable error>, class blink::ExceptionState * new_value = <Value unavailable error>, blink::SerializationTag tag = <Value unavailable error>, class v8::Isolate * isolate = <Value unavailable error>, class v8::PersistentBase<v8::Context> * that = <Value unavailable error>, class std::unique_ptr<unsigned char [],blink::SerializedScriptValue::BufferDeleter> data = <Value unavailable error>, unsigned int size = <Value unavailable error>, class std::unique_ptr<unsigned char [],blink::SerializedScriptValue::BufferDeleter> * _Right = <Value unavailable error>, unsigned char * _Ptr = <Value unavailable error>, unsigned char * buffer = <Value unavailable error>, void * p = <Value unavailable error>, void * ptr = <Value unavailable error>, void * address = <Value unavailable error>, class base::subtle::SpinLock * _Mtx = <Value unavailable error>, int _Value = <Value unavailable error>, std::memory_order _Order = <Value unavailable error>, struct std::_Atomic_int * _Atom = <Value unavailable error>, unsigned long * _Tgt = <Value unavailable error>, unsigned int x = <Value unavailable error>, class scoped_refptr<blink::SerializedScriptValue> * r = <Value unavailable error>)+0xa7 [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\core\v8\serialization\v8_script_value_serializer.cc @ 110]
0022e3b4 598f8593 chrome_child!blink::SerializedScriptValueForModulesFactory::Create(class v8::Isolate * isolate = 0x032ba188, class v8::Local<v8::Value> value = class v8::Local<v8::Value>, struct blink::SerializedScriptValue::SerializeOptions * options = 0x0022e400, class blink::ExceptionState * exception_state = 0x0022e59c, class blink::ScriptState * script_state = <Value unavailable error>, char phase = <Value unavailable error>, char * scope = <Value unavailable error>, unsigned int64 id = <Value unavailable error>, unsigned int flags = <Value unavailable error>, unsigned int64 bind_id = <Value unavailable error>, int thread_id = <Value unavailable error>, class base::TimeTicks * timestamp = <Value unavailable error>)+0x59 [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\modules\v8\serialization\serialized_script_value_for_modules_factory.cc @ 23]
0022e3dc 598f84e5 chrome_child!blink::SerializedScriptValue::Serialize(class v8::Isolate * isolate = 0x032ba188, class v8::Local<v8::Value> value = class v8::Local<v8::Value>, struct blink::SerializedScriptValue::SerializeOptions * options = 0x0022e400, class blink::ExceptionState * exception = 0x0022e59c, unsigned int size = <Value unavailable error>)+0x43 [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\core\v8\serialization\serialized_script_value.cc @ 83]
0022e420 5bf71d08 chrome_child!blink::PostMessageHelper::SerializeMessageByMove(class v8::Isolate * isolate = 0x032ba188, class blink::ScriptValue * message = 0x0022e590, class blink::PostMessageOptions * options = 0x24eb2fa8, class blink::Transferables * transferables = 0x0022e470, class blink::ExceptionState * exception_state = 0x0022e59c, class blink::SerializedScriptValue * ptr = <Value unavailable error>, int _Value = <Value unavailable error>, std::memory_order _Order = <Value unavailable error>, struct std::_Atomic_int * _Atom = <Value unavailable error>, unsigned long * _Tgt = <Value unavailable error>, class blink::SerializedScriptValue * x = <Value unavailable error>, void * p = <Value unavailable error>)+0x75 [C:\b\c\b\win_clang\src\third_party\blink\renderer\bindings\core\v8\serialization\post_message_helper.cc @ 30]
0022e530 5bf71c97 chrome_child!blink::DedicatedWorker::postMessage(class blink::ScriptState * script_state = 0x24eb23e0, class blink::ScriptValue * message = 0x0022e590, class blink::PostMessageOptions * options = 0x24eb2fa8, class blink::ExceptionState * exception_state = 0x0022e59c, class blink::SerializedScriptValue * p = <Value unavailable error>, class blink::SerializedScriptValue * ptr = <Value unavailable error>, int increment = <Value unavailable error>, int _Value = <Value unavailable error>, std::memory_order _Order = <Value unavailable error>, struct std::_Atomic_int * _Atom = <Value unavailable error>, unsigned long * _Tgt = <Value unavailable error>, class scoped_refptr<blink::SerializedScriptValue> * r = <Value unavailable error>, class blink::SerializedScriptValue ** _Right = <Value unavailable error>, class blink::SerializedScriptValue * x = <Value unavailable error>, class WTF::Vector<blink::MessagePortChannel,0,WTF::PartitionAllocator> * other = <Value unavailable error>, struct WTF::VectorBufferBase<blink::MessagePortChannel,0,WTF::PartitionAllocator>::OffsetRange this_hole = <Value unavailable error>, struct WTF::VectorBufferBase<blink::MessagePortChannel,0,WTF::PartitionAllocator>::OffsetRange other_hole = <Value unavailable error>, class blink::MessagePortChannel * begin = <Value unavailable error>, class blink::mojom::blink::UserActivationSnapshot * _Left = <Value unavailable error>, unsigned int length = <Value unavailable error>)+0x68 [C:\b\c\b\win_clang\src\third_party\blink\renderer\core\workers\dedicated_worker.cc @ 136]
0022e554 5b8f9b3f chrome_child!blink::DedicatedWorker::postMessage(class blink::ScriptState * script_state = 0x24eb23e0, class blink::ScriptValue * message = 0x0022e590, class WTF::Vector<blink::ScriptValue,0,WTF::PartitionAllocator> * transfer = 0x0022e584, class blink::ExceptionState * exception_state = 0x0022e59c, unsigned int size = <Value unavailable error>, bool eagerly_sweep = <Value unavailable error>)+0x47 [C:\b\c\b\win_clang\src\third_party\blink\renderer\core\workers\dedicated_worker.cc @ 126]
0022e5cc 5962833e chrome_child!blink::dedicated_worker_v8_internal::PostMessage1Method(class v8::FunctionCallbackInfo<v8::Value> * info = <Value unavailable error>, class v8::Isolate * isolate = <Value unavailable error>, blink::ExceptionState::ContextType context_type = <Value unavailable error>, class v8::Local<v8::Object> object = <Value unavailable error>, class v8::Local<v8::Object> wrapper = <Value unavailable error>, int index = <Value unavailable error>, unsigned int obj = <Value unavailable error>, unsigned int heap_object_ptr = <Value unavailable error>, int offset = <Value unavailable error>, class v8::Local<v8::Context> context = class v8::Local<v8::Context>, class v8::Local<v8::Context> * that = <Value unavailable error>, int i = <Value unavailable error>, class blink::ScriptState * script_state = <Value unavailable error>, class v8::Local<v8::Value> value = <Value unavailable error>, class blink::ScriptState * raw = <Value unavailable error>, unsigned int size = <Value unavailable error>, class v8::Local<v8::Value> handle = <Value unavailable error>, class v8::Value * val = <Value unavailable error>, class blink::SharedPersistent<v8::Value> * p = <Value unavailable error>, class WTF::Vector<blink::ScriptValue,0,WTF::PartitionAllocator> * other = 0x0022e5cc, struct WTF::VectorBufferBase<blink::ScriptValue,0,WTF::PartitionAllocator>::OffsetRange this_hole = <Value unavailable error>, struct WTF::VectorBufferBase<blink::ScriptValue,0,WTF::PartitionAllocator>::OffsetRange other_hole = <Value unavailable error>, class blink::ScriptValue ** _Right = 0x0022e5cc, class blink::ScriptValue * begin = <Value unavailable error>)+0x1ff [C:\b\c\b\win_clang\src\out\Release\gen\third_party\blink\renderer\bindings\core\v8\v8_worker.cc @ 148]
……
0:000> g
(d04.88c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000004 ebx=032ba188 ecx=00000042 edx=00000042 esi=00000042 edi=2f004000
eip=5ab95606 esp=0022e6ec ebp=0022e714 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
chrome_child!Builtins_DataViewPrototypeSetUint32+0x386:
5ab95606 881407          mov     byte ptr [edi+eax],dl      ds:0023:2f004004=??
0:000> dd edi
2f004000  ???????? ???????? ???????? ????????
2f004010  ???????? ???????? ???????? ????????
2f004020  ???????? ???????? ???????? ????????
2f004030  ???????? ???????? ???????? ????????
2f004040  ???????? ???????? ???????? ????????
2f004050  ???????? ???????? ???????? ????????
2f004060  ???????? ???????? ???????? ????????
2f004070  ???????? ???????? ???????? ????????
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,423评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,339评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,241评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,503评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,824评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,262评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,615评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,337评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,989评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,300评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,829评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,193评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,753评论 3 230
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,970评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,708评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,295评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,207评论 2 258

推荐阅读更多精彩内容

  • 本篇文章是基于谷歌有关Graphic的一篇概览文章的翻译:http://source.android.com/de...
    lee_3do阅读 6,962评论 2 20
  • 文件操作一直是早期浏览器的痛点,全封闭式的不给JavaScript操作的空间。随着H5新接口的推出这个壁垒被打破了...
    JunChow520阅读 1,399评论 0 2
  • 本文目录 简介 Napi简介 铁打的hello_world 关于文件头 关于基础数据类型 关于错误处理 关于异常处...
    被叫做逸轩的可儿阅读 7,138评论 1 1
  • 一、参考字符编码ASCII UTF8 字节序 大端 小端 二、参考[HTML5] ArrayBuffer与类型化数...
    合肥黑阅读 3,051评论 0 5
  • 正如昨天所说的,今天是一场硬仗,月度数据总成、各种信息统计按部就班的进行,好在有很多老兵的密切契合,工作...
    指挥官阅读 227评论 0 5