[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를 먼저 덮고, 그 다음 리턴 주소를 덮게 된다.
ret이 리턴 주소를 "신뢰"하는 이유함수 끝에서 컴파일러는 leave; ret을 생성한다. leave는 mov rsp, rbp; pop rbp와 동일 — 스택 프레임을 정리한다. 이어지는 ret은 CPU 관점에서 **"현재 RSP가 가리키는 8바이트를 읽어서 그 값을 RIP에 넣고, RSP를 8만큼 증가시켜라"**는 매우 단순한 하드웨어 명령이다. CPU는 그 8바이트가 컴파일러가 원래 넣어둔 정상 리턴 주소인지, 오버플로우로 덮인 공격자 값인지 전혀 구분하지 않는다 — ret은 스택의 그 위치에 있는 값을 무조건 신뢰하고 그리로 점프하는 명령일 뿐이다. SQL 파서가 데이터와 명령을 구분 못 하는 것과 동일한 구조가, 여기서는 CPU 명령어 레벨에서 재현된 것 — "그 자리에 있어야 할 값"과 "실제로 그 자리에 있는 값"이 다르면, 실행 흐름 자체가 공격자에게 넘어간다.
cyclic(200)(pwntools) 같은 유니크 패턴을 입력해 크래시 → 크래시 시점 RIP 값을 cyclic_find에 넣어 정확히 몇 바이트를 채워야 리턴 주소 자리에 도달하는지(오프셋) 확인win() 함수 주소, 또는 셸코드가 있는 스택 주소)로 채운 페이로드 전송gets가 이 페이로드를 그대로 buf에 복사하며 리턴 주소까지 덮어씀ret을 실행하는 순간 CPU가 그 값을 RIP에 로드 → 공격자가 지정한 주소로 실행 흐름 전환execve("/bin/sh")까지 직행 — 가장 원초적인 형태메모리 손상 취약점 전체가 결국 "CPU와 프로그램이 스택의 특정 위치에 있는 값을 무조건 신뢰한다"는 전제 위에 서 있다는 걸 이해하면, 왜 카나리·NX·ASLR 같은 보호 기법들이 전부 "그 신뢰를 검증 가능하게 만들거나(카나리), 신뢰해도 실행을 못 하게 만들거나(NX), 신뢰할 주소 자체를 예측 못 하게 만드는(ASLR)" 서로 다른 각도의 대응이라는 게 같은 논리로 이어진다.