02: 어떻게 취약점을 찾는가: 과제

논문(https://intellisec.de/pubs/2016-ccs.pdf)을 읽고 요약 및 각색한 내용이다.

Integer-Related Vulnerabilities

C언어의 정수 타입을 사용함에 있어서 많은 취약점이 발생할 수 있다. 다음은 그 예시를 제시한 것이며 크게 다음과 같이 나눌 수 있다.

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에 저장되는 모습이다. x0x10FFF이라고 생각해보자. y0x0FFF가 되고, buffer의 크기는 0x0FFF가 된다. 이때 buffer0x10FFF만큼 쓰려고 하므로 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가 충분히 크다고 가정하겠다.) x0xFFFFFFFF라고 가정해보자. add eax, 50에서 integer-overflow가 발생하고, eax0x31이 된다. 곧, buffer의 크기는 0x31이 된다. 이때, buffer0xFFFFFFFF만큼 쓰려고 하므로 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) xmovzx로 넘어가는 반면, memcpy로 넘어가는 xmovsx로 넘어가는 모습이다.

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 –. ...

한 정수 타입을 다른 정수 타입으로 변환 시키는 것이 어떤 문제를 일으키는지 도표로 정리해보자면 위와 같다.

64-Bit Migration Vulnerabilities

64bit환경으로의 마이그레이션이 불러올 수 있는 이슈들은 크게 4가지로 나눌 수 있다.

  1. New truncations
  2. New signedness issues
  3. Dormant integer overflows
  4. 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에 저장된다. ll0x10FFFFFFF이라고 하자. 이때, 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번째 인자에 들어가는 lenmovsz를 통해 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_tint가 모두 4B로 동일한 크기를 가지고 있을 때에는 발생하지 않지만, 64bit환경으로 마이그레이션하며 발생하여 buffer-overflow를 초래하는 취약점이다. 적절히 큰 크기의 문자열을 libarchive에 넘겨 정수의 부호를 바꾸어 메모리 검사를 우회할 수 있고, buffer-overflow를 일으킬 수 있다.