들어가며
스택 오버플로우는 보안직이 아닌 일반 개발직무 분들도 많이 들어본 용어일 것이다.
심지어 구글링할 때 항상 참고하게 되는 Stack Overflow 이름도 말그대로 스택 오버플로우이다.
아래는 CVE에 등록된 보안 취약점의 종류를 보여주는 도표이다. 이중에서 주황색 막대기로 체크된 Overflow가 꽤나 높은 수치를 가지는 것을 볼 수 있다.
그렇다면 정확히 스택 오버플로우와 스택 오버플로우는 어떤 공격이고 어떻게 공격할 수 있는지 간단한 실습을 통해 알아보는 시간을 가질 것이다.
스택 오버플로우(Stack Overflow) vs 스택 버퍼 오버플로우(Stack Buffer Overflow)
스택은 함수를 실행하면서 동적으로 크기가 줄거나 늘어날 수 있다. 이때 스택 오버플로우는 스택의 크기가 너무 확장되어서 힙 영역까지 확장되는 등의 문제를 이야기한다.
한편 스택 버퍼 오버플로우는 스택 안의 버퍼의 크기가 너무 확장되어 버퍼 외 다른 부분까지 침범하며 생기는 문제를 이야기한다. 버퍼 오버플로우를 줄여 BOF라고 많이 부르곤 하니 익혀두자. 우리는 오늘 스택 '버퍼' 오버플로우에 집중하여 알아볼 것이다.
버퍼(Buffer)
버퍼는 '데이터가 목적지로 이동되기 전에 보관되는 임시 저장소'라고 생각하면 된다.
일종의 '완충장치' 혹은 '연결다리'라고 생각하면 편하다.
데이터 처리 속도가 다른 두 장치가 있을 때 그 속도차로 인한 데이터들을 버퍼는 저장해준다.
한 번에 10글자 처리가 가능한 키보드와 한 번에 2글자만 처리가 가능한 컴퓨터가 있다고 생각해보자.
버퍼가 없으면 컴퓨터는 0,1만 처리하고 2부터 9까지는 처리할 수 없어 데이터의 유실이 일어난다.
이처럼 중간에 처리되지 못하는 데이터들을 '버퍼'라는 저장소에 저장하면 이 문제는 해결될 수 있을 것이다.
컴퓨터가 아무리 2글자밖에 처리를 못해도 나머지 8글자는 버퍼, 즉 임시 저장소에 저장되어 있기 때문에 데이터는 유실되지 않는다.
한편 송신 측의 데이터 처리 속도가 느리고 수신 쪽이 빨라 수신 측에서 계속 로딩이 뜨는 현상을 '버퍼링'이라고 한다.
하지만 요즘은 완충 장치로서의 버퍼의 의미보다 단순 '저장장치'로의 버퍼라는 용어를 많이 쓰곤 한다. 예를 들어 스택에 있는 지역 변수는 '스택 버퍼', 힙에 저장된 메모리 영역은 '힙 버퍼'라고 부른다.
버퍼 오버플로우 (Buffer Overflow, BOF)
BOF는 할당된 버퍼의 크기보다 더많은 크기의 데이터가 할당되어 버퍼 외의 영역까지 데이터가 침범하는 사태를 이야기한다. 예를 들어 int 형 정수를 저장할 때는 4byte의 크기의 버퍼가, char 형의 정수를 저장할 때는 1byte 크기의 공간이 할당된다.
아래 그림에서 버퍼 A는 8칸을 차지하고 있다. 하지만 모종의 이유로 15칸을 입력하면 A가 채워지고 데이터 영역 B까지 채워지게 된다. 이런 경우 만약 B에 중요한 정보가 있었다면 그 정보가 덮힐 가능성이 있다. 혹은 B에 있는 데이터를 내가 원하는 값으로 변조할 수도 있게된다. .
ex) 악성데이터 감지프로그램에서 악성의 조건이 변경되면 악성파일이어도 알림 안울림/instagram이랑 소통하는 서버인데 거기를 악의적으로 변조해서 evil.com과 소통하게함 등등… 존재
이러한 이유때문에 BOF가 위험하다는 것이다.
중요 데이터 변조
아래 스택 오버플로우 예제 코드를 보자.
// Name: sbof_auth.c
// Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int check_auth(char *password) { //패스워드가 맞는지 확인하는 함수
int auth = 0;
char temp[16]; //16byte만큼 temp크기 할당
//auth는 temp 뒤에 존재함. 이처럼 보통 메모리 할당이 연속적으로 이루어지기 때문에 문제가 생기는 것이다.
strncpy(temp, password, strlen(password));
//temp에 password를strlen(password) 길이만큼 저장
//strlen(password)>16이면 버퍼 오버플로우 발생하게됨
//따라서 밑에서 argv[1]을 16byte보다 높게 설정하면 버퍼오버플로우 발생
if(!strcmp(temp, "SECRET_PASSWORD"))
auth = 1;
//auth가 temp뒤에 존재하므로 temp의 값을 길게해서 조작하면 auth의 값을 걍 0말고 다른 숫자로 만들어버릴 수 있
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");
}
이처럼 argv[1]에 들어가는 값을 길게 해줘서 temp 버퍼에 오버플로우를 발생시키면 auth의 값을 0이 아닌 다른 값으로 조작할 수있다. 따라서 실제 인증 여부와는 달리 main함수의 if(check_auth(argv[1]))은 항상 참이 된다.
주석에도 작성해놓았지만, password의 길이를 16보다 길게 설정하면 auth에 나머지 값이 들어가므로 auth의 값을 1로 설정할 수 있게된다.
password 자리에 aaaaaaaaaaaaaaabbbbnaaa를 적어주었다. 16개의 a는 temp에 저장되고 bbbn은 auth에 나머지 aaa는 아래에 저장된다.
메모리에는 인코딩된 형태로 저장된다.
스택 모습을 수직으로 그려보면 위와 같다.
데이터 유출
중요한 데이터들은 보통 문자열로 저장된다. C에서는 문자열의 끝을 null 문자(\0)로 인식한다.
우리는 BOF를 이용해 문자열의 끝을 조작하여 원하는 곳의 정보를 가져올 수 있다.
name을 입력하고 이를 출력하는 프로그램이 있다고 하자. 그리고 우리가 아래 그림에서 secret이라고 되어있는 부분의 메시지(secret message)를 우리가 알고 싶다고 가정하자.
name 버퍼의 크기는 8byte이지만 name의 길이를 8byte보다 길게 입력한다면 secret 부분까지 접근할수 있다. 이전 예제에서는 인가되지 않은 부분(여기서는 secret)에 글을 쓰는 것이 목표였다면, 우리는 secret 부분의 내용을 알아내는 것이 목표이다. 그래서 그부분에 무언가를 쓰면 안된다. 보통 저 초록색부분은 보안을 위한 barrier로 보통 널바이트들이 저장되어있다. 이를 코드로 나타내면 아래와 같다.
// Name: sbof_leak.c
// Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(void) {
char secret[16] = "secret message"; //스택 맨 아래서부터 16byte만큼 secret message 부분 할당
char barrier[4] = {}; //그 위에 4바이트만큼 barrier 설정
char name[8] = {}; //그 위에 8바이트만큼 name 설정
memset(barrier, 0, 4); //barrier 시작위치부터 4byte만큼 0으로 세팅
printf("Your name: ");
read(0, name, 12); //12byte만큼 name 받기
printf("Your name is %s.", name); //name 출력
}
printf()를 통해 name을 출력할 때, name 시작주소부터 \0가 오기 전까지 출력한다.
(참고) void* memset(void* ptr, int value, size_t num);
인자 : 메모리 시작 위치, 원하는 값, 사이즈
메모리를 원하는 사이즈만큼 원하는 값으로 초기화하는 함수 (참고 : https://blockdmask.tistory.com/441)
여기서보면 0xa부분까지 출력됐다. 왜냐하면 0xb가 \0이기 때문이다.
그렇다면 우리가 name 부분부터 barrier 끝부분까지 널문자를 아예 다른 값으로 덮어 씌우면 어떨까?
secret message가 잘 출력되었다.
실행 흐름 바꾸기
BOF를 통해 실행 흐름 또한 바꿀 수 있다. 함수 호출 및 반환이 어떻게 되는지 되짚어보면, 함수를 호출할 때 ret_addr를 쌓고, 그 위에 기존 스택 rsp, 그리고 그위부터 새로 다른 버퍼들을 쌓았다.
만약 우리가 BOF를 일으켜서 ret_addr부분을 조작한다면, 함수는 반환할 때 우리가 조작한 위치로 갈 것이다.
// Name: sbof_ret_overwrite.c
// Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char buf[8]; //버퍼 8만큼 할당
printf("Overwrite return address with 0x4141414141414141: "); //ret_addr를 0x4141414141414141로 조작해보세요
gets(buf); //버퍼에 입력받기
return 0;
}
위 코드의 스택을 그림으로 나타내면 아래와 같다.
따라서 buf에 큰 크기를 입력해서 ret_addr를 조작하면된다.
우리는 이 함수가 반환할 때 0x4141414141414141로 가게 하고 싶다.
A를 써주어야지 저장될 때 0x41로 될 것이므로 위와같이 써주었다.
이처럼 하면 실행 흐름을 우리가 원하는 0x41로 바꿀 수 있다. (참고 : 메모리 한칸에 들어갈 수 있는 값의 크기는 1byte이다.)
힙 오버플로우(Heap Overflow)
힙 오버플로우는 위와 비슷하게 그냥 힙에서 일어나는 오버플로우이다.
// heap-1.c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char *input = malloc(40);
char *hello = malloc(40);
memset(input, 0, 40);
memset(hello, 0, 40);
strcpy(hello, "HI!");
read(0, input, 100);
printf("Input: %s\n", input);
printf("hello: %s\n", hello);
}
input과 hello가 바로 연속되어 할당되어있고, read(0,input,100);으로 100만큼 할당하기 때문에 input의 메모리가 hello의 메모리를 침범하는 오류가 발생할 수 있다.
오늘은 스택 오버플로우, 스택 버퍼 오버플로우, 힙 오버플로우에 대해 알아보고 실습까지 진행해보았다.
다음 포스트는 이와 관련한 실습을 풀어볼 예정이다.
Reference
'Linux Exploitation > Fundamentals' 카테고리의 다른 글
[Pwnable] PIE (0) | 2023.05.15 |
---|---|
[gdb] 로컬/서버의 라이브러리 다른 경우 (0) | 2023.04.28 |
[ROP 시리즈 (3)] 드림핵(Dreamhack) - Return to Library 개념 및 실습 (1) | 2023.04.18 |
[ROP 시리즈 (2)] PLT, GOT (0) | 2023.04.17 |
[Shell Code] 셸코드 개념 및 ORW 코드 작성을 통한 실습 (0) | 2023.03.26 |