논문(https://intellisec.de/pubs/2016-ccs.pdf)을 읽고 요약 및 각색한 내용이다.
Integer-Related Vulnerabilities
C언어의 정수 타입을 사용함에 있어서 많은 취약점이 발생할 수 있다. 다음은 그 예시를 제시한 것이며 크게 다음과 같이 나눌 수 있다.
- Integer Truncations
- Integer Overflows
- Integer Signedness Issues
Integer Truncations
extern int attacker_controlled();
int main(void)
{
char src[128];
src[127] = 0;
read(0, src, 127);
unsigned int x = attacker_controlled();
unsigned short y = x;
char *buffer = malloc(y);
memcpy(buffer, src, x);
return 0;
}
// gcc -m32 -march=i686
main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 160
mov BYTE PTR [esp+147], 0
mov DWORD PTR [esp+8], 127
lea eax, [esp+20]
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp], 0
call read
call attacker_controlled
mov DWORD PTR [esp+156], eax
mov eax, DWORD PTR [esp+156]
mov WORD PTR [esp+154], ax
movzx eax, WORD PTR [esp+154]
mov DWORD PTR [esp], eax
call malloc
mov DWORD PTR [esp+148], eax
mov eax, DWORD PTR [esp+156]
mov DWORD PTR [esp+8], eax
lea eax, [esp+20]
mov DWORD PTR [esp+4], eax
mov eax, DWORD PTR [esp+148]
mov DWORD PTR [esp], eax
call memcpy
mov eax, 0
mov esp, ebp
pop ebp
ret
(src
의 크기가 128인건 무시하자. 여기선 src
가 충분히 크다고 가정하겠다.)
mov WORD PTR [esp+154], ax
와 같이 x
의 상위 16bit가 소실되고 y
에 저장되는 모습이다.
x
가 0x10FFF
이라고 생각해보자.
y
는 0x0FFF
가 되고, buffer
의 크기는 0x0FFF
가 된다.
이때 buffer
에 0x10FFF
만큼 쓰려고 하므로 buffer-overflow가 발생한다.
Integer Overflows
extern int attacker_controlled();
#define CONST 50
int main(void)
{
char src[128];
src[127] = 0;
read(0, src, 127);
unsigned int x = attacker_controlled();
char *buffer = malloc(x + CONST);
memcpy(buffer, src, x);
return 0;
}
// gcc -m32 -march=i686
main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 160
mov BYTE PTR [esp+151], 0
mov DWORD PTR [esp+8], 127
lea eax, [esp+24]
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp], 0
call read
call attacker_controlled
mov DWORD PTR [esp+156], eax
mov eax, DWORD PTR [esp+156]
add eax, 50
mov DWORD PTR [esp], eax
call malloc
mov DWORD PTR [esp+152], eax
mov eax, DWORD PTR [esp+156]
mov DWORD PTR [esp+8], eax
lea eax, [esp+24]
mov DWORD PTR [esp+4], eax
mov eax, DWORD PTR [esp+152]
mov DWORD PTR [esp], eax
call memcpy
mov eax, 0
mov esp, ebp
pop ebp
ret
(src
의 크기가 128인건 무시하자. 여기선 src
가 충분히 크다고 가정하겠다.)
x
가 0xFFFFFFFF
라고 가정해보자.
add eax, 50
에서 integer-overflow가 발생하고,
eax
는 0x31
이 된다.
곧, buffer
의 크기는 0x31
이 된다.
이때, buffer
에 0xFFFFFFFF
만큼 쓰려고 하므로 buffer-overflow가 발생한다.
Integer Signedness Issues
extern int attacker_controlled();
#define CONST 50
char src[128];
int main(void)
{
read(0, src, 127);
short x = attacker_controlled();
char *buffer = malloc((unsigned short) x);
memcpy(buffer, src, x);
return 0;
}
// gcc -m32 -march=i686
src:
.zero 128 ;원래 전부 로컬 변수로 만드려 했으나, global 변수로 두는게 어셈블리가 깔끔해보여 수정함.
main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
mov DWORD PTR [esp+8], 127
mov DWORD PTR [esp+4], OFFSET FLAT:src
mov DWORD PTR [esp], 0
call read
call attacker_controlled
mov WORD PTR [esp+30], ax
movzx eax, WORD PTR [esp+30]
mov DWORD PTR [esp], eax
call malloc
mov DWORD PTR [esp+24], eax
movsx eax, WORD PTR [esp+30]
mov DWORD PTR [esp+8], eax
mov DWORD PTR [esp+4], OFFSET FLAT:src
mov eax, DWORD PTR [esp+24]
mov DWORD PTR [esp], eax
call memcpy
mov eax, 0
mov esp, ebp
pop ebp
ret
malloc
의 인자로 넘어가는 (unsigned short) x
는 movzx
로 넘어가는 반면,
memcpy
로 넘어가는 x
는 movsx
로 넘어가는 모습이다.
movzx: MOVe with Zero-eXtension의 약자로, 부족한 비트는 0으로 채운다.
따라서, 만약 x
가 -1이라면, malloc
의 인자로 넘어가는 값은 0x0000FFFF
이고,
movsx: MOVe with Sign-eXtension의 약자로, 부족한 비트는 부호비트로 채운다.
따라서, 만약 x
가 -1이라면, memcpy
의 인자로 넘어가는 값은 0xFFFFFFFF
가 된다.
(이후 자세히 설명한다.)
즉, 만약 x
가 -1이라면, buffer
의 크기는 0xFFFF
가 되나,
memcpy
로 쓰는 크기는 0xFFFFFFFF
이므로, buffer-overflow가 발생한다.
Summary
int | unsigned int | long | unsigned long | ssize_t | ptr/size_t | long long | |
---|---|---|---|---|---|---|---|
int | — | ... | –= | ..= | -== | .== | === |
unsigned int | ... | — | ..= | –= | .== | -== | === |
long | — | ..- | — | ... | -=- | .=. | ==- |
unsigned long | ... | — | ... | — | .=- | -=- | ==. |
ssize_t | — | .– | — | .-. | — | ... | =– |
ptr/size_t | ... | — | ... | — | ... | — | =.. |
long long | — | — | — | –. | — | ... | — |
- .: denotes no problem, -: a change in signedness and possibly with sign extension, =: marks a truncation
한 정수 타입을 다른 정수 타입으로 변환 시키는 것이 어떤 문제를 일으키는지 도표로 정리해보자면 위와 같다.
64-Bit Migration Vulnerabilities
64bit환경으로의 마이그레이션이 불러올 수 있는 이슈들은 크게 4가지로 나눌 수 있다.
- New truncations
- New signedness issues
- Dormant integer overflows
- Dormant signedness issues
1. New Truncations
long ll = atol(attacker_controlled());
int l = ll;
char *buffer = malloc(l);
memcpy(buffer, src, ll);
이 코드는 모든 32비트 플랫폼에서 완전히 안전하다.
그러나, LP64모델에서, 2번째 줄에서 Truncation이 발생하게 된다.
long
은 8B, int
는 4B이므로, ll
의 상위 4B가 소실된 채로 l
에 저장된다.
ll
이 0x10FFFFFFF
이라고 하자.
이때, buffer
의 크기는 0x0FFFFFFF
가 되는데,
0x10FFFFFFF
만큼 쓰려고 하기 때문에 buffer-overflow가 발생한다.
2. New Signedness Issues
extern int attacker_controlled();
int main()
{
char src[128];
src[127] = 0;
read(0, src, 127);
int len = attacker_controlled();
char *buffer = malloc((unsigned) len);
memcpy(buffer, src, len);
return 0;
}
(src
의 크기가 128인건 무시하자. 여기선 src
가 충분히 크다고 가정하겠다.)
이 코드는 모든 32비트 플랫폼에서 완전히 안전하다.
그러나, LP64모델에서, 10번, 11번 줄은 11번 줄에서 64bit 부호 확장이 일어날때 취약점을 드러낸다.
//gcc -m32 -march=i686
main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 160
mov BYTE PTR [esp+151], 0
mov DWORD PTR [esp+8], 127
lea eax, [esp+24]
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp], 0
call read
call attacker_controlled
mov DWORD PTR [esp+156], eax
mov eax, DWORD PTR [esp+156]
mov DWORD PTR [esp], eax
call malloc
mov DWORD PTR [esp+152], eax
mov eax, DWORD PTR [esp+156]
mov DWORD PTR [esp+8], eax
lea eax, [esp+24]
mov DWORD PTR [esp+4], eax
mov eax, DWORD PTR [esp+152]
mov DWORD PTR [esp], eax
call memcpy
mov eax, 0
mov esp, ebp
pop ebp
ret
// gcc -march=x86-64
main:
push rbp
mov rbp, rsp
sub rsp, 144
mov BYTE PTR [rbp-17], 0
lea rax, [rbp-144]
mov edx, 127
mov rsi, rax
mov edi, 0
mov eax, 0
call read
mov eax, 0
call attacker_controlled
mov DWORD PTR [rbp-4], eax
mov eax, DWORD PTR [rbp-4]
mov eax, eax
mov rdi, rax
call malloc
mov QWORD PTR [rbp-16], rax
mov eax, DWORD PTR [rbp-4]
movsx rdx, eax
lea rcx, [rbp-144]
mov rax, QWORD PTR [rbp-16]
mov rsi, rcx
mov rdi, rax
call memcpy
mov eax, 0
leave
ret
위의 소스코드가 각각 i686, x86-64플랫폼에서 컴파일된 결과로 두 어셈블리어를 얻을 수 있었다.
위의 어셈블리 소스와는 달리, 아래의 어셈블리 소스의 25번 줄에서 movsx
를 볼 수 있다.
MOVSX - MOVe with Sign-eXtension
MOVSX r64, r/m32
movsx
(단, 이 섹션에서 말하는 movsx
는 별다른 언급이 없다면 movsx r64, r/m32
임.)는 32bit register의 값을 64bit register에 설정하는데,
이때, 값의 일관성을 유지하기 위해 확장되는 32bit는 설정될 값의 부호비트를 따른다.
mov eax, -1
movsx rax, eax
예를 들어, -1의 경우, 32bit integer에서 0xFFFFFFFF
이다.
이를 64bit integer로 확장하더라도 -1이어야 하므로,
sign-extension을 통해 rax의 값은 0xFFFFFFFFFFFFFFFF
가 된다.
extern int attacker_controlled();
int main()
{
char src[128];
src[127] = 0;
read(0, src, 127);
int len = attacker_controlled();
char *buffer = malloc((unsigned) len);
memcpy(buffer, src, len);
return 0;
}
(src
의 크기가 128인건 무시하자. 여기선 src
가 충분히 크다고 가정하겠다.)
다시 처음의 소스로 돌아와, 64bit 환경을 대상으로 컴파일 되었고, attacker_controlled
가 -1을 반환한다고 가정해보자.
할당되는 buffer
의 크기는 0xFFFFFFFF
가 되며,
memcpy
의 3번째 인자에 들어가는 len
은 movsz
를 통해 size_t
로 암시적 변환되어 0xFFFFFFFFFFFFFFFF
가 된다.
즉, 0xFFFFFFFF
만큼 할당된 버퍼에 0xFFFFFFFFFFFFFFFF
만큼 쓰려고 하므로, buffer-overflow가 발생한다.
3. Dormant Integer Overflows
지금까지는 32bit 데이터 모델과 64bit 데이터 모델의 차이 때문에 발생하는 문제를 다뤄왔지만, 순전히 메모리 주소 공간의 크기가 커졌기에 발생한 문제들 또한 존재한다.
GNU C Library. glibc의 2.23버전에 포함된 wcswidth
는 integer-overflow를 포함하고 있다. 이 함수는 wide-character string의 요소의 수를 세는데 내부적으로 int
를 사용하고 있다.
64bit환경의 메모리 주소 공간의 크기는 INT_MAX
보다 큰 크기의 메모리를 할당하는 것을 허용하기 때문에, 만약 인자로 제공되는 문자열의 길이가 UINT_MAX
보다 크고, 이가 다른 메모리를 할당하는데에 쓰인다면, 검출하기 어려운 buffer-overflow를 초래할 위험성이 있다.
C++ shared pointers. shared-pointer는 포인터의 사용을 추적하고, 일종의 GC를 구현해 포인터를 안전하게 관리할 수 있게 한다. 버전 1.60의 Boost C++ Libraries
(boost::shared_ptr<T>
)는 버전 52.0의 Chromium
, GCC 6.1.0에서 제공되는 GNU Standard C++ Libraries
에 포함되었는데, 이 역시 내부적으로 참조수를 저장할 때에 int
를 사용하고 있었다. 상기된 문제와 같이 참조수에 오버플로우가 발생하여 0이 되어버리면, shared-pointer가 담고 있는 포인터는 할당 해제되어버린다. 이는 use-after-free를 초래하여 arbitary code execution을 일으킬 수 있다.
4. Dormant Signedness Issues
마지막으로 알아볼 것은 32bit 환경에서는 발생시킬 수 없지만, 64bit에서는 발생시킬 수 있는 버전 3.2.0의 libarchive에 포함된 signedness issue를 알아볼 것이다.
이 취약점은 iso9660
의 처리과정 중 Joliet
식별자를 처리하기 위한 최대 크기를 고려하여 size_t
를 이용하는데 이를 int로 명시적으로 형변환하는 과정에 있다.
이는 상술된 바와 같이 size_t
와 int
가 모두 4B로 동일한 크기를 가지고 있을 때에는 발생하지 않지만, 64bit환경으로 마이그레이션하며 발생하여 buffer-overflow를 초래하는 취약점이다.
적절히 큰 크기의 문자열을 libarchive에 넘겨 정수의 부호를 바꾸어 메모리 검사를 우회할 수 있고, buffer-overflow를 일으킬 수 있다.