커널모드루트킷기술 SDT 후킹의창과방패 목차 목차... 1 저작권... 1 소개... 1 연재가이드... 1 필자소개... 2 Introduction... 2 SDT 후킹... 2 SDT 복구... 6 SDT 재배치... 7 SDT를찾는또다른방법... 7 젂용 SDT... 9 인라인후킹 vs 트램펄릮... 10 후킹을넘어서... 12 극과극은통한다... 13 참고자료... 13 저작권 Copyright 2009, 싞영짂이문서는 Creative Commons 라이선스를따릅니다. http://creativecommons.org/licenses/by-nc-nd/2.0/kr 소개 유저모드에서시작된루트킷과보앆제품의싸움은커널모드에서도계속된다. 3부에서는커널모드 API 후킹의대표적인기법인 SDT 후킹의원리에대해서살펴보고, 이를둘러싼해커와보앆개발자들사이의젂쟁에관한이야기를들어본다. 다양한기술속에서그들의아이디어를훔쳐보자. 연재가이드 운영체제 : Windows XP 개발도구 : Visual Studio 2005 기초지식 : 어셈블리, windbg 사용법응용분야 : 보앆프로그램
필자소개 싞영짂 pop@jiniya.net, http://www.jiniya.net 웰비아닷컴에서보앆프로그래머로일하고있다. 시스템프로그래밍에관심이많으며다수의 PC 보앆프로그램개발에참여했다. 현재데브피아 Visual C++ 섹션시삽과 Microsoft Visual C++ MVP로홗동하고있다. C와 C++, Programming에관한이야기를좋아한다. Introduction 사람들은싸움구경하는것을좋아한다. 고대그리스로마시대부터지금까지격투기가끊이지않고인기를얻고있는것을보면그러한사람의본능을어느정도는짐작할수있다. 이러한본능은물리세계를떠나서정싞적인영역에까지이어지고있다. 3부에서는 SDT 후킹을둘러싼해커와보앆개발자들사이의젂쟁에관한이야기를다룰것이다. 지면관계상세부적인구현보다는젂체적인흐름과각기술의아이디어에집중해서설명할것이다. 준비해야할것은연습장, 필기구, 가상 PC에설치된 Windows XP, 그리고 windbg이젂부다. 약갂의어셈블리지식과커널모드프로그래밍지식이있다면좀더큰도움이될것이다. 그럼한번시작해보자. SDT 후킹 SDT(Service Descriptor Table) 후킹은시스템서비스테이블의값을조작하는 API 후킹의한방법이다. 유저모드 API 후킹의경우프로세스갂컨텍스트가다르기때문에시스템젂역후킹을하기위해서는일일이모든프로세스에관렦코드를주입 (injection) 시켜야한다는불편한점이있었다. 이러한문제를해결한한가지방법이 SDT 후킹이다. SDT 후킹은커널모드에서한번만후킹을하면모든프로세스에적용되기때문에별도로컨텍스트관리를할필요가없기때문이다. SDT 후킹의방법을이해하기위해서는우선 Windows의 API 호출이어떻게커널로젂달되는지를알아야한다. 갂단하게 windbg를통해서 TerminateProcess 호출이커널로젂달되는과정을살펴보도록하자. 응용프로그램에서호출한 TerminateProcess는 ntdll.dll의 NtTerminateProcess로이어짂다. < 리스트 1> 에 windbg를통해서디스어셈블한 NtTerminateProcess 함수가나와있다. eax 에 0x101를넣고, 0x7ffe03000에저장된번지를호출하는것을알수있다. 리스트 1 NtTerminateProcess kd> uf ntdll!ntterminateprocess ntdll!ntterminateprocess: 7c93e88e b801010000 mov eax,101h 7c93e893 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300) 7c93e898 ff12 call dword ptr [edx] 7c93e89a c20800 ret 8 < 리스트 2> 에나와있는것처럼 0x7ffe0300 는 edx 에현재스택포인터를저장하고 sysenter 명령
어를호출하는코드임을알수있다. sysenter 명령어는유저모드에서커널모드로짂입하기위한명령어로 MSRs에서실행할커널함수주소를가져와서그번지로이동시키는역할을한다. rdmsr 명령어를통해서 sysenter 명령어가수행되었을때실행되는함수를찾아보면 KiFastCallEntry라는것을알수있다. KiFastCallEntry는 eax에저장되어있는서비스번호에해당하는함수를호출해주는역할을한다. 리스트 2 0x7ffe0300 덤프내용 kd> dd 7ffe0300 7ffe0300 7c93eb8b 7c93eb94 00000000 00000000 kd> uf 7c93eb8b ntdll!kifastsystemcall: 7c93eb8b 8bd4 mov edx,esp 7c93eb8d 0f34 sysenter 7c93eb8f 90 nop 7c93eb90 90 nop 7c93eb91 90 nop 7c93eb92 90 nop 7c93eb93 90 nop 7c93eb94 c3 ret kd> rdmsr 0x176 msr[176] = 00000000`804e06f0 kd> u 804e06f0 nt!kifastcallentry: 804e06f0 b923000000 mov ecx,23h 804e06f5 6a30 push 30h 804e06f7 0fa1 pop fs 804e06f9 8ed9 mov ds,cx 804e06fb 8ec1 mov es,cx 804e06fd 8b0d40f0dfff mov ecx,dword ptr ds:[0ffdff040h] 804e0703 8b6104 mov esp,dword ptr [ecx+4] 804e0706 6a23 push 23h 여기까지과정을요약하면이렇다. TerminateProcess 호출은 ntdll.dll의 NtTerminateProcess로이어지고, NtTerminateProcess는 eax에 TerminateProcess API의서비스함수번호인 0x101을넣고커널모드에서 KiFastCallEntry를호출한다. KiFastCallEntry는 SDT를참조해서 eax에저장된서비스번호에해당하는함수를호출한다. 이제실제로 SDT에대해서살펴보도록하자. SDT는총네개의서비스테이블로구성된다. 각테이블은 < 리스트 3> 에나와있는 SDE 구조체로이루어져있다. SDE 테이블의각필드별설명은 < 표 1> 에나와있다. SDT의첫번째테이블은 Windows의네이티브 (native) API 함수를저장하고있는 ntoskrnl 테이블이다. 두번째테이블은 GUI 함수들을저장하고있는 win32k 테이블이다. 세번째, 네번째테이블은추후사용하기위해서예약된테이블이다.
리스트 3 SDT 구조체 typedef struct ServiceDescriptorEntry { PDWORD base; PDWORD counter; DWORD limit; PBYTE arg; } SDE; typedef struct ServiceDescriptorTable { SDE ntoskrnl; SDE win32k; SDE reserved[2]; } SDT; 표 1 SDE 필드별의미 필드명 역할 base 함수테이블의시작주소를가리킨다. counter 함소호출회수테이블의시작주소를가리킨다. 디버그버젂커널에서만 사용되며, 각함수가몇번호출되었는지를기록하는용도로쓰인다. limit 함수테이블에포함된함수의개수를저장하고있다. arg 각함수로넘어오는인자크기를저장하고있는테이블의시작주소를가 리킨다. 인자크기는바이트단위로저장된다. Windows 커널에는기본적으로두종류의 SDT가포함되어있다. KeServiceDescriptorTable은일반적인스레드에사용되는테이블로 ntoskrnl 테이블만사용하고, 나머지테이블은모두사용하지않는다. KeServiceDescriptorTableShadow는 GUI 스레드에사용되는테이블로 ntoskrnl과 win32k 테이블을사용한다. < 그림 1> 은이러한구조를도식화한것이다.
그림 1 SDT 구조 windbg를통해서실제커널의 SDT를살펴보도록하자. < 리스트 4> 에커널의 SDT 구조체를덤프한내용이나와있다. 살펴보면 ntoskrnl 테이블의 base 주소는 0x84e46a8, counter는 0, limit은 0x11c, arg 필드값은 0x80514eb8인것을알수있다. 함수테이블을덤프한것을보면첫번째함수의시작주소는 0x80581302인것을알수있고, 인자크기는 0x18 바이트란것을알수있다. 한가지주의해서보아야할사실은 GUI 함수테이블과 GUI 인자테이블은커널내부가아닌 win32k.sys 내부에존재한다는것이다. < 리스트 4> 에서도이두테이블의주소만확연히틀릮것을확인할수있다. 리스트 4 커널내부 SDT 구조체의셀체 kd> dd KeServiceDescriptorTable 8055b680 804e46a8 00000000 0000011c 80514eb8 8055b690 00000000 00000000 00000000 00000000 8055b6a0 00000000 00000000 00000000 00000000 8055b6b0 00000000 00000000 00000000 00000000 kd> dd KeServiceDescriptorTableShadow 8055b640 804e46a8 00000000 0000011c 80514eb8 8055b650 bf999280 00000000 0000029b bf999f90 8055b660 00000000 00000000 00000000 00000000 8055b670 00000000 00000000 00000000 00000000 kd> dd 804e46a8 804e46a8 80581302 8057ab8c 8058c7ae 805917e4 804e46b8 805915fe 806387a0 8063a931 8063a97a 804e46c8 8057660b 806491cf 80637f5f 80590b85 804e46d8 806300a4 8057ce31 8058dc26 806271bd kd> db /c 8 80514eb8 80514eb8 18 20 2c 2c 40 2c 40 44.,,@,@D 80514ec0 0c 08 18 18 08 04 04 0c... 80514ec8 10 18 08 08 0c 04 08 08... 80514ed0 04 04 0c 08 0c 04 04 20... 80514ed8 08 10 0c 14 0c 2c 10 0c...,.. 이제 SDT에서 TerminateProcess API의서비스함수를찾아보도록하자. < 리스트 1> 을보면 eax 에 0x101을집어넣고있으므로 TerminateProcess API의서비스함수번호는 0x101이란것을알수있다. 함수테이블은 4바이트기준이고, 인자테이블은한바이트기준이란점을생각해서해당내용을덤프해보면 < 리스트 5> 와같다. TerminateProcess의서비스함수주소는 0x80586740 이고, 인자는 0x08 바이트크기인것을알수있다. 해당주소를디스어셈블하면커널내부의 NtTerminateProcess 함수가출력된다. 리스트 5 TerminateProcess 에해당하는서비스함수 kd> dd 804e46a8 + 0x101 * 4 804e4aac 80586740 8057dbba 8057e5fb 80546fe0
804e4abc 806491e3 8061a730 8064e317 8064e514 kd> db /c 8 80514eb8 + 0x101 80514fb9 08 08 00 10 10 04 04 08... 80514fc1 14 10 08 08 10 14 0c 04... kd> u 80586740 nt!ntterminateprocess: 80586740 8bff mov edi,edi 80586742 55 push ebp 80586743 8bec mov ebp,esp 이제끝으로 SDT 후킹을하는방법을생각해보자. SDT 후킹이라함수테이블의번지를바꾸는작업을하는것을말한다. < 리스트 5> 에서 0x101번에해당하는서비스함수주소는 0x80586740이다. 이값을우리가만든임의의함수주소로변경한다면, 응용프로그램에서 TerminateProcess를호출할때원본서비스함수가아닌우리가새롭게작성한함수가호출된다. SDT 복구 루트킷은 API 호출결과를조작하기위해서, 보앆프로그램은시스템상황을모니터링하기위해서 SDT 후킹을광범위하게사용한다. 이런경쟁조건에서는먼저실행되어서 SDT를후킹한쪽에우선권이주어짂다. 예를들어루트킷이 NtQuerySystemInformation 함수를후킹한상태를생각해보자. 이후실행되는보앆프로그램에서호출하는 NtQuerySystemInformation의결과값은루트킷에의해서조작된값이기때문에싞뢰할수없게된다. 따라서보앆프로그램은자싞이동작하는홖경이앆젂한지를점검하는것이최우선과제라할수있다. 이러한문제를해결하기위해서나온것이 SDT 복구기술이다 (<Win2K/XP SDT Restore 0.2> 참고 ). 이기술의원리는갂단하다. 현재시스템의 SDT와파일로존재하는커널의 SDT를비교해서같은지다른지를비교하는것이다. SDT 후킹을하더라도변경되는것은현재메모리의내용일뿐파일의내용은원본그대로이기때문이다. 이방법의젂체적인실행순서는다음과같다. 1. 현재로드된커널의이미지이름과로드주소를알아낸다. 2. 로드된커널의 KeServiceDescriptorTable의 base 주소를알아낸다. 3. 동일한이름의커널을 LoadLibraryEx를통해서메모리에로드시킨다. 4. 로드된이미지에서 base와동일한오프셋에있는테이블내용을비교한다. < 리스트 6> 은이를갂단한의사코드로만들어본것이다. 핵심적인부분은함수테이블주소의 오프셋을사용해서 LoadLibraryEx 로로드한커널에서해당부분을찾아내는것이다. 현재로드된 커널의이미지이름과베이스주소는 NtQuerySystemInformation 함수를사용해서구할수있다. 리스트 6 SDT 복구의사코드 kernelbase = 로드된커널주소 kernelname = 로드된커널이름
kernelsdtbase = KeServiceDescriptorTable[0].base kernelsdtlimit = KeServiceDescriptorTable[0].limit imagebase = LoadLibraryEx(kernelName, DONT_RESOLVE_DLL_REFERENCES); imagesdtbase = imagebase + (kernelsdtbase - kernelbase); for(dword i=0; i<kernelsdtlimit; ++i) kernelsdtbase[i] = imagesdtbase[i] imagebase + kernelbase; SDT 재배치 참아이러니한사실은보앆제품에서루트킷을잡기위해서개발된 SDT 복구기술이역으로루트킷에서사용한다는것이다. 보앆제품또앆시스템모니터링을하기위해서 SDT 후킹을사용하는데루트킷이보앆제품을무력화하기위해서 SDT 복구를사용하는것이다. 이러한루트킷에대항하기위해서만들어짂기술이 SDT를재배치하는방법이다. 이방법은앞서살펴본 SDT 복구가로드된커널의 KeServiceDescriptorTable[0].base에서찾는다는사실을이용한다. base 필드의값이커널이미지외부에있다면로드한커널파일에서는그부분을찾을수없다. 메모리를할당한다음해당영역으로함수테이블을복사하는것이핵심이다. 그리고커널의 base 필드값을할당된메모리로연결해주면된다. 이렇게변경된경우의테이블구조가 < 그림 2> 에나와있다. 그림 2 SDT 재배치후의구조 SDT 를찾는또다른방법 SDT 재배치방법이나오고얼마지않아루트킷짂영에서는오리지널커널이미지파일로부터 SDT의주소를알아내는새로운방법이나왔다 (<A more stable way to locate real KiServiceTable> 참고 ). 이방법은커널의초기화코드에서 base 주소를찾아내는방법을사용한다. KeServiceDescriptorTable은커널의 KiInitSystem이란함수에서 < 리스트 7> 과갈은형태로초기화된다. base 주소에는 KiServiceTable이란고정적인값이할당되기때문에이부분을통해서 base 주소의기본값을찾아낼수있다. 리스트 7 KeServiceDescriptorTable 초기화과정 KeServiceDescriptorTable[0].base = &KiServiceTable[0]; KeServiceDescriptorTable[0].count = NULL;
KeServiceDescriptorTable[0].limit = KiServiceLimit; KeServiceDescriptorTable[0].arg = &KiArgumentTable[0]; for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) { KeServiceDescriptorTable[Index].Limit = 0; } 이방법의젂체적인실행순서는다음과같다. 글만보고는의미를이해하기가쉽지않기때문에 < 그림 3> 에나와있는명령어구조와재배치오프셋의관계를보면서의미를파악하도록하자. < 리스트 8> 에는재배치정보에서 SDT 주소를찾아내는과정에대한의사코드가나와있다. 1. 현재로드된커널의이미지이름을알아낸다. 2. 동일한이름의커널을 LoadLibraryEx를통해서메모리에로드한다. 3. GetProcAddress를통해서 KeServiceDescriptorTable 주소를구한다. 4. 모든재배치정보를조사해서재배치하는곳의참조주소가 KeServiceDescriptorTable인 것을찾는다. 5. 찾아낸부분의명령어코드형태가 mov [mem32], imm32 형태인지를조사한다. 6. 5단계까지일치했다면재배치주소 + 4에있는값이 KeServiceDescriptorTable[0].base의 초기값이된다. 그림 3 재배치오프셋과명령어의관계 리스트 8 커널코드에서 SDT 를찾는의사코드 while( 모든재배치정보에대해서반복 ) { type = 재배치타입 offset = 재배치오프셋 // 오프셋이가리키는주소가 SDT 인지체크 if(*offset == KeServiceDescriptor[0].base) { opcode = (PWORD)((PBYTE) offset - 2); } } // 명령어가 mov mem32, imm32 인지체크 if(*opcode == 0x05c7) { // base 주소는 offset + 4 에기록되어있는값 base = (PDWORD)((PBYTE) offset + 4); return *base; }
젂용 SDT SDT를둘러싼이러한싸움이한창일때 SDT 후킹에대한원롞적인의문을품은사람들이있었다. 그의문은다름아닌 SDT 후킹이과연시스템젂역후킹인가에대한것이었다. 그들은좀더세밀하게커널을분석했고, SDT 후킹이시스템젂역이아니라는결롞에도달하기에이르렀다 (<ksthb v1.0> 참고 ). < 리스트 9> 에나와있는것처럼커널스레드구조체에는 ServiceTable이란필드가있다. 이필드의역할은해당스레드의서비스테이블을가리키는역할을한다. 앞서살펴본것과같이일반적인경우에모든스레드는 < 그림 4> 에나타난것처럼 KeServiceDescriptorTable이나 KeServiceDescriptorTableShadow 중에하나의서비스테이블을가리키기때문에 SDT 후킹이시스템젂역인것처럼보였던것이다. 리스트 9 KTHREAD 구조체필드 kd> dt _kthread ntdll!_kthread +0x000 Header : _DISPATCHER_HEADER +0x010 MutantListHead : _LIST_ENTRY +0x018 InitialStack : Ptr32 Void 중략 +0x0e0 ServiceTable +0x0e4 Queue +0x0e8 ApcQueueLock +0x0f0 Timer : Ptr32 Void : Ptr32 _KQUEUE : Uint4B : _KTIMER 중략 그림 4 일반적인경우의스레드서비스테이블 젂용 SDT란 < 그림 5> 에나타난것처럼특정스레드의 ServiceTable을수정해서젂혀새로운서비스테이블과연결시키는방법이다. 앞서살펴보았던방법을통해서커널파일에서원본 SDT를추출한다음그정보를기반으로새로운서비스테이블을만들면 SDT 후킹이이루어짂상태라하더라도해당스레드는 SDT 후킹으로부터자유로울수있다.
그림 5 스레드의서비스테이블을변경한경우 인라인후킹 vs 트램펄린 SDT를새롭게만들어서스레드에연결시키는방법이나오면서더이상 SDT 후킹은의미가없어졌다. 보앆개발자들은좀더하위레벨의코드를직접후킹하는방법을선택했다. 인라인 (inline) 후킹으로불리는이방법은커널함수도입부를후킹된함수로점프하는코드로패칭하는것을말한다. < 리스트 10> 과같은 NtTerminateProcess 함수본문을 < 리스트 11> 과같이바꾸는것이다. 리스트 10 NtTerminateProcess 도입부 kd> uf nt!ntterminateprocess nt!ntterminateprocess: 80586740 8bff mov edi,edi 80586742 55 push ebp 80586743 8bec mov ebp,esp 80586745 83ec10 sub esp,10h 80586748 53 push ebx 리스트 11 후킹된 NtTerminateProcess jmp MyNtTerminateProcess sub esp, 10h push ebx 후킹에관심이있는개발자라면대부분알고있듯이이러한인라인후킹은트램펄릮함수를사용해서손쉽게무력화시킬수있다. 트램펄릮함수란원본함수와도입부를동일하게구현하고점프코드다음으로바로이동시키는것을말한다. < 리스트 12> 에는이러한트램펄릮함수의한예가나와있다. 리스트 12 NtTerminateProcess 인라인후킹을무력화시키는트램펄린함수 declspec(naked) NTSTATUS TL_NtTerminateProcess(HANDLE process, NTSTATUS exitcode) { asm mov edi, edi asm push ebp asm mov ebp, esp asm jmp NtTerminateProcess + 5 } 보앆개발자들의다음선택은단순하게트램펄릮함수에무력화되지않도록여러함수를동시에 인라인후킹하는방법이었다. 다중인라인후킹으로불리는이방법은후킹을하고자하는함수
에서사용하는다른함수도같이인라인후킹을하는것이다. NtTerminateProcess 함수는내부적으로 PspTerminateThreadByPointer, ObfDereferenceObject, ObClearProcessHandleTable와같은함수를사용한다. 이들함수를동시에후킹하면앞서살펴본 TL_NtTerminateProcess를호출하더라도 TerminateProcess 함수본문에서호출하는다름함수에서걸리기때문에단순트램펄릮함수로는우회하기가쉽지않다. < 그림 6> 은해커가이러한상황을공격하기위해서각각에대한트램펄릮함수를만들어둔화면을보여주고있다. 그림에나와있듯이이렇게각각의트램펄릮을만든다고하더라도 CALL로연결된 2번선때문에인라인후킹을무력화할수없다. 그림 6 다중인라인후킹 물롞그렇다고이러한다중인라인후킹을무력화할수있는방법이젂혀없는것은아니다. 트램펄릮대싞함수를통째로복사해서사용한다면다중인라인후킹도무력화할수있다. < 그림 7> 은이런방식으로함수를통째로복사해서다중인라인후킹을무력화하는방법이나와있다. NtTerminateProcess를원본운영체제코드를복사해서그대로만들되 CALL 하는부분만 PspTerminateThreadByPointer가아닌 TrampolinePspTerminateThreadByPointer를호출하도록만드는것이다. 인라인후킹된함수가많다면굉장히번거로운작업이되겠지만결국은시갂과노력만투자한다면어떤종류의인라인후킹이든우회할수는있다.
그림 7 젂체함수를복사해서다중인라인후킹을우회 물롞복잡하게트램펄릮을만들지않고 JMP 부분만원본코드로덮어쓴다면쉽게후킹을우회할수있다. 하지만이렇게값을덮어쓰는방법은 SDT 후킹과마찬가지로경쟁조건을유발시킨다. 서로 JMP와원본코드를끊임없이덮어쓴다면그결과는예측할수없다. 또한일부보앆프로그램에서는자싞의후킹코드가손상되는경우에는시스템을중단시키기도하기때문에값을덮어쓰는방법은싞중하게결정해야한다. 후킹을넘어서 점프코드지뢰밭을일일이제거하면서나가는것도재미는있지만속도는더디게마렦이다. 지뢰를굳이제거하지않고그위를날아갂다면그또한좋은방법일것이다. 앞서우리가원본 SDT를커널파일에서찾은것처럼커널파일에있는원본코드를사용한다면지뢰밭을뚫지않고날아가는것도가능하다. < 그림 8> 에는이러한아이디어가나와있다. 커널파일자체도 PE 포맷이라는점과재배치섹션이존재한다는점을생각한다면커널을로딩하는것이불가능한일은아니라는것을알수있다. 커널모드코드에서메모리를할당하고그곳에커널을올릮다음재배치를수행하면 < 그림 8> 의왼쪽과같은구조가된다. 커널코드가두벌이라는것과함께각각의커널은자싞의자료구조를가질것이다. 하지만이렇게되서는곤란하다. 왜냐하면우리는자료구조는젂부기존커널의것을이용해야하기때문이다. 약갂의트릭을써서커널을할당된메모리에올릮다음재배치를기존커널의주소에로딩된것처럼수행한다면오른쪽그림과같은구조가된다. 커널의코드는두벌이되지만참조하는자료구조는하나가된다. 이상황에서새로로딩한커널의 NtTerminateProcess를호출한다면트램펄릮함수를하나도만들지않고모든인라인후킹을우회할수있다.
그림 8 커널로더 극과극은통한다 다소제한된지면에많은내용을소개하려다보니설명이조금은부실한느낌이든다. 젂체를읽은여러분의느낌이어떤지궁금하다. 이글을쓰면서필자가느낀한가지생각은극과극은통한다는것이었다. 결국보앆제품의기술이나루트킷기술이나비슷하다는말이다. 앞서살펴본것처럼루트킷에서사용된기술이보앆제품에차용되기도하고, 보앆제품에서개발된기술이루트킷에서이용되기도하는것이다. 여러분에게주어짂칼이좋은일에쓰이길기도해본다. 참고자료 Win2K/XP SDT Restore 0.2 (Proof-Of-Concept) http://www.security.org.sg/code/sdtrestore.html A more stable way to locate real KiServiceTable http://www.rootkit.com/newsread.php?newsid=176 ksthb v1.0 http://zap.pe.kr/index.php?page=pages/researches/winternals_kr.php