DKOM을이용한은닉기법 이동수 alonglog@is119.jnu.ac.kr
개 요 유저모드에서프로세스나디바이스드라이버등을확인할수없도록만드는기법중하나가커널 Hooking과 DKOM 기법이있다. DKOM 기법은 Hooking과다르게커널개체를직접변경한다. 이는 Hooking보다훨씬강력하고탐지가힘들다. 이문서에서는 DKOM에대해서다룰것이다. DKOM 기법을통해다양한효과를얻을수있다. 그중에서프로세스와드라이버를은닉하는방법을살펴보겠다. DKOM에들어가기전에사전지식과유저모드프로세스와디바이스드라이버를연동하는방법을알아보겠다. 테스트환경은 Windows XP SP2 에서하였다.
Content 1. 목적 1 2. DKOM(Direct Kernel Object Manipulation 2 2.1. kernel Object 2 2.2. DKOM의장단점 3 3. 유저모드프로세스작성 4 3.1. 운영체제판단하기 4 3.2. 유저모드프로세스와디바이스드라이버의통신 5 3.3. 키보드처리함수의 KINTERRUPT 얻기 6 4. DKOM 기법을이용한은닉기법 8 4.1. 프로세스은닉 8 4.2. 디바이스드라이버은닉 12 5. 실험및결과 14 6. 결론 16 참고문헌 17
1. 목적 이번기술문서의주제는 DKOM 이다. DKOM은이전의 Hooking 기술과는다르게윈도우커널의테이블이나 Native API를 Hooking하지않는다. 윈도우즈커널에의해관리되는커널개체 (Kernel Object) 를직접적으로건드리는기법이다. Hooking을하지않고도은닉하는기법을공부할것이다. 이문서에서는커널개체에대해서알아보고, 프로세스와드라이버를숨기는기법을살펴볼것이다. - 1 -
2. DKOM(Direct kernel Object Manipulation) 에관하여. 2.1 Kernel Object Windows Internals 에커널개체에대해서다음과같이설명이되어있다. 내부적으로 Windows는실행부개체와커널개체의두종류의개체를가지고있는데, 실행부개체들은실행부의다양한구성요소에의해구현된개체들로프로세스관리자, 메모리관리자, I/O 서브시스템등이있다. 커널개체들은 Windows 커널에의해구현된더근본적인개체들의집합이다. 이들개체들은사용자모드코드에서는볼수없고실행부내에서만생성되고사용된다. 커널개체들은실행부개체들이만드는동기화와같은기본적인기능들을제공한다. 따라서많은실행부개체들은하나이상의커널개체들을포함한다. 위의정의에서확인할수있듯이커널개체는커널에의해만들어지고관리되는개체이다. 생성되는개체에따라커널개체에는여러정보가담겨져있다. [ 그림 1] 은실행부개체와커널개체의관계를그림으로보여주고있다. [ 그림 1] 실행부개체와커널개체 DKOM은이커널개체에직접접근하여원하는행위 ( 프로세스은닉, 드라이버숨기기, 토큰권한의상승등..) 를하는기법이다. 다음절에서 DKOM기법의장단점을살펴보자. - 2 -
2.2 DKOM 의장단점 커널개체의변경은개체관리자에의해서이루어져야한다. 하지만 DKOM은개체관리자를통하지않고직접커널개체에접근하기때문에커널개체에대한어떤권한체크도이루어지지않는다. 이는강력한기능을제공한다. SSDT나 IDT Hooking 기술은쉽게발견이가능하지만 DKOM은발견이쉽지않다. 이런 DKOM이가지는제한점도존재한다. 우선커널개체의종류는여러종류가존재한다. 그리고이런개체정보는문서화되어있지않기때문에많은정보를얻기위해서많은정보를투자해야한다. 또커널개체는운영체제의버전에따라그위치가달라지기때문에 DKOM 기법을사용하는데운영체제의버전을항상체크해야하는번거로움이있다. 이버전에따른차이점은 4장에서자세히알아보겠다. DKOM 기법은커널개체를수정하는것이기때문에메모리에커널개체가존재해야한다는제약조건이있다. 예를들어운영체제는프로세스에대한정보를개체로만들어서관리하지만파일에대한정보를관리하는개체는존재하지않는다. 이는파일을은닉할수없다는것을뜻한다. 하지만프로세스와드라이버만을은닉하는거자체만으로도엄청강력한기법이라고할수있다. DKOM 기법으로프로세스 드라이버 포트은닉과프로세스권한을상승시킬수있다. 이문서에서는프로세스와드라이버은닉기법을다룰것이다. - 3 -
3. 유저모드프로세스작성 커널개체를수정하는드라이버작성에들어가기전에유저모드프로세스에대해서간단히소개하고넘어가겠다. 드라이버를만들더라도실행을하기위해서는유저모드의프로세스와연동을해야한다. 이번장에서드라이버와연동하는부분과프로세스은닉할때필요한정보를얻는부분을설명하고넘어가겠다. 3.1 운영체제판단하기 앞장에서말했듯이커널개체는운영체제의버전에따라번경되기때문에운영체제의확인은불가피하다. 이장에서는유저모드에서운영체제의버전을확인하는법을알아보겠다. Win32 API인 GetVersionEx() 함수를이용하면운영체제의버전을구할수있다. GetVersionEx는인자로 OSVERSIONINFOEX 구조체를가리키는포인터를가진다. GetVersionEx는운영체제버전에관한정보를인자가가리키는 OSVERSIONINFOEX 구조체에넣는다. [ 그림 2] 는 OSVERSIONINFOEX의구조체를보여준다. [ 그림 2] OSVERSIONINFOEX 구조체 위에서주목해야할변수는 dwmajorversion, dwminorversion, dwplatformid이다. dwmajorversion과 dwminorversion은운영체제의버전인데, dwmajorversion은버전의정수부분이저장되고, dwminorversion에는버전의소수부가저장된다. dwplatformid는운영체제의플랫폼값을가지고있다. [ 표 1] 은운영체제버전에따른각변수의값을정리하였다. 구분 Windows NT Windows 2000 Windows XP Windows 2003 dwmajorversion 4 5 5 5 dwminorversion 0 0 1 2 dwplatformid VER_PLATFORM_WIN32_NT [ 표 1] 운영체제에따른변수 MajorVersion, MinorVersion, PlatformId 값 - 4 -
dwplatformid변수값에는윈도우즈3.1을나타내는 VER_PLATFORM_WIN32S 값과윈도우즈 95/98을나타내는 VER_PLATFORM_WINDOWS가있다. 그리고또하나주목할변수가있다면 wservicepackmajor변수이다. 이변수는플랫폼의서비스팩버전의정보를포함하고있다. [ 그림 3] 은테스트에사용한 PC의운영체제버전을알려주고있다. [ 그림 3] 테스트 PC 의운영체제버전 버전에따른차이점은 4.1 장프로세스은닉에서자세히다루겠다. 3.2 유저모드프로세스와디바이스드라이버의통신 디바이스드라이버를로드하고실행하기위해서는유저모드와의통신은필수적이다. 드라이버자체는 PE 파일처럼클릭만으로실행이되지않기때문이다. 이번장에서는유저모드프로세스에서디바이스드라이버를구동하는방법을알아보고메시지를교환하는방법을살펴보겠다. 중점을메시지교환방법이다. 디바이스드라이버를구동하는방법을간단히알아보고넘어가자. 여기에서설명하는방법은서비스컨트롤매니저 (SCM) 을이용하는방법이다. 순서를요약해보면, 1. OpenSCManager() 를이용하여 SCM을획득한다. 2. CreateService() 를이용하여필요한드라이버를로드한다. 3. OpenSCManager() 를이용하여 SCM을획득한다. 4. 로드에성공하면 OpenService() 를이용하여실행시킨다. 5. CreateFile() 을통하여디바이스드라이버를파일의형태로만든다. 이문서는디바이스드라이버를로드하는방법이주가아니므로여기에서끝내겠다. 인자라든지더자세한내용을알고싶다면디바이스드라이버전문서적을참고하기바란다. 위에서소개한방법으로드라이버가정상적으로동작한다면로드한유저모드프로세스와로드된디바이스드라이버간의통신은어떻게이루어질까? 유저모드프로세스와디바이스드라이버간의통신은 I/O Co ntrol Code(IOCTL) 을이 - 5 -
용해서전달한다. 이런 IOCTL은 IRP_MJ_DEVICE_CONTROL IRP를통해전달된다. [ 그림 4] 는공부하는과정에작성한 IOCTL을보여준다. [ 그림 4] IOCTL 의예 [ 그림 4] 에서볼수있듯이 CTL_CODE를이용하여 IOCTL_PID라는메시지를만들었다. CTL_CODE의인자값을간단하게살펴보자. 첫번째인자는넘어가는메시지의타입이다. 두번째인자는고유 ID값이고, 세번째인자값은메시지를주고받는방식이다. 마지막인자는이메시지의특성이다. 메시지를어떻게정의했는지알아봤으니실제유저모드프로세스에서처리하는방법을알아보자. [ 그림 5] 는유저모드프로세스에서디바이스드라이버로메시지를보낼때사용하는 DeviceIoControl() 의예를보여주고있다. [ 그림 5] DeviceIoControl() 의예 DeviceIoControl 함수의인자값을확인하고가자. 첫번째인자는전달한드라이버의핸들값이다. 이핸들값은위에서나온 CreateFile() 의리턴값이다. 두번째인자는메시지의 ID이다. 세번째와네번째인자는유저모드프로세스에서디바이스드라이버로넘어가는데이터와데이터의크기를알려준다. 다섯번째와여섯번째인자는디바이스드라이버에서유저모드프로세스로데이터를넘길때사용한다. 일곱번째인자는리턴값을저장하는변수이다. 이제디바이스드라이버에서는어떻게처리하는지확인해보자. 메시지를처리하는 IRP_MJ_DEVICE_CONTROL IRP를우리가정의한함수로바꿀필요가있다. [ 그림 6] 은 IRP_MJ_DEVICE_CONTROL IRP를바꾸는예를보여주고있다. [ 그림 6] IRP_MJ_DEVICE_CONTROL IRP 교체 [ 그림 6] 에서 pdriverobject를디바이스드라이버를시작할때생성되는드라이버개체이다. IoDeviceControl() 에서받은메시지를어떻게처리하는지살펴보자. [ 그림 7] 은 IoDeviceControl() 을보여주고있다. - 6 -
[ 그림 7] IoDeviceControl() 의예 [ 그림 7] 의코드에서 IoGetCurrenIrpStackLocation() 을이용하여받은메시지의위치를구한다. 그리고 Parameters.DeviceIocontrol.IoControlCode의값을비교하여원하는메시지에따른행위를취하도록하면된다. 다음장에서는실제로 DKOM기법을이용하여프로세스은닉과디바이스드라이버은닉방법을알아보자. - 7 -
4. DKOM 기법을이용한은닉기법 프로세스가실행되거나디바이스드라이버가로드될때커널은커널개체 ( 구조체 ) 를만들어서정보를메모리상에저장하고관리한다. 우리가사용하는 API는이런커널개체에서정보를가져와서보여준다. 만일커널개체의내용을바꾼다면 API를후킹할필요없이 API는우리가원하는값을가져오게된다. 4.1 프로세스은닉 윈도우즈는프로세스리스트를 EPROCESS 구조체안의이중링크드리스트로연결되어있다. EPROCESS 구조체안에는이중링크드리스트구조체인 LIST_ENTRY 구조체를포함하고있다. LIST_ENTRY 구조체는앞프로세스를가르키는 FLINK 멤버와뒤프로세스를가르키는 BLINK 멤버로구성되어있다. 숨기고자하는프로세스의뒤프로세스의 FLINK멤버과앞프로세스의 BLINK멤버의값을바꾼다면원하는행위를성공할수있다. 그럼 EPROCESS 구조체의주소를알아보자. 운영체제의버전에따라 EPROCESS 구조체의주소는다르지만 PsGetCurrentProcess() 를이용하면쉽게 EPROCESS 구조체의주소를구할수있다. [ 그림 8] 은 WinDbg를이용하여구한 PsGetCurrentProcess() 의어셈블리코드이다. [ 그림 8] PsGetCurrentProcess() 의어셈블리코드 [ 그림 8] 에서보면 fs 레지스터의 0x124번째값을읽어온다. fs 레지스터의 0x124번째값은 ETHREAD 구조체의주소를가지고있다. [ 그림 9] 는 ETHTEAD 구조체의일부를보여준다. [ 그림 9] ETHREAD 구조체 [ 그림 8] 에서보면 ETHREAD 구조체의 0x44번째값을리턴한다. 그런데 [ 그림 9] 에서볼수있듯이 0x44번째값은 KTHREAD 구조체의안의값이라는것을확인할수있다. 그럼 KTHREAD 구조체를확인해보자. [ 그림 10] 은 KTHREAD 구조체의일부를보여준다. - 8 -
[ 그림 10] KTHREAD 구조체 [ 그림 10] 에서볼수있듯이 0x44번째값은 KAPC_STATE 구조체내부의값이다. KAPC_STATE 구조체를확인해보자. [ 그림 11] 은 KAPC_STATE 구조체를보여준다. [ 그림 11] KAPC_STATE 구조체 [ 그림 10] 에서 KAPC_STATE 구조체의오프셋값이 0x34이다. 그럼 KAPC_STATE 에서오프셋 0x10의값이리턴하는값이다. [ 그림 11] 에서보면 0x11의값은 KPROCESS 구조체를가리키는값이다. KPROCESS 구조체가위에서언급한 EPROCESS 구조체의첫번째멤버이다. [ 그림 12] 와 [ 그림 13] 은 KPROCESS 구조체의일부와 EPROCESS 구조체의일부를보여준다. [ 그림 12] KPROCESS 구조체 - 9 -
[ 그림 13] EPROCESS 구조체 EPROCESS 구조체를구하는과정을디버깅을통해알아보았다. [ 그림 13] 에서오프셋 0x84에위치한 UnigueProcessId 멤버가현재프로세스의 PID 값을가지고있다. 오프셋 0x88에위치한 LIST_ENTRY 구조체가현재프로세스의앞과뒤프로세스를가리키는주소를가지고있다. 즉 EPROCESS 구조체의 0x88번째멤버와 0x8번째멤버의값을바꾸면우리가원하는프로세스은닉이가능하다. 위에서운영체제마다커널개체의구조가다르다고했었다. EPROCESS 구조체의경우 PID를가지는 UniqueProcessId 멤버와 LIST_ENTRY 구조체인 ActiveProcessLinks 멤버의오프셋값이다른다. [ 표 2] 는운영체제버전에따른오프셋을보여준다. 구분 Windows NT Windows 2000 Windows XP Windows XP SP2 Windows 2003 PID offset 0x94 0x9C 0x84 0x84 0x84 LST_ENTRY offset 0x98 0xA0 0x88 0x88 0x88 [ 표 2] 운영체제버전에따른오프셋 프로세스은닉을위한원하는정보를얻었으니프로세스를은닉하는코드를작성해보자. [ 그림 14] 는현재프로세스의 EPROCESS 구조체의주소를구하는코드의예를보여준다. - 10 -
[ 그림 14] 현재프로세스의 EPROCESS 구조체구하기 [ 그림 14] 에서보면 PsGetCurrentProcess() 를이용하여 EPROCESS 구조체의주소를구한다. 구한 EPROCESS 구조체가우리가찾고자하는프로세스의 EPROCESS 구조체라면 While문을빠져나가고, 일치하지않으면다음프로세스의 EPROCESS 구조체로넘어간다. 원하는프로세스인지는 PID를이용하여서비교한다. PID는유저모드의프로세스가 GetCurrentProcessId() 함수를이용하여얻은 PID 값을 IRP를이용하여디바이스드라이버에게넘겨준값이다. 현재프로세스의 EPROCESS 구조체의주소를구했다면 LIST_ENTRY 구조체를이용하여 FLINK 멤버가가리키는 EPROCESS 구조체의 BLINK 멤버의값과 BLINK 멤버가가리키는 EPROCESS 구조체의 FLINK 멤버의값을바꾸어야한다. [ 그림 15] 는멤버의값을바꾸는코드의예를보여준다. [ 그림 15] 프로세스은닉하기 [ 그림 15] 에서보면현재프로세스의 FLINK 멤버가가리키는 EPROCESS 구조체의 BLINK 멤버의값을현재프로세스의 BLINK 멤버의값으로바꾼다. 그리고현재프로세스의 BLINK 멤버가가리키는 EPROCESS 구조체의 FLINK 멤버의값을현재프로세스의값을현재프로세스의 FLINK 멤버의값으로바꾼다. 그런후에현재프로세스의 FLINK 멤버와 BLINK 멤버의값을현재프로세스의 EPROCESS 구조체를가리키도록바꾼다. 자기자신을가리키도록바꾸는이유는주변프로세스가종료되면현재프로세스의 FLINK 멤버와 BLINK 멤버의값이잘못된메모리공간을가리키게된다. 이를방지하기위해서이다. [ 그림 16] 은프로세스은닉을그림으로보여주고있다. - 11 -
[ 그림 16] 프로세스은닉 4.2 디바이스드라이버은닉 커널은드라이버정보를관리하기위해 MODULE_ENTRY 구조체를사용한다. MODULE_ENTRY 구조체의첫번째멤버는 LIST_ENTRY멤버이다. 이는프로세스와마찬가지로드라이버리스트가이중링크드리스트를사용하여관리된다는것을확인할수있다. 그럼먼저현재디바이스드라이버의 MODULE_ENTRY 구조체의주소를알아보자. 프로세스의경우와다르게디바이스드라이버의경우구조체의주소를구해주는함수가존재하지않는다. 그럼어떻게해야할까? 다행히 DRIVER_OBJECT 구조체의 DriverSection 멤버가현재디바이스드라이버의 MODULE_ENTRY 구조체의주소를가지고있다. [ 그림 17] 은 DRIVER_OBJECT 구조체를보여주고있다. [ 그림 17] DRIVER_OBJECT 구조체 [ 그림 17] 에서보면 DriverSection 멤버의설명이 Void 형태의포인터라고만되어있 - 12 -
다. 즉 MODULE_ENTRY 구조체는문서화되지않은구조체라는것을뜻한다. [ 그림 18] 은 MODULE_ENTRY 구조체를보여주는데 'ROOTKIT' 책에서발췌하였다. [ 그림 18] MODULE_ENTRY 구조체 [ 그림 18] 에서보면첫번째멤버가 LIST_ENTRY 구조체라는것을알수있다. 이구조체를이용하면디바이스드라이버를은닉할수있다. MODULE_ENTRY 구조체의주소를구하는방법을알았으니실제코드를작성해보자. [ 그림 19] 는 MODULE_ENTRY 구조체의주소를구하는방법의예를보여준다. [ 그림 19] MODULE_ENTRY 구조체의주소구하기 [ 그림 19] 의코드에서 newdriverobject는디바이스드라이버가시작할때만들어지는 DRIVER_OBJECT 구조체이다. [ 그림 17] 에서보면 DriverSection 멤버의오프셋이 0x14라는것을확인할수있다. 다음으로 LIST_ENTRY 구조체를이용해디바이스드라이버를은닉하는코드를작성해보자. [ 그림 20] 은디바이스드라이버를은닉하는코드의예를보여주고있다. [ 그림 20] 디바이스드라이버은닉하기 [ 그림 20] 에서드라이버리스트를순회하면서원하는디바이스드라이버를찾으면프로세스와같은방법으로 FLINK 멤버와 BLINK 멤버의값을변경한다. - 13 -
5. 실험및결과 위에서작성한코드를테스트해보겠다. [ 그림 21] 은작성한유저모드프로세스를실행시킨모습이다. [ 그림 21] 유저모드프로세스의실행모습 [ 그림 21] 에서확인할수있듯이이프로그램의 PID는 2208이다. 그럼우리가원하는대로프로세스가은닉되었는지확인하기위해작업관리자를확인해보자. [ 그림 22] 는작업관리자를보여주고있다. [ 그림 22] 작업관리자를통해프로세스은닉확인 [ 그림 22] 는 PID로정렬된작업관리자를보여주고있는데현재프로세스의 PID인 2280을찾을수가없다. 이는우리가원하는대로프로세스가은닉되었다는것을알려준다. 다음은디바이스드라이버도정상적으로은닉되었는지를확인해보자. MS사는현재로드된드라이버의리스트를보여주는 drivers.exe라는실행파일을제공해준다. [ 그림 23] 은 drivers.exe을실행시켜얻은현재디바이스드라이버의목록이다. - 14 -
[ 그림 23] 로드된디바이스드라이버목록 [ 그림 23] 에서볼수있듯이현재로드된드라이버의이름을검색했지만존재하지않는다고나온다. 이는우리가작성한디바이스드라이버가자신을숨겼다는것을말한다. 즉, 정상적으로우리가원하는대로은닉되었다. - 15 -
6. 결론 지금까지커널개체를직접번경하는 DKOM 기법에대해알아보았다. 테스트환경에서알약이라는백신프로그램이동작하고있었음에도정상적으로작동하였다. 즉, 이기법을통한프로세스은닉과디바이스드라이버은닉은상당히강력하다. Hooking의경우간단하게테이블값비교를통해 Hooking 되었는가를판별할수있었지만 DKOM 기법의경우확인자체가불가능하다. 유저모드에서프로세스나드라이버를확인할수있는방법은 API를통한쿼리를날려서정보를확인하는방법이통상적인방법인데이방법으론은닉된드라이버와프로세스를확인할수없기때문이다. 아직윈도우즈커널에대한이해의깊이가많이부족해서 DKOM을어떻게막을수있는지모르겠다. 더많은공부를하면방법이있지않을까하는생각뿐이다. 윈도우즈커널을좀더깊이있게공부하여 DKOM을막을수있는방법을고려해보아야겠다. 이것으로 DKOM 기법에대한기술문서를마치겠다. - 16 -
참고문헌 [1] 김상형, " 윈도우즈 API 정복 ", 한빛미디어 ( 주 ), June 2006 [2] Mark E. Russinovich ㆍ David A. Solomon, "WINDOWS INTERNALS 4th", 정보문화사, January 2006 [3] Greg Hoglund ㆍ Jamie Butler, " 루트킷 : 윈도우커널조작의미학 ", 에이콘, July 2008-17 -