[시스템 해킹]
Format String Bug (FSB, 포맷 스트링 공격)
※ 공부 목적으로 작성했습니다. 악용하지 마세요
포맷 스트링 공격은 '데이터의 형태에 대한 불명확한 정의'로 인해 발생한다. 버퍼 오버플로우 공격처럼 RET 값을 변조하여 임의의 코드 실행을 가능하도록 한다.
* 참고 : 버퍼 오버플로우는 데이터 길이에 대한 불명확한 정의에 의해 발생
포맷 스트링은 C언어에서 입출력 함수의 포맷 문자열 처리하는 부분이다.
파라미터 | 설명 |
%d | 정수형 10진수 상수 (integer) |
%f | 실수형 상수 (float) |
%lf | 실수형 상수 (double) |
%s | 문자 스트링 ((const) (unsigned) char *) |
%u | 양의 정수 (10진수) |
%o | 양의 정수 (8진수) |
%x | 양의 정수 (16진수) |
%s | 문자열 |
%n | 쓰인 총 바이트 수 (* int) %n은 이전까지 입력되었던 문자열의 길이(byte) 수를 다음 변수에 저장한다.(메모리의 내용도 변조 가능) 이를 이용해 문자열의 길이를 내가 변조시키고 싶은 값의 길이만큼 만든 후 %n을 써주게 되면 메모리상에 내가 원하는 값을 넣을 수 있게 된다. |
%hn | %n의 반인 2byte 단위 |
%n 포맷이용한 예:
#include <stdio.h>
void main(){
int a, b;
printf("Where there%n is a will there%n is a way\n",&a,&b);
printf("a : %d\n",a);
printf("b : %d\n",b);
}
실행결과 :
Where there까지 띄어쓰기를 포함해서 11자
Where there is a will there까지 띄어쓰기를 포함해서 27자
1. 메모리 열람
문자열 %x를 입력해서 문자열의 메모리 주소를 알아낼 수 있다.
[정상 출력]
printf() 함수에서 포맷을 문자열(%s)로 지정하면 %x가 문자열로 처리된다.
#include <stdio.h>
void main(){
char *buffer = "jennana\n%x\n";
print("%s\n",buffer);
}
[포맷스트링으로 메모리 열람]
하지만, 포맷을 지정하지 않고 출력할 때 입력값에 %x를 넣어주면 문자열이 저장된 다음의 메모리 값이 출력된다.
#include <stdio.h>
void main(){
char *buffer = "jennana\n%x\n";
print(buffer);
}
추가로, 아래의 코드를 실행하면, BBBB가 들어 있는 메모리에서 전후 값을 알 수 있다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv){
char buf[200];
for(int x=0;x<200;x++){
buf[x]='A';
}
strcpy(buf, argv[1]);
printf(buf);
return 0;
}
2. 메모리 변조
%n은 이전까지 입력되었던 문자열의 길이(byte) 수를 다음 변수에 저장한다.offset 같은 것이라고 보면 될 거 같다.
아래 코드를 보면
#include <stdio.h>
void main{
long i=0x00000064;
j=1;
printf("i address : %x\n",&i);
printf("i value : %x\n",i);
printf("%64d%n\n",j,&i); // j와 i의 주소 값에 64의 16진수의 값을 입력
printf("changed i value : %x\n",i); // 16진수 값으로 출력
}
실행 결과:
i의 값은 0x64이지만, 위의 코드를 실행했더니 0x40으로 값이 변경되었다.
printf("%64d%n\n",j,&i); 에서 %64d에서 64bytes만큼 간격을 두고 j값을 넣는 것이고, %n에는 그 앞의 길이(64(10진수) => 0x40(16진수))가 저장되었다.
main()
{
long i = 0x00000014, j=1, *k;
printf("i's address : %x\n", &i);
printf("i's value : %x\n", i);
k=&i;
printf("%65281d%n%49406d%n\n",j,k,j,k+2);
printf("changed i's value : %x\n", i);
}
i의 값에 임의의 주소번지 bfffff01을 넣기 위한 코드이다.
주소를 10진수로 변경하면 범위에서 벗어난다.
따라서 주소를 반으로 나누어서 작업을 해야한다.
ff01 | bfff |
k | k+2 |
리틀엔디언 방식으로 k=ff01, k+2=bfff
(16진수) bfff → (10진수) 49151
(16진수) ff01 → (10진수) 65281
j는 형식만 맞추기 위해 서용한것.
리틀엔디언으로 뒤에 65281(ff01)을 넣고 49406d는 bfff를 넣어야 하는데
%n의 의미는 이 주소 안에다가 전체 길이를 넣어야 하는데 이미 앞에 65281(ff01)가 지정되어 있다.
그러면 ff01 + @를 더해서 bfff가 되도록하면 된다.
1bfff - ff01 = @ 가 되는데 @값을 10진수로 전환하면 49406이다.
값을 계산하는 방법은 $(printf AAAA(주소)BBBB(주소+2) = 작은수(bfff)에서 큰수(ff01)을 뺄 수 없기 때문에 1을 더해서 뺀다. 기계상에서는 결과값이 동일하다.
변경전 | bffffb84 | 0x00000014 |
변경후 | bffffb84 | 0x00000028 |
65281(ff01)개를 출력하여 그 갯수를 %n을 통해 i값에 넣게된다.
다음에 49406개를 출력하여 i주소의 2증가한 곳에 넣게되는데
이때 먼저 출력된 갯수(65281) + 후에 갯수(49406) = 114687(lbfff)를 넣게 되는 것이다.
결론적으로 i의 값에는 bfffff01이 들어가게 된다.
포맷 스트링 공격에 위험한 함수
* Fprintf
* printf
* sprintf
* snprintf
* vfprintf
* vprintf
* vsprintf
* syslog
대안
- 포맷 스트링을 함수 입력 파라미터로 직접 사용하지 않는다.
참고 :
정보 보안 개로 : 한 권으로 배우는 보안 이론의 모든 것, 양대일, 한빛아카데미(주)