SSDT Hooking Last Update : 2007 년 1 월 4 일 Written by Jerald Lee Contact Me : lucid78@gmail.com 본문서는커널모드후킹기술중의하나인 SSDT 후킹에대해정리한것입니다. 제가알고있는지식이너무짧아가급적이면다음에언제봐도쉽게이해할수있을정도로쉽게쓸려고노력하였습니다. 제가작성하였던기존의 Windows Hook 시리즈가유저모드에서의후킹을다루었던반면본 SSDT는커널모드에서의후킹을사용하므로디바이스드라이버를다루는부분이포함되어있습니다. 윈도우디바이스드라이버의경우많은부분을상세히설명하지는못했으나코드작성에필요한부분을중점으로설명하였습니다. 다양한분야의방대한자료들중에서필요한것만골라요약하다보니두서없이써내려간부분도많이있습니다. 읽으시는분들의양해를부탁드립니다. 이번에도역시기존에나와있는여러문서들을짜집기하는형태로작성되었으며기술하지못한원문저자들에게매우죄송할따름입니다. 본문서는읽으시는분들이어느정도 Windows API를알고있다는가정하에쓰여졌습니다. 본문서에서자세히다루지못한내용은참고자료를살펴보시기바랍니다. 제시된코드들은 Windows XP Professional Service Pack 2, Windows Server 2003 SP1 DDK 1 에서테 스트되었습니다. 문서의내용중틀린곳이나수정할곳이있으면연락해주시기바랍니다. 1 http://www.microsoft.com/whdc/devtools/ddk/default.mspx 1
목차 1. WINDOWS SYSTEM의구조...5 2. NATIVE API...6 3. SYSTEM SERVICE DESCRIPTOR TABLE...13 4. DEVICE DRIVER 기초및활용...17 5. SSDT HOOK...35 6. 추가...51 7. 참고자료...52 2
그림목차그림 1. SIMPLIFIED WINDOWS ARCHITECTURE 5 그림 2. WINDOWS ARCHITECTURE 6 그림 3. NTQUERYDIRECTORYFILE() API 7 그림 4. ZWQUERYDIRERCTORYFILE() API 8 그림 5. NATIVE API 진입단계 9 그림 6. NTDLL LOAD 10 그림 7. NTDLL LOAD에성공한화면 10 그림 8. SOFTICE로진입한화면 11 그림 9. FINDFIRSTFILE() AND FINDNEXTFILE(), -INSIDE WINDOWS ROOTKITS에서발췌- 15 그림 10. SSDT HOOKING, -INSIDE WINDOWS ROOTKITS에서발췌- 16 그림 11. PROTECTION RINGS 18 그림 12. SEGMENT DESCRIPTOR 19 그림 13. DUMP GDT 20 그림 14. SOURCES 22 그림 15. MAKEFILE 22 그림 16. HELLOWORLD.C 22 그림 17. BUILD 23 그림 18. BUILD 결과 23 그림 19. 드라이버 LOAD 24 그림 20. DEBUG MESSAGE 24 그림 21. SOURCE 24 그림 22. MINIMAL.H 25 그림 23. MINIMAL.CPP #1 25 그림 24. MINIMAL.CPP #2 26 그림 25. MINIMAL.CPP #3 27 그림 26. DEBUG MESSAGE 27 그림 27. NEWMINIMAL.H 28 그림 28. NEWMINIMAL.CPP #1 28 그림 29. NEWMINIMAL.CPP #2 29 그림 30. NEWMINIMAL.CPP #3 30 그림 31. DEBUG MESSAGE 30 그림 32. LASTMINIMAL.H 31 그림 33. LASTMINIMAL.CPP #1 32 그림 34. NTDDK.H 33 그림 35. LASTMINIMAL.CPP #2 34 3
그림 36. KESERVICEDESCRIPTORTABLE DUMP(D) 35 그림 37. KESERVICEDESCRIPTORTABLE DUMP(DD) 36 그림 38. DUMP ADDRESS OF KISERVICETABLE 36 그림 39. DUMP ADDRESS OF NTACCEPTCONNECTPORT 36 그림 40. KESERVICEDESCRIPTORTABLE DUMP(DD) 37 그림 41. DUMP ADDRESS OF SSPT 37 그림 42. 인덱스로 0X25를넘김 37 그림 43. 0X8056F136이 NTCREATEFILE API의주소 38 그림 44. NTCREATEFILE API의주소임을확인 38 그림 45. SYSTEM SERVICE NUMBER TO SYSTEM SERVICE TRANSLATION 39 그림 46. KESERVICEDESCRIPTORTABLE 선언 39 그림 47. SSDT 구조체추가 40 그림 48. SYSTEMSERVICE 매크로 40 그림 49. ZWCREATEFILE의주소 41 그림 50. 0X804FF558 메모리의내용 41 그림 51. 매크로결과확인 42 그림 52. 804FF559 메모리덤프 42 그림 53. SYSTEMSERVICE 매크로사용법 42 그림 54. BUFFERED I/O 45 그림 55. DIRECT I/O 46 그림 56. MDL 구조 46 그림 57. MDL 구조체의원형 47 그림 58. MDL을이용한 WRITE PROTECTION 제거 47 그림 59. INTERLOCKEDEXCHANGE 48 그림 60. NEWZWWRITEFILE 49 그림 61. ZWWRITEFILE HOOKING MESSAGE 50 4
1. Windows System 의구조 먼저 Windows System 에대해알아봅니다. 이문서를읽으시는많은분들이이미해당내용을잘알 고계실테지만만약을위해간략하게설명해봅니다. Windows는유저모드 (User Mode) 와커널모드 (Kernel Mode) 로구분되어프로세스가실행됩니다. 커널모드에서동작하는프로세스만이컴퓨터에장착된모든메모리와하드웨어에대한직접적인접근이가능하며디바이스드라이버가가장대표적인예라고할수있습니다. 일반적인윈도우프로그램의경우유저모드프로세스가작동하는것이므로직접하드웨어장치나메모리에접근할수는없습니다. 하드웨어장치나메모리에접근하기위해유저모드프로그램은시스템서비스 (API라고이해하시면될듯합니다.) 를호출하게되고운영체제는 trap을발생시켜커널모드로의스위칭을위한스레드를호출하게됩니다. 이후이스레드에서하드웨어장치또는메모리에접근하게됩니다. 이를 context switch( 공룡책에는문맥교환이라고번역되어있습니다.) 라고합니다. 정확한시점은알수없지만 (-_-;) 어느시점이후부터최근에제작되는 OS는대부분마이크로커널의형태를가집니다.( 좀더엄밀하게말해서수정된마이크로커널의형태를가집니다.) 운영체제에서프로세스스케쥴링이나메모리관리등과같은가장핵심적이고필수적인부분만커널에구현이되어있고기타기능은서브시스템에서관리하는형태를띄게됩니다. 아래그림 1은 Windows Internal에서발췌한간단한형태의 Windows 구조입니다. 그림 1. Simplified Windows architecture 5
2. Native API 위에서도잠시설명했지만유저모드프로세스는하드웨어장치나메모리에접근하기위해커널모드로스위칭을해야합니다. 일반적으로사용하는 Win32 API는이러한기능을자동으로수행하는데사실 Win32 API는운영체제에서제공하는서브시스템중의하나입니다. 그림 2. Windows Architecture 위의그림 2 에서살펴볼수있듯이 Win32, OS/2, Wow, POSIX 등은모두서브시스템환경의일부이 6
며이는운영체제가다른기종에서작성된프로그램들의호환을위해제공하는것들입니다. 각각의서브시스템들은커널모드에서동작하는서비스 ( 시스템서비스라고합니다.) 들을이용하여하드웨어장치나메모리에접근하게되며이때사용되는커널모드서비스들은 Native API라고불리는함수들을호출하여이러한작업들을수행하게됩니다. 조금더구체적으로알아봅시다. Microsoft는보안을이유로유저모드에서동작하는코드가직접커널모드에서동작하는코드를 call 하는것을허용하지않습니다. MFC 개발자들이디렉토리구조를알기위해서자주사용하는 FindFirstFile(), FindNextFile() 함수가있습니다. FindFirstFile() 로디렉토리를찾은후디렉토리내모든구조를 FindNextFile() 이찾게됩니다. 프로그래머는단순히위의두 API를호출함으로써임의의디렉토리구조를알수있지만실제운영체제내부에서는더많은작업들이수행되게됩니다. 즉, 유저모드코드 FindNextFile() 은 NtQueryDirectoryFile() 이라는 Native API를호출하고 NtQueryDirectoryFile() 은동일한이름의커널모드코드의 Wrapper 역할을함으로써디렉토리구조를얻을려는목적을이루게됩니다. Visual Studio를설치하면기본적으로설치되는 Dependency Walker을이용해 ntdll.dll을열어보면동일한이름을가진 (Nt, Zw로구분되는 ) 함수가존재함을볼수있습니다. 그림 3. NtQueryDirectoryFile() API 7
그림 4. ZwQueryDirerctoryFile() API Native API는운영체제에서사용하기위해만들어진것이며대부분이문서화되어있지않습니다. 하지만수많은해커들의노력에의해많은수의 Native API가 Undocument API 라는이름으로문서화되기에이르렀습니다. Native API들은보통 Nt 또는 Zt 로시작하는이름을가집니다. 유저모드에서동작하는프로그램이 Native API를이용하려면 ntdll.dll 파일을통해야만하며 ntdll.dll 파일은모든시스템서비스들의진입점을포함하고있습니다. MSDN에따르면 Zw로시작하는함수의경우해당함수를호출한프로세스에대한접근권한체크를하지않습니다. 좀더자세한설명은아래의 URL을참고하시기바랍니다. http://msdn.microsoft.com/library/default.asp?url=/library/enus/kernel_r/hh/kernel_r/k111_80b1882a-8617-45d4-a783-dbc3bfc9aad4.xml.asp 이과정은그림 5 와같습니다. 8
그림 5. Native API 진입단계 위의그림 5과같이 ntdll.dll은커널모드로진입하기위해인터럽트를사용합니다. INT 2E는 XP 이전버전의운영체제에서사용되던방법이며성능상의이유로 XP부터는 SYSENTER을사용합니다. INT 2E 는소프트웨어인터럽트, SYSENTER는하드웨어인터럽트라고생각하시면될듯합니다. 이내용은다음절에서다루도록하겠습니다. 그럼실제함수는어떻게동작하도록되어있는지살펴봅시다. 위에서잠깐언급했었던 NtReadFile을예로들어보도록하겠습니다. 이작업은드라이버스튜디오또는 WinDbg가준비되어있다면가능합니다. 본문서에서는드라이버스튜디오를이용하였습니다. 여기서부터는 Devguru의 Attack Native API 의내용을참고하였음을미리밝힙니다. 먼저유저모드에서의함수동작을알기위해 Softice를실행시키기전 NtReadFile 함수가들어있는 ntdll.dll을로드해야합니다.( 앞에서도이야기했지만 ntdll.dll 파일에유저모드에서커널모드로의진입점이존재합니다 ). 이함수는디폴트로로드되지않습니다. 아래그림 6은 Driver Studio에포함된 Symbol Loader 프로그램을이용해 ntdll.dll 을로드하는화면을캡쳐한것입니다. File LoadExports 를클릭하여 ntdll.dll 을로드합니다. 9
그림 6. ntdll Load 로드에성공하면아래그림 7 에서빨간색부분처럼 ntdll 부분이생성됩니다. 그림 7. ntdll Load 에성공한화면 이제 Softice 를실행시킵니다. Softice 가성공적으로실행된후 Ctrl + D 를누르면 SoftIce 화면으로 진입하게됩니다. 10
그림 8. Softice 로진입한화면 위의그림 8 에서빨간색으로표시된부분이명령창입니다. Vmware 를통한이미지캡쳐라실제명령 어를수행한결과화면은캡쳐할수없어서텍스트형태로결과를보이도록하겠습니다. 먼저명령어창에 u ntdll!ntreadfile 을입력한결과입니다. : u ntdll!ntreadfile ntdll!ntreadfile 11
mov eax, 000000b7 mov edx, ntdll!kifastsystemcall call edx ret 0024 : u ntdll!kifastsystemcall ntdll!kifastsystemcall mov edx, esp sysenter eax 레지스터에 NtReadFile 의주소인 0xb7 값을넣고 KiFastSystemCall 을호출합니다. KiFastSystemCall 에서는 edx 에 esp 값을넣은후 SysEnter 을호출하게됩니다. 다음으로 ZwReadFile을살펴보겠습니다. 명령창에 u ntdll!zwreadfile을입력하면 NtReadFile 값이나오는것을확인할수있습니다. 즉, Wrapper인 ntdll을통한 ( 유저모드에서의 ) NtReadFile, ZwReadFile은결국 NtReadFile 함수를호출하게됩니다. 유저모드에서는커널모드함수인 ZwReadFile을호출하더라도결국 NtReadFile을호출하게된다는말입니다. 커널모드 (Ntoskrnl.lib) 에서의 NtReadFile, ZwReadFile 은모두정상적으로해당함수를실행합니다. 명령창에서직접 u ntoskrnl!ntreadfile, u ntoskrnl!zwreadfile 을실행해보시기바랍니다. 12
3. System Service Descriptor Table Windows는수많은테이블들의집합이라고할수있습니다. 앞에서살펴보았듯이인터럽트가발생하면해당인터럽트의종류에따른결과를반환하게됩니다. 인터럽트가발생했을때어떤시스템서비스가호출되어야하는지에대한판단을하기위해윈도우는테이블을참조하게됩니다.( 정확히얘기하면 CPU는해당시스템서비스들이위치하고있는메모리상의주소를알아야할필요가있습니다. 하지만모든주소를내부적으로저장할수없기때문에 CPU는테이블이라는자료구조를사용하는것이며윈도우, 즉운영체제는이를이용하는것입니다. 좀더자세하고정확한내용은 Windows Internals를참고하시기바랍니다.) 이런테이블들 (CPU가참조하는 ) 에는여러종류의테이블이있으며아래와같습니다. GDT : Global Descriptor Table LDT : Local Descriptor Table IDT : Interrupt Descriptor Table 이런테이블들을통칭하여 System Service Descriptor Table이라고부릅니다. 운영체제또한자신만의테이블을만들어참조하는방법을사용하는데운영체제에서구현된중요한 테이블중에아래의테이블이있습니다. SSDT : System Service Dispatch Table 바로이문서의목표인 SSDT 테이블입니다. 이테이블은시스템에서이용가능한모든시스템서비스들의주소를가지고있으며인터럽트발생시운영체제는이테이블을참고하여적절한결과값을돌려주게됩니다. 이테이블을간단히조작 / 변경함으로써시스템의모든서비스를다룰수있기때문에커널루트킷, 안티바이러스프로그램양쪽모두에사용됩니다. 커널루트킷의경우에는프로세스 / 파일 / 디렉토리은닉등을위해사용됩니다. SSDT 를이용한후킹은 GDT, IDT 등과복합형태의후킹코드로서이용되기도합니다. 여기서앞절에서잠시나왔던 INT 2E와 SYSENTER을알아봅시다. 커널모드의 System Call을위해 Intel 기반하에서 Windows는 INT 2E 또는 SYSENTER을이용합니다. Windows 2000과이전버전의 Windows는유저모드에서커널모드로진입하기위해소프트웨어인터럽트인 INT 2E를이용했습니다. 인터럽트가발생하면 0x2E 값은 Interrupt Descriptor Table(IDT) 에서 Processor가참조하는 offset 값이됩니다.( 일반적으로 EAX 레지스터에들어가는값들은대부분 Offset으로쓰입니다.) 이 Offset이가리키는테이블의값은 System Service Dispatcher( 일반적으로 KiSystemService라고합니다.) 의주소입니다. CPU는 Instruction Pointer Register(IP Register) 에이값을 Load하고 System Service Dispatcher가실행되게됩니다. System Service Dispatcher는 System Service Dispatch Table(SSDT) 에기록된 System Service를실행하게되며 ( 실행할 System Service의 13
구분은 EAX 레지스터를참조하며 EDX 레지스터에서시스템서비스에전달된매개변수의목록 - 메모 리주소 - 을참조합니다. Windows NT 의경우 EDX 레지스터가아니라 EBX 레지스터를사용합니다.) 유저모드에서호출된 API 가재호출되게됩니다. 즉, 커널모드에서 API 가수행되게됩니다. Windows XP에서는 INT 2E 대신 SYSENTER이라고하는 Instruction을사용하게됩니다. INTEL은 SYSENTER Instruction을 CPU Instruction Set에포함시킴으로써성능상의향상을꾀했습니다. 이 Instruction은펜티엄 2 이상의 CPU에서지원하기시작하였으며 Windows 2000에서는 INT 2E와 SYSENTER을같이사용했으나 XP부터는 SYSENTER만을사용하게됩니다. 윈도우는이 Instruction을지원하기위해부팅할때명령과연결된레지스터에서커널의 System Service Dispatcher 루틴의주소를저장합니다. ntdll.dll이 SYSENTER Instruction을발생시키면 Processor는 IA32_SYSENTER_EIP 라는특수 Register 에저장된값, 즉 System Service Dispatcher의주소를체크하여 IP Register에 Load 하고실행시킵니다. INT 2E와마찬가지로실행할 System Service의구분은 EAX 레지스터에저장된값을참조하며전달되는매개변수의정보는 EDX 레지스터를참조합니다. 유저모드로되돌아가기위해서는일반적으로 SYSEXIT Instruction을사용합니다. AMD 의경우 SYSCALL 이라는 SYSENTER 과비슷한 Instruction 을사용하는데이는생략하도록하겠습 니다. 더자세한정보는 Windows Internals 와참고자료 web)5 를살펴보시기바랍니다. 위에서설명한과정을도식하면아래와같습니다. 14
그림 9. FindFirstFile() And FindNextFile(), -Inside Windows Rootkits 에서발췌 - 본문서에서다루고자하는 SSDT Hooking 의목적은바로아래그림으로써표현될수있습니다. 15
그림 10. SSDT Hooking, -Inside Windows Rootkits 에서발췌 - 위의그림 10에서볼수있듯이 Rootkit을중간에삽입시키는것이본문서의목적이라고할수있겠습니다. 자그럼어떻게 Rootkit을삽입시키는가, 아니그전에어떻게 SSDT를후킹하여우리가만든 Rootkit 으로향하게하는가, 그것이해결해야할가장중요한문제이며그답은디바이스드라이버에있습니다. 16
4. Device Driver 기초및활용 자디바이스드라이버시간입니다. 기존의 SDK 프로그래밍에익숙한사용자라면약간어색할듯하고프로그래밍에자신이없는분이라면매우어려울듯합니다. 몇년전만해도윈도우드라이버프로그래밍은윈도우프로그래밍의꽃이다라고생각되었었습니다.( 저만의견해인가요? -_-;). 자료라고는그저 MSDN 밖에없던시절, 극악무도또는진짜독한사람들만이하는분야라고취급되었었습니다. 그만큼어렵다는이야기입니다. -_-; 그러던것이시간이흘러한글로된윈도우디바이스드라이버책도나올만큼대중화 (?) 되었습니다만여전히그난해함은있습니다. 이번절은정말정말필요한부분만을모아놓았으니심화학습을원하시는분들은드라이버관련전문 서적을읽으시고정말당장써먹을수있는게필요해 ~ 하시는분들은 RootKits 1 를읽으시길바랍니다. 자이제시작해보겠습니다. 먼저 RING에대한설명으로시작해야할것같습니다. 앞절에서유저모드와커널모드에대한설명이있었습니다. 이것은운영체제에의한분류 (?) 법이라고할수있습니다. INTEL CPU에서는 ( 이하 x86) 이를 Ring이라고부르며우리가잘알고있는유저모드는 Ring 3, 커널모드는 Ring 0를나타내게됩니다. x86 에서지원하는 Ring 모델의구조는아래그림 11 과같습니다. 1 Rootkits: Subverting the Windows Kernel 17
그림 11. Protection Rings 그림 11에서볼수있듯이 x86은총 4개의 Ring을이용하여 Access Control을지원합니다. 실제이 Ring의구분은숫자 0~3으로구분되며물리적으로 Ring 모양과는전혀상관이없습니다. 네개의 Ring 중에서 Ring 0가가장큰권한을, Ring 3이가장낮은권한을가지게됩니다. 4개의 Ring 중 Windows는 Ring 0 와 Ring 3의두개만을사용하며 Ring 0를커널모드, Ring 3을유저모드라고부릅니다. 즉 Windows의모든커널코드들은 Ring 0의권한을가지고실행되는것입니다. Ring 0와 Ring 3에서실행되는프로그램들의접근제한규칙은아래와같습니다. 특권수준이낮은코드세그먼트가특권수준이높은데이터세그먼트로액세스하는것은불가능 특권수준이낮은코드세그먼트에서특권수준이높은코드세그먼트로의제어이행은특별한방법을사용해서만이가능 특권수준이높은코드세그먼트에서특권수준이낮은코드세그먼트로의제어이행은불가능 - Windows 구조와원리그리고 Codes, 정덕영저- 앞에서도얘기했지만실제 Ring 을구분하는것은 0~3 까지의숫자입니다. 현재프로그램을실행하는특권레벨을결정하는것은코드세그먼트의특권레벨입니다. X86 보호모드에서실행되는프로그램의각코드세그먼트는세그먼트디스크립터라고불리는 8바이트의데이터구조체에의해기술됩니다. 세그먼트디스크립터는아래와같은구조를가지고있습니다. 18
그림 12. Segment Descriptor - 출처 : University of Nebraska-Lincoin Computer Science & Engineering CSCE 351- 그림 12에서 DPL(Descriptor Privilege Level) 부분이바로특권레벨을나타내는부분입니다. 운영체제는해당필드를참고하여커널모드, 유저모드를구분하게됩니다. DPL 중현재실행되고있는코드의 DPL 값을 CPL(Current Privilege Level) 이라고부릅니다. 세그먼트디스크립터는메인메모리에저장되며이디스크럽터들을디스크럽터테이블이관리하게됩니다. 메인메모리에는두개의중요한테이블이있는데그것이바로앞절에서살펴본 GDT 와 LDT d입니다. 즉 GDT와 LDT에세그먼트디스크럽터에대한정보가있습니다. GDT, LDT 의두개의테이블이있기때문에어떤테이블에서찾아야할것인지를알필요가있습니다. 이를위해세그먼트셀렉터 (Segment Selector) 라고하는것이존재합니다만이에대한설명은넘어가도록하겠습니다. 실제 GDT는어떤값을가지고있는지 WinDBG에서확인을해보도록합시다. Dump를위해 ProtMode 1 라는 WinDBG 확장 dll 파일을이용했습니다. ProtMode.dll 파일을 C: Program Files Debugging Tools for Windows winxp 에복사합니다.(Debugging Tools for Windows 가설치되어있어야합니다.) WinDBG에서먼저 ntsdexts.dll 파일을 load 한후 ProtMode.dll을 load 합니다. 그리고!ProtMode.Descriptor GDT 1 를입력하면 GDT 를확인할수있습니다. 1 ProtMode : http://www.codeguru.com/dbfiles/get_file/protmode.zip 19
그림 13. Dump GDT 그림 13 은 GDT 를 WinDBG 에서확인한모습입니다. 커널모드에서작동하는프로그램의경우에는 DPL 부분이 0, 유저모드에서작동하는프로그램의경우에는 DPL 부분이 3 으로표시됩니다. 저의경 우는 VMWARE 를이용한커널디버깅모드로동작중이기때문에 DPL 이 0 으로되어있습니다. Ring에의한메모리접근제한과더불어 x86에서제공하는다른하나의보안장치가있습니다. 몇몇명령어들의경우특권을가지고있는것으로간주되어오직 Ring 0에서만실행됩니다. 이명령들은일반적으로 CPU의행동을수정하거나직접하드웨어에접근하기위해사용됩니다. 그명령어들중일부는아래와같습니다. cli : 현재 CPU에서실행중인인터럽트를중지시킵니다. sti : 현재 CPU에서인터럽트를실행시킵니다. in : 하드웨어포트로부터데이터를읽어들입니다. out : 하드웨어포트로데이터를씁니다. - RootKits - 위와같은명령어들이있다정도만알아두시면될듯합니다. 디바이스드라이버를설명하기위해서필수적인 Ring Model에대한설명이여기까지입니다. 디바이스드라이버란위에서설명한유저모드 커널모드스위칭을하지않고직접적으로하드웨어에접근하는소프트웨어라고말할수있습니다. 즉커널모드에서동작하며하드웨어에직접접근할수있는프로그램입니다. 윈도우는이런디바이스드라이버를설계하기쉽도록하나의모델을제시하고있는데이것이 WDM(Windows Driver Model) 이라고합니다. 다만모든운영체제가 WDM 방식을지원하는것은아니 20
며모든드라이버가 WDM 방식을사용해서개발할수있는것도아닙니다. WDM 의역사, 문제, 특징등과같은자세한내용은디바이스드라이버서적을참고하시기바랍니다. 디바이스드라이버프로그램은실행프로그램이아닙니다. 즉확장자가 exe 나 dll 이아니라 sys 입니다. 개발된디바이스드라이버프로그램을사용하기위해서는해당드라이버를 LOAD 시켜줘야만합니다. 리눅스에서의모듈이라고생각하시면훨씬더이해가쉬울듯합니다. 디바이스드라이버의기본골격은아래의 4가지엔트리로구성됩니다. DriverEntry Routine AddDevice Routine IRP Dispatch Routine DriverUnload Routine - 디바이스드라이버구조와원리그리고제작노하우, 이봉석 - 간단히정말간단히필요한것들만설명하겠습니다. 디바이스드라이버전문서적참고를권고합니 다. :) * 디바이스드라이버코드를예시함에있어서커널모드드라이버형태또는 WDM 형태로구현될수있으며이차이점에대해서는상세히기술하지않았습니다. 표현의차이만있을뿐원리는동일합니다. 디바이스드라이버분야는넓고도방대해서일일이설명하려면무지막지한시간이걸리는관계로 ( 본인도잘모른다는게큰이유입니다 -_-;) 대충대충넘어가겠습니다. DriverEntry Routine 일반 C 프로그램의시작점은 main(), 윈도우응용프로그램은 WinMain(), dll 프로그램은 DllMain() 그리고디바이스드라이버의시작점은 DriverEntry입니다. 즉드라이버가로드된후처음으로실행되는곳이바로이부분입니다. 이부분은처음드라이버가로드될때단한번만수행하는부분이므로주로초기화과정에많이사용됩니다. AddDevice Routine 자신이만든새로운디바이스를추가하고자할때사용되는부분입니다. 이부분에서 Device Object 를생성하게됩니다. IRP Dispatch Routine IRP는디바이스와 IO Manager 사이에서명령을전달하는역할을하는구조체이며그실체는하나의버퍼에불과합니다. 유저모드프로그램에서파일을열고쓰는동작같은것이 IRP를통해서이루어지게됩니다. 21
DriverUnload Routine 이름그대로드라이버를언로드할때수행되는부분이며이것저것수행한것들을깨끗하게정리하는 부분으로사용됩니다. 먼저만인의연인 Hello World 를작성해보도록하겠습니다. 빌드를위해서는소스파일외에도 Makefile 과 Source 파일이필요하게됩니다. 역시구체적인것은전문서적을참고하시고아래의화면대로입력하시기바랍니다. 그림 14. Sources 그림 15. Makefile 그림 16. HelloWorld.c 위의세개파일을작성한다음 DDK 를설치하면생기는 Windows XP Checked Build Environment 를실행한다음해당파일들이있는디렉토리로이동합니다. 그리고아래그림 17 처럼컴파일을하고 나면 HelloWorld.sys 파일이생성됩니다. 22
그림 17. build 그림 18. build 결과 이제작성한프로그램을로드하고메시지를볼수있는프로그램이필요합니다. 드라이버를로드하는 방법은두가지가있는데그방법은역시서적을참고하시길바랍니다. 본문서에서드라이버를로드하는툴은 Rootkit.com 1 의 InstDrv 를, 메시지를보기위해서는 sysinternals 2 의 Debugview 프로그램을사용했습니다. 1 http://www.rootkit.com 2 http://www.sysinternals.com 23
그림 19. 드라이버 Load Install -> Start -> Stop -> Remove 버튼을차례로누르면 DebugView 화면에 HelloWorld.c 파일에서 작성한 DbgPrint 함수가출력하는메시지를확인할수있습니다. 그림 20. Debug Message HelloWorld.c 파일을보시면알겠지만위에서구현된것은 DriverEntry 에진입했을때와 Unload 시에 대한것뿐입니다. 이제여기에조금더살을붙여보도록하겠습니다. 그림 21. Source 24
Source 파일은 TARGETNAME 과 SOURCES 부분만변경되었습니다. 그림 22. Minimal.h Minimal.cpp 는조금더많은부분이추가되었습니다. 하나씩보겠습니다. 그림 23. Minimal.cpp #1 CreateDevice 함수가하나더추가된것을볼수있습니다. DriverEntry 부분은 CreateDevice 를호출 하는부분이추가되었습니다. 아래는 CreateDevice 함수입니다. 25
그림 24. Minimal.cpp #2 이부분은 WDM에서는 AddDevice 함수가처리합니다. 디바이스드라이버는, 즉커널내부에서는모두유니코드를사용합니다. 생성할디바이스의이름을유니코드로변환하는부분이 CreateDevice함수의초반부분입니다. 중간쯤에가면 IoCreateDevice 함수를이용해서디바이스를생성하게됩니다. 이 API의자세한설명은전문서적을참고하시기바랍니다. 디바이스생성에성공하면해당디바이스를유저모드어플리케이션에서이용하기위해전역변수를만들어줘야합니다. 이를위해 Symbolic Link를만들게되며이를 IoCreateSymbolicLink API에서담당하게됩니다. 만약 Symbolic Link 생성에실패하게되면 IoDeleteDevice API를이용해생성한디바이스를삭제합니다. 이부분은왜이렇게해야하는지에대한명확한답변을해드리기어려우나아마도일종의안전장치로써사용되는것이아닌가짐작됩니다. Symbolic Link를생성하는방법에있어서 WDM 을이용하게되면 DriverExtension->AddDevice에서처리를하게됩니다만해당내용은생략하도록하겠습니다. CreateDevice 함수가완료되면디바이스드라이버객체가생성되고어플리케이션에서이를이용할수 26
있는 Symbolic Link 까지생성되게됩니다. 그림 25. Minimal.cpp #3 위의그림 25 는 DriverUnload 부분입니다. 드라이버를 Unload 할때 Symbolic Link 를삭제한후디바 이스를삭제합니다. 그림 26. Debug Message 작성한디바이스드라이버를설치하면위의그림 26 과같은화면을볼수있습니다. 자여기까지오면이제우리는 Windows System 에커널모드에서작동하는디바이스를생성할수있 게된것입니다. 다음예제는디바이스익스텐션 (Device Extension) 구조체가추가된것입니다. 디바이스익스텐션은 nonpaged 풀에존재하며디바이스의상태정보등을기록하는데쓰입니다. 즉, 디바이스전용이므로 디바이스익스텐션의구조체는헤더파일에정의되어있어야합니다. 27
아래는디바이스익스텐션구조체가추가된헤더파일입니다. 그림 27. NewMinimal.h 그림 27 에서디바이스익스텐션구조체변수중 PDEVICE_OBJECT 부분은반드시있어야하는부분 입니다. Linked List 에서 Next 포인터의역할을한다고생각하시면됩니다. 나머지는드라이버개발자 가필요한전용변수들을선언하면됩니다. 그림 28. NewMinimal.cpp #1 NewMinimal.cpp 의초반부입니다. CreateDevice 함수에전달되는변수가하나늘었습니다. 바로 uldevicenumber 입니다. 하나의드라이버는하나이상의디바이스를다룰수있습니다. 그러므로 uldevicenumber 변수는드라이버에서다룰디바이스를차례로참조할수있는매개체로써의역할을하게됩니다. 28
그림 29. NewMinimal.cpp #2 CreateDevice 에서는우선 PDEVICE_EXTENSION형 pdevext 변수가생긴것이눈에들어옵니다. 중간쯤에디바이스익스텐션구조체를초기화합니다. 디바이스의정보중 Device Name, Symbolic Link Name 등을저장하고있습니다. 추가로 status 의값에따른조건문하나를넣어놨습니다. 동일한이름의 Symbolic Link가존재할경우메시지를뿌립니다. NTSTATUS형이반환하는값은여러가지가정의되어있는데자세한것은 C: WINDDK 3790.1830 inc ddk wxp ntstatus.h 파일을참고하시기바랍니다. 29
그림 30. NewMinimal.cpp #3 DriverUnload 부분은형태가조금틀려졌습니다. pnextobj 가마지막노드에도달할때까지, 즉생성 된모든디바이스에대하여차례로 Symbolic Link 와 Device 를삭제합니다.(Linked List 라고생각하시 면됩니다.) Symbolic Link Name 은디바이스익스텐션에저장되어있는값을사용하고있습니다. DbgView 화면입니다. 그림 31. Debug Message 이번에는마지막으로디스패치루틴을넣어보겠습니다. 앞에서이야기했던디바이스드라이버의기본 골격은이디스패치루틴이들어감으로써완성되게됩니다. 30
그림 32. LastMinimal.h 헤더파일입니다. 앞에서지저분한소스를좀고칠겸이쪽으로몇개뺐습니다. 그림 32 에서볼수있듯이디바이스익스텐션구조체를간단하게수정했고 DispatchDummy 라는함 수가추가되었습니다. 31
그림 33. LastMinimal.cpp #1 그림 33은 DriverEntry 부분입니다. 기존소스에서따로분리해놓았던 CreateDevice를합쳐놓았습니다. 디바이스익스텐션초기화가간략하게된것을중간쯤에서확인할수있습니다. 마지막부분에 Dispatch 초기화부분이새로이추가된것이보입니다. MajorFunction 테이블의모든디스패치루틴에대해 DispatchDummy 함수를지정하고있습니다. MajorFunction 테이블은특정 I/O 요청에대한디스패치루틴의함수포인터들을저장하고있으며이런 I/O 요청은 IRP_MJ_XXX 형태의심볼로서정의되어있습니다. 실제로는상수값을가지고있는심볼입니다. Ntddk.h 또는 WDM.h 파일을살펴보시면상세히기술되어있습니다. 32
그림 34. ntddk.h IRP 함수코드값중몇가지만나열해보면아래와같습니다. IRP_MJ_CREATE : 핸들을요청합니다. CreateFile에대응됩니다. IRP_MJ_CLENUP : 핸들을닫을때지연된 IRP를취소시킵니다. CloseHandle에대응됩니다. IRP_MJ_CLOSE : 핸들을닫습니다. CloseHandle에대응됩니다. IRP_MJ_READ : 디바이스로부터데이터를받습니다. ReadFile에대응됩니다. IRP_MJ_WRITE : 디바이스로데이터를보냅니다. WriteFile에대응됩니다. IRP_MJ_SHUTDOWN : 시스템이셧다운될때사용됩니다. InitateSystemShutdown에대응됩니다. - 출처 : 원리와예제로배워보는 Windows 2000 디바이스드라이버 - 33
그림 35. LastMinimal.cpp #2 그림 35의주석에도달려있지만실제 DispatchDummy 함수는아무일도하지않는함수입니다. 해당함수가호출되면 status를 STATUS_SUCCESS로대입하고 IoCompleteRequest API를호출한뒤 status를 return 합니다. IoCommpleteRequest API는이름에서도알수있듯이해당요청이완료되었음을알리는역할을합니다. 마지막으로 DriverUnload 부분은 Symbolic Link 이름을지정하는부분만추가되었습니다. 이것으로써디바이스드라이버의기본골격은다다루어보았습니다. 디바이스드라이버항목의마지 막예제로써디바이스하나를생성하여읽고쓰는작업을하는드라이버를소개할까했으나실제우 리가할 SSDT Hooking 시에디스패치루틴은사용되지않으므로패 ~ 쓰하도록하겠습니다. 34
5. SSDT Hook 멀고먼길을돌아드디어 SSDT Hook으로다시진입하게되었습니다. 자이제실제코드를작성하기전에관련내용들을알아보도록하겠습니다. System Service Dispatcher, 즉 KiSystemService는 SSDT(System Service Dispatch Table) 을참고하여적절한 System Service를실행하게됩니다. 하지만이 SSDT, 즉 KiServiceTable은커널에의해익스포트되지않은구조체이므로원칙적으로는내부구조를알수없습니다. 하지만 KeServiceDescriptorTable이라는 ntoskrnl.exe에의해익스포트된구조체 (Service Descriptor Table) 내에 KiServiceTable에대한정보가들어있습니다. KeServiceDescriptorTable 구조체는아래와같습니다. typedef struct ServiceDescriptorTable { SDE ServiceDescriptor[4]; } SDT; SDT 구조체는아래와같습니다. typedef struct ServiceDescriptorEntry { PDWORD KiServiceTable; PDWORD CounterTableBase; DWORD ServiceLimit; PBYTE ArgumentTable; } SDE; KiServiceTable 에는서비스테이블의시작주소가들어있습니다. 이주소로부터 4바이트단위로각 System Service들의주소가들어가있습니다. 세번째필드 ServiceLimit는 SDT에등록되어있는 System Service의개수가들어있습니다. 마지막필드인 ArgumentTable에는각 System Service마다전달되는파라미터가몇바이트인지기록되어있습니다. 이주소로부터 1바이트단위로전달되는파라미터의바이트수가기록되어있습니다. ( 이를 SSPT-System Service Parameter Table라고합니다.) 아래는 KeServiceDescriptorTable 을 WinDBG 에서 Dump 한화면입니다. 그림 36. KeServiceDescriptorTable Dump(d) 35
그림 36에서 4바이트마다 SDE 구조체의구성요소를나타냅니다. 처음 4바이트 0x80503030이바로 ServiceDescriptor[0] 의내용을나타내며이주소는 KiServiceTable의주소를가리키고있습니다. 다음화면부터는편의를위해바이트순서가읽기쉽게표시되도록하였습니다.(dd 명령어를사용하면됩니다. 다시원래의화면으로보시길원한다면 db 명령어를사용하면됩니다.) 그림 37. KeServiceDescriptorTable Dump(dd) 그림 37 에서각줄은 SDE 구조체배열의구성요소들입니다. 그리고빨간색으로표시된부분이바로 KiServiceTable 의주소입니다. WinDBG 로해당주소의메모리를덤프하면아래와같은화면이나옵니다 그림 38. Dump Address of KiServiceTable 그림 38 에서빨간색으로표시한부분처럼각부분은 KiServiceTable 에저장된 System Service 들의주 소를나타냅니다. 0x8059a47e 주소의메모리를덤프하면아래와같습니다. 그림 39. Dump Address of NtAcceptConnectPort 첫번째줄을살펴보면 NtAcceptConnectPort 라는 API 임을확인할수있습니다. 동일한방법을이 용하여 ServiceLimit 개의 System Service 를확인할수있습니다. 자다시처음으로돌아가서위에서확인한 NtAcceptConnectPort Api 로넘겨지는파라미터의크기는 아래의그림 40 에서빨간색상자로표시된주소에저장되어있습니다. 36
그림 40. KeServiceDescriptorTable Dump(dd) 해당주소를덤프하면그림 41 과같은화면이보이는데그중가장첫번째바이트가파라미터의크 기를나타내게됩니다. 여기에서 NtAcceptConnectPort API 의파라미터크기는 0x18 바이트가됩니다. 그림 41. Dump Address of SSPT 보통하나의파라미터는 4바이트를차지하기때문에 NtAcceptConnectPort API는총 6개의파라미터를가짐을추측할수있습니다. 위의그림에서두번째빨간색상자안의숫자 0x20은 NtAcceptConnectPort 다음에 SDT에저장되어있는 API의파라미터크기를나타냅니다. 즉그림 5-3 에서두번째 4바이트 805e7664 주소에저장되어있는 API의파라미터크기를나타냅니다. 이와같은방법을이용하여 KeServiceDescriptorTable 구조체를통해 KiServiceTable에설정되어있는모든 API를살펴볼수있습니다. 만약특정 API를찾고싶은경우일일이서비스테이블주소를찍어보는것은매우많은시간이요구됩니다. 2장 Native API에서살펴보았듯이각 Zw로시작하는함수들은 eax 레지스터로인덱스번호를넘기게됩니다. 그러므로해당 API가위치하는주소는 KeServiceDescriptorTable의 Base 주소 ( 여기서는 0x80503030 입니다 ) 에인덱스 *4 를더한값이됩니다. 아래의그림 42 는 NtCreateFile API 를확인하는화면입니다. 그림 42. 인덱스로 0x25 를넘김 37
그림 43. 0x8056f136 이 NtCreateFile API 의주소 그림 44. NtCreateFile API 의주소임을확인 추가사항으로 SDT에는 KeServiceDescriptorTable 외에도 KeServiceDescriptorTableShaodw 라는배열이하나더포함됩니다. KeServiceDescriptorTable은 ntosrknl에서구현된서비스들을, KeServiceDescriptorTableShadow는 Win32k.sys에서구현된서비스들 (Windows USER, GDI) 을포함하고있습니다. 이를그림으로나타내면아래와같습니다. 38
그림 45. System Service Number to system service translation 출처 : Microsoft Windows Internals 4 th Edition 자이제본격적으로소스코드를보도록하겠습니다. 해당소스는 somma 1 님블로그에서제공된 bkdp3 소스와 HackerDefender 2 SDT Hooking 예제소스를 기반으로사알짝수정만가해진것임을미리알려드립니다. 먼저디바이스드라이버프로그램의골격을만듭니다. 위에서만들었던 LastMinimal을통째로복사해오면되겠습니다. 드라이버가로드되고가장처음해야할일이무엇일까요? 우리의목적은 SSDT에저장되어있는 API의주소를우리가만든함수로바꿔치기하는것입니다. 즉먼저 SSDT 에접근할수있어야만합니다. 그래야테이블내우리가후킹하기원하는 API의주소를바꿔쓸수있기때문입니다. 5장처음에서살펴보았듯이 Kernel에의해 export 된 KeServiceDescriptorTable 변수를통해 SSDT에접근할수있으므로다음과같이선언하면됩니다. 그림 46. KeServiceDescriptorTable 선언 1 http://somma.egloos.com 2 http://hxdef.org/ 39
그리고역시앞에서살펴보았던 SSDT 구조체의원형을선언해줍니다. 그림 47. SSDT 구조체추가 여기까지오면우리는 KeServiceDescriptorTable 이라는변수를통해 SSDT 구조체까지접근할수있게됩니다. 어떤 API를후킹할지는이제입맛에따라고르기만하면됩니다. 쉽게테스트해볼수있도록 ZwWriteFile을후킹해보도록하겠습니다. 짐작하시다시피파일쓰기를하면에러메세지를뱉어내도록할것입니다. ZwWriteFile이 SSDT에서몇번째테이블에저장되어있는지를먼저알아봅니다. 위의그림 43, 44에서살펴본것과동일한방법으로찾아보면 NtWriteFile의주소는 0x805730a0 임을알수있습니다. 이렇게... 무식하게하는방법이하나있고좀더코드가우아하게보이도록하려면아래와같이정의하여사용할수있습니다. 그림 48. SYSTEMSERVICE 매크로 자위의매크로를한번살짝뜯어보겠습니다. 40
먼저 SYSTEMSERVICE 매크로는함수이름을받아서해당함수가위치하는메모리의주소를반환합니다. 앞에서도살펴보았듯이 KeServiceDescriptorTable.ServiceTableBase[ 함수의인덱스번호 ], 이렇게함수의주소로접근할수있습니다.( 위에서넘겨주는함수는 ZwXXX, 리턴값은 NtXXX입니다 ) _func로 ZwCreateFile을넘겨주면해당 API가저장되어있는주소가넘겨집니다. 4장에서작성한디바이스드라이버예제에서 ZwCreateFile을 DbgPrint로찍어보시면확인할수있습니다. 아래그림 49는의심많은분들을위한확인화면입니다ㅎㅎ ; 그림 49. ZwCreateFile 의주소 [DbgPrint("ZwCreateFile Address : [%x] n n", ZwCreateFile)] ZwCreateFile 을찍어보면 0x804ff558 이나옵니다. 해당메모리의내용은아래와같습니다. 그림 50. 0x804ff558 메모리의내용 위의그림 50에서 ZwCreateFile이시작되는메모리는 move eax, 25h 라는내용을포함하고있는것을알수있습니다. 여기에서규칙적인패턴을발견할수있는데모든 ZwXXX의시작은 move eax, index가된다는것입니다. 그러므로매크로에서 ((PUCHAR)ZwWriteFile+1) 이부분은 ZwWriteFile의메모리시작주소의다음 4바이트, 즉 Index 값을가리키게됩니다.(PUCHAR은 unsigned char형입니다 ) 결국 SYSSTEMSERVICE 매크로는 KeServiceDescriptorTable.ServiceTableBase[*(PULONG)( 함수의인덱스 4바이트 )] 가되며 NtXXX 함수가저장되어있는주소를반환하게됩니다. 자그럼이렇게가정한이론이맞는지실제로값을출력해보겠습니다. DbgPrint("ZwCreateFile Address : [%x] n n", ZwCreateFile); DbgPrint("(PUCHAR)ZwCreateFile + 1 Address : [%x] n n", (PUCHAR)ZwCreateFile+1); DbgPrint("*(PULONG)(PUCHAR)ZwCreateFile + 1 Address : [%x] n n", *(PULONG)((PUCHAR)ZwCreateFile+1)); 41
그림 51. 매크로결과확인 그림 51 의두번째결과 0x804ff559 주소의내용은인덱스번호 (0x25) 임을알수있습니다. 그림 52. 804ff559 메모리덤프 SYSTEMSERVICE 매크로는 ZwXXXX 함수주소를입력받아 NtXXXX 함수의주소를반환한다는것이 증명되었습니다. 다음으로 SYSCALL_INDEX 매크로는함수이름을받아서해당함수의 SSDT 내인덱스번호를리턴합 니다. 그러므로 NtXXXX 함수의주소를얻기위해아래그림 53 처럼사용하는것이가능합니다. 그림 53. SYSTEMSERVICE 매크로사용법 자이제우리는 SSDT 안에저장되어있는 ZwWrite 의주소에접근할수있게되었습니다. 그주소를우리가작성한 API가저장된주소로바꿔치기만하면되는데 XP이상에서 SSDT는 Read Only로되어있습니다. 이것은 Windows의메모리보호를위해사용된기법으로써 Write Protection이라고합니다. 이부분을제거해야만 SSDT에바꿔칠주소를쓰는것이가능해집니다. Write Protection 기법을우회하는방법은여러가지가있는데이문서에서는 CR0 레지스터를이용하는방법과 MDL(Memory Descriptor List) 을이용하는방법에대해서만설명하도록하겠습니다. 먼저 CR0 레지스터를이용하는방법입니다. CR0에서 Write Protection을제거하는방법과관련한설명은 somma 님블로그에자세히설명되어있습니다. 저의설명이들어가면원래의뜻을제대로전달하지못할수도있다는생각에해당부분을염치없이죽긁어왔습니다. somma 님의양해를구합니다. cr0 레지스터를이용한 Write Protection 제거 42
[ 그림 x86 의 Control register] 컨트롤레지스터는현재수행중인태스크의특성과프로세스의동작모드를결정짓는특별한레지스 터이다. 32 비트와 32 비트호환아키텍쳐에서이레지스터들은 32 비트이고, 64 비트에서는 64 비트 다. mov CRn 인스트럭션으로이레지스터들을건드릴수있고.. CR3 레지스터는페이지디렉토리를찾아가기위한레지스터이고.. 나머지는.. RTFM! CR0 는 CPU 의 operating mode 와상태를제어하는플래그를포함하고있다. 오홋.. ^^ PG, CD 등... 중요한플래그들이많지만일단관심대상은아니고.. ^^ WP Write Protect (bit 16 of CR0) Inhibits supervisor-level procedures from writing into user-level read-only pages when set; allows supervisor-level procedures to write into user-level read-only pages when clear. This flag facilitates implementation of the copy-on-write method of creating a new process (forking) used by operating systems such as UNIX*. Copy-On-Write 매커니즘과관련있는넘이었군.. 결국이플래그를조작하면 write protection 속성을바꿔치기할수있다는거다. 43
// // 콘트롤레지스터관련 (IA-32 manual vol3, ch 2.5 // CR0 (Control Register Zero) 레지스터의 WP 비트 (16) 는쓰기속성제어에사용됨 // #define CR0_WP_MASK 0x0FFFEFFFF VOID ClearWriteProtect(VOID) { asm { push eax; mov and mov pop eax, cr0; eax, CR0_WP_MASK; // WP 클리어 cr0, eax; eax; } } VOID SetWriteProtect(VOID) { asm { push eax; mov or mov pop eax, cr0; eax, not CR0_WP_MASK; // WP 비트세팅 cr0, eax; eax; } } 더자세한내용은 IA-32 메뉴얼의 4.1 섹션을참고하면된다. 요는페이지레벨의프로텍션을 en/disable 하기위해서는 PDE, PTE 의플래그와 CR0 의 WP 비트 를조작한단거다. Clear the WP flag in control register CR0. 44
Set the read/write (R/W) and user/supervisor (U/S) flags for each page-directory and pagetable entry. - 출처 : 윈도우쪼물딱거리기 (http://somma.egloos.com/2131561)- 두번째방법으로 MDL을이용하는방법입니다. MDL은 Direct I/O를이용해 buffer에접근할때에사용되는구조체입니다. 일반적으로사용자 buffer 와디바이스간의데이터를교환하는방법에는 Neither I/O, Buffered I/O, Direct I/O의세가지방법이있습니다. Neither I/O는직접찾아보시고 Buffered I/O는 I/O Manager가사용자 buffer와동일한 buffer를 nonpaged pool에할당한후데이터를 Swap 하는방법이고 Direct I/O는중간단계의 buffer 생성이생략되는것이라고할수있습니다. 즉 Buffered I/O에서는메모리복사가일어나고 (buffer에데이터를 swap), Direct I/O에서는일어나지않습니다. 드라이버는사용자 buffer가 page 되지않도록 lock을건후 MDL에기술된정보 ( 사용자 buffer의물리적주소 ) 를참고해서사용자 buffer에접근하게됩니다. 그림 54와 55는두방식의차이를그림으로표현한것입니다. 그림 54. Buffered I/O 45
그림 55. Direct I/O 우리가할일은 nonpaged pool 에 MDL 을생성하여그것이 SSDT 를가리키게하는것입니다. 물론생 성된 MDL 은쓰기가능하게설정되어있으므로해당 MDL 이가리키는실제물리적메모리영역, 즉 원래 SSDT 에도쓰는것이가능하게되는것입니다. MDL 의구조는아래그림 56 과같습니다. 그림 56. MDL 구조 MDL 의구조체는아래와같으며 ntddk.h 에정의되어있습니다. 46
그림 57. MDL 구조체의원형 MDL 을이용하여 SSDT 의 Write-Protection 을제거하는코드는아래와같습니다. 그림 58. MDL 을이용한 Write Protection 제거 위의소스를살펴보면먼저 MDL 구조체포인터변수를선언한뒤 (g_pmdsystemcall) 새로운 MDL을만들어서가리키게합니다. MmCreateMdl 1 이라는 API는이름에서도알수있듯이 MDL을생성하는기능을합니다. MSDN에서살펴보면해당 API는없어지고기존에제작된 Driver 바이너리를위해지원용도로만사용되고있으므로 IoAllocateMdl 2 라는 API를사용할것을권장하고있습니다. 그러나! 귀찮기때문에소스에서는그냥 MmCreateMdl을사용하도록하겠습니다. IoAllocateMdl의사용예제는여기 3 에서확인하실수있습니다. 다시소스로돌아가서 MmCreateMdl API로만들어진새로운 MDL을 nonpaged pool 메모리영역에생성하기위해 MmBuildMdlForNonPagedPool 4 API를사용합니다. 여기까지오면 nonpaged pool 메모리영역에 SSDT를가리키는, 마치심볼릭링크같은것이하나생성되게됩니다. 이제 MdlFlags에 MDL_MAPPED_TO_SYSTEM을추가함으로써쓰기가가능하게되었 1 MmCreateMdl : http://msdn2.microsoft.com/en-gb/library/ms801972.aspx 2 IoAllocateMdl : http://msdn2.microsoft.com/en-gb/library/aa490866.aspx 3 여기 : http://bbs.driverdevelop.com/htm_data/16/0105/710.html 4 MmBuildMdlForNonPagedPool : http://msdn2.microsoft.com/en-gb/library/ms801996.aspx 47
습니다. MDL_MAPPED_TO_SYSTEM 은구글님께물어봐도속시원한대답이없어서그냥쓰기가능 하게해주는변수인가보다.. 라고생각하고넘어가겠습니다. -_-; 마지막으로 MmLockedPages 1 API 로생성된 MDL 의물리메모리주소를얻어옵니다. 두번째인수로 는 KernelMode, UserMode가있는데 MSDN에도자세한설명이없고그냥 대부분의드라이버에서는 KernelMode를사용한다 라고만되어있습니다. 두번째인수 AccessMode가 KernelMode일경우 MDL이가리키고있는메모리의유저모드공간주소를구할때, UserMode는그반대의경우일때사용됩니다. 여기까지오면이제우리는 MDL을통해시스템의 SSDT를읽고쓰기가능하게됩니다. 이제후킹하기원하는임의의 API를우리가만든 API의주소로바꿔치기하는과정만이남았습니다. 그림 59. InterlockedExchange 그림 59 에서볼수있듯이 InterlockedExchange 2 라는 API 가사용되었습니다. 원래 InterlockedXXX 함수들은멀티스레드환경에서동기화를위해사용되는함수들인데 InterlockedExchange API의경우인수로전달받은두값을비교하여같지않다면첫번째인수 Target의값을두번째인수 value로바꿉니다. 그리고리턴값으로 Target의값을반환합니다. 그림 59는 ZwWriteFile라는원래의 API를 NewZwWriteFile, 즉우리가작성한 API로바꿔치기하는코드입니다. 마지막으로 NewZwWriteFile을살펴보겠습니다. 1 MmLockedPages : http://msdn2.microsoft.com/en-gb/library/ms801970.aspx 2 InterlockedExchange : http://msdn2.microsoft.com/en-us/library/ms683590.aspx 48
그림 60. NewZwWriteFile NewZwWriteFile 의기능은단순합니다. 원래의 ZwWriteFile 를호출하여정상적으로기능이작동하도록하는데만약 FDel.exe 라는프로그램이 ZwWriteFile 을발생시켰다면특정디버그메시지를출력하도록하였습니다. ZwWriteFile 을발생시킨프로세스의이름확인을위해 PsGetCurrentProcess 1 API 를이용하여현재프 로세스의 _EPROCESS 구조체를얻어온후 ImageFileName 필드를참조하여프로세스의이름을알아냅니다. _EPROCESS 구조체는프로세스를나타내는구조체인데, 설명하려면약간복잡합니다. 따로찾아보시길바랍니다. 해당구조체를포함시키기위해 IFS에서사용되는 ntifs.h 헤더파일을포함시켰습니다. 해당헤더파일에이미 SSDT에대한구조체가정의되어있어컴파일시중복에러를발생시킵니다. ntifs.h 헤더파일에서해당부분을주석처리하면이상없이컴파일이됩니다. 아래는 Fdel.exe 프로그램을실행시켰을때디버그메시지가찍히는것을캡춰한것입니다. Fdel.exe 프로그램은 CreateFile, WriteFile, DeleteFile 을이용해서단순히파일을생성, 삭제하는예제프로그램입니다. 1 PsGetCurrentProcess : http://msdn2.microsoft.com/en-us/library/ms802957.aspx 49
그림 61. ZwWriteFile Hooking Message CR0 레지스터를이용한 SSDT Hooking 은 somma 님의 bkdp3 소스를참고하시기바랍니다. 작성된모든프로그램은아래링크에서다운받으실수있습니다. 코드 : http://pds3.egloos.com/pds/200701/04/51/code.zip ( 코드중 FileDeleteSample 은바이너리파일이름을 FDel.exe 로변경하시기바랍니다 ) 50
6. 추가 SSDT Hooking 에관한흥미로운주제가몇가지있습니다. 1. SSDT Hooking이모든스레드를 Hooking 할수있는것은아닙니다. 각스레드는자신의서비스테이블에대한포인터를가집니다. 그러므로새로운서비스테이블을생성하여그것을가리키게할경우 SSDT Hooking은더이상쓸모가없어지게됩니다. 선경렬님의 ksthb 1 프로그램또는 Dual 님의 saruengang 2 프로그램으로실제확인해볼수있습 니다. 소스가궁금하시다면위의프로그램외 Dual 님이공개하신 HyperOlly 3 과 Dual s ksthb 4 를 보시면많은도움이될것입니다. 2. SDT Restore SSDT 의상태를검사해서원래대로복원시켜버리는프로그램이있습니다. SIG 5 라는회사에서공 개한프로그램인데요해당프로그램을사용하면역시 SSDT Hooking 은무용지물이됩니다. 해당프로그램에대한내용은아래의링크에서확인하시기바랍니다. SDT Restore : http://www.security.org.sg/code/sdtrestore.html 3. Microsft는 2007년 1월 Windows Vista를출시하였습니다. Vista에서는기존의방법대로후킹을하는것이불가능해집니다. 현재수많은해커들에의해분석이시도되고있으며많지는않지만그결과물로 Vista의 Unexported Kernel Symbol이발표되었습니다. 아래의링크에서확인하시기바랍니다. http://www.msuiche.net/2007/01/01/windows-vista-64-bits-and-unexported-kernel-symbols/ 현재마이크로소프트는 Windows Driver Kit 이라는새로운형태의 Framework 를제공하고있으며커널 모드 Driver Framework(KMDF), User Mode Driver Framework(UDMF) 로서구현하고있습니다. 1 Ksthb : http://www.zap.pe.kr/index.php?page=pages/researches/winternals_kr.php 2 Saruen Gang : http://dual.inxzone.net/blog/entry/saruen-gang 寫輪眼 -100-Official-Release 3 HyperOlly : http://dual.inxzone.net/blog/attachment/2571194572.zip 4 Dual s ksthb : http://dual.inxzone.net/blog/attachment/731037.zip 5 Security and Information InteGrity : http://www.security.org.sg 51
7. 참고자료 Documents) 1. Rootkits: Subverting the Windows Kernel, Greg Hoglund 1. Inside Windows Rootkits 2. Windows Driver Model2, Microsoft Press 3. Attack Native API, Devguru 4. Rootkit을이용하는악성코드, 사이버시큐리티 2005년 11월호 5. ROOTKITS, Greg Hoglund, James Butler 6. Defeating Kernel Native API Hookers by Direct Service Dispatch Table Restoration, Chew Keong TAN 7. 원리와예제로배우는 Windows 2000 디바이스드라이버, 인포북 8. Windows Internals, Microsoft Press 9. 64비트윈도우커널분석 AMD64, Micro Software 2005년 10월호 Web) 1. Hooking Windows NT System Services http://www.windowsitlibrary.com/content/356/06/2.html 2. Windows Global API Hooking http://dual.inxzone.net/backup/blog/index.php?pl=125&ct1=4 3. Window 쪼물딱거리기 http://somma.egloos.com/2731001 4. How Do Windows NT System Calls REALLY Work? http://www.codeguru.com/cpp/w-p/system/devicedriverdevelopment/article.php/c8035 5. System Call Optimization with the SYSENTER Instruction http://www.codeguru.com/cpp/w-p/system/devicedriverdevelopment/article.php/c8223 6. Hooking the kernel directly http://www.codeproject.com/system/soviet_direct_hooking.asp 7. That s Just the Way It Is How NT Describes I/O Requests http://www.osronline.com/custom.cfm?name=articleprint.cfm&id=74 52
8. MSDN http://msdn.microsoft.com/library/default.asp 9. 드라이버온라인 http://www.driveronline.org/ 53