드림핵 - basic_exploitation_002

HoHk☔️🐁·2026년 2월 15일

Dreamhack SYS

목록 보기
2/5

FSB에서 카운터에서 GOT overwrite가 성립하는 이유

내가 계속 헷갈렸던 포인트를 한 번에 정리한 문서다
공백을 찍어서 숫자를 만든다는 건 이해했는데, 그게 왜 GOT에 대입처럼 써지는지 연결이 안 됐던 부분을 풀었다


0. 한 줄 결론

FSB write는 결국 이 한 줄로 요약된다

*(uint16_t*)TARGET = (printed_chars & 0xffff);

여기서

  • TARGET이 GOT 엔트리 주소가 되게 만들면 GOT overwrite가 된다
  • printed_chars를 원하는 값으로 맞추면 그 값이 써진다

이 두 개를 포맷 스트링으로 동시에 달성하는 게 exploit의 본체다


1. printf 내부에 카운터가 있다는 게 핵심

printf는 뭘 출력할 때마다 내부적으로 이런 값을 계속 증가시킨다

  • printed_chars = 지금까지 출력한 문자 수

여기서 중요한 건 공백도 문자라는 점이다
보이는 글자든 공백이든 전부 1글자로 카운트된다


2. %n 계열은 출력이 아니라 쓰기다

%n 계열 specifier는 화면에 글자를 찍지 않는다
대신 카운터 값을 메모리에 기록한다

종류별 의미는 이렇게 보면 된다

포맷개념적 동작
%n(int)ptr = printed_chars
%hn(short)ptr = printed_chars
%hhn(char)ptr = printed_chars

즉 %hn을 만나면 printf가 내부적으로 하는 일은 이거다

short *p = (short*)ptr;
*p = (short)printed_chars;

그래서 카운터 올리기가 곧 쓰기 값 만들기로 직결된다


3. 카운터를 원하는 값으로 만드는 방법이 %Nc

exploit에서 쓰는 트릭은 %Nc다

  • %c는 원래 문자 1개 출력이다
  • 근데 width를 N으로 주면 폭을 N으로 맞추기 위해 공백을 채운다
  • 결과적으로 공백이든 뭐든 합쳐서 총 N글자 출력 상태가 된다

%4660c

이게 실행되면 화면에는 공백이 대부분이고 마지막에 문자 1개가 찍히는 느낌인데
중요한 건 printed_chars가 4660이 된다는 점이다


4. 이제 대입이 GOT에 성립하는 연결고리

여기서부터가 핵심이다

4.1 %5$hn은 무엇을 의미하나

%5$hn은 아래를 의미한다

  • 5번째 인자를 포인터로 해석한다
  • 그 포인터가 가리키는 곳에 printed_chars를 2바이트로 기록한다

개념적으로는 이거다

short *p = (short*)arg5;
*p = (short)printed_chars;

즉 5번째 인자 값이 어디에 쓸지 결정한다

4.2 그래서 해야 하는 건 딱 하나

arg5가 GOT 주소가 되게 만들면 된다

arg5 == exit_got

을 성립시키면

*(uint16_t*)exit_got = printed_chars;

가 된다

이 순간 카운터가 GOT에 대입되는 게 성립한다


5. arg5가 왜 exit_got이 되냐

여기가 FSB에서 제일 자주 쓰는 트릭이다

5.1 printf(buf)는 인자를 안 주는데도 인자를 읽으려 한다

코드가 이런 구조다

read(0, buf, 0x80);
printf(buf);

printf는 포맷스트링에 %5$hn 같은 게 있으면 5번째 인자를 읽으려 한다
근데 호출자는 실제로 인자를 안 줬다
그래서 printf는 그 자리에 있던 값을 그냥 인자인 것처럼 읽는다

이게 UB인데, 실전에서는 그대로 재현되는 경우가 많다

5.2 공격자는 그 자리에 있던 값을 내가 원하는 값으로 만든다

payload 끝에 target 주소를 붙인다

포맷스트링 + exit_got 주소 4바이트

그럼 이 4바이트 값이 메모리 어딘가에 올라가고
printf가 5번째 인자 슬롯을 읽을 때 그 위치를 읽도록 오프셋을 맞추면

  • arg5가 곧 exit_got이 된다

즉 운이 아니라 배치다

여기서 5라는 숫자는 보편 공식이 아니라 실측 결과일 수 있다
환경이 바뀌면 6이나 7이 될 수도 있다


6. 전체 흐름을 한 번에 정리

핵심 구조는 보통 이렇다

1) 출력량을 원하는 값으로 맞춘다

%{under}c

이게 끝나면 printed_chars = under 상태가 된다

2) %hn으로 그 값을 목표 주소에 쓴다

%5$hn

이게 실행되면

*(uint16_t*)arg5 = (under & 0xffff);

3) payload 끝에 exit_got를 붙여서 arg5가 exit_got이 되게 한다

결과적으로

*(uint16_t*)exit_got = under;

가 된다

그리고 exit 호출이 일어나면 GOT가 가리키는 함수가 바뀌어서 흐름이 꺾인다


7. under를 왜 int.from_bytes로 만드는가

under는 get_shell 주소의 하위 2바이트를 정수로 만든 값이다

예를 들어 get_shell 주소가 0x08048609면
리틀엔디안 바이트열은

  • 09 86 04 08

하위 2바이트만 보면

  • 09 86

이걸 little endian 정수로 해석하면

  • 0x8609

이게 under다

그리고 %{under}c에서 under는 숫자여야 하니까
bytes를 정수로 바꾸는 과정이 필요하다


8. 내 체크리스트

FSB write가 안 먹힐 때 나는 이 순서로 확인한다

  • %p 스캔으로 내가 붙인 주소가 몇 번째 슬롯에 보이는지부터 확정한다
  • 그 번호를 %n$hn에 넣는다
  • payload 길이를 조절해서 주소가 word 경계에 안정적으로 올라가게 맞춘다
  • %Nc로 출력량을 원하는 값으로 만든다
  • %hn이 쓰는 값은 2바이트로 잘린다는 걸 항상 염두에 둔다

마치며

내가 계속 헷갈렸던 이유는
공백 출력과 메모리 쓰기가 연결되는 규칙을 머릿속에 한 줄로 못 박아두지 못해서였다

이제는 그냥 이렇게 외우면 된다

  • %Nc로 printed_chars를 만들고
  • %n으로 printed_chars를 메모리에 쓴다
  • 그 메모리 주소는 내가 인자 슬롯에 깔아서 공급한다
profile
nyo님 좋아합니다!

0개의 댓글