드림핵
[드림핵] Return Address Overwrite
다다x_x
2025. 4. 30. 23:57
https://dreamhack.io/wargame/challenges/351
- Stack
- Stack Frame
- Stack Overflow
- Buffer Overflow
- 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를 실행해보면 데이터를 과하게 입력 시, 버퍼 오버 플로우가 발생
// 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 → 쉘 획득
동작 흐름 분석

- 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

- 0x4006aa : get_shell()의 주소
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() : 사용자와 상호작용


참조