버퍼 오버플로우
버퍼(Buffer)
버퍼는 '데이터가 목적지로 이동되기 전에 보관되는 임시 저장소'이다.
데이터 처리 속도가 다른 두 장치가 있을 때 그 사이에 버퍼를 두면 완충 작용을 할 수 있다.
송신측은 버퍼로 데이터를 전송하고, 수신측은 버퍼에서 데이터를 꺼내어 쓰면 버퍼가 가득 찰 때까지는 유실되는 데이터 없이 통신할 수 있기 때문이다.
현대에는 이런 완충의 의미가 많이 희석되어 데이터가 저장될 수 있는 모든 단위를 버퍼라고 부른다.
스택에 있는 지역 변수는 '스택 버퍼', 힙에 할당된 메모리 영역은 '힙 버퍼'라고 불린다.
버퍼 오버플로우
말 그대로 버퍼가 넘치는 것을 말한다.
10개의 원소를 가진 char배열은 10바이트의 크기를 가진다. 해당 버퍼에 20바이트의 문자를 저장하게 되면 10바이트 만큼의 데이터가 넘치게 되고 이를 버퍼 오버플로우라고 한다.
버퍼는 메모리에 연속적으로 할당되므로 버퍼의 크기보다 큰 데이터가 저장되면 버퍼 뒤에 있는 데이터들이 조작될 수 있다.
💡 스택 오버플로우와 스택 버퍼 오버플로우의 차이점
스택 영역은 실행중에 크기가 동적으로 확장될 수 있다. 그러나 한정된 크기의 메모리 안에서 스택이 무한히 확장될 수는 없다. 스택 오버플로우(Stack Overflow)는 스택 영역이 너무 많이 확장돼서 발생하는 버그를 뜻한다.
반면, 스택 버퍼 오버플로우는 스택에 위치한 버퍼에 버퍼의 크기보다 많은 데이터가 입력되어 발생하는 버그를 뜻한다.
중요 데이터 변조
버퍼 오버플로우가 발생하는 버퍼 뒤에 중요 데이터가 있다면 해당 데이터가 변조됨으로써 문제가 발생할 수 있다.
아래 예제로 데이터 변조 실습을 해볼 수 있다.
// 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];
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");
}
main에서 argv[1]을 check_auth()에 인자로 전달하고 있다.
check_auth는 16바이트 크기의 temp 버퍼에다가 argv[1]를 옮겨 적는데 argv[1]의 크기만큼 복사하기 때문에 버퍼 오버플로우가 발생할 수 있다.
여기서 temp버퍼뒤가 auth버퍼이므로 버퍼 오버플로우를 이용하면 auth영역의 값을 변조해서 "Hello Admin!"을 출력할 수 있다.
실제로 코드를 돌렸을 때 문자 17개를 입력하면 "Hello Admin!"이 출력되지 않았다.
gdb로 디버깅 해본 결과 auth 버퍼와 temp 버퍼 사이에 다른 공간이 있었는데, 이것이 무엇인지는 자세히 모르겠다.
다만 그 공간의 크기만큼 문자를 더해 입력을 전달하면 위처럼 "Hello Admin!"을 얻을 수 있었다.
데이터 유출
C언어에서 문자열의 끝은 널바이트로 표현하고 있다. 표준 문자열 출력함수들은 널바이트를 문자열의 끝으로 인식하고 있다.
만약 버퍼 오버플로우를 발생시켜서 널바이트를 없애버리면 그 뒤에 있는 다른 버퍼에 있는 내용까지도 읽을 수 있다.
아래 예제를 통해서 데이터 유출 실습을 해볼 수 있다.
// 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";
char barrier[4] = {};
char name[8] = {};
memset(barrier, 0, 4);
printf("Your name: ");
read(0, name, 12);
printf("Your name is %s.", name);
}
name배열에 12바이트 크기의 문자열을 입력받으면 그 뒤에 있는 barrier에 덮어쓰기가 된다.
barrier배열은 4바이트 모두 널인 배열로 read(0, name, 12)에 의해서 널 바이트가 다른 값으로 변경되고, 이때 name을 출력하게 되면 널 바이트가 있는 secret까지 출력하게 된다.
실행 흐름 조작
함수를 호출하게 되면 반환되었을 때 원래 실행 흐름으로 돌아가기 위해 반환 주소(Return address)를 저장한다고 했다.
따라서 버퍼 오버플로우를 이용해 반환 주소를 조작하면 실행 흐름도 조작할 수 있게 된다.
드림핵의 인터렉티브 모듈을 활용해서 아래 예제로 실행 흐름 조작 실습을 해볼 수 있다.
https://learn.dreamhack.io/60#8
// 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];
printf("Overwrite return address with 0x4141414141414141: ");
gets(buf);
return 0;
}
'Study > System Hacking' 카테고리의 다른 글
[dreamhack] Return Address Overwrite (1) | 2022.05.08 |
---|---|
[dreamhack] Exploit Tech: Return Address Overwrite (0) | 2022.05.06 |
[dreamhack] Background: Calling Convention (0) | 2022.05.04 |
[dreamhack] shell_basic (1) | 2022.04.04 |
[dreamhack] Exploit Tech: Shellcode (0) | 2022.04.02 |