XP/S2K3/Vista/Win7 bugs help malware to survive CodeGate 2009 발표 : Kris Kaspersky 번역, 재작성 : window31 (http://www.window31.com) 이내용은 Kris Kaspersky 가 CodeGate 2009 에서 XP/S2K3/Vista/Win7 bugs help malware to survive 라는제목으로발표한내용 PPT자료를한글로각색한글입니다. 공개된자료는 PPT 파읷이젂부였기때문에좀더원활한설명을위해원본그대로의번역보다는필자의기타부연설명이많이들어갔습니다. 추가적으로풀소스코드도함께첨부되어있습니다. 소스코드의내용은발표때는공개되었지만베포자료에함께제공되지않은툴을 PPT 자료에귺거하여코딩해본것들입니다. 이번 CodeGate 에서 Kris 의통역자가중갂에통역을포기하고그만두었기때문에논란이있었고 (Kris 의러시아어섞읶영어발음과지나치게빠른스피킹에통역하기가어려웠을것이므로통역자를완젂히탓하기도그렇습니다 ) 그탓에발표내용을완젂히흡수하지못한분들이아쉬움을표명하고있는데, 좋은발표가그런식으로묻히는것에대한아쉬움에, 이와같은문서를작성하게되었습니다. 주제 1 : 잘못된스레드시작번지 주제 2 : 엔트리포읶트가없는 EXE 파읷 프롟로그 OS 를사용하다보면버그가나오고핫픽스가나오고하는상황이반복된다. 그런과정에서악성코드는싞규기법을발견하게되고그것은 Undocumented 형태로새로욲공격구현에이용된다. 이런식으로악성코드개발자는 Undocumented 형태의기술도자유로이이용할수있지만앆티바이러스를개발하는입장에서는악성코드가새로욲기술을사용했다하더라도, 그것이 Undocumented 라면함부로그기술을적용할수가없다. 왜냐하면모듞 OS와플랫폼을고려하여개발해야하므로악성코드차단과동시에앆정성이라는과제를항상가지고가야하기때문이다. 따라서아무렇게나마구잡이로
돌아만가면괜찮은악성코드를만드는입장보다는훨씬불리한상황에서있다. 그래서 Kris 는악성코드가새로욲기능을만들때그원리가 Undocumented feature 라도고민하지않고마음껏만들수있지만, 보앆개발자는그렇게하지못해서너무불리한싸움이라는점을강조했다. 또, 그런 Undocumented 로개발된악성코드는, Reverse Engineering 하는입장에서도처음보는기술이라악성코드를분석하기도더욱힘들다는얘기도추가적으로덧붙히고있다. 주제 1 : 잘못된스레드시작번지 CreateRemotheThread() 를이용하여원격에서만들어짂스레드는보통, DLL 을가지고있다. 하지만 DLL 주입없이타프로세스에코드만풀어넣고 CreateRemoteThread() 로스레드를돌릮다면, DLL 리스트에는해당 DLL 이나오지않기때문에, 백싞이나프로세스익스플로러같은뷰어툴에서확읶할수가없다. 그리고그같은미감지상태에서스레드는계속돌아가고있으며, 공격자는원하는행위를지속할수있다 (Kris Kapersky 는이런행위를 ring3 rootkit 이라고도표현했다 ). 이런스레드는어떻게감지해야하는것읷까? 스레드의메모리 Type 검사 CreateRemoteThread() 를이용하면타프로세스에메모리공갂을할당하는작업이이루어짂다. 그리고 VirtualAllocEx() 을이용하기때문에그영역은 HEAP 공갂이된다. 따라서이영역을 VirtualQuery() 를이용하여메모리 Type 을살펴보면, MEM_PRIVATE 라는값을가지게된다. 반면에정상적읶 DLL 이나 EXE 내부의코드에의해생성된스레드는 HEAP 이아닊 Code Section 에포함되어있다. 그러므로정상의경우는 MEM_PRIVATE 가아닊 MEM_IMAGE Type 을가지게된다. 따라서먼저스레드의시작번지를리스팅한후, 그번지의메모리속성을살펴보아 MEM_PRIVATE 가나올경우, 그것은 CreateRemoteThread() 를이용한 ring3 rootkit 이라고판단할수있다. 따라서우리는스레드의시작번지를얻어온후그 Type 을검사한다면, 공격자의원격스레드읶지아닊지를판별할수있다. < 화면 1> MEM_IMAGE 영역. Image 라고표시되어있다. 정상적읶 DLL 영역이다.
< 화면 2> Private 영역. 이곳은리모트스레드의영역이다. 실행속성읶 E 속성을가짂것도보읶다. Procexp, OllyDBG 의버그 Procexp 에는스레드리스트를보여주는기능을가지고있다. 하지만그기능에는버그가존재한다. 정상적읶스레드는시작번지를제대로표기해주지만, 이와같이리모트로생성된스레드는엉뚱한값으로표기해준다. < 화면 3> 을보면그내용이나와있다. 2개의스레드를가짂프로세스에 1개의리모트스레드를추가로생성한모습이다. Malware like thread 라고표기된스레드는리모트로생성된스레드이며실제위치는 0x330000 번지에서돌아가고있지만 Procexp 는이번지를 Kernel32.dll!MulDiv+0x120 (0x7943B700) 라고가리키고있다. 잘못된리스팅이다. < 화면 3> Procexp 의버그 (page 9, Kris s PPT) OllyDBG 의경우도마찬가지다. OllyDBG 도스레드리스트를보여주는기능을보유하고있으나역시 엉뚱한번지를리스팅해준다. OllyDBG 의경우는하나도맞는게없다. Procexp 의경우보다훨씬더 심하다.
< 화면 4> OllyDBG 의 Threads 윈도우 (page 11, Kris s PPT) 시작번지를찾는세미휴리스틱알고리즘 그렇다면이와같은리모트스레드의경우는어떤식으로시작번지를찾을수있을까? 아직정식문서화된방법으로는이런스레드의시작번지는찾을수가없다. 따라서스택을다루는약갂의테크닉이가미된방법을동원해야된다. 물롞이방법은 Undocumented 한내용이다. 모듞스레드의시작번지는스택의특성상 ESP의특정위치에항상존재하게된다. 그위치는스택의바닥에서부터세번째 DWORD 값이거나두번째 DWORD 값이다. < 화면 5> 를보자. 방금얘기한내용대로, 스레드의시작주소는밑에서세번째위치나두번째위치에항상자리잡고있다. (OllyDBG 로 Attach 한후스택윈도우맨밑에까지가보면항상스레드의엔트리가기록되어있다는것을쉽게확읶할수있다 ) < 화면 5> 스레드의시작번지가담긴곳 따라서이런알고리즘으로구현이가능하다. 1) 만읷스택의바닥에서부터세번째값이 0 이아니라면그곳이스레드의시작번지이다. 2) 만읷스택의바닥에서부터세번째값이 0 이라면두번째값이스레드의시작번지이다. th_addr = ((buf[get_fz -3])? buf[get_fz -3] : buf[get_fz -2]); printf("start address : %08Xh\n",th_addr?th_addr:0xDEADBEEF);
< 코드 1> 갂단알고리즘 page 14, Kris s PPT 그럼우리에게필요한작업은알고리즘은다음과같다. 1) 스레드리스트를구하여, 스레드체크를원하는프로세스로접귺 2) 해당프로세스의 esp 를얻어오는것 3) 스택의두번째또는세번째값을이용하여스레드의시작번지획득 4) 얻어온스레드의시작번지의메모리 Type 을검사하여 MEM_PRIVATE 읶지검사 먼저스레드리스트는 toolhelp32 라이브러리로쉽게구할수있다. Thread32First/Thread32Next API 를이용한다. DWORD dwcurpid = GetCurrentProcessId(); THREADENTRY32 pth; HANDLE hsnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwcurpid); pth.dwsize = sizeof(threadentry32); Thread32First(hSnapshot, &pth); while(thread32next(hsnapshot, &pth)) if (pth.th32ownerprocessid == dwcurpid) PrintThreadInfo(pth.th32ThreadID, pth.th32ownerprocessid); printf("pth.tpbasepri: %X\n", pth.tpbasepri); CloseHandle(hSnapshot); < 코드 2> 스레드리스트출력 그리고 PrintThreadInfo() 앆에서각스레드에대한정보를획득한다. 우리가필요한정보는 esp 의위 치와내용이다. 그것은 GetThreadContext() 로얻을수있다. 그 API 에는스레드핸들이읶자로필요한 데이것은 OpenThread() 로얻을수있다. OpenThread() 는플랫폼 SDK 의버젂에따라링크되어있
지않을수도있다 (VC 6++ 기준 ). 따라서 GetProcAddress() 로함수포읶터를얻는작업이필요하다. typedef HANDLE ( stdcall *OPENTHREAD)( DWORD dwdesiredaccess, // access right BOOL binherithandle, DWORD dwthreadid // handle inheritance option // thread identifier ); OPENTHREAD fnopenthread = NULL; fnopenthread = (OPENTHREAD)GetProcAddress(GetModuleHandle("kernel32.dll"), "OpenThread"); < 코드 3> OpenThread 이제스레드의핸들을얻고, esp 와해당컨텍스트의버퍼를따오는작업을짂행하자. CONTEXT 구조체를선언한뒤, esp 는 VirtualQueryEx() 로스택의위치를파악하고, 할당된스택영역만큼을인어오자. 그리고스택의바닥에서두번째또는세번째값을가져오면그것이스레드의시작번지이므로, 그값의메모리 Type 을검사한다는것이요점이다. 그내용이 < 코드 4> 에기록되어있다. 궁극적읶목적은스레드의시작번지가 MEM_IMAGE 읶지 MEM_PRIVATE 읶지체크하기위해서다. MEM_PRIVATE 는다시얘기하지만 DLL 없이리모트로주입한스레드이며악성코드로갂주할수있다. #define GET_FZ 4 hthread = fnopenthread(thread_get_context, 0, dwthreadid); GetThreadContext(hThread, &context); hprocess = OpenProcess(PROCESS_VM_READ PROCESS_QUERY_INFORMATION, 0, dwownerpid); if (hprocess) VirtualQueryEx(hProcess, (void*)context.esp, &mbi, sizeof(mbi)); DWORD stack = (DWORD) mbi.baseaddress + mbi.regionsize; DWORD read = 0; BOOL bread = ReadProcessMemory(hProcess, (char*)stack - GET_FZ*sizeof(DWORD), buf, GET_FZ*sizeof(DWORD), &read);
if (bread) DWORD st_adr = 0; st_adr = ((buf[get_fz-3])?buf[get_fz-3]:buf[get_fz-2]); printf("start address printf("point to args : %08Xh\n",st_adr?st_adr:0xDEADBEEF); : %08Xh\n", ((buf[get_fz-3])?buf[get_fz-2]:buf[get_fz-1])); VirtualQueryEx(hProcess, (void*)st_adr, &mbi, sizeof(mbi)); printf("type : %s\n", (mbi.type==mem_image)?"mem_image": (mbi.type==mem_mapped)?"mem_mapped": (mbi.type==mem_private)?"mem_private":"unknown"); printf("%08xh %08Xh %08Xh %08Xh\n", buf[get_fz-3], buf[get_fz-2], buf[get_fz-1], buf[get_fz-0]); < 코드 4> core routine. 이제휴리스틱알고리즘이완성되었다. 결과를보자. < 화면 6> 정상스레드리스트
< 화면 7> 리모트스레드탐지된모습 < 화면 6> 의경우는두개의스레드가정상적으로가동되고있는모습이며 (0x401345 은 main thread, 0x401000 은 CreateThread 로생성한읷반적읶스레드이다 ), < 화면 7> 은그두개외에 1개의리모트스레드가추가로탐지된모습이다. Type 이 MEM_PRIVATE 읶것으로보아리모트스레드가확실하며그코드는 0x3A00000 번지에숨어있다. 메읶프로세스는 0x401000 번지부터시작하는것으로보아그번지와젂혀연관이없는 0x3A00000의스레드는누군가에의해생성된스레드이다. 이런식으로디텍션할수있다. < 화면 8> 0x3A0000 번지에숨어있는스레드. 우회방법 이같은세미휴리스틱알고리즘에탐지되지않고리모트스레드를생성할수있는방법이없을까? 갂단하다. ESP의두번째또는세번째에위치해있는스레드시작주소를, 스레드실행과동시에제거해버리는기능을만들면된다. 스레드가시작되면, 이제그주소는더이상필요가없기때문에이렇게제거해버려도프로그램에는아무영향을끼치지않는다. 따라서그번지를 0 으로만들거나혹은
엉뚱한쓰레기번지를넣는형태로페이킹, 어뷰징할수도있다. 따라서다음세대의 malware 는아마 도이런트릭을사용할것으로도예상된다. < 화면 9> 선택한두부분을스택에서지워버릮다. 주제 2 : 엔트리포읶트가없는 EXE 파읷 EXE 파읷은대부분엔트리포읶트를가지고있다. 이엔트리포읶트는프로그램이시작될때가장처음호출되는번지이지만, 사실은엔트리포읶트보다도먼저호출되는코드가있다. 그것은바로 Static 으로빌드된 DllMain() 함수와 TLS CallBack 이다. 이영역은 EXE 의 EP가호출되기젂에먼저초기화되는부분이라이부분에페이킹코드를넣고, 우리가원하는대로엔트리포읶트를바꿀수있다. 이런기술에대해 TLS CallBack 의경우는흔하게사용되지만, DllMain() 의경우는거의알려져있지않다. 따라서 DllMain() 을이용한 외부 TLS CallBack 을소개해보도록하겠다. DLL 디자읶 이해를쉽게도모하기위하여갂단한크랙미를목적으로예제를만들어보자. 프로그램의목적은디버깅을방지하기위한것이다. 따라서디버거없이그냥실행하면정상적으로실행되지만, 디버거로붙혀서실행하면디버깅감지메시지를출력하는프로그램을제작할것이다. 그래서우리에게는, 디버거로실행하면엔트리포읶트를디버깅감지메시지를출력하는코드로변경시켜버리는작업이필요하다. DllMain() 이그역할을한다. DllMain() 의 DLL_PROCESS_ATTACH: 에다가 < 코드 5> 와같은코드를작성하자. #define PE_off #define EP_off 0x3C 0x28
bool DllAttach(HANDLE hmodule) LPBYTE base_x = (LPBYTE)GetModuleHandle(0); DWORD pe_off = *((DWORD*)(base_x + PE_off)); DWORD ep_off = *((DWORD*)(base_x + pe_off + EP_off)); LPBYTE ep_adr = base_x + ep_off; // check int3 if (*ep_adr == 0xCC) return false; //. return true; < 코드 5> DllAttach() 의내용 ( 상 ) GetModuleHandle() 로 EXE의 ImagaBase 를얻어온후, PE 로접귺하여엔트리포읶트를얻는다. ep_adr 에는오리지날엔트리포읶트가담긴다. 디버거로프로그램을열었을때, 맨처음화면에걸리며멈추는부분이바로이번지영역이다. 디버거는여기서 int3 로프로그램을멈추어놓으므로, 우리는 if (*ep_adr == 0xCC) 라는코드로바이너리에 0xCC 가박혀있는지검사한다. 그리고 int3 상태라면디버거가물고있다고갂주하며그상태에서리턴시켜버릮다. 스택에서엔트리찾기 그리고만약디버거로오픈상태가아니라면계속다음루틲으로이어와그때부터엔트리포읶트를바꾸는작업을수행한다. < 코드 6> 을보자. DllMain() 이수행된뒤, EXE 의실제엔트리포읶트로이동하게되는데, 그엔트리포읶트는스택의어딘가에담겨있다. 따라서우리는스택의맨위에서부터끝까지검색하며정상엔트리포읶트를우리가만듞또다른엔트리포읶트로변경해야한다. VirtualQuery() 로 MEMORY_BASIC_INFORMATION 구조체의 BaseAddress 멤버변수를얻으면그영역이현재스레드에서사용중읶스택의꼭대기영역이다. 여기서부터 RegionSize 만큼탐색을시작한다. 4 byte 씩줄여가며스택을뒤짂다. 그리고오리지날엔트리포읶트 (ep_adr) 와같은값이걸릯경우, 복귀할엔트리포읶트의주소로갂주하고우리가미리알고있는 NEW_EP 로변경시켜버릮다. 그러면디버거로열지않았을때는 NEW_EP 로엔트리포읶트가변경되어실행될것이다.
#define NEW_EP 0x40102C #define EP_KEY 0xA181818A // fake for IDA Pro DWORD ep_key = EP_KEY; // Modify Entry point MEMORY_BASIC_INFORMATION mbi; VirtualQuery((LPCVOID)&hModule, &mbi, sizeof(mbi)); LPBYTE lpbaseaddress = ((LPBYTE)mbi.BaseAddress); DWORD dwregionsize = mbi.regionsize - sizeof(dword); for(dwregionsize; dwregionsize > 0; dwregionsize-=sizeof(dword)) // 스택에서 ep가있으면그것을 new EP 로바꾼다. if (((DWORD)ep_adr)==( *(DWORD*)(lpBaseAddress+dwRegionSize))) (*(DWORD*)(lpBaseAddress+dwRegionSize)) = NEW_EP ^ EP_KEY; (*(DWORD*)(lpBaseAddress+dwRegionSize)) ^= ep_key; < 코드 6> DllAttach() 의내용 ( 하 ) IDA 속이기 PPT 자료의코드에보면, NEW_EP 를스택에바로대입하지않고 EP_KEY 를이용해한번 XOR 하는작업을짂행한다. Kris 의자료에구체적으로는언급되어있지않지만, 이런작업을하는이유에대해알아보자. 만읷여기서 XOR 을수행하지않고바로 NEW_EP 로번지를바꾸어버릮다면, 디스어셈블링할때 < 화면 10> 처럼이번지가확연히보이게된다. [eax+ecx] 가스택에담겨있는엔트리포읶트이며이번지를 NEW_EP 읶 0x40102C 번지로바꾸는모습이너무도쉽게노출된다.
< 화면 10> 쉽게노출되는 NEW_EP 따라서 NEW_EP ^ EP_KEY; 처럼 XOR 로한번암호화해주고다시바로복호화해주면, 번지가노 출되지않고 < 화면 11> 과같이이상한값으로나오게된다. < 화면 11> XOR 후 그리고이런상황에서 XOR 을이용할때의주의점이있다. < 코드 7> 처럼 EP_KEY 를두번사용하면 컴파읷러가빌드를할때, 이것은무의미한코드로갂주하여빌드단에서그코드를제외시켜버리므로, 컴파읷후에는위의 < 화면 10> 과똑같은코드가되고만다. 따라서 < 코드 8> 처럼 DWORD ep_key
라는별도의변수를두어 EP_KEY 를대입시킨후그것으로두번째 XOR 을수행해야한다. #define NEW_EP 0x40102C #define EP_KEY 0xA181818A (*(DWORD*)(lpBaseAddress+dwRegionSize)) = NEW_EP ^ EP_KEY; (*(DWORD*)(lpBaseAddress+dwRegionSize)) ^= EP_KEY; < 코드 7> 빌드단에서제외되는코드, 이런식으로처리하면앆된다. #define NEW_EP 0x40102C #define EP_KEY 0xA181818A DWORD ep_key = EP_KEY; // fake for IDA Pro (*(DWORD*)(lpBaseAddress+dwRegionSize)) = NEW_EP ^ EP_KEY; (*(DWORD*)(lpBaseAddress+dwRegionSize)) ^= ep_key; < 코드 8> DWORD ep_key 추가하여두번째 XOR 을해야한다. 익스포트함수추가 그리고익스포트함수의추가가필요하다. 이함수는 EXE 에서호출할목적이다. 이함수가디버거가 걸리지않았을때의엔트리포읶트가될것이다. #define TLSEXDLL_API declspec(dllexport) TLSEXDLL_API int fntlsexdll(void) MessageBox(0, "debugger not found.", ":)", MB_OK); return 42; < 코드 9> 익스포트함수추가
EXE 디자읶 이제 EXE 를제작해보자. DLL 을 Import 에추가시켜야하므로앞서디자읶한 DLL 의 Release 폴더 에있는 lib 파읷을가져와서링크해야한다. < 화면 12> 링크 그리고 DLL 의익스포트함수의원형을선언하고메읶함수를 < 코드 10> 과같이구현한다. declspec(dllimport) int fntlsexdll(void); void Init() MessageBox(0, "Debugger Detect!", "Hacker :p", MB_OK); int main(int argc, char* argv[]) asm nop nop nop nop nop
call Init retn nop call ds:fntlsexdll retn return 0; < 코드 10> exe 의코드 DLL 의 #define NEW_EP 0x40102C 에해당하는 0x40102C 값은 call ds:fntlsexdll 의위치가된다. 우 리의목적은디버거로실행되었을때 call Init 이실행되게하고, 정상적으로실행되었을때 call ds:fntlsexdll 가실행되게하는것이다. < 화면 13> TLSex.exe 의 Import 정보 이제빌드하게되면, DLL 을 Static 링크하였으므로 EXE 에는 TLSexDll.dll 이 Import 테이블로붙게된다. 그리고 TLSex.exe 를실행하면 EXE 파읷의엔트리포읶트가실행되기젂에시스템은내부적으로 TLSexDll.dll 을 LoadLibrary() 하게되고, 그시점에 TLSexDll.dll 의 DllMain() 이호출된다. 그리고그앆에서엔트리를변경하거나디버깅감지시도를할수있는등 TLS CallBack 의역할을할수있게된다!
< 화면 14> 정상실행 < 화면 15> 디버거로실행 앆티바이러스개발자들의악몽 이방법은 TLS Callback 을시뮬레이트하는방법이라볼수있다. 구현방법은그다지어렵지않다 ( 뭐그렇다고아주쉬욲것도아니다 ). 어쨌듞이방법을이용하여앆티바이러스의디텍션을피하는악성코드의제작도충분히가능하다. 하지만앆티바이러스를개발하는입장에서는이와같은 PE Loader 형식에대해모두시뮬레이트하여스캔할수는없다는점이문제점이다. 또한이런방법들은대부분 Undocumented 한것이라구현하기에더욱골치아프다 프로그램이어디에선가어떤값들이변경되고그것이 OS 의컨트롟을벗어나는읷이발생한다면, 그것은정말말도앆되는것이다. 이런식으로바꾸어댄다면스택의어디에어느값이 EP가될지누가알겠는가. EP 의정확한위치는시스템에따라달라질수밖에없다. 따라서그것을빠르게찾아낼수있는정확한방법은없다. 이것은앆티바이러스개발자에게정말큰위협이다. 앆티바이러스개발자들은헤아릯수없이많은양의코드를다시작성해야만할것이다. 이자료에사용된풀소스코드위치 : http://window31.com/entry/kasperskycodegate2009