[Pwnable] Stack Overflow

haruster·2022년 11월 27일
0

> 스택 오버플로우와 스택 버퍼 오버플로우의 차이점

  • 스택 영역은 실행 중에 크기가 동적으로 확장될 수 있지만, 한정된 크기의 메모리 안에서 스택이 무한히 확장할 수는 없습니다.
    (즉, 스택 오버플로우 (Stack Overflow)는 스택 영역이 너무 많이 확장돼서 발생하는 취약점을 말합니다.)

  • 반면, 스택 버퍼 오버플로우는 스택에 위치한 버퍼에 버퍼의 크기보다 많은 데이터가 입력되어 발생하는 취약점을 말합니다.

> Stack Buffer Overflow

* 버퍼 오버플로우 (Buffer Overflow)

  • 스택 버퍼 오버플로우는 스택의 버퍼에서 발생하는 오버플로우를 말합니다.

> 버퍼 (Buffer)

  • 버퍼(Buffer)는 컴퓨터 과학에서 '데이터가 목적지로 이동되기 전에 보관되는 임시 저장소'의 의미로 사용됩니다.

  • 즉, 데이터의 처리 속도가 다른 두 장치가 있을 때, 두 장치 사이에 오가는 데이터를 임시로 저장해 두는 것은 일종의 완충작용을 하는데, 해당 완충작용을 하는 것을 버퍼라고 할 수 있다.

ex) 에를 들자면, 키보드에서 데이터가 입력되는 속도보다 데이터를 처리하는 속도가 느린 프로그램이 있다고 가정했을 때, 해당 프로그램과 키보드 사이에 별도의 장치가 없다면, 키보드의 입력 중에 수용하지 못한 데이터는 유실될 가능성이 매우 높습니다. 예를 들어 "abcdefg"를 입력했는데 "abcd"라는 데이터만 전달될 가능성이 있습니다.

  • 이러한 문제를 해결하고자 수신 측과 송신 측 사이에 버퍼라는 임시 저장소를 둗고, 이를 통해 간접적으로 데이터를 전달하게 됩니다.

  • 송신 측은 버퍼로 데이터를 전송하고, 수신 측은 버퍼에서 데이터를 꺼내 사용합니다.

  • 이렇게 한다면, 버퍼가 가득찰 때까지는 유실되는 데이터 없이 통신을 할 수 있으며, 빠른 속도로 이동하던 데이터가 안정적으로 목적지에 도달할 수 있도록 완충 작용을 하는 것이 바로 버퍼(Buffer)의 역할이라고 할 수 있습니다.

> 현대에서는 스택에 있는 지역 변수는 스택 버퍼, 힙에 할당된 메모리 영역은 힙 버퍼라고 부른다.

> 버퍼 오버플로우 (Buffer Overflow)

  • 버퍼 오버플로우(Buffer Overflow)는 문자 그대로 버퍼가 넘치는 것을 의미합니다.

  • 버퍼는 제각각의 크기를 가지고 있는데, int로 선언한 지역 변수는 4바이트의 크기를 가지고 있고, 10개의 원소 배열을 갖는 char 배열은 10바이트의 크기를 가지고 있습니다.

  • 여기서 10바이트 크기의 버퍼에 20바이트 크기의 데이터가 들어가려고 하면 오버플로우가 발생합니다.

  • 일반적으로 버퍼는 메모리상에 연속해서 할당되어 있으므로 어떤 버퍼에서 오버플로우가 발생하면, 뒤에 잇는 버퍼들의 값이 조작될 위험이 있습니다.

  • 버퍼 오버플로우는 일반적으로 어떤 메모리 영역에서 발생하여도 큰 보안 위협으로 이어지며, 발생할 수 있는 위협들로는 아래와 같습니다.

* 중요 데이터 변조

  • 버퍼 오버플로우가 발생하는 버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조됨으로써 문제가 발생할 수 있습니다.

ex) 예를 들어, 입력 데이터에서 악성 데이터를 감지하여 경고해주는 프로그램이 있을 때, 악성의 조건이 변경되면, 악성 데이터가 있음에도 경고가 울리지 않을 수 있으며, 또한, https://naver.com 와 통신하는 프로그램이 있으면, https://github.com/haruster 로 조작하여 잘못된 정보와 데이터를 주고 받게 할 수도 있습니다.

> 스택 버퍼 오버플로우 예제

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int check_auth(char *password) {

	int auth = 0;
    
    char temp[16];
    
    strncpy(temp, password, strlen(password));
    
    if(!strcmp(temp, "SETRET_PASSWORD)) {
    
    	auth = 1;
     
    }
    
    return auth;
    
}

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

	if (argc != 2) {
    
    	printf("Usage : ./sbof_auth ADMIN_PASSWORD\n");
        
        exit(-1);
        
    }
    
    if (check_auth(argv[1])) {
    
    	printf("Hello Admin\n");
        
    }
    
    else {
    
    	printf("Access Denied!\n");
        
    }
    
}
  • 위의 소스코드를 보자면, main 함수는 argv[1]을 check_auth 함수의 인자로 전달한 후, 반환 값을 받아오며, 이때, 반환 값이 0이 아니라면, "Hello Admin!"을, 0이라면 "Access Denied"라는 문자열을 출력합니다.

  • check_auth() 함수에서는 16바이트 크기의 temp 버퍼에 입력받은 패스워드를 복사한 후 이를 "SECRET_PASSWORD" 문자열과 비교합니다. 이때, 문자열이 같다면 auth를 1로 설정하고 반환합니다.

  • 그런데, check_auth에서 strncpy 함수를 통해 temp 버퍼를 복사할 때, temp의 크기인 16바이트가 아닌 인자로 전달된 password의 크기만큼 복사합니다.

  • 그러므로 argv[1]에 16바이트가 넘는 문자열을 전달하면, 이들이 모두 복사되어 스택 버퍼 오버플로우가 발생하게 됩니다.

  • auth는 temp의 버퍼 뒤에 존재하므로, temp 버퍼에 오버플로우를 발생시키면 auth의 값을 0이 아닌 임의의 값으로 바꿀 수 있으며, 해당 경우에 실제 인증 여부와는 상관없이 main() 함수의 if (check_auth(argv[1]))는 항상 참이 됩니다.

> 스택 오버 플로우 실습

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int check_auth(char *password) {

	int auth = 0;
    
    char temp[16];
    
    strncpy(temp, password, strlen(password));
    
    if (!strcmp(temp, "SECRET_PASSWORD)) {
    
    	auth = 1;
        
    }
    
    return auth;
    
}

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

	if (argc != 2) {
    
    	printf("Usage : ./sbof_auth ADMIN_PASSWORD\n");
        
        exit(-1);
        
    }
    
    if (check_auth(argv[1])) {
    
    	printf("Hello Admin. \n");
        
    }
    
    else {
    
    	printf("Access Denied. \n");
        
    }
    
}
  • 해당 실습 문제를 풀어보자면,

  • 해당 버퍼의 공간을 초과해서 데이터를 주입시켜주면, 다른 버퍼의 데이터를 건들이게 되기 때문에 이러한 문제를 이용하면 됩니다.

* 데이터 유출

  • C언어에서 정상적인 문자열은 널바이트로 종결되며, 표준 문자열 출력 함수들은 널바이트를 문자열의 끝으로 인식합니다.

  • 만약 어떤 버퍼에 오버플로우를 발생시켜서 다른 버퍼와의 사이에 있는 널바이트를 모두 제거하면, 해당 버퍼를 출력시켜서 다른 버퍼의 데이터를 읽을 수 있습니다. 또한, 획득한 데이터는 각종 보호기법을 우회하는데 사용될 수 있으며, 해당 데이터 자체가 중요한 정보가 될 수도 있습니다.

> 스택 버퍼 오버플로우와 메모리 릭 예제

#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void) {

	char secret[16] = "secret message";
    char barrier[4]= {};
    char name[8] = {};
    
    memset(barrier, 0, 4);
    
    printf("Your name : ");
    
    read(0, name, 12);
    
    printf("Your name is %s.", name);

}
  • 위의 소스코드에서 8바이트 크기의 name 버퍼에 12바이트의 입력을 받으며, 읽고자 하는 데이터인 secret 버퍼와의 사이에 barrier라는 4바이트의 널 배열이 존재하는데, 오버플로우를 이용하여 모든 널 바이트를 다른 값으로 바꾸면, secret 배열의 문자열을 읽을 수 있습니다.

  • 여기서 널 바이트를 모두 채워주면, secret 배열의 문자열도 같이 출력되는 것을 확인할 수 있습니다.

* 실행 흐름 조작

  • 함수를 호출할 때, 반환 주소를 스택에 쌓으며, 함수에서 반환될 때 이를 꺼내어 원래의 실행 흐름으로 돌아갑니다.

  • 이를 공격자의 관점에서 바라보자면, 스택 버퍼 오버플로우로 반환 주소(Return Address)를 조작하는 것에 대해서 궁금증을 가질 수 있고, 실제로, 함수의 반환 주소를 조작하면 프로세스의 실행 흐름을 바꿀 수 있습니다.

> 스택 버퍼 오버플로우를 통한 반환 주소 덮어쓰기 예제

#include <stdio.h>
#include <stdlib.h>

int main(void) {

	char buf[8];
    
    printf("Overwrite return address with 0x4141414141414141 : ");
    
    gets(buf);
    
    return 0;
    
}
  • 따라서 반환 주소를 0x4141414141414141로 바꿔주면, 0x41 = 'A'이기 때문에 A로 채워주면,

  • 아래와 같이 Success가 출력되는 것을 볼 수 있습니다.

profile
다양한 스택을 공부하는 정보보안 전문가 지망생입니다. (Pwnable, Reversing, Webhacking, ...)

0개의 댓글