#V8 #IntegerOverflow

최근 취약점들을 살펴보면 순수하게 V8과 관련된 취약점은 별로 없고, 그나마 JavaScript로 시작하되 최종적으로는 BlinkChrome에서 취약점이 발생하는 경우들이 많은 것 같다. 와중에 최근에 등록된 버그가 눈길을 끌었다. V8 내부에서 정수 오버플로우(Integer Overflow)를 유발하는 PoC(Proof of Concept)를 제시하고 있었다. 대략적인 V8의 구조와 정수 오버플로우가 어떤 것인지 알게된 의미있는 분석이었다.

RegExp.prototype[@@replace]

해당 PoC는 결국에 C++ 함수인 Runtime_RegExpReplace()에서 발현된다.(이하 RegExpReplace) 이 함수는 자바스크립트 레벨에서 RegExp.prototype[@@replace] 메소드(이하 replace)에 대응되는 함수이다. RegExp는 무엇일까? 정규표현식을 표현하고 처리하기 위한 객체로 이해하면 된다. 다음 코드를 예로 들면,

var re = /-/g; 

re는 RegExp 객체이다. re에 대입한 “/-/g"의 의미는 임의의 문장에서 “-“와 일치하는 모든 부분을 의미한다. 뒤에 붙은 ‘g’는 “global"의 약자로서 일치하는 부분을 모두 식별하겠다는 의미이다. 한편, MDN(Mozilla Developers Network)에서 제시하는 replace() 메소드의 용법과 예제는 다음과 같다.

var str = '2016-01-01';
// regexp[Symbol.replace](str, newSubStr|function)
var newstr = re[Symbol.replace](str, '.');
console.log(newstr);  // 2016.01.01

replace() 메소드는 RegExp 객체의 메소드이다. 이 메소드는 Symbol 타입으로 정의되어 있다. 이 타입은 최신 ECMA Script에 새로 추가된 것으로서, 잘 모르기도 하지만 본 포스트의 주제와 맞지 않으므로 생략한다. 어쨌든, replace() 메소드는 두 개의 인자를 받는다. 첫 번째 인자는 원본 문자열이고, 두 번째 인자는 원본 문자열에서 정규표현식과 일치하는 부분을 치환하고자 하는 치환 문자열이다. 위 예에서 “2016-01-01“이라는 원본 문자열은 ‘-‘이 ‘.’으로 모두 치환된다.

RegExp.prototype.exec()

RegExp 객체의 exec() 메소드는 원본 문자열에서 정규표현식과 일치하는 부분을 객체 형태로 반환한다. 아래 예제를 보자.

var re = /foo/g;
var result = re.exec('___foo___foo');

객체 result는 원본 문자열에서 주어진 정규표현식과 일치하는 부분들에 대한 정보를 담는다. 아래는 해당 객체의 내용을 출력해 본 것이다.원본 문자열인 “___foo___foo”를 input 프로퍼티에, 정규표현식과 일치하는 부분 문자열인 “foo”를 0 프로퍼티에 담고 있다. 두 번 일치하지만 “foo”는 하나이므로 length가 1인 것으로 생각된다. 또, 원본 문자열에서 정규표현식과 일치하는 첫 번째 문자열이 시작하는 위치가 4이므로, index 프로퍼티에 4가 저장된 것으로 생각된다.

result = {
  0:"foo"
  index:3
  input:"___foo___foo"
  length:1
  __proto__:Array(0)
}

RegExpUtils::RegExpExec()

RegExp.prototype.exec() 함수는 V8 내부에서 RegExpExec() 함수를 호출하는 것으로 생각된다. 당연한 말이지만, 정규표현식과 일치하는 부분을 찾아서 치환하기 위해서는 먼저 “찾아야” 한다. 따라서, RegExpReplace() 함수는 ECMA Script에 기술된 대로 RegExpExec() 함수를 호출한다. 해당 함수는 Object 클래스의 객체인 result를 반환한다. 아래는 해당 객체에 대하여, Object 클래스의 Print() 멤버함수를 실행시킨 결과이다.

0x24c761393899: [JSArray]
 - map: 0x3288b1786611 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x39681e485539 <JSArray[0]>
 - elements: 0x24c7613938d1 <FixedArray[1]> [HOLEY_ELEMENTS]
 - length: 1
 - properties: 0x17db08a02251 <FixedArray[0]> {
    #length: 0x17db08a4ff89 <AccessorInfo> (const accessor descriptor)
    #index: 3 (data field 0)
    #input: 0x39681e4aa699 <String[12]: ___foo___foo> (data field 1)
    #groups: 0x17db08a022e1 <undefined> (data field 2)
 }
 - elements: 0x24c7613938d1 <FixedArray[1]> {
           0: 0x24c761393879 <String[3]: foo>
 }

객체 result의 정보가 출력되는데, 이를 통해 대략 알 수 있는 것은 다음과 같다.

  1. JSArray 클래스의 객체이다. (JSArray 클래스는 Object 클래스를 상속 받음)
  2. elements와 properties라는 FixedArray 객체를 갖는다.
  3. elements 객체의 엔트리 0에는 문자열 “foo”가 저장되어 있다.
  4. length는 1이다. 객체 elements의 엔트리 개수를 의미한다.
  5. properties 객체는 index, input, group이라는 엔트리를 가진다.

앞서, 자바스크립트 계층에서 result 객체를 출력한 내용과, C++ 레이어에서 result 객체를 출력한 내용이 동일한 것으로 생각된다. JSArrayObject 클래스에 대해서는 추후에 별도로 살펴보아야 할 것 같다.

Flow of Runtime_RegExpReplace

동일한 방법으로 해당 PoC에 대하여 C++ 레이어에서 객체 result의 정보를 출력해보면 다음과 같다.

0x2bea81a10e31: [JS_OBJECT_TYPE]
 - map: 0x98850a8cf41 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x22efc2684649 <Object map = 0x98850a822b1>
 - elements: 0x7268cc02251 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x7268cc02251 <FixedArray[0]> {
    #length: <unboxed double> 4.29497e+09 (data field 0)
 }

해당 PoC는 exec() 메소드를 임의의 함수로 대체한다. 대체한 함수는 0xfffffffe를 반환한다. 한편, 위 결과를 보면 FixedArray 객체인 properties의 엔트리 length가 double 타입의 4.29497e+09 값임을 알 수 있다. 이 값은 0xfffffffe와 동일한 값이다. 즉, exec() 메소드가 반환하는 double 타입의 값이 length 프로퍼티에 저장된다.

length는 원본 문자열에서 정규표현식과 일치하는 부분의 개수이다. 따라서, RegExpReplace() 함수는 본연의 목적을 위해 length 만큼 루프를 돌면서 정규표현식과 일치하는 모든 부분들을 치환하여야 한다. 이러한 루프는 코드 상에서는 물론, ECMA Script 명세에서도 확인할 수 있다. RegExpReplace() 함수는 객체 result의 length 프로퍼티의 값을 int 타입의 변수인 captures_length에 저장한다.

Integer overflow by explicit type conversion

정수 오버플로우가 발현되는 부분은 이 link 부분이다. 변수 captures_length는 PositiveNumberToUint32() 함수를 통해 객체 result의 length 프로퍼티의 값을 받는다. 함수의 이름에서부터 감이 오기 시작한다. 해당 함수는 length 프로퍼티의 값을 받아와 uint 타입으로 변환하여 반환한다. 문제는 이 반환 값을 받는 변수인 captures_length가 int 타입이라는 점이다. 결국, length 프로퍼티의 값이 4,294,967,295 값임에도 변수 captures_length의 값은 -2가 된다.

Crashing

이후의 코드를 따라가다 보면, int 타입의 변수 argc가 contents_length + 2로 계산이 되어 결국 0이 된다. 만약, PoC가 변조한 exec() 메소드를 0xfffffffd가 반환하도록 수정하면 argc의 값은 -1이 된다. 결국, 이후에 RegExpReplace() 함수는 0 이하의 Handle<Object> 배열을 동적할당 시도하는데, 이 과정에서 크로미움은 강제로 OOM(Out of Memory) 에러를 발생시키고 렌더러 프로세스는 종료된다.