对Chrome issue 1062091的一点分析
前言
感谢大佬们的无私分享:
Mojo基础:https://keyou.github.io/blog/2020/01/03/Chromium-Mojo&IPC/
p0 sandbox escape:https://googleprojectzero.blogspot.com/2019/04/virtually-unlimited-memory-escaping.html
plaidctf2020mojo:https://eternalsakura13.com/2020/09/20/mojo/#more
issue1062091漏洞利用的详细分析: https://blog.theori.io/research/escaping-chrome-sandbox/
SCTF2020 EasyMojo:https://github.com/SycloverSecurity/SCTF2020/tree/master/Pwn/EasyMojo
D^3CTF EasyChromeFullChain:https://mp.weixin.qq.com/s/Gfo3GAoSyK50jFqOKCHKVA
以下漏洞利用都是基于这些文章的基础,再加上自己的一点点理解,就不多说漏洞原理了(俺讲不清楚,,,),以上文章中就有详细的分析
调试环境
window 10 x64 2004
chromium 81.0.4044.0:https://chromium.cypress.io/win64/beta/81.0.4044.69 (下载地址)
漏洞利用
触发UAF
该漏洞是因为InstalledAppProviderImpl里保存一个了render_frame_host_的原始指针,但是并没有通过任何方法来将InstalledAppProviderImpl和RenderFrameHost的生命周期绑定,所以我们可以free frame来释放掉对应的render_frame_host,而我们此时只要保持住InstalledAppProviderImpl的ptr,就可以通过FilterInstalledApps来触发UAF:
1 | void InstalledAppProviderImpl::FilterInstalledApps( |
在查看了issue1062091漏洞利用的详细分析和D^3CTF EasyChromeFullChain WP两篇文章之后,我们可以先创建一个sub frame中绑定一个InstalledAppProvider接口,将其对象传递给main frame,在释放sub frame,在main frame中使用sub fream传入的InstalledAppProvider接口对象来调用其FilterInstalledApps方法,就能触发UAF:
1 | // child.html |
enable mojo
从上面触发UAF的过程看到,我们需要在一个sub frame中开启Mojo接口,根据p0的文章中提到的为render frame开启Mojo接口的函数:
1 | void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context, |
我们只需要满足if里的条件即可给该render frame开启Mojo接口,IsMainFrame函数也是通过RenderFrameImpl对象里的一个成员来判断的
所以enable mojo的代码如下:
1 | function enable_mojo(oob){ |
其中寻找RenderFrameImpl对象在SCTF和p0的文章中都有提到,就不多赘述了
escape Sandbox
这里都是参考的issue1062091漏洞利用的详细分析和SCTF2020 EasyMojo
用于堆喷的那部分代码已经被大佬们封装成一个通用的函数,我们拿来用就好了:
1 | function getAllocationConstructor() { |
escape sandbox的步骤为:
- 泄露出一个堆地址
- 根据第一步中泄露的堆地址泄露出this(RenderFrameImpl)指针(也就是我们可控的堆地址)
- 通过找callback对象来任意调用函数
- 泄露
current_process_commandline_
地址 - 调用
SetCommandLineFlagsForSandboxType
来将--no-sandbox
添加到current_process_commandline_
中 - 再次使用v8的漏洞来RCE(页面要不同源)
以下对各个步骤进行一些说明(其实在SCTF2020 EasyMojo的wp中都已经提到了),我这里写下我找的几个gadget
泄露堆地址
首先查看UAF出的cpp代码:
1 | void InstalledAppProviderImpl::FilterInstalledApps( |
为了避免crash,我们需要让render_frame_host_->GetProcess()->GetBrowserContext()->IsOffTheRecord()
能正常返回,查看其汇编代码:
1 | .text:0000000180D32B98 ; public: virtual void content::InstalledAppProviderImpl::FilterInstalledApps(class std::vector<class mojo::InlinedStructPtr<class blink::mojom::RelatedApplication>, class std::allocator<class mojo::InlinedStructPtr<class blink::mojom::RelatedApplication>>>, class GURL const &, class base::OnceCallback<void (class std::vector<class mojo::InlinedStructPtr<class blink::mojom::RelatedApplication>, class std::allocator<class mojo::InlinedStructPtr<class blink::mojom::RelatedApplication>>>)>) |
我的做法是让render_frame_host_->GetProcess()->GetBrowserContext()
返回我们可控区域的地址,最后的IsOffTheRecord
再用于泄露堆地址,我们需要找到一个这样的函数:
1 | SomeType* SomeClass::SomeMethod() { |
这种函数的名字一般叫getxxx之类的,再ida中一通找(用CodeQL应该才是正解)后,找到一个合适的:
1 | .text:0000000180002180 ; public: class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char>> const & syncer::LoopbackServerEntity::GetId(void)const |
这个函数会把this+8的地址返回回来,我们只要调用这个函数两次,对应render_frame_host_->GetProcess()->GetBrowserContext()
,注意的是找到的函数要是再虚表中的函数,也就是ida中的.rdata段会有指针指向该函数;为了泄露堆地址,我们需要找到一个这样的虚函数:
1 | void VirtualFunction() override() { |
这里选择使用的虚函数是content::WebContentsImpl::GetWakeLockContext
:
1 | struct device::mojom::WakeLockContext *__fastcall content::WebContentsImpl::GetWakeLockContext(content::WebContentsImpl *this) |
而且content::WakeLockContextHost::WakeLockContextHost(someobj, this);
,还会将this的地址写入到someobj地址偏移+8处,为我们后面泄露出this的地址给了很大的方便
相对应的js代码:
1 | const kWakeLockContextOffset = 0x650; |
泄露this
在上一步中,我们已经拿到了一个堆地址,而且知道该堆地址+8偏移处有我们的this指针,我们只要想办法利用它泄露处this指针就好。
泄露this
的虚函数的形式如下:
1 | void VirtualFunction() override { |
这里选择的虚函数是DictionaryIterator::Start
:
1 | __int64 __fastcall anonymous_namespace_::DictionaryIterator::Start(__int64 this) |
对应的js代码:
1 | const kGetFieldVptr = 0x69101D8n + ChromeDllBase; // DictionaryIterator::Start |
至此我们就泄露了this地址,等于我们知道了我们可控区域的地址,这里的泄露建议多调试调试,一堆的虚函数call很容易搞乱
寻找callback
我们需要找到一个能传递多个参数的回调函数:
1 | .text:0000000184B6210A ; void base::internal::Invoker<base::internal::BindState<void (gpu::SharedImageInterfaceInProcess::*)(gpu::CommandBufferTaskExecutor *,gpu::ImageFactory *,gpu::MemoryTracker *),base::internal::UnretainedWrapper<gpu::SharedImageInterfaceInProcess>,gpu::CommandBufferTaskExecutor *,gpu::ImageFactory *,gpu::MemoryTracker *>,void (void)>::RunOnce(void) |
这种函数在ida中还是有很多的,找一个用就好,为了能任意地址调用,我们还需要一个虚函数来调用该callback,找一个类似这种的:
1 | .text:0000000180E518C2 ; void __fastcall blink::FileSystemDispatcher::WriteListener::DidWrite(blink::FileSystemDispatcher::WriteListener *__hidden this, __int64, bool) |
封装一下:
1 | const kInvokeCallbackVptr = 0x66309C8n + ChromeDllBase; // blink::FileSystemDispatcher::WriteListener::DidWrite |
泄露current_process_commandline_
地址
这个我们只要用前面泄露的可控地址,来吧current_process_commandline_
拷贝过去就好,可以使用这种函数:
1 | .text:00000001810ABBA0 ; void __fastcall extensions::SizeConstraints::set_minimum_size(extensions::SizeConstraints *__hidden this, const struct gfx::Size *) |
调用SetCommandLineFlagsForSandboxType
最后我们只要使用我们封装好的callFunction调用SetCommandLineFlagsForSandboxType
将--no-sandbox
标志添加到全局变量current_process_commandline_
中就好了,最后只需要再次使用V8的漏洞来进行RCE即可
这样的好处就是payload不依赖于具体的windows版本
后记
在网站下载的chromium虽然有提供idb文件,但是里面还是有很多结构和变量都是没有符号的,我的做法是自己编译了一个版本相近的带符号的chromium,用这个自己编译的来找对应的变量和查看结构偏移,再根据函数名和变量的交叉引用来确定最终的偏移,以下是打成功的效果:
PS:复现下来,感觉好像会了一点点,但是又感觉缺了好多Mojo的知识。。可能写payload不需要那么的精通背后的原理把。