← 목록으로

스택 버퍼 오버플로우: 가장 기본적인 메모리 손상 공격

[Pwn] Pwnable · 작성: 2026-07-19 19:28:48 · 수정: 2026-07-21 23:09:05 · 조회 10

스택 버퍼 오버플로우를 "긴 문자열 넣으면 터지는 버그"로만 알면, 왜 하필 **함수가 끝나는 시점(ret 명령어)**에 CPU가 실행 흐름을 리턴 주소로 넘겨주는지, 그리고 그 리턴 주소가 스택의 어느 정확한 위치에 있는지를 놓치게 된다. 이 글은 컴파일러가 생성하는 실제 어셈블리와 CPU의 ret 명령어 동작까지 내려가서 본다.

소스 레벨: 취약한 코드와 컴파일러가 만드는 스택 프레임

void vuln() {
    char buf[64];
    gets(buf);  // 길이 검사 없음
}

이 함수가 컴파일되면 함수 진입부(prologue)에서 대략 이런 어셈블리가 생성된다.

push rbp          ; 이전 함수의 base pointer를 스택에 저장
mov  rbp, rsp      ; 현재 rsp를 새 프레임의 base로 설정
sub  rsp, 0x40     ; buf[64]를 위한 공간 확보 (스택은 아래로 자람)

이 시점에서 스택 레이아웃은 낮은 주소부터 buf[64]saved RBP(8바이트) → 리턴 주소(8바이트, call vuln 시 CPU가 자동으로 push한 값) 순이다. gets(buf)가 64바이트 경계를 검사 없이 넘어 쓰면, 정확히 이 순서대로 saved RBP를 먼저 덮고, 그 다음 리턴 주소를 덮게 된다.

CPU 레벨: ret이 리턴 주소를 "신뢰"하는 이유

함수 끝에서 컴파일러는 leave; ret을 생성한다. leavemov rsp, rbp; pop rbp와 동일 — 스택 프레임을 정리한다. 이어지는 ret은 CPU 관점에서 **"현재 RSP가 가리키는 8바이트를 읽어서 그 값을 RIP에 넣고, RSP를 8만큼 증가시켜라"**는 매우 단순한 하드웨어 명령이다. CPU는 그 8바이트가 컴파일러가 원래 넣어둔 정상 리턴 주소인지, 오버플로우로 덮인 공격자 값인지 전혀 구분하지 않는다ret은 스택의 그 위치에 있는 값을 무조건 신뢰하고 그리로 점프하는 명령일 뿐이다. SQL 파서가 데이터와 명령을 구분 못 하는 것과 동일한 구조가, 여기서는 CPU 명령어 레벨에서 재현된 것 — "그 자리에 있어야 할 값"과 "실제로 그 자리에 있는 값"이 다르면, 실행 흐름 자체가 공격자에게 넘어간다.

페이로드가 실제로 작동하는 순서

  1. cyclic(200)(pwntools) 같은 유니크 패턴을 입력해 크래시 → 크래시 시점 RIP 값을 cyclic_find에 넣어 정확히 몇 바이트를 채워야 리턴 주소 자리에 도달하는지(오프셋) 확인
  2. 오프셋만큼 더미 값을 채우고, 그 뒤 8바이트를 원하는 주소(예: win() 함수 주소, 또는 셸코드가 있는 스택 주소)로 채운 페이로드 전송
  3. gets가 이 페이로드를 그대로 buf에 복사하며 리턴 주소까지 덮어씀
  4. 함수가 ret을 실행하는 순간 CPU가 그 값을 RIP에 로드 → 공격자가 지정한 주소로 실행 흐름 전환

여기서 끝나지 않는다: 체이닝

메모리 손상 취약점 전체가 결국 "CPU와 프로그램이 스택의 특정 위치에 있는 값을 무조건 신뢰한다"는 전제 위에 서 있다는 걸 이해하면, 왜 카나리·NX·ASLR 같은 보호 기법들이 전부 "그 신뢰를 검증 가능하게 만들거나(카나리), 신뢰해도 실행을 못 하게 만들거나(NX), 신뢰할 주소 자체를 예측 못 하게 만드는(ASLR)" 서로 다른 각도의 대응이라는 게 같은 논리로 이어진다.

Pwnable 카테고리의 글 (2/14)

  1. 메모리 구조 기초: 스택, 힙, 레지스터부터 이해하기
  2. 스택 버퍼 오버플로우: 가장 기본적인 메모리 손상 공격
  3. Format String 취약점: printf가 위험해지는 순간
  4. Stack Canary: 원리와 우회 방법
  5. NX/DEP 우회와 Return-to-libc
  6. ASLR과 정보 유출(Leak)
  7. ROP (Return-Oriented Programming) 완전 정복
  8. 힙 익스플로잇 기초: Use-After-Free와 Double Free
  9. pwntools 실전 활용법
  10. UDF(User-Defined Function) 기반 DB 권한 상승: 원리부터 실전까지
  11. Pass-the-Hash / Pass-the-Ticket: 원리와 실전
  12. eBPF 기반 지속성(Persistence) 기법
  13. Full RELRO 환경에서의 GOT Overwrite와 우회
  14. ret2dlresolve: 심볼 정보 없이 임의 함수 호출하기
← 메모리 구조 기초: 스택, 힙, 레지스터부터 이해하기 Format String 취약점: printf가 위험해지는 순간 →