[PAPER] Twice the Bits, Twice the Trouble: Vulnerabilities Induced by Migrating to 64-Bit Platforms
https://intellisec.de/pubs/2016-ccs.pdf
논문 연구 내용
1. 어떻게 32bit platform에서 문제 없는 코드가 64bit platform에서 취약해지는지 연구
2. platform 사이에서 변하는 data model의 영향을 시스템적으로 분석
연구 결과, 64bit 플랫폼으로 이식 시 integer 타입의 큰 width와 사용 가능한 메모리의 증가로 32bit 플랫폼 코드에는 없었던 취약점이 발생함을 발견했다. 이러한 케이스를 Debian stable ("Jessie") 소스 코드와 Github 오픈소스 프로젝트를 대상으로 평가한다.
또한 연구 과정 발견한 Chromium, Boost C++ Library, Libarchive, Linux Kernel, zlib 취약점을 대상으로 64bit migration 취약점을 논의한다.
플랫폼 간 이식의 문제
플랫폼을 옮기는 기술적 장벽은 64bit data model로 해결할 수 있었다. 하지만 플랫폼을 옮기면 결과 코드에서 차이가 생긴다.
LP64 data model의 경우, typedef(new tyoe aliases)와 signedness 문제가 발생한다. 64bit migration 코드 문제 예로는 CVE-2005-1513(qmail), CVE-2007-1884(PHP), CVE-2013-0211(libarchive), CVE-2014-9495(libpng) 취약점들이 있다.
주요 연구 내용
1. 64bit migration 취약점 시스템적 연구
2. C/C++에서의 64bit data model 분석하고 32bit 아키텍처로부터 코드 이식 시 안전하지 않은 프로그래밍 패턴 설명
64bit migration 취약점에서 두 가지 상호의존적 요소 정의
1. 정수 크기의 변화
2. 주소 공간의 증가
각 요소마다 취약한 root cause를 분석하고, 취약점 발생을 위해 필요한 조건을 분석하여 migration 취약점에 대한 통찰력을, 개발자들에게는 참고자료를 제공한다.
integer 기반의 취약점에 대한 연구는 많이 있어왔지만, 64bit migration만의 취약점 조사는 이 연구가 처음으로 알고 있다.
64bit migration 문제 적용
200개의 Github 프로젝트 소스코드와 Required, Important, Standard로 표시된 Debian stable ("Jessie")의 모든 패키지를 대상으로 이식 문제를 분석하였다. 연구 결과, 두 dataset에서 integer truncation과 signedness 문제가 가장 많다는 것을 발견했다.
더 자세한 내용은 섹션 4에서 설명한다.
case study를 통한 64bit migration의 보안적 위험 설명
마지막으로, 연구 과정에서 찾은 Chromium, GNU C Library, Linux Kernel, Boost C++ Libraries 취약점을 예로 들어서 64bit migration의 보안적 위험을 설명한다. 이는 소프트웨어 개발과 auditing 과정에서 알아채기 힘든 migration 취약점임을 알 수 있다.
Contribution
논문 구성
Section 2: integer 문제
Section 3: 64bit migration 취약점 분석
Section 4: 취약점에 대한 empirical study
Section 5: 취약점에 대한 case study
Section 6: Discussion
Section 7: 관련 연구
Section 8: 결론
64bit 아키텍처로 코드를 이식할 때 integer 타입 사이 변환 시 나타나는 integer 취약점이 발생할 수 있다.
C의 integer type
integer type T에 대한 속성
논문에서 사용하는 용어는 다음과 같이 정의한다.
Signedness S(T)
Conversion rank R(T)
width W(T)
range I(T)
R(T1) < R(T2) 이면 W(T1) <= W(T2)
data model
플랫폼마다 서로 다른 크기의 integer 타입을 data model에 정의한다.
Table 1에서는 data model마다 각 integer 타입의 크기를 보여준다. OS마다 어떤 data model을 사용하는지도 확인할 수 있다.
모든 data model에서, pointer크기/size_t 타입은 아키텍처의 레지스터 크기에 해당한다.
32bit에서 64bit로의 코드 이식을 중점적으로 생각하기 때문에, 이 연구에서는 ILP32 data model을 기준점으로 사용한다.
즉, 주어진 프로그램이 ILP32에서는 의도하는대로 동작한다고 가정하고 64bit data model을 사용하는 프로그램으로 컴파일되었을 때의 차이에 주목한다.
ILP32와 LLP64, LP64 data model
int 타입은 모든 32bit 아키텍처에서 32bit 크기이다.
ILP32의 경우 int와 pointer 타입 모두 동일한 32bit 크기를 가지고, 64bit data model의 경우 int 타입크기(32bit)는 pointer 타입 크기(64bit)의 반이다. 따라서 int 타입은 64bit data model에서 메모리를 나타내는데 사용할 수 없다.
이 섹션에서는 3가지의 정수 취약점 root cause에 대해 설명한다.
예시에서는 ILP32, LP64, LLP64 data model에서의 케이스를 가정한다.
임의의 할당문 x = e(x: type <x>의 변수, e: type <e>의 expression) 에 대해, W(<x>) < W(<e>)일 때 truncation이 발생한다.
타겟 변수 x의 크기가 expression e의 크기보다 작은 경우이다.
rank나 signedness에 상관없이 정수 width에 의존하여 truncation이 발생한다.
1 unsigned int x = attacker_controlled();
2 unsigned short y = x; // integer truncation occurs
3 char buffer = malloc(y);
4 memcpy(buffer, src, x); // buffer overflow!
위 코드는 buffer overflow로 이어지는 integer truncation의 예이다.
공격자가 제어하는 값이 unsigned int 타입의 x 변수에 저장된다.
unsigned int (4byte)값은 unsigend short 타입(2byte)의 y 변수에 할당된다. 이때 ILP32, LP64, LLP64 data model에서 모두 integer truncation 문제가 발생한다.
x = 0xffffffff이면, line 2에서 y = 0x0000ffff가 되어 integer truncation 문제가 발생하고 line 3에서 0x0000ffff 크기의 buffer가 할당된다. line 4에서는 0xffffffff(x) 크기만큼 src를 buffer에 복사하게 되어 buffer overflow가 발생한다.
임의의 표현문 e1 ◦ e2 에 대해, 표현문 e1 ◦ e2 평가 결과가 I(<e1 ◦ e2>)가 아닐 경우 integer overflow/underflow가 발생한다.
산술 연산 ◦ 에 따라 overflow 존재 여부가 결정된다.
1 unsigned int x = attacker_controlled();
2 char *buffer = malloc(x + CONST); // integer overflow
3 memcpy(buffer, src, x); // buffer overflow!
위의 코드는 integer overflow로 인해 buffer overflow가 발생하는 경우이다.
공격자가 제어할 수 있는 x 변수는 unsigned int형이다.
buffer 크기는 x + CONST으로, x + CONST 연산 결과는 unsigned int 범위 밖일 수 있다.
x = 0xffffffff라면, line 2에서 buffer 크기는 0xffffffff + 0x100이 되어 unsigned int 범위에서 벗어나 0x000000ff가 된다.
line 3에서 0x000000ff 크기의 buffer에 0xffffffff(x)만큼 src를 복사하여 buffer overflow가 발생한다.
Sign-extension
임의의 할당문 x = e에 대해, W(<x>) >= W(<e>)일 때 S(<x>) != S(<e>)라면 부호(signedness) 변경이 발생한다.
타겟 타입이 타겟 타입에 할당받는 표현보다 작은 경우는 truncation으로 분류한다.
S(<e>) = 1(signed)이고 W(<x>) > W(<e>)인 경우 추가적으로 sign-extension 문제가 발생한다.
크기가 작은 타입(type <e>, signed)의 most significant bit가 큰 크기를 갖는 타겟 변수(type <x>)에 채워지는 경우이다.
1 short x = attacker_controlled(); // signed short
2 char *buffer = malloc((unsigned short) x));
3 memcpy(buffer, src, x); // buffer overflow!
위 코드에서 부호 변화와 sign-extension(unsigned short에서 size_t로)에 의해 buffer overflow가 발생한다.
공격자는 short 타입의 변수 x를 제어하여 x = -1이 된다면, line 2에서는 unsigned short로 casting이 일어나서 0x0000ffff가 된다.
line 3에서는 x가 0xffffffff(sign-extension)으로 버퍼 크기 0x0000fffff보다 큰 값을 buffer에 복사하게 되어 buffer overflow가 발생한다.
Signed comparison
비교문 e1 ~ e2(e1: type , e2: type )에 대해, unsigned가 아닌 signed로 비교하고 평가 후 비교 대상 정수가 unsigned 타입으로 변경되는 경우에 정수 signedness 문제가 발생한다.
이 경우의 signedness 문제는 비교하는 타입 <e1>, <e2>의 sign, rank, width에 모두 영향을 받는다.
1 int x = attacker_controlled();
2 unsigned short BUF_SIZE = 10;
3 if(x >= BUF_SIZE) // comparison in signed type -> bypass this condition
4 return;
5 memcpy(buffer, src, x); // buffer overflow!
위의 코드에서 서로 다른 signed, unsigned 타입을 가지는 두 변수를 비교할 때 integer signedness 문제가 발생한다.
공격자가 제어하는 변수 x가 int형, 즉 signed 타입일 때, x에 음수를 주면 공격자는 line 3에서의 buffer overflow를 방지하는 조건을 우회할 수 있다.
x = -1으로 line 3에서 조건을 우회한 후, memcpy에서는 세 번째 인자 음수 x가 unsigned 형인 size_t로 변경되어 src를 크기가 10인 buffer에 0xffffffff만큼 복사되어 buffer overflow가 발생한다.
이번 섹션에서는 64bit migration 취약점을 발생시키는 주요 요인을 다음의 2가지로 분류하여 설명한다.
1) 정수 크기의 변화 2) 64bit 시스템에서의 큰 주소 공간의 가용성
32bit 플랫폼에 있는 모든 정수 타입은 64bit 플랫폼에 동일하게 있지만, 32bit 플랫폼에서와 정수 타입의 크기가 다를 수 있다. 이러한 정수 크기 변화는 할당문에서의 또다른 truncation과 sign extension 문제를 야기한다.
할당하는 표현의 타입보다 할당받는 타입의 크기가 더 작은 경우 truncation이 발생한다.
Table 2는 할당문에서의 정수 문제(자신의 정수 타입의 값을 잃는)를 보여준다. truncation의 경우 filled circle로 표현됐다. ILP32, LLP64, LP64의 할당문에서 서로 다른 특징을 보인다.
이번 new truncation는 32bit data model에서 64bit data model로의 이식 과정에서 발생한다.
이 new truncation으로, pointer를 처리하는 과정에서 두 가지 취약점이 발생할 수 있다.
Incorrect pointer differences
메모리 공간의 길이는 포인터를 빼서 구할 수 있고, `ptrdiff_t`(포인터와 동일한 크기) 타입을 반환한다. 하지만 일반적으로 반환되는 값을 int 타입에 저장해서 문제가 생긴다.
이 ‘Incorrect pointer differences’ 문제의 경우 ILP32 data model에서는 int 크기와 ptrdiff_t 크기가 같기 때문에 모든 32bit 플랫폼에서는 해당하지 않고, LP64, LLP64 data model인 경우에 해당한다. 이러한 데이터 크기 차이는 32bit로 전환될 때 truncation가 발생하게 하고, 그 결과 원본 값을 잃게 된다.
1 char buf[MAX_LINE_SIZE];
2 char *eol = strchr(str, '\n');
3 *eol = '\0';
4
5 unsigned int len = eol - str; // truncation
6 if(len >= MAX_LINE_SIZE)
7 return -1;
8 strcpy(buf, str); // buffer overflow!
Figure 5는 64bit로 컴파일 시 warning이 없지만, 라인 5에서 integer truncation이 발생한다.
strchr으로 한 라인을 구하고, eol - str 연산으로 한 라인의 길이를 구하는 코드이다.
만약 한 라인의 길이가 4GB를 넘게 되면, len이 32bit 크기이기 때문에 잘린 길이를 저장해서 부정확한 값이 len에 저장된다.
MAX_LINE_SIZE가 100이고 eol - str = 0x100000ff라면, 라인 5에서 len = 0x000000ff가 된다. 이후 라인 6의 사이즈를 체크하는 if 조건을 우회하고 라인 8에서 buf에 buf 크기보다 큰 값을 복사하게 되어 buffer overflow가 발생한다.
이러한 류의 취약점은 리턴값이나 인자값 크기로 int나 long를 갖는 fgets, fseek, snprintf와 같은 표준 라이브러리 함수에서도 볼 수 있다.
Casting pointers to integers
pointer에서 integer로 타입 변환을 하는 경우에서 문제가 발생할 수 있다.
32bit 플랫폼(ILP32)에서 pointer와 integer는 동일한 4바이트 크기이기 때문에 pointer → integer 타입 변환은 문제가 되지 않는다.
그러나 LP64, LLP64 data model에서는 pointer 크기 > int 크기이기 때문에 latent pointer truncation 문제가 발생한다. 포인터가 처음 4GB의 주소 공간을 참조하기 전까지는 문제를 알 수 없기 때문에 latent pointer truncation이라 한다. 이러한 pointer의 경우 0만 제거되기 때문에 truncation으로 값을 변경되지는 않지만, 공격자는 프로그램에 할당되는 메모리를 많이 늘려서 안전한 주소범위 이외의 주소을 pointer가 가리키도록 할 수도 있다.
이러한 취약점은 드물고 ASLR (Address Space Layout Randomization) 보호기법이 적용되어있기 때문에 이 취약점을 익스플로잇하기엔 어렵다.
32bit에서 64bit 플랫폼으로 코드를 이식할 때 두 가지 signedness 문제가 발생할 수 있다.
sign extensions
[signed 타입 → 더 큰 크기의 signed 타입]으로 변환
[signed 타입 → 더 큰 크기의 unsigned 타입]으로 변환
M1: 64bit data model, M2: 32bit data model
S(<x>) = 0 != S(<e>) = 1
x 타입 크기(64bit) > e 타입 크기(64bit) = x 타입 크기(32bit) = e 타입 크기(32bit)
위와 같다고 할 때 할당문 x = e 는
M1에서 sign extension으로 unsigned로 잘 해석되지만,
M2에서는 그렇지 않다.
signedness error in assignment: 회색 원
signed extension in assignment: 회색 원+E로 표시했다. LLP64 data model의 경우 int와 long → size_t 로 변환하는 경우, LP64에서는 int → unsigned long과 size_t로 변환하는 경우 새로운 signed extension 문제가 발생한다.
보안 측면에서 취약점을 찾으려면 size_t 타입으로 변환되는 코드에 주목하는게 좋다.
Signedness of comparions
buffer overflow를 검사할 때는 부호없는 정수와 비교하는 값 사이에서 옳기만 하면 된다.
일반적으로 모든 정수 타입은 비교 연산 전 unsigned 타입으로 변환되어야 한다. 하지만 많은 케이스에서 32bit의 정수 변환 규칙에 따라 명시적 타입 변환이 생략될 수 있어서 unsigned으로 비교가 이루어진다. 하지만 64bit 플랫폼에서는 이를 허용하지 않아서 64bit 플랫폼으로 이식 시 signedness를 바꿔서 비교한다.
M1: 64bit data model, M2: 32bit data model
S(<a>) = 0, S(<b>) = 1 (서로 다른 signedness)
일 때, 비교 연산 a ~ b는
Table 3는 각 타입마다 signedness 비교를 보여준다.
unsigned로 비교: filled cirlce
signed로 비교: empty circle
ILP32와 LLP64에서 모두 long과 unsigned int를 비교할 때 unsigned로 비교하는데, LP64에서는 signed로 비교한다.
1 const unsigned int BUF_SIZE = 128; // unsigned
2 long len = attacker_controlled(); // signed
3
4 if(len > BUF_SIZE) // compare in signed -> bypass BOF check condition
5 return;
6 memcpy(buffer, src, len); // buffer overflow!
Figure 6는 signedness comparison에 대한 예시 코드이다.
attacker_controlled() 함수 결과를 signed 타입 변수 len에 저장한다. 이후 len(signed)과 BUF_SIZE(unsigned)를 비교하여 overflow를 체크한다.
32bit 플랫폼이었다면 long과 unsigned int가 동일한 크기를 가져서 unsigned int의 범위를 초과할 수 없고 len은 unsigned로 변환되어 비교된다. len = -1이면 comparison에서 len = 0xffffffff으로 0xffffffff > 0x00000080이기 때문에 조건에 걸린다. 따라서 32bit 플랫폼에서는 개발자의 의도대로 작동하게 된다.
LP64에서는 long 타입 크기(8바이트) unsigned int 타입 크기(4바이트)이다. long 타입과 unsigned int 비교 시 long이 unsigned int 범위에서 벗어나기 때문에 signed로 비교하게 된다. 그럼 len = -1일 때 -1 > 0x80 결과가 false로 조건문을 우회하게 되고, memcpy 시 len = 0xffffffffffffffff(unsigned int)가 되어 buffer에 0x80보다 큰 값을 복사하는 buffer overflow 취약점이 발생하게 된다.
64bit 플랫폼으로 이식 시 사용 가능한 주소 공간도 4GB에서 수백 TB로 늘었다.
이러한 변화로, 32bit data model에서도 존재하는 추가적인 integer truncation과 overflow 문제는 32bit 플랫폼에서는 트리거할 수 없었지만 64bit에서는 가능하게 된다.
큰 주소 공간으로 가능한 것
(a) 큰 객체 생성
(b) 많은 객체 사용 가능
객체의 크기나 개수에 대한 산술 연산에서 변수가 pointer 크기가 작으면 64bit 플랫폼으로 이식 시 integer overflow가 발생한다.
1 unsigned int i;
2 size_t len = attcker_controlled(); // len > UNIT_MAX (max range value of unsigned int)
3 char *buf = malloc(len);
4
5 for(i = 0; i < len; i++){ // endless loop
6 *buf++ = get_next_byte();
7 }
위의 코드에서 큰 객체로 인해 integer overflow가 발생한다.
LP64, LLP64 data model에서 unsigned int 크기 < size_t 크기이다.
공격자가 제어 가능한 len > UNIT_MAX (unsigned int 범위 최댓값)으로 준다면, 반복문에서는 i의 최대 범위(UINT_MAX)를 넘어서 무한으로 buf에 값을 복사한다.
이러한 취약점으로 큰 객체 buf를 만들게 되고, 큰 객체는 size_t 크기보다 작은 reference counter에 묶이게 된다.
예시로는 strlen 의 리턴값을 int 형 변수에 저장하는 경우이다. INT_MAX보다 긴 문자열의 경우, strlen 리턴값은 음수가 된다. 32bit 플랫폼에서는 메모리 공간의 제약 때문에 익스플로잇이 거의 불가능한데, 64bit 플랫폼에서는 INT_MAX보다 큰 문자열도 하나의 프로세스에 할당될 수 있어서 이 취약점을 트리거할 수 있다.
1 char buffer[128];
2 int len = strlen(attacker_str); // if len(attacker_str) > INT_MAX, return value < 0
3
4 if(len >= 128) // bypass BOF condition as len < 0
5 return;
6 memcpy(buffer, attacker_str, len); // buffer overflow! (as len convert to unsigend int)
위 코드에서 dormant signedness 문제로 buffer overflow 취약점이 발생한다.
공격자가 제어 가능한 attacker_str 문자열의 크기를 l이라고 할때, INT_MAX < l < UINT_MAX인 경우, strlen 리턴값은 음수로 len에는 음수가 저장된다. len < 0이기 떄문에 BOF 체크 로직을 우회하고, memcpy 시에는 len이 unsigned int 타입으로 변환되어 buffer 128 크기보다 큰 값을 복사하는 buffer overflow가 발생한다.
여러 standard C library 함수가 32bit data model 기준으로 설계되어서, truncation, overflow, signedness 문제에 취약하게 되었다. 64bit data model 용으로 이러한 함수가 따로 나왔지만, 변경된 함수 사용에 대해 신경을 쓰지 않는 개발자들이 많다.
String formatting
문자열을 출력하는 함수들(fprintf, snprintf, vsnprintf)은 문자열 길이가 INT_MAX보다 크지 않는 가정하에 설계되었다.
하지만 32bit 플랫폼에서만 유효하며, 64bit data model에서는 적용되지 않는다.
int snprintf(char *s, size_t n, const char *fmt, ...)
fmt 문자열에 s를 n 크기만큼 적는 snprintf 함수의 경우, 리턴값으로 적은 byte 수를 반환한다. 64bit 플랫폼에서 문자열 길이가 INT_MAX보다 클 수 있다. 따라서 리턴값이 int 범위가 아닐 수 있다. C99에서 이러한 예외 상황일 때 고정된 값인 -1을 반환하게 하는데, 리턴값에 shift pointer를 사용해서 취약점으로 이어질 수 있다.
1 int pos = 0;
2 char buf[BUF_LEN-1];
3
4 int log(char *str){
5 int n = snprintf(buf+pos, BUF_LEN-pos, "%s", str); // n = -1 (string size > INT_MAX)
6
7 if(n > BUF_LEN - pos){ // bypass as n < 0
8 pos = BUF_LEN;
9 return -1;
10 }
11 return (pos += n); // stack corruption occurs
12 }
위는 전역변수 buf에 BUF_LEN + 1 크기의 문자열을 적는 취약한 코드이다.
snprintf 함수에서는 str 길이가 INT_MAX 크기보다 크면(64bit 플랫폼에서) -1을 반환한다.
n = -1이 되어 if 조건을 우회하게 되고 라인 11에서 pos에서 음수를 빼게 되어 underflow가 발생한다. (pos가 음수가 됨)
log에는 stack corrupt에 대한 로그가 남게 된다.
File processing
Standard C 라이브러리에서는 파일 처리 관련 함수들(ftell, fseek, fgetpos 등)을 제공한다. 이러한 함수는 64bit 정수에 최적화가 되지 않게 설계되어서 파일이 4GB보다 클 수 있다.
ftello, ftello64, __ftelli64 함수를 대안으로 사용하게 했지만, ftell은 큰 파일을 처리할 때 문서화 되지 않은 동작을 한다. (섹션 4에서 관련 연구를 설명함)
ftell은 long 타입의 파일 포인터를 반환하는데, LLP64 data model에서는 32bit 더 크다. 현재 position이 LOG_MAX(0xffffffff)보다 클 경우 C99에서는 실패 시 -1을 반환하고, Microsoft Visual C++에서는 0을 반환하도록 되어있다. 이는 보안 문제가 발생할 수 있다.
1 int i;
2 char *buf;
3
4 FILE* const f = fopen(filename, "r");
5 fseek(f, 0, SEEK_END); // seek end of file to get file size
6 const long size = ftell(f); // save file position
7
8 buf = malloc(size/2 + i); //
9
10 fseek(f, 0, SEEK_SET);
11 for(; fscanf(f, "%02x", &i) != EOF; buf++) // write to buffer until EOF
12 *buf = i;
파일에서 hex 값 읽어서 디코딩된 값을 buf에 저장하는 코드이다.
먼저 파일 크기를 알기 위해 fseek를 하고, ftell로 그 위치를 size에 저장한다.
이후 size로 buf를 할당하고, fscanf를 통해 buf에 EOF를 만나기 전까지 값을 복사한다.
Microsoft Windows 64bit에서는 4GB보다 큰 파일을 사용해서 취약점을 트리거할 수 있다. 4GB보다 큰 파일을 사용하면 ftell은 0을 반환해서 buffer에는 1바이트만 할당된다. 이후 반복문을 통해 할당되지 않은 힙 영역에 값을 쓸 수 있는 취약점이 발생한다.
오늘날 소프트웨어에서 64bit migration으로 인한 취약점이 얼마나 있는지 확인하기 위해 분석을 진행했다.
두가지 실험을 진행했다.
얼마나 자주 잘못된 type conversion을 사용하는지에 대한 실험이다.
GCC, LLVM clang과 다른 컴파일러에서는 할당문, 산술 연산, 비교 연산에서의 변환될 수 없는 정수 타입과 암묵적 타입 변환을 사용할 때의 warning을 내야 한다. 하지만 너무 많은 warning이 발생하기 때문에 이러한 warning에 대한 compiler flag를 사용하지 않는다. 198 Debian 패키지의 경우 이러한 warning을 위한 flag가 하나도 사용되지 않았다.
-Wconversion: wdith conversion warning
-Wsign-conversion: sigendness에서의 변화 warning
-Wsign-compare: signed과 unsigned 비교에서 warning
-Wfloat-conversion: 소수점 정확도를 손실하는 비교에 대한 warning
Table 4는 결과에 대한 표이다. 64bit 패키지 당 컴파일러에 의해 발생한 각 conversion 타입에서의 warning을 수치화했다.
연구 결과, width와 sign conversion은 패키지 당 각각 442, 250 warning으로 가장 많은 비중을 차지했다. (32bit에서는 발생하지 않고 64bit에서만 발생한다.) sign comparison의 경우, 64bit에서는 signed으로 비교하기 때문에 섹션 3.1.2에서의 취약점 유형에 대한 warning이 많다.
64-bit platform에서 이러한 암묵적 type conversion으로 의도하지 않은 연산이 이루어지는 코드 패턴을 분석했다. 분석 후, 64-bit migration 문제 패턴을 control flow 분석과 data flow 분석 측면에서 모델화했다. 패턴을 5개로 카테고리화하고, Debian stable에서와 인기있는 Github C/C++ 프로젝트를 대상으로 이러한 문제 코드 패턴을 카운팅했다.
잘못된 atol 함수 사용으로 64-bit 시스템에서 truncation이 발생할 수 있다.
atol 함수에서 리턴값을 long이 아닌 int 형으로 반환해서 truncation이 발생하는 경우를 모두 카운팅했다.
메모리 연산을 진행 시 signedness 문제는 보안상 위험하다. 이 카테고리에서는 memcpy (memory copy) 연산에서의 의도하지 않은 sign-extension을 의미한다.
memcpy 시 signed int를 사용하는 모든 경우를 전체 memcpy 사용과 비교해서 카운팅했다.
정수 underflow나 overflow는 여러 상황에서 발생 가능하다. 이 패턴은 64-bit의 for loop에서 loop 변수 size_t 타입이고, 내부 loop의 increment/decrement로 사용하는 변수 타입이 unsigned int인 코드 패턴이다.
잘못된 strlen 함수 사용 코드 패턴에 해당한다.
파라미터로 string literal을 사용하지 않아서 strlen 리턴값에 unsigned int 타입이 반환되는 경우를 카운팅했다.
개발자가 예상 가능하거나 C99 표준 문서에 명시되어있지 않은 두 개의 library 함수에서 관찰한 내용이다. 첫 번째로, snprintf 함수에서 리턴값의 유효성을 검사하지 않는 코드 패턴을 확인한다. 두 번째로, ftell* 함수 (ftell, ftello, ftell64, _ftelli64 ) 사용 패턴을 카운팅한다.
Table 6는 이렇게 5가지 코드 패턴을 카운팅한 결과를 나타내는 표이다.
atol 함수 호출의 21%는 리턴값을 long이 아닌 int 타입에 할당했다. 이것으로 64-bit 시스템에서 truncation이 발생한다. 또한 size_t로 정의된 함수 파라미터에 signed int 타입을 가지는 인자를 전달했다. (P1)
memcpy 함수에서는 10%가 잘못 사용되고 있었다. (P2)
for loop에서 확인할 수 있는 integer overflow 문제 패턴은 loop-counter가 size_t인데 increment를 int 변수로 사용해서 발생하는 경우로, 9.5%가 이에 해당했다. (P3)
strlen 호출의 15%는 size_t가 아닌 int 타입에 결과를 할당했다. (P4)
마지막으로 snprintf와 ftell 함수에서 각각 30%, 70%가 잘못 사용되고 있었다. (P5a, P5b)
이 연구 결과 Debian에서가 Github 프로젝트에서보다 더 적은 64bit migration 문제 코드 패턴을 보인다는 것을 확인할 수 있었다.
섹션 3에서 설명한 64bit migration 취약점들을 설명한다.
각 문제에 대해 control flow, data flow에 대한 패턴을 만들고, 유명한 코드에서 취약한 프로그램 코드를 찾았다.
64bit migration에서 64bit에서의 1) 정수 크기 변화 2) 메모리 공간 증가를 주요 요인임을 강조한다.
Table 7는 두 개의 카테코리(정수 크기 증가, 메모리 공간 증가) 기반으로 정리한 취약점들을 보여준다.
32-bit system에서 64-bit system으로 코드를 이식하는 경우 truncation과 관련하여 두 가지 취약점이 발생할 수 있음을 설명했다. 2007년에 공개된 PHP에서의 취약점과 이번 연구로 발견한 Linux Kernel 취약점이 이에 해당했다.
PHP 4.4.5 이전과 5.2.1 버전에서 code execution을 가능하도록 하는 취약점(CVE-2007-1884)이다. 취약점은 32bit system에서 없었던 integer truncation 문제로, php_sprintf_getnumber 함수에서 리턴 타입이 long인데 int 타입인 변수에 저장했다. ILP32, LLP64(Windows)를 사용하는 시스템에서는 문제가 없었지만, LP64의 경우 long 타입이 8byte이기 때문에 truncation이 발생했고 INT_MAX를 사용하는 것으로 임의 코드 실행으로 이어질 수 있었다.
C standard library 함수에서 발생한 취약점이었다. GNU C library와 다르게 Linux Kernel 4.6 이하 버전에서는 snprintf 함수의 큰 입력값을 체크하지 않았다. pointer 간 뺄셈 연산에서 리턴값으로 ptrdiff_t을 가지는데, 32bit integer를 반환하도록 구현되어있다. 하지만 64-bit system에서 ptrdiff_t는 64bit 크기로 큰 입력을 주는 경우 리턴값 크기가 truncation되는 문제가 발생한다. (관련 내용은 섹션 3.1.1에서 참고)
zlib 1.2.8 버전에서 취약점이 발생했다. gzprintf 함수에서 buffer에 formatted string을 적을 때 INT_MAT 바이트보다 큰 입력에 대한 처리를 고려하지 않았다. size가 unsigned int 타입일 때, gzvprintf 함수는 내부적으로 int 형으로 casting을 진행한다. 이후 vsnprintf 함수의 두 번째 파라미터로 사용되는데 size_t 형으로 정의되어있다. LP64, LLP64에서는 int보다 size_t 크기가 2배 더 크기 때문에 sign-extension이 발생하여 변환 결과 큰 unsigend 값을 크기로 가지게 되어 buffer overflow 취약점이 발생한다.
libarchive 3.1.2 버전의 archive_write_zip_data 함수에서 취약점이 발생했다. (CVE-2013-0211) archive_write_zip_data 함수는 zip archive 쓰기의 callback으로 사용되는데, 인자로 목적지 buffer와 size를 사용한다. 쓰기 전, buffer가 zip archive에 쓸 수 있는 최댓값을 초과하는지 체크한다. size는 명시적으로 size_t에서 int64_t로 캐스팅되는데, 32bit system에서는 문제가 되지 않는다(INT64_MAX > SIZE_MAX이기 때문). 하지만 64-bit system에서는 size_t와 int64_t가 동일한 크기로, 입력으로 UNIT_MAX을 주게 되면 허용 가능한 최대 범위와 상관없이 arthive에 사용될 수 있게 된다.
32bit data model에서도 발생하는 integer overflow이지만 주소 공간 크기의 증가로 인해 64bit에서만 트리거 가능한 경우이다.
GNU C Library 2.23 버전에서 wcswidth 함수는 integer overflow 취약점을 가지고 있었다. wide-character 문자열에 필요한 column 수를 세는데 counter 변수는 리턴값에 따라 내부적으로 int 타입으로 정의되어 있다. 64bit system에서 UNIT_MAX보다 긴 문자열을 입력으로 주는 경우 리턴값은 양수로 변환되어 메모리 할당에 이 값을 사용하는 경우 buffer overflow가 발생할 수 있다.
Boost C++ Libraries 1.60 버전의 boost::shared_ptr<T> 와 Chromium 52.0 버전, GNU Standard C++ Libraries에서는 참조 counter를 int 타입으로 구현했다.
64bit sytem에서, 공격자는 1을 저장할 때까지 많은 shared pointer를 생성해서 counter가 overflow가 발생하도록 할 수 있다. counter가 1이 되면, 다음 shared pointer는 파괴되면 해제된 영역을 가리키는 UNIT_MAX 인스턴스가 남는다. 이 use-after-free 취약점을 사용하여 공격자는 arbtrary code execution을 할 수 있다.
libarchive 3.2.0 버전에서 signedness 문제를 가지고 있는 취약점은 , 64bit에서의 큰 주소 공간으로 익스플로잇 가능하다.
iso9660 컨테이너를 처리할 때 Joliet 식별자 길이를 검사하는 로직을 가지고 있다. name 길이는 size_t로 저장되는데 명시적으로 int로 캐스팅되어 signedness integer로 길이 검사를 우회할 수 있다. 우회하려면 3x개의 메모리를 할당받아야 이 로직을 우회할 수 있는데, 64bit에서 큰 주소 공간을 가져서 가능하다.
32bit 플랫폼에서 64bit 플랫폼으로 이식한 결과로 발생한 취약점 중에서 type conversion 관련 코드 패턴은 코드 오디팅으로 찾을 수 있다. 64bit 프로세서가 10년 넘게 마켓을 점유해오고 있는데, 여전히 migration 과정에서 이러한 취약점을 보이고 있고 주목해야 할 문제이다.
64bit migration 문제를 하나의 프로세스에 큰 메모리를 할당하는 것으로 트리거할 수도 있다. (섹션 3.2에서 설명) 따라서 이러한 취약점은 메모리 사용을 모니터링해서 확인할 수 있다.
섹션 4.1에서 설명했듯이, 잘 알려진 C/C++ 프로젝트들과 Debian stable 패키지 코드에서는 64bit migration으로 인해 발생 가능한 warning들이 있었는데, 이 warning들이 모두 보안적 문제가 있다고 말할 수는 없지만 이러한 warning들은 취약점으로 이어질 수 있는 지표이다. 또한 Debian 패키지에서는 -Wconversion flag를 사용하지 않았고, 이러한 문제를 warning으로 가렸다.
결론적으로 64bit migration 문제와 취약점에서 가장 효과적인 예방방법은 이러한 문제를 보안적으로 인지하고 암묵적 conversion을 하지 않는 것이다.
많은 연구자들은 integer 관련 문제와 취약점들을 다뤄왔고, 두 문제를 식별하고 방지하기 위해 노력해왔다.
64bit 아키텍처로 이식할 때의 가이드들이 있어왔다.
IBM, Oracle, Apple은 ILP32에서 LP64로 코드를 이식 시 문제를 위한 가이드를 냈지만, 이러한 가이드는 64bit로 이식 시 어떻게 취약점을 익스플로잇할 수 있는지에 대해서는 제공하지 않는다.
64bit 시스템 관련 취약점을 식별하는 툴들이 개발되었다. (ex. Viva64 static analyzer)
Medeiro와 Correia는 64bit 이식 취약점을 탐지하는 툴을 제안했다. 이 방법은 타입 체크와 taint tracking의 결합을 기반으로 정수 문제를 탐지한다. 하지만 평가 결과 모든 탐지는 false positive가 존재한다.
이 논문에서는 64bit migration으로 발생 가능한 취약점을 시스템적으로 분류하고 정의하였다.
이후 유명한 C/C++ Github 프로젝트와 Debian Stable 코드 대상으로 64bit migration 관련 warning 들을 컴파일 옵션에 따라 수치화하였다.
마지막으로, 32bit에서 64bit 플랫폼으로 코드 이식과 관련해서 Linux Kernel, Chromium, Boost C++ Libraries, libarchive, zlib에서 6개의 취약점을 발견할 수 있었다.