Kernel Mode Hooking


3장에서는 사용자 모드 후킹에 대하여 알아보았다면 이번 장에서는 커널 모드 후킹에 대하여 알아볼 것이다. 커널 모드에서 이루어지는 후킹의 경우 단순히 JMP 명령어를 설치하는 것이 아니라, 특정한 구조체에 포함된 값을 수정하는 등 작업을 수행해야 하기 때문에 아무래도 사용자 모드의 후킹보다 복잡하다. 이제 이러한 커널 모드 후킹에 대하여 알아보자.


System Call

운영체제는 사용자 모드(Ring 3)와 커널 모드(Ring 0)라는 두 가지 형태의 권한이 존재하고 있다. 이렇게 분리되는 이유는 다양하지만, 아무래도 보안과 관련된 점 또한 매우 중요하다. 만약 분리되어 있지 않을 경우 어떠한 프로세스든지 운영체제의 핵심 기능을 조작할 수 있게 되므로 이를 방지하기 위해선 분리되는 것이 좋다. 그렇기에 커널 모드에서는 사용자 모드를 조작할 수 있지만, 반대로 사용자 모드에서 커널 모드는 조작할 수가 없다.

하지만 커널 영역에 접근할 수 없다는 것은 해당 프로세스가 디스크의 내용을 읽을 수가 없게 되고 그 외에도 하드웨어나 프로세스에 직접 접근할 수 없게 된다. 그렇다면 어떻게 우리가 제작한 사용자 모드의 프로그램이 프로세스나 디스크와 관련된 작업을 수행할 수 있을까? 이는 바로 시스템 호출(System Call)을 사용하여 사용자 모드의 프로세스가 커널 영역에 접근을 요청할 수 있기 때문이다.

그림 18. System Call 과정

위 그림과 같이 응용프로그램이 Kernel32.dll의 API를 호출하면 해당 API는 Ntdll.dll의 함수를 호출한다. 그리고 호출된 Ntdll.dll은 자신이 커널에 요청해야 할 서비스 번호를 가지고 시스템 호출을 진행하며 이 과정이 바로 응용프로그램이 커널에게 시스템 자원 접근을 요청하는 과정이다. 이때 지정한 서비스를 요청하기 위해 EAX에 원하는 서비스 번호를 저장하고 EDX에는 이 서비스에 사용될 인자를 가리키는 포인터를 넘겨준다. 이러한 과정을 통해 커널에서는 어떠한 서비스가 필요한지, 어떠한 인자를 넘겨주었는지 알 수가 있다.

System Call은 "INT 0x2E"와 "SYSENTER" 두 가지 명령어로 나누어진다. 이렇게 나누어지는 기준은 바로 Windows XP 이전과 이후로, 이전에는 INT 0x2E를 사용하였으며, XP부터는 SYSENTER를 사용한다. INT 0x2E의 경우 상대적으로 무거운 인터럽트를 진행하므로 클럭 수를 많이 소모하였기 때문에, 이를 보완하기 위해 SYSENTER가 나온 것이다. 이러한 차이 외에 앞으로 진행할 후킹 과정에서도 차이점을 가지므로 이에 대하여 알아보자.

그림 19. INT 0x2E와 SYSENTER

우선 INT 0x2E의 경우 IDT(Interrupt Descriptor Table)을 참조하여 바로 System Service Dispatch(KiSystemService)로 간다. 하지만 SYSENTER는 SYSENTER_EIP(MSR)를 참조하여 KiFastCallEntry로 진행한 다음 KiSystemService로 간다. 마지막으로 INT 0x2E의 경우 IRET라는 명령어로 커널 모드에서 다시 사용자 모드로 복귀하고, SYSENTER의 경우 SYSEXIT라는 명령어를 통해 사용자 모드로 복귀한다. 이것이 별로 중요하게 느껴지지 않을 수 있지만, 후킹을 진행할 때 두 명령어에 따라 후킹 지점이 달라진다. 이러한 각 후킹 방법에 대해서는 바로 뒤에서 알아보자.

  

INT 0x2E Hooking

INT 0x2E는 인터럽트 0x2E로 IDT에 정의된 인터럽트 서비스 루틴(ISR)을 수행한다. 여기서 IDT는 256개의 Entry로 이루어진 배열로 엔트리 하나당 하나의 인터럽트에 대응하며 각 인터럽트는 IDT로부터 처리할 함수의 주소(ISR)을 전달받는다. 각 엔트리에는 지정된 값이 담겨 있으며 WinDBG로 확인했을 경우 아래와 같은 모습을 볼 수가 있으며 INT 0x2E의 경우 IDT에서 바로 KiSystemService를 가리키고 있는 것을 확인할 수 있다.

kd> !idt

Dumping IDT: 8003f400

…(skip)

2e:    8053f481 nt!KiSystemService

37:    806d3728 hal!PicSpuriousService37

3d:    806d4b70 hal!HalpApcInterrupt

41:    806d49cc hal!HalpDispatchInterrupt

50:    806d3800 hal!HalpApicRebootService

…(skip)

그림 20. IDT 구조

이 과정을 요약하면, XP 이전 버전에는 응용프로그램이 API를 호출하면 Ntdll.dll의 Zw*, Nt* 함수를 호출하게 된다. 이러한 함수는 결국 INT 0x2E를 통해 운영체제에게 커널 모드 작업을 요청한다. 이때 INT 0x2E가 IDT에서 KiSystemService의 주소를 참조하여 진행하는 것이다. 따라서 우리가 후킹 해야 할 부분은 바로 IDT이다. IDT가 가리키는 2E의 주소로 가보면 실제로 KiSystemSerive가 존재하고 있는 것을 아래와 같이 확인할 수 있다.

nt!KiSystemService:

8053f481 6a00 push 0

8053f483 55 push ebp

8053f484 53 push ebx

8053f485 56 push esi

8053f486 57 push edi

8053f487 0fa0 push fs

8053f489 bb30000000 mov ebx,30h

그림 21. INT 0x2E의 ISR(KiSystemService)

IDT를 후킹 할 것이므로 우선 IDT의 주소를 알아야 하는데, IDTR 레지스터에 IDT의 Base Address와 IDT의 크기가 저장되어 있다. WinDBG를 통해 알 수 있는 방법은 IDTR 레지스터의 값(주소)를 출력하거나 "!idt"를 통해 해당 주소를 알아낼 수가 있다. IDT의 Entry 구조는 아래 그림과 같이 8 바이트씩으로 이루어져 있으며 Entry가 가리키는 ISR의 주소가 하위 2바이트, 상위 주소 2바이트로 나누어져 있다.

kd> r idtr

idtr=8003f400

kd> !idt    

Dumping IDT: 8003f400

 

kd> dt _KIDTENTRY

ntdll!_KIDTENTRY

+0x000 Offset : Uint2B // 하위 오프셋

+0x002 Selector : Uint2B

+0x004 Access : Uint2B

+0x006 ExtendedOffset : Uint2B // 상위 오프셋

그림 22. IDT 주소와 각 엔트리 구조

하나의 엔트리가 8바이트로 이루어져 있다는 것을 확인했다. 그렇다면 우리가 찾고자 하는 엔트리는 0x2E 번째 엔트리이므로 IDT Base Address에 0x170을 더한 위치에 존재하고 있다. 아래 결과와 같이 8003f570부터 해당 엔트리가 존재하고 있다. 하위 2바이트와 상위 2바이트를 조합하여 INT 0x2E의 ISR은 8053f481이라는 것을 알 수 있다.

kd> db 8003f400 8003fC00

8003f400 9c 01 08 00 00 8e 54 80-14 03 08 00 00 8e 54 80 ......T.......T.

8003f410 3e 11 58 00 00 85 00 00-e4 06 08 00 00 ee 54 80 >.X...........T.

…(skip)

8003f560 80 fc 08 00 00 ee 53 80 - c0 05 08 00 00 ee 54 80 ......S.......T.

8003f570 81 f4 08 00 00 ee 53 80-80 27 08 00 00 8e 54 80 ......S..'....T.

…(skip)

그림 23. IDT 0x2E 번째 Entry

이제 우리가 어떤 주소(8003f570)를 후킹 해야 하는지 알았으니, 본격적인 후킹을 진행해보자. 이번 후킹 역시 프로그래밍을 통해 진행하는 것이 아니라 원리를 이해하기 위해 커널 디버거 WinDBG를 통해 진행할 것이다. 해당 0x2E의 ISR을 FFFFFFFF로 조작하는 과정으로 "0000"으로 채운 곳은 오프셋이 아닌 부분으로 구분을 위해 "0"으로 채운 것이다.

kd> !idt 2e // 기존 2E의 ISR 확인

2e:    8053f481 nt!KiSystemService

kd> ed 8003f570 // Windbg를 통해 직접 수정

8003f570 0008f481 0000ffff

8003f574 0000ffff ffff0000

kd> !idt 2e // 조작된 2E의 ISR 확인

2e:    ffffffff

kd> db 8003f570 8003f580 // 조작된 IDT 2E Entry 확인

8003f570 ff ff 00 00 00 00 ff ff-80 27 08 00 00 8e 54 80 .........'....T.

그림 24. IDT Entry 0x2E 후킹

이처럼 IDT가 후킹 된 상황에서 INT 0x2E를 통한 시스템 호출(System Call)이 발생하면 사용자 모드에서 커널 모드로 넘어갈 때 후킹된 주소로 가게 된다. 실제 후킹도 이와 같은 과정으로 진행되며, 대신 IDT의 주소를 얻을 때 "sidt" 명령어를 사용하여 주소를 얻는다. Sidt 명령어는 IDT의 주소를 저장하고 있는 IDTR 레지스터의 값을 참조하여 값을 얻어 온다. 또한 디버거를 통해 수정하는 것이 아니라 프로그래밍을 통해 수정하고자 할 때, 인터럽트를 비활성화("CLI" 명령어)해야 한다. 그리고 후킹이 완료되면 인터럽트를 다시 활성화("STI" 명령어) 시켜 정상적으로 구동되게끔 해야 한다.

  

SYSENTER Hooking

윈도우 XP 이상의 버전에서는 INT 0x2E가 아닌 SYSENTER를 사용한다. 시스템 호출이 요청되면 NTDLL은 EAX 레지스터에 해당 시스템 호출의 번호를 저장하고 EDX 레지스터에는 인자로 사용될 주소를 넣어준다. 그리고 SYSENTER 명령을 실행하여 커널 영역으로 들어오게 되는데, 이때 바로 커널로 들어가는 것이 아니라 실행될 커널의 주소(KiFastCallEntry)를 SYSENTER_EIP(MSR 0x176 레지스터)에서 참조하여 KiFastCallEntry 로 넘어가게 되는 것이다.

MSR은 Model-Specific Register로 디버깅이나 프로그램 실행 추적, 컴퓨터 성능 모니터링, 특정 CPU 기능 전환에 사용되는 각종 제어 레지스터x86 명령어의 집합이다. 이러한 집합 중 MSR 0x176에는 IA_SYSENTER_EIP(KiFastCallEntry)가 존재하고 있으며, MSR에 접근하고자 할 때는 "rdmsr", "wrmsr" 명령어를 통해 접근할 수가 있다. 우리가 찾아야 할 것은 MSR 0x176이며 다음과 같은 결과를 얻을 수가 있다.

kd> rdmsr 176

msr[176] = 00000000`8053f540

 

kd> u 8053f540

nt!KiFastCallEntry:

8053f540 b923000000 mov ecx,23h

8053f545 6a30 push 30h

…(skip)

그림 25. Read MSR 0x176

MSR 0x176이 제대로 KiFastCallEntry를 가리키고 있는 것을 확인할 수 있다. 바로 이 부분의 값을 바꾸어 SYSENTER를 후킹 할 수 있다. MSR 레지스터를 읽을 때는 rdmsr 명령어로 읽었다면, MSR 레지스터의 값을 변경할 때는 "wrmsr" 명령어를 사용하면 된다. 아래의 그림을 보자.

kd> wrmsr 176 11111111 // MSR 0x176의 값을 변경

kd> rdmsr 176 // MSR 0x176 값 변경 확인

msr[176] = 00000000`11111111

그림 26. Write MSR 0x176

실제로 위와 같이 옳지 않은 주소로 변경하면 당연히 블루 스크린을 맞이할 수 있을 것이다. 그렇다면 abex'sCrackMe01.exe를 가지고 직접 코드의 흐름을 조작하여 보자. 우선 아래의 코드는 정상적인 흐름을 나타낸다. Ntdll.dll의 함수를 추적하여 들어가보면 KiFastSystemCall이라는 부분을 볼 수 있는데 이는 SYSENTER 명령어를 통해 KiFastCallEntry로 가기 위한 부분이다. KiFastSystemCall에는 SYSENTER 명령어가 위치하고 있는 것을 확인할 수 있다.

ntdll!KiFastSystemCall:

001b:7c93e4f0 8bd4 mov edx,esp

001b:7c93e4f2 0f34 sysenter

kd> rdmsr 176 // MSR 0x176의 주소를 확인

msr[176] = 00000000`8053f540    

 

kd> bp 8053f540    // MSR 0x176의 주소에 BP 설정

 

kd> p    // Kernel의 KiFastCallEntry 에 올바르게 진입

Breakpoint 3 hit

nt!KiFastCallEntry:

8053f540 b923000000 mov ecx,23h

그림 27. 정상적인 SYSENTER 진입

정상적인 SYSENTER의 흐름을 wrmsr을 통해 조작해보자. MSR 0x176를 11111111 로 수정하므로 비정상적인 흐름으로 동작하도록 수정하였다. 그리고 이전과 같이 SYSENTER 명령어를 실행하기 전에 원래 MSR 0x176(KiFastCallEntry)의 주소에 BP를 설정한 다음 진행을 해보자. 정상적인 흐름이라면 KiFastCallEntry 에서 멈추어야 하지만, 조작된 MSR 0x176이 가리키는 주소 11111111로 흐름이 바뀌었다.

ntdll!KiFastSystemCall:

001b:7c93e4f0 8bd4 mov edx,esp

001b:7c93e4f2 0f34 sysenter

 

kd> bp 8053f540    // 기존 KiFastCallEntry에 BP 설정

kd> wrmsr 176 11111111    // MSR 0x176 주소 조작

 

kd> p    // 조작한 주소로 이동

Access violation - code c0000005 (!!! second chance !!!)

11111111 ?? ???

그림 28. 후킹 된 SYSENTER 진입

이러한 방법을 통해 SYSENTER Hooking(또는 MSR Hooking)을 진행할 수 있으며, 실제 11111111에 후킹 함수가 존재할 경우 시스템 호출이 발생할 때마다 후킹 함수를 지나가게 된다. 공격자의 입장에서 이러한 System Call Hooking을 진행하며 후킹 함수에 과도한 조건을 걸어놓는다면 시스템 성능이 크게 하락하여 사용자가 쉽게 알아차릴 수 있을 것이다.

'Reversing > Theory' 카테고리의 다른 글

윈도우 후킹 원리 [PDF]  (1) 2016.04.23
윈도우 후킹 원리 (3) - Kernel [SSDT]  (0) 2016.04.23
윈도우 후킹 원리 (1) - User Mode  (3) 2016.04.23
System Call & SSDT Hooking  (0) 2016.04.10
BOF에 취약한 함수  (1) 2016.03.30