드림핵

[드림핵] Return Address Overwrite

다다x_x 2025. 4. 30. 23:57

 

https://dreamhack.io/wargame/challenges/351

 

  1. Stack
    1. Stack Frame
    2. Stack Overflow
  2. Buffer Overflow
  3. Return Address Overwrite Write-Up

 

Stack

후입선출(LIFO, Last-In First-Out)의 자료구조

저장 Push, 삭제(인출) Pop

BP(Base Pointer Register) : 스택 메모리의 시작 주소를 저장

SP(Stack Pointer Resister) : 택 메모리의 끝 주소를 저장

스택 메모리 할당 → SP값 빼기

스택 메모리 정리 → SP값 더하기

Stack Frame

함수가 호출되면 사용하는 스택 영역 (반환 주소, 지역 변수, 매개변수가 저장)

함수의 호출과 함께 할당(기본적으로 1MB), 함수의 호출이 완료되면 정리

  • (high address)
    • 매개변수
    • RET(반환 주소)
    • SFP(이전 BP)
    • 지역 변수
  • (low address)

높은 주소에서 낮은 주소 방향으로 할당

Stack Overflow

스택 영역의 크기보다 많이 확장되어 발생하는 버그

함수의 재귀 호출이 무한히 반복

→  스택의 모든 공간을 다 차지
→  또 다시 스택 프레임을 저장

→ 해당 데이터는 스택 영역을 넘어가서 저장

  • 프로그램 오동작, 보안상의 취약점
  • C언어에서는 실행 중인 프로그램에서 스택 오버플로우가 발생하면, 에러를 발생하고 곧바로 강제 종료

Buffer Overflow

버퍼에 버퍼 크기보다 많은 데이터가 입력되어 발생되는 버그

  • 공격 예시
    • 중요 데이터 변조
    • 데이터 유출 (null바이트 제거)
    • 실행 흐름 조작 (Return Address Overwrite)
  • 메모리 보호기법
    • NX bit : 메모리 구역 지정하여, 데이터 저장을 위해서만 사용 (실행 가능 공간 보호)
    • ASLR :  Heap, Stack, Library의 base 주소를 randomization
    • SSP : canary을 이용해 스택 버퍼 오버플로우가 발생을 검증
    • RELRO : 프로세스의 섹션 보호
    • PIE : Binary base 주소를 randomization

Return Address Overwrite Write-Up

다운로드 받은 파일을 압축 해제하면 rao와 rao.c 존재

실행 전, 보호 기법이 걸려있는지 확인
rao 실행

rao를 실행해보면 데이터를 과하게 입력 시, 버퍼 오버 플로우가 발생

// 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;
}

 

rao.c는 rao의 소스파일

  • main()
    • char buf[0x28] : 버퍼의 크기를 0x28 (16진수. 10진수로 40)로 정의
    • scanf("%s", buf) : scanf로 buf에 값을 입력 
  • init()
    • 표준 입출력 버퍼링을 비활성화
    • setvbuf(FILE *stream, char *buf, int mode, size_t size)는 C의 표준 입출력 버퍼를 설정하는 함수
    • mode = 2 : _IONBF (No buffering, 비버퍼링)
  • get_shell()
    • 쉘을 실행하는 함수
    • execve(const char *pathname, char *const argv[], char *const envp[])는 다른 프로그램을 실행하는 함수
      • pathname : 실행할 프로그램의 경로 ("/bin/sh")
      • argv[] : 프로그램에 전달할 인자 목록 (["/bin/sh", NULL])
      • envp[] : 환경 변수 목록 (NULL)

공격 시나리오

    • buf는 40바이트, scanf("%s", buf)는 입력 길이 미제한 → 버퍼 오버플로우 가능
    • get_shell()은 쉘을 실행 → 리턴 주소를 get_shell()로 Overwrite   쉘 획득

동작 흐름 분석

gdb rao로 확인한 main의 어셈블리어

  • 0x00000000004006e8 <+0>: push   rbp
    • rbp를 스택에 저장 (리턴 주소 저장)
  • 0x00000000004006e9 <+1>: mov    rbp,rsp
    • rsp의 값을 rbp에 대입
    • 현재 스택 위치(rsp)를 기준점(rbp)으로 설정
  • 0x00000000004006ec <+4>: sub    rsp,0x30
    • rsp - 0x30
    • 스택을 48바이트(0x30) 만큼 할당
    • 지역 변수용 버퍼 공간
  • 0x00000000004006f0 <+8>: mov    eax,0x0
    • eax를 0으로 초기화
    • 반환값으로 0을 준비
  • 0x00000000004006f5 <+13>: call   0x400667 <init>
    • init() 호출
  • 0x00000000004006fa <+18>: lea    rdi,[rip+0xbb]        # 0x4007bc
    • [rip+0xbb]의 주소를 rdi에 저장
    • printf()의 첫 번째 인자 준비.
    • rdi에 "Input: " 문자열 주소를 로드
  • 0x0000000000400701 <+25>: mov    eax,0x0
  • 0x0000000000400706 <+30>: call   0x400540 <printf@plt>
    • printf() 호출
  • 0x000000000040070b <+35>: lea    rax,[rbp-0x30]
    • [rbp-0x30] (buf의 시작 주소)를 rax에 저장
  • 0x000000000040070f <+39>: mov    rsi,rax
    • rsi(scanf()의 두 번째 인자(입력 받을 주소))에 buf 주소
  • 0x0000000000400712 <+42>: lea    rdi,[rip+0xab]        # 0x4007c4
    • rdi에 " %s" 형식 문자열의 주소 저장
  • 0x0000000000400719 <+49>: mov    eax,0x0
  • 0x000000000040071e <+54>: call   0x400570 <__isoc99_scanf@plt>
    • scanf() 호출
    • 사용자 입력을 받아 buf에 저장
  • 0x0000000000400723 <+59>: mov    eax,0x0
  • 0x0000000000400728 <+64>: leave 
    • 스택 프레임 정리 (mov rsp, rbp; pop rbp)
  • 0x0000000000400729 <+65>: ret
    • 리턴 주소를 스택에서 꺼내 실행 흐름 복귀
    • return address로 반환

주소 정보

  • rbp - 0x30 : 스택 버퍼를 48바이트(0x30) 만큼 할당
    • buf[0x28] : 40바이트
    • 더미 8바이트
  •  rbp : 8바이트, SFP(Stack Frame Pointer, 현재 실행 중인 함수의 시작 위치 )
    • push   rbp
  • rbp + 0x8 : RET(Return Address)
    • 32bit 운영체제 - 4byte
    • 64bit 운영체제 - 8byte

get_shell()의 주소

  • 0x4006aa : get_shell()의 주소

exploit code

exploit code

    • pwntools을 이용
    • remote("host", port) : 소켓 연결 생성
    • payload 작성
      • 파이썬에서 b는 바이트 문자열 (byte string)
      • 버퍼를 'A'로 Overwrite
      • SFP를 'B'로 Overwrite
      • RET를 get_shell()의 주소로 Overwrite ( get_shell() 실행 )
      • get_shell()의 주소를 리틀 에디안 형식으로 입력
  • sendlineafter(출력대상, 전송대상) : "Input:"이 출력되면 payload 전송
  • interactive() : 사용자와 상호작용

interactive 모드로 전환되어 flag에 접근 가능!
Return Address Overwrite 해결!

 

 

참조

https://www.tcpschool.com/c/c_memory_stackframe