본문 바로가기

리버싱/x64dbg 디버거를 활용한 리버싱과 시스템 해킹의 원리

ch09 - 버퍼 오버플로우 공격

728x90

01 시스템 메모리

02 스택 오버플로우 공격

03 데이터 공간 오버플로우 공격

04 UAF 공격

 

01 - 시스템 메모리

 1.1 시스템 메모리 영역

 - 프로세스 메모리는 코드, 데이터, 스택, 힙 등으로 나누어진다. 각 영역은 세그먼트로 나누어 있으며 일정한 크기를 가진다.

  • PE 헤더 정보(PE Header) : 메모리 맵에서 첫 세그먼트는 PE 헤더 정보가 들어가는 부분으로 실행 파일의 헤더와 같은 내용이다. 세그먼트 크기가 0x1000이기 때문에 PE 헤더가 들어간 곳의 주소는 0x400000~0x400FFF이다. 이 세그먼트는 읽기 권한만 부여되어 있다.
  • 코드 세그먼트(Code Segment) : ".text"라고 표시된 세그먼트는 텍스트 세그먼트 또는 코드 세그먼트라고 불린다. 그래서 실행과 읽기 권한만 부여되어 있다.[디버깅과 코드의 문제점을 찾기 위해 보는 부분]
  • 데이터 세그먼트(Data Segment) : ".data"라고 표시된 세그먼트는 초기화된 데이터가 저장되는 공간이다. 일반적으로 전역 변수나 정적 변수로 선언되었고 초기값이 설정된 것이다. 읽기와 쓰기 권한이 부여
  • BBS 세그먼트(Block Started by Symbol) : ".bss"라고 표시된 세그먼트는 초기화되지 않은 변수를 저장하는 공간이다. 데이터 세그먼트와 같이 읽기와 쓰기 권한이 주어진다. 

이 외에 ".idata"라고 표시된 세그먼트는 IAT(Import Address Table) 정보가 저장된다. 프로그램에서 사용하는 라이브러리 함수의 이름, 시작 주소 등이 저장된다.

".CRT" 라고 표시된 세그먼트는 CRT(Cross Reference Table) 정보가 저장된다.

".tls"는 TLS(Thread Local Storage) 정보가 저장된다.

 

.data와 .bss 차이

int g_val_1; //초기화되지 않아 BBS 세그먼트에 저장

char g_val_2 = "String";초기화되서 data 세그먼트에 저장

 

스택 세그먼트(Stack Segment)

프로세스가 여러 스레드로 동작될 때, 코드와 데이터 세그먼트는 공유하지만, 스택과 레지스터는 공유하지 않음

 

힙 세그먼트(Heap Segment)

동적으로 메모리를 할당할 때 사용되며 여러 스레드에서 공유한다.

 

 1.2 함수 호출 시의 스택 구조

 - 스택 영역에서 사용되는 중요 레지스터 EIP, EBP, ESP 가 있다.

메모리 스택은 함수를 사용할 때 매개변수, 지역변수, 함수 종료 시 되돌아갈 리턴 주소 등을 저장한다. 이렇게 호출된 함수의 정보 틀을 스택 프레임(stack frame)이라 한다. 그리고 이 정보의 주소를 SFP(Stack Frame Pointer)에 저장한다.

SFP(=saved frame pointer) : 호출되기 이전 함수의 EBP를 저장함으로써 나중에 EBP를 원래 상태로 되돌리는데 사용한다.

 

EBP(Extended Based Pointer)는 현재 스택에 가장 바닥을 가리키는 포인터로 새로운 함수가 호출될 때마다 EBP 레지스터 값이 지금까지 사용했던 스택 꼭대기의 위에 위치하게 되고 새로운 Stack이 시작된다.

EBP는 함수 내의 정보를 접근하는 기준이 되는데 지역 변수는 EBP에서 특정 값을 빼서 접근할 수 있고 매개변수(함수 인자)는 EBP에 특정 값을 더해서 접근할 수 있다.

함수의 매개변수는 호출할 때 LIFO 구조에 따라 역순으로 스택에 추가된다. 함수가 호출되면 EIP는 코드 영역의 그 함수 시작 주소로 설정되고 함수가 종료되면  그 이전으로 갈 수 있도록 리턴 주소가 저장된다.

 

 - 함수 호출시의 스택 구조를 보는 프로그램

(foo()함수의 매개변수와 지역변수의 저장 위치 확인)

할당받은 메모리는 20인데 입력받는 메모리는 48 이기때문에 오버플로우가 발생

다음은 foo() 함수 스택 구조로 foo() 함수 호출을 위한 값 3,4가 전달되고, foo()함수에서는 매개변수 x,y가 받게된다.

그리고 함수 종료 후에 돌아갈 리턴 주소와 main()함수에서 사용하던 SFP인 EBP정보가 보관된다. 그리고 새로운 EBP와 ESP가 설정.

 

EBP의 스택 주소

ESP의 스택 주소(fgets의 값이 들어가 있음)

해당되는 주소의 값

snowman으로 poo함수를 봤을 때

main함수의 영역

 

 

 1.3 버퍼 오버플로우

 - C프로그램에서는 내부 변수의 메모리 경계를 검사하는 기능을 제공하지 않는다.(visual studio는 함)

이유는 검사 시에 운영체제에 부담을 줘서 속도 저하가 일어나기 때문에 시큐어 코딩으로 프로그래머가 직접 체크해줘야 한다.

 

버퍼 오버플로우 : 메모리 공간에 값을 넘치게해서 특정 값의 변경을 시도하는 공격이다.

데이터를 쓸 수 있는 어느 영역에서도 발생할 수 있으며 이를 막기 위한 시큐어코딩이 있다.

strcpy() -> strncpy() / strcat() -> strncat() 등..

 

 1.4 포맷 문자열

포맷 문자열(format string) : C언어의 printf() 함수에서 사용되는 출력 형식 문자열을 말한다.

가변인자 함수(variadic functions)는 매개 변수의 수가 가변적이다.

printf() -> int printf(const char *format, ...);

ex) 포맷 문자열의 수와 매개변수의 개수가 일치하지 않는 경우

 

결과

-> %x를 8번 입력했을 때, a에서는 문자열 상수에 포맷 문자열이 두개(%x %d)가 있으나 매개변수가 빠져있다.

b에서는 입력 문자열이 printf() 함수의 문자열 상수로 사용되었다.

위의 new message2 옆의 값들은 변수의 값들이다.

포맷 문자열에 해당하는 매개변수가 입력되지 않지만, printf()함수에서는 문자열 상수 다음에 저장된 데이터를 매개변수로 인식해서 그 값을 출력한다.

즉 printf() 함수도 스택 구조를 이용하는데 두 번째 매개 변수가 없어 스택에서 두번째 매개변수의 주소를 EBP를 기준으로 계산해서 불러오게 되는 것이다.

-> 포맷 스트링의 %x를 사용하면 메모리의 스택 정보를 볼 수 있다.

 

02 - 스택 오버플로우 공격

 2.1 스택 오버플로우

char *gets (char *str);

char * fgets(char* str, int size, FILE* fp);

-> 표준 함수인 gets()와 fgets의 프로토 타입이다.

 

fgets() : 주어진 파일(fp)에서 주어진 크기만큼 읽어서 주어진 변수로 저장.

gets() : 크기에 제한을 두지 않고 표준 입력으로부터 문자열을 입력받는다.

->입력 문자열의 크기에 제한이 없어서 오버플로우 문제가 있다.

 

- 시큐어코딩

strcpy(buffer, str); -> strncpy(buffer, str, sizeof(buffer)); 

strcat(buffer, str);  -> strncat(buffer, str, sizeof(buffer));

gets(buffer);        -> fgets(buffer, sizeof(buffer), stdin);

 

 - 스택 오버플로우 실습

저장된 패스워드를 입력하면 allow를 출력하고 틀리면 denied를 출력하는 프로그램이다.

gets()함수가 사용되어 메모리를 20바이트로 정해져 있지만 그보다 더 큰 값을 입력할 수 있다.

 

-다음과 같이 스택 오버플로우 되면서 allowed가 나오게 된다.

 

check_password() 함수의 반환값이 0이 아니면 접근 허용이 되고 아니면 거부가 되는데 이 함수의 반환값을 결정하는 것은 지역 변수 flag의 값이다. 맞으면 1, 아니면 0

하지만 오버플로우 되면 flag가 저장된 공간까지 덮어 씌게 되어 넘친 값 1이 값으로 저장된다.

 - x32dbg로 보면 20개의 메모리에 20개의 값을 넣어서 어디서 오버플로우 됐는지 확인

 

-> 저장된 값들을 보면 차례대로 저장되어 있는데 enter키가 저장되지 않았다.(dev c++기준)

 

 - 스택 버퍼 오버플로우 보안 대책

함수 버퍼 제한(시큐어 코딩)

운영체제 패치

스택에서 프로그램 실행 금지

 

 2.2 스택 오버플로우 공격 실습

 

- passwd에 1234를 넣었을 때 스택 EBP를 따라가서 1234와 passwd를 비교하는 부분

 

이 부분을 건너뛰기 위해서 스택 값을 수정을 통해서 jump다음 값으로 옮기면 패스워드 비교를 넘어갈 수 있다.

 

 

03 - 데이터 공간 오버플로우 공격

 3.1 힙 오버플로우

힙 : 사용자가 데이터 공간을 동적으로 할당하고 해제하며 자유롭게 사용할 수 있는 공간이다.[C에서 malloc함수]

이런 데이터 공간에서 연속적인 메모리 할당으로 생성된 공간에서 앞선 주소의 데이터를 넘침시켰을 때 힙 오버플로우가 발생할 수 있다.

 

 - 힙 오버플로우 취약 코드

typedef struct fun_list{

  char fun_name[20];

  void (*fun_ptr)(void*);

}_fun_struct_t;

 

...

 

int main(){

...

printf("Enter function name: ");

fgets(userinput, 100, stdin); // fgets가 취약한 부분으로 변수 userinput은 20바이트 메모리를 할당했지만, 그보다 더 많은 100바이트 문자를 저장하면서 오버플로우가 발생 가능

printf("Type any key to clear screen\n");

getch();

heap_test->fun_ptr(user);

...

}

 

 - 힙 오버플로우 공격

  • 1) 메모리 할당 부분 확인하기

힙공간을 사용하면 메모리 할당을 수행하므로 malloc()함수를 확인

 

fgets에 중단점을 걸어서 주소를 확인해보니 cls 함수의 주소가 시작의 주소와 같았다.

-main()함수 부부에서 메모리 20바이트 확인

 

즉, cls 함수는 다음과 같이 call 함수에 의해 system을 거치고 clear screen을 출력하는 것을 알 수있다.

cls함수가 포함된 부분

 

 

 - 해당 코드는 fgets에 hi_function을 넣어도 cls로 가도록 되어 있다.

 ->  버퍼 오버플로우로 주소를 다른 곳으로 옮겨보자

 

- malloc()함수 주변의 주소를 따라가면 

메모리 덤프해서 따라가면 값이 보이고 초기화를 하지 않아서 쓰레기값이 들어간 것을 확인

 

 - f8을 통해서 계속 내려가면 주소가 하나씩 커멘드 창에 나온다.

그리고 cls함수와 hi함수 부분도 나온다. 주소를 덤프에서 값을 확인

그리고 call 함수에 의해 00401520으로 가는데 무조건 cls 함수로 간다.

 -> 버퍼 오버플로우를 통해서 결과를 바꿔보자

 

 - 메모리 할당을 생각해서 48바이트 값이 어떻게 들어가는지 확인한다.

주소를 덤프해서 값을 확인

코드의 메모리 값을 확인하면 userinput에 값 48바이트 / 20바이트를 추가하면 그 다음 값에 cls 함수의 주소가 있다.

이 부분을 수정하면 주소를 다른 곳으로 바꿀 수 있다.

(메모리를 계산하면 48바이트가 되지만 프로그램에 따라서 코드를 수행했을 때 가지는 메모리가 다르다.

디버거 모드로 했을 때 32바이트를 차지함)

 

 

  • 2) 취약한 부분 찾아가기

힙 오버플로우가 발생할 수 있는 fgest를 찾아가 메모리를 확인하면 0x30만큼 차이가 나므로 이를 넘어버리면 userinput 변수에서 heap_test 변수의 공간까지 침범할 수 있다.

 

  • 3) 힙 오버플로우 공격 시도

두 변수의 주소 차이가 0x30이고 코드를 보면 두 번째 힙 공간 변수 heap_test는 구조체이고 함수 포인터로 되어 있다.

그리고 cls_function() 함수의 주소가 저장되어 있다. 이 주소를 hi_function()함수로 변경할 수 있다.

 

 3.2 데이터 공간의 오버플로우 공격

 - 메모리 영역은 코드, 데이터, IAT, TLS, 스택, 힙 등의 세그먼트로 구성

  • .data : 초기화된 전역 변수(403000~403FFF)
  • .rdata : 이미지 등 개체 데이터(404000~404FFF)
  • .bss : 초기화되지 않은 전역 변수(405000~405FFF)

 

 - BBS 오버플로우 취약 코드

static char buf[10];

int accout = 100;

...

int main(){

char ch;

sranc(time(NULL));

...

while(1){

printf("가위 바위 보 중 하나선택");

fgets(buf, 50, stdin); // 취약한 부분 발견

func_ptr(atoi(buf));

...

  }

...

}

 

  • 1) 실행 과정을 파악하기

-> 가위바위보를 연속으로 5번해서 이기면 상금 지면 투자금이 깎이는 프로그램이다.

 

버퍼 오버플로우를 통해서 다음과 같은 이기는 부분으로 이동하게 할 수 있다.

 

 

  • 2) 취약한 부분 찾아가기

버퍼 오버플로우에서 공격의 취약한 부분을 찾을 때는 값을 입력하는 명령어를 찾으면 쉽게 찾을 수 있다.

 

검색에 gets를 입력하면 다음과 같이 관련 검색이 나온다.

 

 

- 주소004015D3은 가위 바위 보를 선택하는 구간으로 fgets함수가 있어 취약한 부분이다.

 

 - exe에 적당한 값을 넣고 메모리 덤프로 값들이 어떻게 들어가는지 확인[20바이트]

주소를 보면 00405024부터 값이 들어갔고 EBX에 401759라는 값이 들어갔는데 덤프에서 확인해보면 포인터의 주소가 들어있다. 이 부분에 주소를 걸면 다른 곳으로 흐름을 움직일 수 있다.

 

 - 401759의 주소를 가보면 rand함수인 것을 확인할 수 있다.

 

 

이 코드에서 승리를 위한 주소는 몇번 연속해서 이기는 다음 부분이다.[00401829]

 

 - [alt+ins]로 주소를 복사하고 바꾸려던 주소로 돌아와 주소를 변경해준다.

표현식에 주소 붙여넣으면 해당 주소로 이동하는 것을 확인 

 

  • 3) BSS 오버플로우 공격 지점 찾기

 - 위에서 1부터 0까지 넣었을 때 2칸을 제외하고 함수가 있었기 때문에 12바이트의 값을 넣어 오버플로우를 일으키면 에러가 발생한다.

덤프해서 따라가면 값이 깨진 것을 확인

 

 

 

 

# 덤프영역에서 다음과 같이 코드를 한글로 바꿔서 볼 수 있다.

 

04 - UAF 공격

 4.1 힙 메모리 영역 관리 방법

 

힙 영역 : 프로그램에서 큰 메모리를 효율적으로 사용하기 위한 영역이다. 이영역은 필요에 따라 할당(malloc)하여 사용하고, 사용이 끝나면 해제(free)한다.

다음은 메모리 할당과 해제과정이다. 메모리 할당이 수행되면 할당된 공간의 시작 주소를 반환하고, 할당할 수 있는 공간이 없으면 널(Null)값을 반환한다.

 

 - 시스템에서 힙 영역의 메모리는 할당하여 사용 중인 곳과 사용 중이 아닌 곳이 있을 것이다.

사용 중이 아닌 공간(unused heap)은 이중 연결 리스트로 관리된다. 중간에 메모리 해제가 발생하면 그 영역을 unused heap에 추가하고 새롭게 메모리 할당 요청이 오면 unused heap에서 첫 번째 매칭(first fit)이 되는 공간을 할당하고 그 주소를 넘겨준다.

 

  • 해제된 메모리에 대한 접근

다음은 할당된 메모리가 해제된 후 새롭게 할당되어 사용되고 있는 경우로 해제했던 vuln에 오버플러우가 나타난다.

mallo() 함수에는 병합 지연(deffered Coalescing)속성이 있는데 이건 해제된 힙 공간이 다시 할당 될 때까지 캐시에 남겨뒀다가 같은 크기로 다시 할당할 때 재사용하는 기능이다.[할당 해제된 영역이 바로 쓰일 수 있는 이유이다.]

 

 4.2 UAF(Use Aftrer Free) 공격

UAF(Use After Free)취약점 : 메모리 할당하여 사용하고 해제한 후에 해당 메모리 공간을 다시 사용할 때 나타나는 문제점.

힙 영역의 메모리를 할당하고 해제하는 과정에는 문제가 없는데 해제한 후에도 그 영역에 접근하여 이전에 할당하였을 때처럼 사용하는 데서 문제가 발생한다.[위 그림]

 

  • 1) 해제된 공간의 재할당 확인하기

char *ptr, *vuln;

vuln = malloc(20);

printf("vuln = 0x%p [%s] \n", vuln, vuln);

strcpy(vuln, "test data");

printf("vuln = 0x%p [%s] \n", vuln, vuln);

free(vuln);

ptr = malloc(20);

printf("ptr = 0x%p [%s] \n", ptr, ptr);

strcpy(ptr, "overflow data");

printf("ptr = 0x%p [%s] \n", ptr, ptr);

printf("vuln = 0x%p [%s] \n", vuln, vuln);

 

실행결과

 -> 해제된 vuln과 새롭게 할당된 ptr이 같은 주소를 가지고 있다. UAF 발생

 

 

  • 2) 해제된 공간의 사용 확인하기

typedef struct samplestruct {

 int number;

} _sample_t;

 

int main(){

 _sample_t *one;

 _sample_t *two;

 

one = malloc(sizeof(_sample_t));

printf("[1]one -> number:%d \n", one-> number);

one->number = 54321;

printf("[2]two -> number:%d \n", one-> number);

printf("[*]one address: 0x%p \n", one);

 

free(one);

two = malloc(sizeof(_sample_t));

printf("[3]one -> number:%d \n", one-> number);

printf("[*]two address: 0x%p\n", two);

printf("[4]one -> number:%d  \n", one -> number);

one->number = 54321;

printf("[5]two -> number:%d  \n", two -> number);

getch();

free(two);

return 0;

}

 

 - 실행결과

 -> 메모리를 해제와 할당하는 코드이고 해제된 구조체 변수 one과 할당된 구조체 변수 two가 같은 공간을 가리킨다.

그리고 해제된 one 변수의 number 값을 바꾸게 되면  two 변수에도 영향을 끼친다. UAF 발생

 

  • 3) UAF 취약점의 활용 및 공격 시도

 - 다음은 4개의 변수로 메모리 공간을 할당한 후 2개의 공간을 해제. 그리고 해제된 공간에 ptr이란 변수로 다시 할당했다.

이런 경우 해제된 vuln과 할당된 ptr은 같은 공간을 가리키게 된다. 만약 해제된 vuln 변수가 그림처럼 system() 함수의 매개변수로 사용되는 취약점이 존재한다면, ptr 변수에 기록한 내용이 실행될 수 있을 것이다.

 

 - 두 변수(vuln과 ptr)가 동일 공간을 가리키게 된다. vuln -> x / ptr -> x

코드를 보면 fgets() 함수로 새로운 값을 입력 받았을 때, 저장된 clean() 함수의 주소를 바꿀 수 있다. 

 

 - ptr1으로 입력받은 내용에서 함수 포인터 멤버 변수인 clean()의 값을 다음과 같이 바꾸면 UAF가 발생한다.

 

 

 - 코드

typedef struct somestruct{

 int id;

 char name[20];

...

}_vuln_struct_t;

 

...

int main(int argc, char *argc[]){

 char buf[100];

 void *ptr1; int *ptr2, i;

_vuln_struct_t *vuln = malloc(sizeof(_vuln_struct_t)*2);

fgets(buf, 100, stdin);

...

vuln->clean = cleanMemory; // 기존에 저장된 정보

if(vuln->id > 400){

 printf("Your id is too big! \n");

 vuln->clean(vuln);

 free(ptr1);

}

 

- 실행 결과

 

 - name [20]에 오버 플로우를 시켰을 때 결과

19바이트까지 메모리에 들어갔다.

 

  • 시작 주소와 id, name을 받는 위치

 

 - 모듈간 호출로 fgets()를 찾아 취약점 찾기

 

  • USF 성공 시 가는 위치

 

 - name에 값을 넣어서 메모리에 어떻게 들어가는지 확인