Node.js 社区近期在美国独立日周末的狂欢之时爆出漏洞
https://medium.com/@iojs/important-security-upgrades-for-node-js-and-io-js-8ac14ece5852
先给出一段会触发该漏洞的代码
直接在v0.12.4版本的node上运行,立即crash。
下面我们详细的分析下该漏洞的原理。
调用栈上面的代码构造了一个长度为1025的buffer,然后调用该buffer的toString方法解码成utf8字符,平时开发中再平常不过的调用了。但是为什么在这里会导致crash呢,和平时的写法到底有什么差别?
示例代码虽少,但是里面涉及到的各种调用可不少,从js到node中的c++,再到更底层的v8调用。大致过程如下图所示。
导致该漏洞产生的有几个比较关键的调用过程。
Utf8DecoderBase::Reset每一个Utf8DecoderBase类实例化的对象都有一个私有的属性buffer_,
private: uint16_t buffer_[kBufferSize];其中utfDecoder的kBufferSize设置为512,buffer用做存储解码后的utf8字符缓冲区。这里需要注意的是512不是字节数,而是字符数,有些utf8字符只需要一个这样的字符就能表示,有些则需要2个。示例代码中构造buffer用的微笑字符则需要2个这样的字符来表示,4个字节来存储。所以buffer能存储的字节数是512*2=1024。
如果待解码的buffer长度不超过1024时,在buffer中就能完全被解码完。解码到buffer的字符通过调用v8::internal::OS::MemCopy(data, buffer_, memcpy_length*sizeof(uint16_t))被拷贝到返回给node使用的字符串内存区。
Utf8DecoderBase::WriteUtf16Slow但是当待解码的buffer长度超过1024个字节时,前1024个字节解码后还是通过上面讲的buffer_缓冲区存储,剩余待解码的字符则交给Utf8DecoderBase::WriteUtf16Slow处理。
void Utf8DecoderBase::WriteUtf16Slow(const uint8_t* stream, uint16_t* data, unsigned data_length) { while (data_length != 0) { unsigned cursor = 0; uint32_t character = Utf8::ValueOf(stream, Utf8::kMaxEncodedSize, &cursor); // There's a total lack of bounds checking for stream // as it was already done in Reset. stream += cursor; if (character > unibrow::Utf16::kMaxNonSurrogateCharCode) { *data++ = Utf16::LeadSurrogate(character); *data++ = Utf16::TrailSurrogate(character); DCHECK(data_length > 1); data_length -= 2; } else { *data++ = character; data_length -= 1; } } }WriteUtf16Slow对剩余的待解码buffer调用 Utf8::ValueOf进行解码, 调用Utf8::ValueOf时每次输出一个utf8字符。其中data_length表示还需要解码的字符数(注意不是utf8字符个数,而是uint16_t的个数),直至剩余的data_length个字符全部被解码。
Utf8::ValueOf上面讲到调用Utf8::ValueOf从剩余buffer中解码出一个utf8字符,当这个utf8字符需要多个字节存储时,便会调用到Utf8::CalculateValue, Utf8::CalculateValue根据utf8字符的编码规则从buffer中解析出一个utf8字符。关于utf8编码的详细规则可以参考阮一峰老师博客的文章《字符编码笔记:ASCII,Unicode和UTF-8》,里面非常详细的讲解了utf8的编码规则。
uchar Utf8::CalculateValue(const byte* str, unsigned length, unsigned* cursor)其中第一个参数表示待解码的buffer,第二个参数表示还可以读取的字节数,最后一个参数cursor表示解析结束后buffer的偏移量,也就是该utf8字符所占字节数。
实例分析简单的讲解了实例代码执行时的调用链路后,我们再结合示例代码进行具体的调用分析。
buffer创建首先示例代码使用一个占用4字节的微笑字符,构造出一个长度为257*4=1028的buffer,接着又调用slice(0,-3)去除最后面的3个字节,如下图所示。
buffer解码