exestack 풀이 기록출처: DreamHack 워게임
exestack
분야: Pwnable
키워드: Stack Buffer Overflow, Execstack, ret2shellcode, ASLR brute-force(개념)
주의: 원격에 바로 재사용 가능한 익스플로잇 코드/정확한 타겟 정보/고정 주소값은 의도적으로 제거했다. 대신, 네가 잡아낸 핵심 인사이트(에필로그 구조 + execstack + ASLR 대응 아이디어)를 중심으로 정리했다.
scanf("%s", buf) 길이 제한이 없어서 1MB 스택 버퍼를 넘어서는 BOF가 난다. -z execstack -fno-stack-protector -m32라서 스택 실행 가능 + 카나리 없음 + 32비트 조합이 된다. pop ecx → lea esp, [ecx-4] 형태라서, 단순 saved EIP 덮기보다 스택 피봇(ESP 재설정) 관점으로 봐야 한다.프로그램은 입력 길이 제한이 전혀 없는 상태에서 1MB 크기의 스택 버퍼에 문자열을 저장한다.
게다가 Makefile을 보면 NX/Canary 같은 현대 방어를 의도적으로 꺼놨다. 목표는 요약하면 이거다.
| 항목 | 상태 | 한 줄 해석 |
|---|---|---|
| Arch | i386-32-little | 32비트라 주소 공간이 좁다 |
| RELRO | Full | GOT overwrite류는 의미 없다 |
| Canary | 없음 | BOF가 단순해진다 |
| NX | GNU_STACK missing (unknown) | 대신 스택이 executable로 보인다 |
| PIE | Enabled | 코드 베이스는 랜덤일 수 있다 |
| Stack | Executable | ret2shellcode가 성립한다 |
| RWX | 있음 | 실행 가능한 writable 영역이 있다 |
핵심은 No canary + Executable stack 조합이다. 이 조합이면 “스택에 코드 올리고 실행”이 가장 직관적인 방향이 된다.
CC = gcc
CFLAGS = -Wall -Wextra -Werror -g -z execstack -m32 -fno-stack-protector
옵션 해석은 아래처럼 정리된다.
| 옵션 | 의미 | 이 문제에서 중요한 이유 |
|---|---|---|
-m32 | 32비트 바이너리 생성 | 주소 공간이 상대적으로 좁아서 ASLR 대응 난이도가 내려간다 |
-z execstack | 스택 실행 가능 | 스택에 넣은 셸코드를 “그대로” 실행할 수 있다 |
-fno-stack-protector | Stack Canary 제거 | BOF가 발생하면 카나리 없이 바로 프레임이 깨진다 |
#include <stdio.h>
int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
char buf[0x100000];
scanf("%s", buf);
}
setvbuf(... _IONBF ...)는 입출력 버퍼링을 꺼서 원격/로컬에서 출력 타이밍을 예측 가능하게 만든다.scanf("%s", buf)가 진짜 핵심이다. %s는 공백 전까지 읽지만 길이 제한이 없다.buf 이후 스택 데이터를 덮는다.일반적인 스택 프레임 개념도는 이렇다.
높은 주소
+---------------------------+
| saved return address (EIP)|
+---------------------------+
| saved EBP |
+---------------------------+
| saved regs / alignment |
+---------------------------+
| local buf[0x100000] |
+---------------------------+
낮은 주소
보통 BOF는 saved EIP를 덮어서 흐름을 바꾼다.
근데 이 문제는 함수 에필로그가 특이해서 “EIP 덮기 전에 스택이 먼저 터지는” 그림이 자주 나온다.
ECX로 ESP를 잡는다GDB에서 main 끝부분을 보면 이런 흐름이 나온다.
call __isoc99_scanf@plt
add esp, 0x10
mov eax, 0
lea esp, [ebp-0x8]
pop ecx
pop ebx
pop ebp
lea esp, [ecx-0x4]
ret
여기서 핵심은 두 줄이다.
pop ecxlea esp, [ecx-0x4]BOF로 스택이 덮이면, 에필로그에서 pop ecx가 읽어야 할 “정상 값”이 깨진다.
그럼 ECX가 공격자가 만든 값(혹은 쓰레기 값)이 된다.
그리고 바로 다음 줄에서:
ESP = ECX - 4가 돼버린다. 즉, 스택 포인터가 ECX 기반으로 재설정된다.
결과적으로 흔히 이런 현상을 본다.
| 현상 | 의미 |
|---|---|
ret에서 EIP가 패턴값으로 안 바뀌고 그냥 죽음 | saved EIP를 읽기도 전에 ESP가 스택 밖으로 튀었다 |
ESP가 0x4141413d 같은 값으로 변함 | ECX가 0x41414141로 덮였고 ECX-4로 ESP가 갔다 |
이게 이 문제의 “기본 BOF랑 다른” 포인트다.
에필로그에서 한 줄씩 보면 답이 나온다.
pop ecx 직후: ECX 값이 정상인지/깨졌는지lea esp, [ecx-4] 직후: ESP가 스택 범위 안인지/밖인지ret 실행 시점: [ESP]를 못 읽어서 SIGSEGV가 나는지이 문제에서 남는 난점은 이거다.
그래서 실전에서는 보통 다음 중 하나를 고민한다.
여기서 중요한 건, 1MB 버퍼가 커서 ‘맞을 공간’이 넓어질 수 있다는 점이다.
(정확한 수치/주소는 환경마다 달라서 여기선 원리만 적는다.)
아래는 실행 가능한 코드가 아니라 “흐름 요약”이다.
| 포인트 | 요약 |
|---|---|
| 취약점 | scanf("%s")로 1MB 버퍼를 넘어서는 BOF |
| 환경 | execstack + no canary + 32-bit로 ret2shellcode가 유리 |
| 진짜 함정 | pop ecx → lea esp, [ecx-4] 때문에 “EIP 덮기 전에” ESP가 깨질 수 있음 |
| ASLR 대응 | 주소 누출 없으면 “확률/반복” 관점으로 접근(개념) |
이 문제는 “execstack이니까 그냥 EIP 덮고 점프”로 끝나는 문제가 아니었다.
에필로그가 ECX로 ESP를 다시 잡는 구조라서, 디버깅을 제대로 안 하면 ret에서 계속 멈춰 죽는 것처럼 보인다.
결국 답은 항상 디버거에 있었다.
pop ecx 이후 ECX가 무엇이 되는지, 그리고 lea esp, [ecx-4]로 ESP가 어디로 가는지만 보면 전체 그림이 정리된다.