Reference
https://github.com/google/google-ctf/tree/master/2018/finals/pwn-just-in-time/
Thanks for Stephen, I learned a lot from his amazing challenge.
Setup
I am lazy, so I use Xcode to compile V8 version 7.2.0 (candidate)
1 | cd ~/v8/v8 |
Some features
Max Safe Integer Range of Doubles
1 | Number.MAX_SAFE_INTEGER = 2^53 - 1 |
PoC
1 | function foo(doit) { |
Run PoC until remove checkbounds
is called:
1 | index_type.Print(); |
All in all, the result of range analyzes
is different from the result of optimized range
. After simplified lower
removes boundary check, we can do OOB read/write.
Exploit
Partial OOB read/write
We place array a
next to array b
, use OOB write from a
to change the length of array b
. Now, the length of b
is 0x400
.
And we can OOB via array b
:
1 | function foo(doit) { |
From Partial OOB R/W to Arbitrary R/W Primitive
Add a Float64Array
. We can edit the backing store
of ArrayBuffer
to arbitrary R/W primitive.
We won’t create Float64Array unless the memory is in g2[ab_off]
:
1 | function foo(doit) { |
Find the address of array buffer backing store:
1 | const ab_backing_store_off = ab_off + 0x15; |
I wonder which address records the backing store. After checking for a little while, it’s my first time to see new Float64Array() directly. Normally, it should be:
1 | var ab = new ArrayBuffer(20); |
Finding the elements of Float64Array
.
Add +0x10
to the address, we can get backing store
.
The address of elements is 0x0000093f18ac9ed
. In 0x0000093f18ac9ed+0x20
, we have the first element -5.5
.(0x4016000000000000
in the picture):
When we edit backing store to get arbitrary r/w primitive, assume addr
as the target address, change the value of backing store to addr-0x20
,then we can leak content in this address.
User mode object leak primitive
Appending an object to g3
. Then, double array g2
can leak that object, resulting type confusion. The leaked content is float
. f2i
is able to convert it to integer:
1 | function leak_ptr(o) { |
Here is the output:
1 | let Array_addr = leak_ptr(Array); |
Arbitrary Address R/W Primitive
Script:
1 | function readq(addr) { |
Let’s have a look on readq
We can get the original value of backing store
form g2[ab_off+2]
. Change it to the target address. Pay attention to the last 1
. This is a mechanism called Tagged Value
. Only when 1 is in the last of the address can it be a valid pointer of HeapObject
.
Edit it to the content we want to read. E.g, leak the code:
I explained why we should - 20
previously, so skip this part here:
1 | g2[ab_backing_store_off-2] = (addr-0x20n|1n).i2f(); |
Now the backing store
is changed to addr-0x20
We can leak code address 0x000001db14a8c821
from 0x0000093f11611288
now.
Output:
1 | let Array_addr = leak_ptr(Array); |
writeq
is the same as readq
.
Security Feature
Earlier than version 6.7
, the function code is writable. Therefore, we can write shellcode to functions and call the function to execute.
Later, however, the code is not writable and we need to chain ROP.(https://github.com/v8/v8/commit/f7aa8ea00bbf200e9050a22ec84fab4f323849a7)
leak ArrayConstructor
Now, leak the address of Array
, Then find the address of Array’s code. In the final, calculate the address of ArrayConstructor:
1 | let Array_addr = leak_ptr(Array); |
Reverse Chrome and libc
We can leak the address of ArrayConstructor
It’s mapped to the memory of chrome binary.
Use IDA to reverse. Seek the offset of ArrayConstructor
in chrome binary.
1 | >>> hex(0x55b677f727c0-0x55b673f16000) |
chrome binary base address
=ArrayConstructor
-0x405c7c0
. Let’s store the result in bin_base
:
1 | let bin_base = Array_builtin_addr - 0x405c7c0n; |
Find got table
. cxa_finalize
is a libc function,there is a got
in chrome pointing to it, the offset to the pointer is 0x8DDBDE8
.
Then leak cxa_finalize
:
Reverse the libc.so, use cxa_finalize_got
-0x43520
to get the base address
of libc:
1 | let cxa_finalize_got = bin_base + 0x8ddbde8n; |
Find environ
to leak stack address:
1 | let environ = libc_base+0x3ee098n; |
ROP
This section is easy, we use mprotect
to change the permission of memory and execute shellcode:
1 | let nop = bin_base+0x263d061n; |
The addresses in red rectangle is environment variable, and the contents in yellow rectangle are 0x200*retn
, variable nop
here represents retn
instruction but not 0x90(nop
instruction), we the code executes retn
,it will keep returning until executing our ROP。
1 | for (let i = 0; i < 0x200; i++) { |
Exploit
1 | cd ~/chrome |
Other
Acknowledgement
I would acknowledge stephen(@_tsuro) who guides me and points out my stupid mistakes.
Debugging d8 is quite different from chrome,when leaking cxa,it will map builtin to a random address,and cxa is mapped to libv8.so,so we cannot find offset via cxa.
When you complete a arbitrary r/w primitive in v8,you can exploit chrome via the script without additional debug(yes, u don’t need to debug a full chrome)
Thanks Auxy(@realAuxy233) for translating~
If you find any errors or corrections, contact me.