본문 바로가기

공부/보안

[dreamhack] System Exploitation 3강

반응형

3강 Memory Corruption - C (2)

3.1 포맷 스트링 버그(Format String Bug)

초기화되지 않은 메모리 : printfsprintf와 같이 포맷 스트링을 사용하는 함수에서 발생하는 취약점으로, %x%s와 같이 프로그래머가 지정한 문자열이 아닌 사용자의 입력이 포맷 스트링으로 전달될 때 발생하는 취약점

1
2
3
4
5
6
7
8
// fsb-1.c
#include <stdio.h>
int main(void) {
    char buf[100= {0, };
    
    read(0, buf, 100);
    printf(buf);
}
cs

위 코드는 char형 배열 buf에 100 바이트를 입력받고 printf 함수를 통해 입력받은 버퍼를 출력하는 예제이다. 만일 "%x"와 같은 포맷 스트링을 문자열로 입력한다면 printf(buf)는 printf("%x")가 된다. printf("%x")에 인자가 전달되지 않기에 쓰레기 값을 인자로 취급해 출력한다.

  • printf
  • sprintf / snprintf
  • fprintf
  • vprintf / vfprintf
  • vsprintf / vsnprintf

위와 같이 포맷 스트링을 인자로 받는 함수들을 사용할 때 주의해야 한다.

 

3.2 Double Free & Use-After-Free

C언어는 프로그래머가 수동적으로 동적 메모리를 관리하는 언어로 메모리를 정확히 관리하지 못해 해제된 메모리에 접근하거나, 메모리를 할당하고 해제하지 않아 메모리 누출이 발생할 수 있다.

 

- Double Free : 해제된 메모리를 정확히 관리하지 않아 발생하는 문제로 이미 해제된 메모리를 다시 한번 해제하는 취약점

 

Double Free 예제 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <malloc.h>
int main(void) {
    char* a = (char *)malloc(100);
    char *= (char *)malloc(100);
    memset(a, 0100);
    strcpy(a, "Hello World!");
    
    memset(b, 0100);
    strcpy(b, "Hello Pwnable!");
    
    printf("%s\n", a);
    printf("%s\n", b);
    
    free(a); // --(1)
    free(b);
    
    free(a); // --(2)
}
cs

위 코드는 메모리를 할당하고 할당된 메모리에 "Hello World!" 문자열을 복사한 뒤 이를 출력하는 코드이다. 그러나 (2)에서 해제하는 메모리 a는 이미 (1)에서 해제된 메모리 포인터이다.

 

이를 Ubuntu 16.04 환경에서 실행해봐도 정상적으로 종료된다는 것을 확인했다. 따라서 해제된 메모리 a를 다시 해제하는 것이 불가능하지 않다.

 

char* a = (char *)malloc(100); 에서 메모리를 할당했을 때 a가 저장하고 있는 값은 특정 힙 메모리 주소이다. 그러므로 free(a) 했을 때 시스템에 해당하는 힙 메모리 할당자의 구현에 따라 메모리가 해제된다.

 

같은 포인터를 두 번 해제하는 것과 같은 일이 발생하면 공격자가 프로그램을 예상치 못한 실행 흐름으로 만든다.

 

- After-Free(UAF) : 해제된 메모리에 접근해 값을 쓸 수 있는 취약점

 

After-Free 예제 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// uaf1.c
#include <stdio.h>
#include <string.h>
#include <malloc.h>
int main(void) {
    char *= (char *)malloc(100);
    memset(a, 0100);
    
    strcpy(a, "Hello World!");
    printf("%s\n", a);
    free(a);
    
    char *= (char *)malloc(100); 
    strcpy(b, "Hello Pwnable!");
    printf("%s\n", b);
    
    strcpy(a, "Hello World!");
    printf("%s\n", b);
}
cs

위 코드는 100 바이트 크기의 메모리 a를 할당한 후 "Hello World!" 문자열을 복사한다. 처음 힙 메모리 상태는 아래 그림과 같다.

그 다음 free(a), char *b = (char *)malloc(100); 하고 새로 할당된 메모리는 "Hello Pwnable!" 문자열을 복사한다. 그럼 현재 힙 메모리 상태는 아래와 같다.

[!] 여기서 포인터 a에 저장된 메모리 주소 값은 바뀌지 않았고, 메모리 a와 메모리 b가 같은 주소를 가리키고 있다.

→ 이미 해제되었던 메모리 a가 메모리 할당자로 들어가고, 새로운 메모리 영역을 할당할 때 메모리를 효율적으로 관리하기 위해 기존에 해제된 메모리가 그대로 반환되어 일어나는 일.

 

그래서 이미 해제된 메모리 a에 접근하면 메모리 b가 같이 영향을 받아 프로그래머가 의도하지 않은 일이 발생한다. strcpy(a, "Hello World!"); 에서 문자열을 복사하는 포인터는 해제된 메모리 포인터인 a이다. 그럼 현재의 힙 메모리 상태는 아래와 같다.

 

이미 해제된 포인터 a새로이 할당한 포인터 b같은 메모리 영역을 가리키고 있어 strcpy(a, "Hello World!"); 해주고 포인터 b의 내용을 출력하면 "Hello World!" 문자열이 출력된다.

 

메모리 할당자는 환경에 따라 다르지만, 일반적으로 효율성을 위해 이미 해제된 메모리를 재사용한다.

 

이때 해제된 메모리 포인터에 데이터를 쓴다면, 이미 다른 곳에서 사용되고 있는 메모리에 데이터가 작성된다.

 

이와 같이 이미 해제된 메모리를 다시 사용해 의도치 않은 동작을 발생시키는 취약점을 Use-After-Free(UAF) 취약점이라한다.

 

 

3.3 초기화되지 않은 메모리

변수를 선언하거나 인스턴스를 생성할 때는, 프로그래머가 의도한 경우를 제외하고는 반드시 초기화해야 한다.

 

메모리를 초기화하지 않는다면 쓰레기 값이 들어가게 되고, 이는 프로그램의 흐름을 바꿀 수 있다.

 

공격자가 메모리를 조작해 초기화되지 않은 영역에 공격자의 입력이 들어간다고 생각해 보자. 만약 초기화되지 않은 메모리를 초기화되었다고 가정하는 코드가 있다면 이는 보안 취약점으로 이어질 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct person {
    char *name;
    int age;
} Person;
int main(void) {
    Person p;
    unsigned int name_len;
    
    printf("Name length: ");
    scanf("%d"&name_len);
    
    if(name_len < 100)
        p.name = (char *)malloc(name_len);
    read(0, p.name, name_len);
    
    printf("Age: ");
    scanf("%d"&p.age);
    
    printf("Name: %s\n", p.name);
    printf("Age: %d\n", p.age);
}
cs

위 코드의 구조체 Person char *형 변수 name int형 변수 age를 멤버변수로 갖는다.

 

main함수에서는 Person의 인스턴스를 선언한 후 name의 길이를 name_len 변수에 입력받는다. 만약 길이가 100보다 작으면 malloc 함수를 통해 메모리를 할당한 후 name_len만큼 입력받는다.

 

그러나 이 코드에서는 초기화되지 않은 값의 사용으로 인해 두 가지 문제가 발생한다.

 

첫 번째는, name에 할당된 메모리초기화하지 않았다.

read 함수는 입력받을 때 널 바이트와 같은 별도의 구분자를 붙이지 않습니다. 따라서 이후 name 을 출력하는 부분에서 초기화되지 않은 다른 메모리가 출력될 수 있습니다.

 

두 번째는, name_len 변수의 값이 100보다 크거나 같은 경우에 대한 예외 처리가 없다.

이 경우 p.name  malloc 으로 할당된 값이 아니라 쓰레기 값이 됩니다. 만약 이 값을 조작한다면, line 16에서 read함수를 통해 데이터를 입력받을 때 원하는 메모리 주소에 원하는 값을 쓸 수 있게 된다.

 

 

3.4 Integer issues

C언어를 사용할 때 정수의 형변환을 제대로 고려하지 못해 발생하는 취약점이 있다. 이를 방지하기 위해 정수 자료형이 표현할 수 있는 범위를 정확히 알아야 한다.

정수 자료형의 표현 범위

size_t와 long 자료형은 아키텍쳐에 따라 표현할 수 있는 수의 범위가 달라진다.

  long 자료형 size_t 자료형
32비트 int long long
64비트 unsigned int unsigned long


묵시적 형변환 : 연신 시 연산의 피연산자로 오는 데이터들의 자료형이 서로 다를 경우, 다양한 종류의 형 변환이 일어난다.

  • 대입 연산

    대입 연산자의 좌변과 우변의 자료형이 다를 경우 묵시적 형 변환이 일어난다. 즉, 작은 정수 자료형에 큰 정수를 저장하는 경우, 작은 정수의 크기에 맞춰 상위 바이트가 소멸된다.

  • 정수 승격

    charshort 같은 자료형이 연산될 때 일어난다. (컴퓨터는 int 형을 기반으로 연산하기 때문)

  • 피연산자가 불일치할 경우 → 형변환

    int < long < long long < float < double < long double

    순으로 변환되어, 작은 바이트→ 큰 바이트, 정수 → 실수로 형 변환이 일어난다.

    예로, int 와 double을 더하면 int 가 double 형으로 변환된 후 연산이 진행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
int main(void) {
    char *buf;
    int len;
    
    printf("Length: ");
    scanf("%d"&len);
    
    buf = (char *)malloc(len + 1);
    
    if(!buf) {
        printf("Error!");
        return -1;
    }
    
    read(0, buf, len);
}
cs

위 코드는 len 값을 사용자에게 입력받은 후 len + 1 만큼 메모리를 할당받고 그 포인터를 buf에 저장한다. 그리고 read 함수를 통해 buf 에 데이터를 len 만큼 입력받는다.

그렇다면 len의 값으로 -1을 넣었을 때는 어떨까!

malloc(0)은 "널 포인터 또는 free() 에 성공적으로 전달할 수 있는 고유 포인터"

→ 아무것도 할당 할수는 없지만 free() 호출에 전달

len = -1 이므로 buf = (char *)malloc(0) 이 호출되고, 정상적인 힙 메모리가 반환되어 read(0, buf, -1) 이 호출된다. 인자로 전달된 int 형 값은 -1 이고 read 함수의 세번째 인자는 size_t 형으로 묵시적 형 변환이 일어난다. 따라서 read 함수를 호출할때 32비트 아키텍처에서는 read(0, buf, pow(2, 32)-1)이 호출된다. → 힙 오버플로우 발생

 

 

 

 


Review(3강 Memory Corruption - C)

포맷 스트링 버그

  • printf나 sprintf 함수와 같이 포맷 스트링을 사용하는 함수들을 안전하게 쓰지 않을 경우 발생하는 취약점(사용자의 입력이 포맷 스트링으로 전달될 수 있을 때 발생)

Double Free

  • 동적으로 할당된 하나의 힙 메모리를 두번 해제할 때 발생(free 함수)

Use After Free

  • 동적 할당 시 힙 메모리를 효율적으로 관리하기 위해 기존에 해제된 메모리가 반환되어 발생하는 취약점(이미 해제된 메모리다시 사용할 수 있을 때 발생)

초기화되지 않은 메모리

  • 변수를 선언하거나 인스턴스 생성할 때 초기화를 하지 않은 경우 발생하는 취약점(할당된 변수가 기존에 있던 쓰레기 값을 가지며 발생)

Integer issues

  • 정수의 형 변환을 제대로 처리하지 못해서 발생하는 문제(각 자료형에 대한 범위 고려 X)

 

반응형