미역줄기의 이모저모

[ Dreamhack ] 포맷 스트링 버그(FSB) 본문

Hacking/DreamHack 시스템 해킹 강의

[ Dreamhack ] 포맷 스트링 버그(FSB)

미역줄기줄기 2024. 2. 24. 20:36
728x90

포맷 스트링 버그(FSB)

 

c 언어에는 printf 외에도 포맷 스트링을 인자로 사용하는 함수들이 많다. scanf, fprintf, fscanf, sprintf, sscanf 가 있다. 이 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져온다. 

 

하지만 이들 내부에 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 기능이 없기때문에 공격자가 포맷 스트링을 입력할 수 있다면 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽어낼 수 있다. 

 

더 나아가 다양한 형식 지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능하다. 

 

 

💡 여기서 알아두면 좋은 생각의 흐름  💡

읽기 -> leak 할 수 있다

쓰기 -> exploit 할 수 있다

 

 

포맷 스트링 함수를 잘못 사용하여 발생하는 위와 같은 버그를 포맷 스트링 버그(FSB)라고 부른다.

 

 


포맷 스트링이란?

char str[10] = "World!";
printf("Hello, %s\n", str);

 

" " 안에 포함되어 있는 "Hello, %s\n" 이 포맷 스트링이다. 즉 포맷 스트링은 이 포맷 스트링을 사용하는 함수에 대해, 어떤 형식 혹은 형태를 지정해 주는 문자열을 의미한다.

 

형식 지정자 설명
d 부호 있는 10진수 정수
s 문자열
x 부호 없는 16진수 정수
n 인자에 현재까지 사용된 문자열의 길이를 저장
p void형 포인터
너비 지정자 설명
정수 정수의 값만큼을 최소 너비로 지정
* 인자의 값 만큼을 최소 너비로 지정

 

#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;
}

 

 

💡 "%n" 의 쓰임 💡

만약 프로그래머가 완성된 포맷 스트링의 길이를 코드에 사용해야 한다면, %n을 사용하여 이런 문제를 해결할 수 있다.

위의 예시에서, printf("%s%n: hi\n", "Alice", &num);의 첫 번째 인자가 문자열 변수(5글자! 그래서 n = 5 가 된다! -> num =5)이고, 사용자가 “Alice”보다 긴 문자열을 입력해도 “  Bob: Hello”(그냥 Bob이 아니라   Bob)이다 즉. 정렬된 결과를 출력할 것이다.

 

parameter

참조할 인자의 인덱스를 지정한다. 이 필드의 끝은 $로 표기한다. 인덱스의 범위를 전달된 인자의 갯수와 비교하지 않는다.

#include <stdio.h>

int main() {
  int num;
  
  printf("%2$d, %1$d\n", 2, 1);  // "1, 2"
  
  return 0;
}

내가 이해한 것: %2$d 는 d에 2번째 인자 즉 1을 넣겠다!! 이런식

 

 


임의 주소 읽기

#include <stdio.h>

int main() {

  char format[0x100];
  
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  
  return 0;
}

이 코드는 사용자가 임의의 포맷 스트링을 입력할 수 있는 예제 코드이다. 코드를 컴파일하고 

%p %p %p %p %p %p %p %p %p %p을 입력하면, 임의 값들이 출력된다. 

 

$ ./fsb_stack_read
Format: %p %p %p %p %p %p %p %p
0x1 0x7fe1ea75e8d0 0x7fe1ea75ca00 (nil) (nil) 0x7025207025207025 0x2520702520702520 0x2070252070252070

printf 함수에 전달한 인자가 없는데 임의 값들이 출력된 이유는, 10개의 인자를 필요로 하는 포맷스트링을 사용했기에 레지스터와 스택에 존재하는 값이 출력된 것이다.

 

여기서 잠깐
x64의 함수 호출규약을 다시 생각해보면 rsi - rdx - rcx - r8 - r9 - rsp - rsp+8 - rsp+0x10 - rsp+-x18 - rsp+0x20 이다.

 

 

스택 읽기의 결과에서 주목할 점

6번째 출력 값인 rsp부터는 사용자의 입력을 8글자씩 참조한다.

0x7025207025207025 => ”%p %p %p”  여기서도 무슨 말인지 몰라서 %p %p %p를 hex encode 했다!! 

5

아하! 0x7025207025207025 %p %p %p를 hex enode 한 결과를 리틀엔디언으로 표현한 거다!!

(그리고 띄어쓰기 까지 포함해서 %p %p %p는 8글자임)

 

이를 응용하면 포맷 스트링에 참조하고 싶은 주소를 넣고, %[n]$s의 형식으로 그 주소의 데이터를 재 참조해 읽을 수 있다.

 

 


드림핵 연습문제 - FSB Easy

(하나도 안 쉬웠음 )

 

#include <stdio.h>

int main(void) {
    int auth = 0x42424242;
    char buf[32] = {0, };
    
    read(0, buf, 32);
    printf(buf);
    
    // make auth to 0xff
}

 

코드의 8번째 줄에서 사용자가 입력한 buf를 인자로 printf를 호출하기 때문에 포맷 스트링 버그가 발생한다. 

auth 변수를 0xff(10진수로 255)로 덮어쓰는 포맷 스트링을 입력해보자

 

앞에서 설명한 %n이 힌트이다. 0xff로 채우기 위해서 %n의 역할은..

각 문자는 1로 카운트하고, %x는 값만큼 카운트 한다. 예를 들어 AAAA%8x%8x%8x%8x4+8+8+8+8 = 36의 값을 집어넣는 것이다. 이걸로 255를 만들면 되겠다!

 

이제 auth의 주소를 찾아야 한다. 근데 여기선 주소를 알려줬다. 모듈의 0x42를 클릭해서 복붙하면 주소가 나온다.

auth의 주소는 0x7fffffffdddc이다. 이렇게 알아내는게 아닌 것 같지만 아무튼 구했으니 됐다. 

0x7fffffffdddc를 dcddffffff7f0000로 한다.

--> cdddfffffff70000 이게 아니라 dcddffffff7f0000 이거인지 모르겠다....

 

%246c %9$n를 hex encoding한다. 그럼 25323436632539246E 이렇게 나온다.

-->%246c%9$n 이게 아니라 %246c %9$n 인지도 모르겠다

 

 

dcddffffff7f0000 와 25323436632539246E를 붙인다. dcddffffff7f00002532343663202539246E 을 입력하면 끝.

 

 

 

 

드림핵 https://learn.dreamhack.io/114#5 강의를 바탕으로 작성했습니다

'Hacking > DreamHack 시스템 해킹 강의' 카테고리의 다른 글

[ Dreamhack ] Out of Bounds  (0) 2024.02.20
[ Dreamhack ] Hook Overwrite  (0) 2024.02.17
[ Dreamhack ] link  (0) 2024.02.09
[ Dreamhack ] NX & ASLR  (0) 2024.02.08
[ Dreamhack ] Stack Canary  (1) 2024.02.08