만족

[정보보안] 코드 보안: 포맷 스트링 공격 본문

[정보보안] 코드 보안: 포맷 스트링 공격

기타 CS 이론/IT Security Satisfaction 2021. 12. 13. 06:51

포맷 스트링 공격은 버퍼 오버플로우 공격과 매우 유사하다.

 

포맷 스트링

int main(){
  char* buf= "str";
  printf("%s", buf);
}

위와 같이 %s 와 같이 출력 서식을 지정하는 문자를 포맷 스트링이라고 한다.

 

조금 특이한 %n에 대해서 알아볼 것이다.

 

int main(){
  int n;
  printf("hahaha%n", &n);
}

 

%n은 현재까지 출력된 바이트 수를 지정한 변수에 넣는다.

 

여기에서 %n 전에 출력된 문자열은 hahaha로 6byte이므로, n에 6이 들어간다.

 

%n은 값을 4byte크기로 쓰기작업을 하는데,

만약 2byte만 쓰기에 사용하고 싶을 경우 %hn을 사용한다.

 

포맷 스트링 공격 원리

 

만약 printf에서 포맷 스트링을 사용하고 추가 인자를 넘겨주지 않는다면 어떻게 될까?

 

함수를 호출할 때 어셈블리를 살펴보면,

인자에 해당하는 값을 %eax에 넣는다.

 

이후 호출된 함수에서는 %eax에 오프셋을 더하면서 매개변수가 있다고 가정하고

해당 메모리 영역을 매개변수로써 참조한다.

 

int main(){
  char* buf= "str";
  printf("%s");
}

이렇게 하면 "%s"의 주소값이 스택에 쌓이고,

%s에 대응되는 값을 찾으려 할 때, "%s"이전에 스택에 쌓인 값을 참조한다.

 

//test.c

#include <stdio.h>

int main(){
  char buf[64];
  fgets(buffer, 63, stdin);
  printf(buffer);
  return 0;
}

이 코드를 실행하고 stdin에 "\x41\x41\x41\x41\x78\xfb\xff\xbf%%d%%n" 을 넣어주면 어떤 일이 발생할까?

 

 

함수 호출 시점에서 스택은 다음과 같은 모습이 된다.

 

우선 fgets로 \x41... 의 값이 buf 주소값에 들어간다.

 

그리고 printf를 호출하기 전 printf의 매개변수인 buf를 스택에 쌓고,

call printf해서 printf함수를 호출하게 된다.

 

printf함수는 문자열을 출력하다가 포맷스트링 %d를 만나면

매개변수로 받은 주소부터 4byte씩 줄여가며 매개변수로 취급한다.

 

첫 포맷 스트링인 %d를 만나면 buf의 주소의 +0위치에 있는 \x41\x41\x41\x41을 출력한다.

(출력값은 1094795585; 출력 크기는 10byte크기) 

 

두 번째 포맷 스트링인 %n을 만나면 다음 위치(이전에 참조한 주소에서 4byte만큼 이동)의 \x78\xfb\xff\xbf 를 참조하는데

%n은 현재까지 출력한 바이트의 갯수를 전달받은 주소에 넣어버린다.

 

현재까지 출력된 문자열은 \x41\x41\x41\x41\x78\xfb\xff\xbf과 1094795585이므로

18byte를 출력하였고, 18이라는 값을 \x78\xfb\xff\xbf, 즉 0xbffffb78 위치에 18을 쓰게된다.

 

따라서 개발자가 의도하지 않은 변수 덮어쓰기가 발생한다는 것을 알 수 있다.

 

포맷 스트링 공격

%n을 이용해 원하는 위치에 원하는 값을 넣을 수 있음을 알았다.

 

이것은 함수의 return address를 알고 있다면 그 위치의 값을 변조해 원하는 위치로 점프할 수 있다는 것을 의미한다.

 

return address는 0xbffffba8처럼 매우 큰 수인데, 이 엄청나게 긴 문자열을 모두 작성해야 할까?

 

그렇지 않다.

 

//test.c

#include <stdio.h>

int main(){
  char buf[64];
  fgets(buffer, 63, stdin);
  printf(buffer);
  return 0;
}

 

마찬가지로 위 코드에서, \x41\x41\x41\x98\xfb\xff\xbf%%3215719092d%%n을 입력하면 어떻게 될까?

 

\x41\x41\x41\x98\xfb\xff\xbf에 해당하는 값이 출력되고(8byte)

%%3215719092d로 인해 3215719092만큼의 길이를 갖는 문자열에서 \x41\x41\x41\x41에 해당하는 정수가 출력된다.

 

그리고 %%n에서 \x98\xfb\xff\xbf에 해당하는 주소인 0xbffffb98에

3215719092+8= 3215719100이 덮어씌워진다

 

는 아니고 안된다.

 

3215719100은 signed int로 표현할 수 없는 큰 수이기 때문에 값이 입력되지 않는다.

 

%hn을 이용해 2byte씩 나누어 저장한다면 가능하다.

 

예를 들어 \x41\x41\x41\x98\xfb\xff\xbf%%64180%%hn 을 입력한다면

%%64180d에 의해 64180+8= 64188만큼 출력되고

0xbffffb98에서 2byte만큼의 크기에 64188(0xfabc)가 출력된다.

 

이 성질을 이용해서

\x41\x41\x41\x41\x98\xfb\xff\xbf

\x41\x41\x41\x41\x9a\xfb\xff\xbf

%%64172d%%hn%%50415d%%hn 을 입력하게 되면

 

%%64172d에서 64172+16= 64188만큼 출력되고

%%hn에서 0xbffffb98위치에 64188(0xfabc)가 쓰여진다.

 

그리고 %%50415d에서 64188+ 50415= 0x1bfab 만큼이 출력되고

%%hn에서 0xbffffb9a위치에  0x1bfab에서 2바이트 만큼(0xbfab) 쓰여진다.

 

따라서 0xbffffb98~0xbffffb9c에는 \xbc\xfa\xab\xbf가 쓰여지게 된다.

 

여기에서 0xbffffb98이 만약 함수의 return address라고 하고

\xbc\xfa\xab\xbf가 원하는 함수의 위치라고 한다면 main 함수가 종료된 후 그 함수(심지어는 쉘)로 점프한다.

 



Comments