}
// Remove all refs from our watched dispatchers and fire cancellations.
for (auto& entry : watches) {
entry.second->dispatcher()->RemoveWatcherRef(this, entry.first);
entry.second->Cancel();
}
return MOJO_RESULT_OK;
}
实际上,std::flat_map由std::vector支持,并且watched_handles_在大多数情况下仅包含一个元素,该元素恰好占用16个字节。这意味着,我们可以覆盖Watch指针!
Watch类的大小相对较大,为104个字节,由于tcmalloc的原因,我们只能将大小相似的对象作为目标对象的部分覆盖对象。此外,目标对象在某些偏移处应该包含有效的指针,以使Watch方法的调用不受影响。遗憾的是,网络进程似乎没有包含可以满足上述简单类型混淆要求的类。
但是,我们可以利用Watch是一个引用计数类的事实。我们的思路是,喷射许多Watch size的缓冲区,tcmalloc将其放置在实际Watch对象的旁边,并希望带有被覆盖的最低有效字节的scoped_refptr指向我们的缓冲区之一。缓冲区应该有第一个64位字,也就是伪引用计数器,设置为1,其余设置为0。在这种情况下,对WatcherDispatcher::Close的调用将释放scoped_refptr,这将会导致删除虚假的Watch,析构函数将正常完成,缓冲区将被释放。
如果我们计划将缓冲区发送到攻击者的服务器或返回渲染器进程,那么将会泄露tcmalloc的freelist指针,另一种思路是,如果我们想办法在此期间分配其他内容,则可能会泄露一些有用的指针。因此,我们现在需要尝试在网络进程中创建此类缓冲区,并延迟发送它们,直到发生损坏。
事实证明,Chrome中的网络进程还负责处理WebSocket连接。重要的是,WebSocket是一种低开销的协议,它允许传输二进制数据。如果我们使连接的接收端足够慢,并且发送足够的数据来填充OS套接字发送缓冲区,直到TCPClientSocket::Write变为“asynchronous”(异步)操作为止,随后对WebSocket::send的调用将导致原始帧数据存储为IOBuffer对象,而每个调用仅有两个额外的32字节分配。此外,我们可以通过调整接收方的延迟,来控制缓冲区的生命。
看上去,我们找到了一个近乎完美的堆喷射(Heap Spraying)原语。不过,它有一个缺点——无法释放单个缓冲区。在发送当前批处理或断开连接时,与连接相关的所有帧都会立即释放。我们显然不能为每个喷射对象建立一个WebSocket连接,并且上述每个操作都会在堆中产生很多我们不希望得到的“噪音”。但是,我们先不考虑这些。
下面是该方法的概述:
遗憾的是,我们很快就证明了,watched_handles_不是太理想的方案。其缺点在于:
1、实际上,有两个flat_map成员,但我们只能使用其中一个成员,因为watched_handles_的损坏会在RemoveWatcherRef虚拟方法调用期间立即引起崩溃。
2、每个WatcherDispatcher分配,都会在我们关注的size类中产生很多不希望得到的“噪音”。
3、对于Watch size类的指针,其LSSB可能有16个(= 256 / GCD(112, 256))可能的值,其中大多数甚至都不指向对象的开头。
尽管我们可以利用这种方法泄露一些数据,但成功率相对偏低。这种方法本身似乎是合理的,但我们必须找到一个更“方便”的容器来进行覆盖。
WebSocket框架
现在,我们可以仔细研究一下如何发送WebSocket框架。
class NET_EXPORT WebSocketChannel {
[...]
std::unique_ptr data_being_sent_;
// Data that is queued up to write after the current write completes.
// Only non-NULL when such data actually exists.
std::unique_ptr data_to_send_next_;
[...]
};
class WebSocketChannel::SendBuffer {
std::vector frames_;
uint64_t total_bytes_;
};
struct NET_EXPORT WebSocketFrameHeader {
typedef int OpCode;
bool final;
bool reserved1;
bool reserved2;
bool reserved3;
OpCode opcode;
bool masked;
uint64_t payload_length;
};
struct NET_EXPORT_PRIVATE WebSocketFrame {
WebSocketFrameHeader header;
scoped_refptr data;
};
ChannelState WebSocketChannel::SendFrameInternal(
bool fin,
WebSocketFrameHeader::OpCode op_code,
scoped_refptr buffer,
uint64_t size) {
[...]
if (data_being_sent_) {
// Either the link to the WebSocket server is saturated, or several
// messages are being sent in a batch.
if (!data_to_send_next_)
data_to_send_next_ = std::make_unique();
data_to_send_next_->AddFrame(std::move(frame));
return CHANNEL_ALIVE;
}
data_being_sent_ = std::make_unique();
data_being_sent_->AddFrame(std::move(frame));
return WriteFrames();
}
WebSocketChannel使用两个单独的SendBuffer对象来存储传出帧。在连接饱和后,新的帧将会进入data_to_send_next_。并且,由于缓冲区由std::vector
如上所述,我们的堆喷射技术为每个所需的分配提供了两个额外的32字节分配。但遗憾的是,WebSocketFrame(我们打算覆盖的指针)大小正好是32个字节。这意味着,除非我们使用其他的堆操作技巧,否则在堆喷射期间生成的所有对象只有1/3属于正确的类型。另一方面,与Watch相比,这个size类中LSB的可选值只有一半,并且指向正确分配的开始部分的概率更大一些。更重要的是,与WatcherDispatcher不同,WebSocket::Send除了调整目标std::vector的大小之外,不会触发任何分配,因此size类的堆喷射会非常简洁。总而言之,我们现在认为data_to_send_next_是最好的目标。