관리 메뉴

Bull

[System Hacking] Format String Bug (FSB) 본문

Computer Science/System Hacking

[System Hacking] Format String Bug (FSB)

Bull_ 2024. 4. 18. 07:11

FSB


C언어에 포맷 스트링이라하면 대표적으로 printf, scanf, fprintf, fscanf, sprintf, sscanf가 있다. 

이 함수들은 포맷 스트링을 채울 값들을 레지스터나 스택에서 가져온다.

 

그런데 이들 내부에는 포맷 스트링이 필요로 하는 인자의 개수와 함수에 전달된 인자의 개수를 비교하는 루틴이 없다.

 

그래서 만약 사용자가 포맷 스트링을 입력할 수 있다면, 악의적으로 다수의 인자를 요청하여 레지스터나 스택의 값을 읽을 수 있다.

 

 

Printf()

https://cplusplus.com/reference/cstdio/printf/

 

https://cplusplus.com/reference/cstdio/printf/

function <cstdio> printf int printf ( const char * format, ... ); Print formatted data to stdout Writes the C string pointed by format to the standard output (stdout). If format includes format specifiers (subsequences beginning with %), the additional arg

cplusplus.com

흔히 알고있는 printf("%d",a); 에 대해서 %d가 정수를 입력한다는 사실을 안다.

 

%04d처럼 %와 d사이에 많은 인자들을 넣을 수도 있다.

 

그 사이의 정규표현식은 다음과 같다.

%[flags][width][.precision][length]specifier

 

여러 형식지정자가 있지만 Dreamhack에서 FSB의 취약점에 대한 형식지정자를 알아보겠다.

 

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

%s는 string 형식지정자로 문자열이 들어간다.

 

%n인자에 대한 길이를 변수에 저장한다.

 

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

$는 몇번째 인자에 대한 행위를 할 것인지 정한다.

 

FSB 연습 문제


// 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
}

드림핵에 있는 연습 문제이다.

https://learn.dreamhack.io/114#7

auth 변수에 0xff를 덮어 씌어줄 것이다.

 

https://learn.dreamhack.io/114#7

%p 포매터는 void형 포인터로 해당 위치의 value를 알려준다.

 

---------- Debugger -----------
%p: rdi , value = 7fffffffdde0
%p: rsi , value = 7fffffffdde0
%p: rdx , value = 20
%p: rcx , value = 7ffff7af2151
%p: r8 , value = 7ffff7dced80
%p: r9 , value = 7ffff7dced80
%p: $rsp+0 , value = 1
%p: $rsp+8 , value = 42424242555547cd
%p: $rsp+16 , value = 7025207025207025
%p: $rsp+24 , value = 2520702520702520
%p: $rsp+32 , value = 2070252070252070
-------------------------------

여기서 printf()의 인자에 어떤 레지스터 값이 들어가는지 확인할 수 있다.

 

rdi, rsi, rdx, rcx, r8, r9 $rsp, $rsp+8 ...

 

처음에는 레지스터를 사용하고  r9 이후에는 스택을 8바이트 단위로 사용하면서 무한히 사용하는 것을 알 수 있다.

그리고 자세히 확인하면 아래 그려진 부분은 rsp가 가리키고 있는 스택이라는 점을 확인할 수 있다.

 

어떻게 확인할 수 있는지 직접 확인해보자.

 

$rsp+32 2070252070252070를 잘 보면 해당 되는 부분에 8바이트 단위로 "리틀 엔디언" 형식으로 저장되는 것을 볼 수 있다.

그림이 살짝 엉망이지만 대충 요런식이다.

$rsp+32부분의 오른쪽 아래에 들어간 값들의 순서를 보면 같은 것을 볼 수 있다.

 

이제 %n이 길이를 인자에 저장할 수 있다는 사실을 이용해보자.

 

printf()는 분명 출력하는 함수인데 %n인자는 write가 가능하는 것이 취약점을 보여주는 거 같다.

 

우선 인자에 덮어쓸 주소를 적어야 한다.

 

0x7fffffffdddc auth변수의 시작주소는 왼쪽과 같다.

 

주소에 대한 정보를 해석할 때 리틀엔디언 방식으로 해석하기 때문에

 

이를 dc dd ff ff ff 7f 00 00으로 8바이트 단위로 만들어준다.

 

dcddffffff7f0000

그리고 hexcode로 변환!

ÜÝÿÿÿ

 

%255c은 255바이트 만큼을 공백으로 채우는 것이다.

 

그래서 %n은 길이를 저장하니까 0xff가 지정한 인자에 저장될 수 있다.

 

%n을 이용해 인자를 사용하기 위해 스택을 이용할 것이다.

 

이전의 Debugger를 확인해보면 몇 번째에 무슨 레지스터가 들어가는 지 알 수 있다.

---------- Debugger -----------
%p: rdi , value = 7fffffffdde0
%p: rsi , value = 7fffffffdde0
%p: rdx , value = 20
%p: rcx , value = 7ffff7af2151
%p: r8 , value = 7ffff7dced80
%p: r9 , value = 7ffff7dced80
%p: $rsp+0 , value = 1
%p: $rsp+8 , value = 42424242555547cd
%p: $rsp+16 , value = 7025207025207025
%p: $rsp+24 , value = 2520702520702520
%p: $rsp+32 , value = 2070252070252070
-------------------------------

즉,

rdi →  $1
rsi →  $2
rdx →  $3
rcx →  $4
r8 →  $5
r9 →  $6
$rsp+0 →  $7
$rsp+8 →  $8
$rsp+16 →  $9
$rsp+24 →  $10
$rsp+32 →  $11

 

내가 입력하는 문자열은 $rsp+16에 들어가게 된다.

 

즉, len("%255c%??$n") = 10이 되기 때문에  적어도 16번째부터 시작해야한다.

 

즉 $rsp+32부분에 해당 주소가 입력될 수 있도록하면 됀다.

 

len("%255c%11$n") = 10

len("aaaaaaa") = 6

len(ÜÝÿÿÿ) = 8

%255c%11$naaaaaaÜÝÿÿÿ

https://learn.dreamhack.io/114#7

그림으로 확인해보면 어떤 느낌인지 감이 온다.

 

$11%n을 통해 $rsp+32부분에 적힌 hex encode로 들어간 주소에 값이 저장되면서 auth변수에 0xff값을 저장할 수 있다.

 

순서도(?)

사실 작년에도 대충은 이해하고 봤었는데 다시 보니까 하나도 모르겠어서...

 

다음에 또 볼  때 이해하기 쉽게 그림으로 그려보았다.

참고자료


[DreamHack 강의]

[출처]https://dreamhack.io/lecture/courses/114