System Hacking/FSB(Format String Bug)

32bit에서 FSB (Format String Bug) 이해하기 -(1)

jir4vvit 2020. 10. 8. 17:50

업데이트 : 2020.10.10 - 틀린 내용 수정 

참고자료 : JSec님 블로그(blog.naver.com/yjw_sz/221889244689)

 

 

FSB 저번 8월 말인가에 봤던 건데 오랜만에 보니까 까먹어서.. 정리해보려고 한다. 

 


printf(buf) 에서 흔히 FSB(포맷 스트링 버그) 취약점이 발견된다.

printf의 인자 개수는 포맷 문자 개수로 결정된다.

buf의 값을 우리가 마음대로 정할 수 있다면 포맷문자를 넣어버리면 우리가 원하는 값을 출력이 가능하다는 뜻이다. 

예를 들어 인자로 %d만 넣었을 경우 main함수의 영역이 출력될 수도 있다.

printf(%d, 1) 과 printf(%d)

사진 상 우측이 FSB 취약점이 발생한 경우이다.

이를 이용하여 main 함수 스택 내용을 모두 노출 시킬 수 있다.

 

 

%p를 통해 메모리를 유출할 수 있고 %[숫자]$p를 통해 [숫자]만큼 떨어져 있는 메모리를 유출이 가능하다.

참고로 %p는 포인터의 주소값을 찍어내는 서식 지정자이다.

%p로 메모리 유출

%p와 %x 둘 다 16진수를 찍는데, %p를 더 많이 쓴다고 한다.

왜냐하면 %x는 앞에 0x도 잘 안붙여주고 무조건 4바이트로 출력해준다. 32bit 에서는 상관없겠지만 64bit는 8바이트 체제인데 4바이트만 출력해주면 .. 좀 그렇지 않은가;

참고로 %p는 32bit에서 4바이트, 64bit에서 8바이트로 출력해준다. 


main 함수 영역 출력이 가능하면 입력도 가능하지 않겠는가? 

우리는 출력만 해서는 안되고 스택의 값을 변조를 해야 한다.

이것은 %n 서식 지정자를 사용하면 가능하다.

 

printf 함수의 서식 지정자들은 %n을 제외하고는 전부 인자에 지정된 변수를 읽어 문자열로 출력한다. 

하지만 %n은 지정된 변수를 읽는 게 아니라 지정된 변수에 %n 전까지 출력된 문자의 개수를 지정된 변수에 10진수 형식으로 쓴다. 한마디로 %n은 출력이 아닌 입력을 하는 포맷 스트링 문자이다.

 

%n 4바이트
%hn 2바이트
%hhn 1바이트

%n 앞에 쓰인 바이트 수 만큼 입력을 한다.

%n 사용 예시
출력

%4660c 쓰는 이유는 뭘까? %n이 앞에 쓰인 바이트 수만큼 입력을 한다고 했었다. 현재 val 변수에는 0x11111111이 들어가 있고, %n에 의해서 4660이라는 값이 val 변수에 들어간다,

 

그래서 0x1234가 출력이 되는 것이다. (4660 = 0x1234)

 


 

위에서 언급한 %[숫자]$p에 대해 알아보자.

이것은 [숫자]만큼 떨어져 있는 메모리의 유출이 가능하다.

 

예를 들어 buf에 AAAA%2$n 이 들어갔다면? 그럼 메모리 스택 구조는 아래와 같을 것이다.

 

%p를 하면 buf 바로 아래의 영역을 가르키고, %2$p를 하면 두칸 아래 있는 영역을 가르킬 것이다.

마찬가지로 %2$n도 역시 두칸 아래 있는 영역을 가르킨다.

근데 여기서 두칸 아래 있는 영역이 만약 우리의 인풋 값이라면 AAAA에 원하는 값을 입력할 수 있다는 소리다. 스택 변조가 가능해진다.

 

이 경우에서는 AAAA라는 주소에 4바이트니까 숫자 4가 들어가게 된다. 즉, 0x00000004가 들어가게 된다. 

 


방금 설명한 것을 바탕으로 실습을 하나 진행해보자.

실습 코드

check 값을 변조시켜 Nice Try!!를 출력하는 것이 목표이다.

0아니면 True니까 대충 check값을 자연수로 주면 되는 느낌이다.

 

실습코드에서 printf(buf); 에서 포맷 스트링 버그가 발생하는 것을 확인할 수 있다.

덕분에 우리는 %p를 통해 main을 살펴볼 수 있다. 

 

우리는 먼저 AAAA를 넣어 0x41414141이 언제 발생하는지 파악을 해야한다.

왜 main에 0x41414141이 있을까? 우리는 현재 스택을 보는것이고 buf 변수는 main의 지역변수이다. buf에 AAAA 스트링이 들어갔으니... 당연히 main에 있어서 출력이 되는 것이다. 

 

0x41414141 발생하는 부분에 원하는 주소를 넣고 %7$n을 이용해 원하는 값을 쓴다. 

여기서 원하는 주소는 check 변수의 주소일 것이다.

 

그러면 원하는 값이 뭐지??

%n을 사용하기 때문에 지정된 변수에 %n 전까지 출력된 문자의 개수를 지정된 변수에 10진수 형식으로 쓴다.

어쨌든 0 아닌 값이니까 if문이 True가 되어 Nice try!!가 출력될 것이다. 

 

exploit code
성공

%n 서식지정자를 이용해 check 변수에 0이 아닌 원하는 값을 넣을 수 있었다.

 


두번째 실습을 진행해보자.

 

실습 코드2

첫번째 실습과 마찬가지로 printf(buf);에서 포맷스트링버그 취약점이 발생하고 있다.

첫번째 실습 코드와 달라진 점은 check 변수에 0아닌 아무 자연수를 넣어야하는 것이 아니라 1을 넣어야 한다는 것이다.

 

 

 

 

어떻게... 하면 좋을까?? 이번에도 역시 %n 서식지정자를 이용해야 한다.

%n 서식지정자는 %n 전까지 출력된 문자의 개수를 지정된 변수에 넣는 것이기 때문에 1개의 문자만 읽도록 해야하는 것이라고 예상할 수 있다. 

더 나아가서 생각을 해보면 1바이트만 읽으면 되게 하면되지 않나!??! 라고 생각을 해볼 수 있겠다..

 

1바이트만 읽게 하는 방법은? 앞에서 언급한 %hhn 을 이용하자.

 

exploit code

format함수를 이용하여 몇 바이트 문자열을 출력할지?? 정해보자.

그런데 왜 (0x100 + 1 - 4) 일까?

 

(0x100 + 1 - 4)만큼 값을 쓰는데 이 앞에 p32(check)가 있다. 이건 4바이트다.

다 더하면 (0x100 + 1 - 4 + 4)가 된다.

결론은 0x101

여기서 %hhn 덕분에 1바이트만 읽게 된다. 

 


세번째 실습을 진행해보자.

 

실습코드 3

앞에서 두 실습과 마찬가지로 printf(buf);에서 포맷스트링버그 취약점이 발생한다.

다른점은 check 변수에 0x77778888이 들어가야 한다. 

 

0x77778888이면 4바이트!! 앞에와 마찬가지로 %n를 이용해서 넣을 수 있겠다. 

 

payload += p32(check)

payload += '%{}c'.format(0x77778888 - 4)

payload += '%7$n'

 

4를 뺴주는 이유는 역시 앞에 먼저 써준 p32(check) 덕분에 4바이트를 덜 써야 한다. 

 

근데 실제로 이러한 익스플로잇 코드로 작성하지 않는다.

0x77778888 = 200432440

문자들이 실행되려면 약 20억... 만큼 문자들이 실행되어야 동작이 실행되는데... 거의 불가능에 가깝다.(개 오래걸림)

 

 

그래서 두 번에 걸쳐서 나누어 담아야 한다.

이것이 가능한 이유는 %7$n이 실행되기 전에 그 앞에 쓰여있는 바이트 수만큼 check 변수에 들어가기 때문이다.

 

 

그 전에 AAAABBBB를 입력해서 AAAA와 BBBB가 몇 칸 떨어져 있는지 확인해야 한다.

AAAA는 7칸, BBBB는 8칸 떨어져 있군..

 

익스플로잇 코드

p32(check)에는 0x7777이 저장되어 있고 그 다음 2바이트, 즉 p32(check+2)에는 0x8888을 넣어야 한다.

(하위 2바이트에는 0x8888, 상위 2바이트에는 0x7777)

 

7칸 아래 0x8888을 써주고 8칸 아래 0x7777을 써주자. 

 

빨간색 네모부터 살펴보자

0x8888 - 8 + 8

8을 빼주는 이유는 앞에서 p32를 씀으로써 4바이트짜리 주소값을 두번 썼기 때문이다.

 

그리고 0x8888은 2바이트이기 때문에 2바이트만 읽어주기 위해 %7$hn을 이용하자.

%n은 바로 앞에 바이트 수만큼 해당 변수의 값으로 넣어준다. 

 

그 다음, 노란색 네모.. 0x7777을 넣어줄 차례이다.

0x10000 + 0x7777 - 0x8888 + 0x8888

0x17777 - 0x8888 + 0x8888

0x8888을 빼주는 이유는 바로 앞에서 0x8888을 써줬기 때문이다... 

 

결론적으로 값이 0x17777이 되는데 %8$hn을 이용하여 2바이트만 넣어주자.

 

그래서 저런 익스플로잇 코드를 작성할 수 있다.

 

 

 

 

 


.. 

그런데 사실 시스템 해킹의 주된 목적은 쉘을 획득하여 임의의 명령을 실행하는 것이다.

이건 다음 포스팅에서 알아보자...^0^