윈도우 후킹 원리 [PDF]
목차1 Intro. 42 Prior Knowledge. 52.1 What is an API?. 52.2 What is an API Hooking?. 63 User Mode Hooking. 73.1 IAT Hooking. 73.2 Message Hooking. 134 Kernel Mode Hooking. 154.1 System Call 154.2 INT 0x2E Hooking. 164.3 SYSENTER Hooking. 194.4 SSDT Hooking. 225 Conclusion. 266 Reference. 27 그림그림 1. User Mode & Kernel Mode. 5그림 2. 정상호출과 후킹된 호출... 6그림 3. PE View로 본 IAT. 7그림 4. Sleep API 7그림 5. 메모리에서..
2016.04.23
no image
윈도우 후킹 원리 (3) - Kernel [SSDT]
SSDT HookingSSDT(System Service Dispatch Table)는 시스템 호출을 요청한 뒤, 전달되는 서비스 번호에 맞는 함수를 찾을 때 참조한다. 위 과정에서 시스템 호출을 요청하는 두 가지 명령어(INT 0x2E와 SYSENTER)에 대하여 알아보았는데, 결국 두 명령어 모두 KiSystemService(System Service Dispatcher)를 호출한다고 언급하였다.KiSystemService가 호출될 때 EAX에는 사용자 영역에서 요청한 서비스 번호가 저장되어 있으며, EDX에는 이러한 서비스에 사용될 인자가 저장되어있다. 이러한 시스템 호출 번호(EAX)에 맞게 KeServiceDescriptorTalbe에서 Native API의 주소를 가지고 온다. 그 후 시스템 ..
2016.04.23
no image
윈도우 후킹 원리 (2) - Kernel [SYSTEM CALL]
Kernel Mode Hooking3장에서는 사용자 모드 후킹에 대하여 알아보았다면 이번 장에서는 커널 모드 후킹에 대하여 알아볼 것이다. 커널 모드에서 이루어지는 후킹의 경우 단순히 JMP 명령어를 설치하는 것이 아니라, 특정한 구조체에 포함된 값을 수정하는 등 작업을 수행해야 하기 때문에 아무래도 사용자 모드의 후킹보다 복잡하다. 이제 이러한 커널 모드 후킹에 대하여 알아보자. System Call운영체제는 사용자 모드(Ring 3)와 커널 모드(Ring 0)라는 두 가지 형태의 권한이 존재하고 있다. 이렇게 분리되는 이유는 다양하지만, 아무래도 보안과 관련된 점 또한 매우 중요하다. 만약 분리되어 있지 않을 경우 어떠한 프로세스든지 운영체제의 핵심 기능을 조작할 수 있게 되므로 이를 방지하기 위해선..
2016.04.23
System Call & SSDT Hooking
System CallWindows 운영체제는 사용자 모드와 커널 모드라는 두 가지 형태의 권한이 존재하고 있다. 굳이 하나가 아닌 두 가지로 분류되는 것은 모든 프로세스가 하나의 권한으로만 동작할 경우, 각 프로세스는 하드웨어나 프로세스에 직접 접근할 수 있게 된다. 이는 어떠한 프로세스라도 운영체제의 핵심 기능을 조작할 수 있게 되는 것이므로 보안에 있어 매우 취약하게 된다. 이러한 요소를 방지하기 위해 두 개의 영역으로 분류되었고, 당연히 사용자 모드에 존재하고 있는 프로세스는 커널 영역에 접근할 수가 없다. 하지만 커널 영역에 접근할 수 없다는 것은 해당 프로세스가 디스크의 내용을 읽을 수가 없게 되고, 그 외에도 많은 작업들에 제한이 생긴다. 따라서 이러한 불편함을 보완하기 위해 "사용자 모드의 ..
2016.04.10


Windows_API_Hooking.pdf


목차

1    Intro. 4

2    Prior Knowledge. 5

2.1    What is an API?. 5

2.2    What is an API Hooking?. 6

3    User Mode Hooking. 7

3.1    IAT Hooking. 7

3.2    Message Hooking. 13

4    Kernel Mode Hooking. 15

4.1    System Call 15

4.2    INT 0x2E Hooking. 16

4.3    SYSENTER Hooking. 19

4.4    SSDT Hooking. 22

5    Conclusion. 26

6    Reference. 27

 

그림

그림 1. User Mode & Kernel Mode. 5

그림 2. 정상호출과 후킹된 호출... 6

그림 3. PE View로 본 IAT. 7

그림 4. Sleep API 7

그림 5. 메모리에서 Sleep API 8

그림 6. Sleep API in IAT. 8

그림 7. 코드 패치... 9

그림 8. 호출할 함수 주소 변경... 9

그림 9. 함수 호출 - Debugger 10

그림 10. Sleep이 호출하는 주소 변경... 10

그림 11. Code Cave를 사용한 후킹... 11

그림 12. (C) 단계 원래 명령어와 조작된 명령어... 11

그림 13. 조작 코드... 12

그림 14. Ntdll.dll API 12

그림 15. 메시지 전달 방식... 13

그림 16. SetWindowsHookEx API 14

그림 17. DLL Injection. 14

그림 18. System Call 과정... 15

그림 19. INT 0x2E SYSENTER. 16

그림 20. IDT 구조... 17

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

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

그림 23. IDT 0x2E 번째 Entry. 18

그림 24. IDT Entry 0x2E 후킹... 19

그림 25. Read MSR 0x176. 20

그림 26. Write MSR 0x176. 20

그림 27. 정상적인 SYSENTER 진입... 21

그림 28. 후킹 된 SYSENTER 진입... 21

그림 29. 전체적인 시스템 호출 과정... 22

그림 30. SSDT Hooking 과정... 23

그림 31. KeServiceDescriptorTable 구조... 24

그림 32. SSDT를 통한 Native API 접근... 24

그림 33. SSDT Hooking. 25

 


 

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

DLL이란?  (4) 2016.05.29
PE구조의 이해  (0) 2016.05.04
윈도우 후킹 원리 (3) - Kernel [SSDT]  (0) 2016.04.23
윈도우 후킹 원리 (2) - Kernel [SYSTEM CALL]  (0) 2016.04.23
윈도우 후킹 원리 (1) - User Mode  (3) 2016.04.23
SSDT Hooking


SSDT(System Service Dispatch Table)는 시스템 호출을 요청한 뒤, 전달되는 서비스 번호에 맞는 함수를 찾을 때 참조한다. 위 과정에서 시스템 호출을 요청하는 두 가지 명령어(INT 0x2E와 SYSENTER)에 대하여 알아보았는데, 결국 두 명령어 모두 KiSystemService(System Service Dispatcher)를 호출한다고 언급하였다.

KiSystemService가 호출될 때 EAX에는 사용자 영역에서 요청한 서비스 번호가 저장되어 있으며, EDX에는 이러한 서비스에 사용될 인자가 저장되어있다. 이러한 시스템 호출 번호(EAX)에 맞게 KeServiceDescriptorTalbe에서 Native API의 주소를 가지고 온다. 그 후 시스템 호출을 종료하고 다시 사용자 모드로 복귀하게 되는데, 이러한 과정은 아래의 그림과 같다.

그림 29. 전체적인 시스템 호출 과정

결국 SSDT(KiServiceTable)에서 서비스 호출 번호에 맞는 주소를 얻은 다음 이를 호출하는 형태로 진행되는 것이다. 그렇다면 SSDT Hooking은 어느 부분을 후킹해야 하는 것일까? 위 그림의 과정에서 설명하자면 바로 GetFuncAddress 과정에서 후킹한다고 할 수 있다. SYSENTER를 통해 KiFastCallEntry로 진입한 후 서비스 번호에 맞는 서비스 루틴을 SSDT에서 얻어온다. 따라서 SSDT에 존재하고 있는 각 서비스 루틴의 주소를 조작하므로 후킹을 진행할 수 있다. 이를 표현한 그림은 아래와 같다.

그림 30. SSDT Hooking 과정

해당 시스템 호출의 서비스 루틴을 가지고 오는 과정에서 SSDT를 참조하는데, SSDT의 해당 번호가 나타내는 주소를 후킹하므로 우리가 원하는 흐름으로 조작할 수 있다. 위 그림을 예로 시스템 호출이 요청되었을 때 서비스 번호가 저장되어 있는 EAX의 값이 0xAD라면 SSDT에서 0xAD가 가리키는 서비스 루틴의 주소 0xCCCCCCCC가 반환되어 이를 호출한다. 하지만 만약 공격자가 SSDT를 후킹하여 0xDDDDDDDD로 서비스 루틴의 주소를 변경하였다면, 시스템 호출 0xAD가 발생할 때마다 0xDDDDDDDD를 지나가게 된다.

KeServiceDescriptorTable은 네 가지 항목을 가지고 있는 구조체로 아래 그림과 같이 나타나는 것을 확인할 수 있으며 중요한 첫 번째 항목과 네 번째 항목에 대하여 알아보자. 첫 번째 항목은 KiServiceTable(SSDT)의 주소를 담고 있는 항목으로 이 값을 통해 SSDT에 접근하여 Native API의 주소를 얻을 수가 있으며. 네 번째 항목은 ParamTableBase는 KiArgumentTable의 주소 값을 담고 있는데, 이들 각각은 SSDT의 Native API와 일 대 일로 대응한다.

kd> dd KeServiceDescriptorTable

80554fa0 80503b8c 00000000 0000011c 80504000 //ServiceDescriptor[0]

80554fb0 00000000 00000000 00000000 00000000 //ServiceDescriptor[1]

80554fc0 00000000 00000000 00000000 00000000 //ServiceDescriptor[2]

80554fd0 00000000 00000000 00000000 00000000 //ServiceDescriptor[…]

…(skip)

그림 31. KeServiceDescriptorTable 구조

SSDT의 주소는 첫 번째 항목의 값인 0x80503b8c로 해당 주소를 확인해보면 여러 주소들이 존재하고 있는 것을 아래와 같이 확인할 수 있다. 각 값들은 Native API의 실제 주소이며 해당 주소를 확인해보면 Native API의 이름을 같이 볼 수 있다.

kd> d 80503b8c // SSDT Base

80503b8c 8059b948 805e8db6 805ec5fc 805e8de8

80503b9c 805ec636 805e8e1e 805ec67a 805ec6be

80503bac 8060ddfe 8060eb50 805e41b4 805e3e0c

80503bbc 805ccde6 805ccd96 8060e424 805ad5ae

80503bcc 8060da3c 8059fdbe 805a7a00 805ce8c4

…(skip)

 

kd> u 8059b948

nt!NtAcceptConnectPort:

8059b948 689c000000 push 9Ch

8059b94d 6838b14d80 push offset nt!_real+0x128 (804db138)

8059b952 e8b9e5f9ff call nt!_SEH_prolog (80539f10)

그림 32. SSDT를 통한 Native API 접근

시스템 호출을 통해 커널 모드로 진입할 때 EAX에는 요청한 서비스 번호를 저장하고 있고 EDX에는 인자로 사용될 포인터를 포함하고 있다고 하였다. 그러므로 어떠한 Native API를 요청하는지 알기 위해선 SSDT의 주소에 [EAX*4]를 더해주면 그 주소를 알 수 있다. 실제 SSDT Hooking도 이와 같은 방식으로 진행한다. 그렇다면 이제 실제 SSDT의 주소를 변경해보자.

SSDT를 통해 접근할 수 있는 Native API 함수 5개의 주소를 변경해보자. WinDBG를 사용하기 때문에 특정 주소 값을 수정하기 위한 "ed" 명령어를 사용하였으며, 기존의 주소를 아무 의미 없는 값들로 변경하였다. 그 후 SSDT를 확인해보면 위 그림 32에서 확인할 수 있던 주소들이 내가 수정한 값으로 변경되어 있는 것을 확인할 수 있다.

kd> ed 80503b8c

80503b8c 8059b948 ffffffff

80503b90 805e8db6 00000000

80503b94 805ec5fc ffffffff

80503b98 805e8de8 00000000

80503b9c 805ec636 ffffffff

80503ba0 805e8e1e 11111111

 

kd> d 80503b8c

80503b8c ffffffff 00000000 ffffffff 00000000

80503b9c ffffffff 11111111 805ec67a 805ec6be

80503bac 8060ddfe 8060eb50 805e41b4 805e3e0c

80503bbc 805ccde6 805ccd96 8060e424 805ad5ae

그림 33. SSDT Hooking

이렇게 후킹을 하면 해당 Native API가 요청될 때마다 후킹된 주소로 넘어가게 된다. 위 실습은 아주 극단적인 예를 보여주기 위한 과정으로 바로 블루 스크린이 나타난다. 실제 후킹 공격을 진행하기 위해선 메모리 쓰기 보호(Write Protect)를 해제하는 작업을 추가해야 하며, 매크로와 같은 방식을 통해 공격을 진행한다.

  

Conclusion


사용자 영역 후킹과 커널 영역 후킹에 대해 디버거를 통해 접근해보며 어떻게 후킹이 이루어지는지 알아보았다. 실제 공격을 하는데 있어 디버거를 사용하는 것보다는 프로그래밍을 통해 쉽게 공격이 이루어질 수 있도록 한다. 그렇기에 다른 사람들의 글 대부분이 이러한 프로그래밍에 초점을 맞추고 어떻게 코드를 설계하는지, 코드가 의미하는 것이 어떤 내용인지 잘 설명하고 있으므로 이후에 코드와 관련된 내용을 학습하면 더 좋을 것이다.

필자도 코드를 통해 어떻게 동작하겠구나 생각해볼 수 있었지만 직접 디버거를 통해 접근해볼 때마다 "여기를 수정하면 어떻게 될까?"라는 등 좀 더 깊이 있는 생각을 해볼 수가 있었다. 이후에는 여기서 다루지 못한 후킹들에 대하여 추가로 학습해볼 것이며 윈도우 운영체제와 관련된 내용을 더 공부해보아야겠다는 생각을 할 수가 있었다.

  

Reference


[+] 리버싱 핵심 원리(악성 코드 분석가의 리버싱 이야기) |이승원|인사이트|2012.09.30

[+] http://blog.naver.com/ikariksj/140056467421    

[+] http://www.codeproject.com/Articles/2082/API-hooking-revealed

[+] http://www.reversecore.com/23

[+] http://yokang90.tistory.com/58

[+] http://xcoolcat7.tistory.com/542

[+] http://egloos.zum.com/maxtrain/v/2775961

[+] http://kernel32.tistory.com/15

[+] https://msdn.microsoft.com/en-us/library/windows/desktop/ms644990(v=vs.85).aspx

[+] https://blogs.msdn.microsoft.com/kocoreinternals/2009/03/16/idt-isr/

[+] https://en.wikipedia.org/wiki/Model-specific_register

[+] http://amur.tistory.com/entry/커널모드에서-유저모드-분석하기

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

PE구조의 이해  (0) 2016.05.04
윈도우 후킹 원리 [PDF]  (1) 2016.04.23
윈도우 후킹 원리 (2) - Kernel [SYSTEM CALL]  (0) 2016.04.23
윈도우 후킹 원리 (1) - User Mode  (3) 2016.04.23
System Call & SSDT Hooking  (0) 2016.04.10
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
System Call

Windows 운영체제는 사용자 모드와 커널 모드라는 두 가지 형태의 권한이 존재하고 있다. 굳이 하나가 아닌 두 가지로 분류되는 것은 모든 프로세스가 하나의 권한으로만 동작할 경우, 각 프로세스는 하드웨어나 프로세스에 직접 접근할 수 있게 된다. 이는 어떠한 프로세스라도 운영체제의 핵심 기능을 조작할 수 있게 되는 것이므로 보안에 있어 매우 취약하게 된다. 이러한 요소를 방지하기 위해 두 개의 영역으로 분류되었고, 당연히 사용자 모드에 존재하고 있는 프로세스는 커널 영역에 접근할 수가 없다.


하지만 커널 영역에 접근할 수 없다는 것은 해당 프로세스가 디스크의 내용을 읽을 수가 없게 되고, 그 외에도 많은 작업들에 제한이 생긴다. 따라서 이러한 불편함을 보완하기 위해 "사용자 모드의 프로세스가 커널 영역에 접근할 수 있는 방법을 만들자."라는 의도에 부합되는 것이 바로 시스템 호출(System Call)이다. 즉, Windows는 커널에 대한 직접적인 접근을 제한하는 대신 사용자 영역에서 API를 호출하면 NTDLL.DLL을 거쳐 커널 모드로 진입하게 된다.

시스템 콜은 크게 2가지 방법으로 진행될 수 있는데, 바로 "INT 0x2E"와 "SYSENTER"이다. INT 0x2E의 경우 소프트웨어 인터럽트로 Windows XP 이전에는 이를 통해 시스템 콜을 진행하였다면, XP부터는 SYSENTER를 통해 시스템 콜을 진행하였다. 이 둘의 차이점이 몇 가지 존재하고 있지만 가장 큰 차이점은 바로, SYSENTER는 수행 시간에 따른 부하가 적다는 것이다. 또한 각 명령어가 호출된 후 과정에 차이가 있는데 이는 아래의 그림에서와 같이 SYSENTER 명령어가 진행될 경우 KiFastCallEntry()를 호출한 후, 이를 통해 다시 KiSystemService()를 호출한다. 이에 반해 INT 0x2E의 경우 KiFastCallEntry()를 거치지 않고 바로 호출된다는 것이다.


INT 0x2E나 SYSENTER 명령어의 경우 우리가 직접 확인할 수도 있다. 흔히 자주 접할 수 있는 OllyDBG와 같은 유저 모드 디버거를 통해 특정한 함수를 호출하는 부분을 계속 따라가다 보면 ntdll.dll의 함수를 볼 수가 있다. 그리고 이러한 함수를 계속 트레이싱하다 보면 INT 0x2E나 SYSENTER 명령어를 확인할 수 있다. 단, 유저 모드 디버거이기 때문에 해당 명령어가 수행되는 자세한 과정은 확인할 수가 없으므로 이를 확인하기 위해선 WinDBG와 같은 커널 디버거를 이용해야 한다. 진입하는 과정은 아래의 그림과 같다.

CALL EDX를 통해 지정된 주소를 호출하게 되는데, 해당 부분은 아래의 그림과 같다. 여기서 FS:[0x30]은 TEB의 0x30번째에 있는 값을 나타낸다. 이는 PEB를 나타내는 것으로 이러한 방법을 통해 PEB에 접근할 수가 있다. 그리고 PEB의 0x464번째 바이트 값을 통해 해당 값이 2인지 비교한 다음 만약 2라면 INT 0x2E를 통해 커널 모드에 진입하게 된다.

만약 해당 값이 2가 아니라면 JMP 명령어를 통해서 KiFastSystemCall 부분으로 넘어오게 되는데, 해당 함수의 부분은 SYSENTER 명령어를 통해 KiFastCallEntry로 이동하게 된다.

이렇게 커널 모드에 접근하는 과정을 확인할 수가 있었다. 그렇다면 어떻게 다시 사용자 영역으로 복귀할까? 이 역시 두 명령어가 차이를 보인다. SYSENTER 명령어의 경우 커널 영역에서 "SYSEXIT" 명령어를 통해 사용자 영역으로 돌아오게 되며, INT 0x2E의 경우 "IRET" 명령어를 통해 원래의 코드로 복귀가 가능하다.


IDT Hooking

IDT는 256개의 Enyrt로 이루어진 배열이며 엔트리 하나당 하나의 인터럽트에 대응되며 이러한 각 인터럽트는 IDT로부터 처리할 함수의 주소(ISR)를 전달받는다. 다시 말해, IDT에서는 각 인터럽트를 처리할 함수의 주소를 갖고 있다. 인터럽트 테이블의 메모리 주소를 얻으려면 IDTR 레지스터 값을 읽어야 하는데 이는 "sidt" 명령을 통해 알 수가 있으며, 반대로 'lidt' 명령을 통해 IDTR 레지스터의 값을 변경할 수 있다. sidt 명령에 의해 반환되는 idt 데이터 구조는 아래와 같다.

typedef struct
{
    unsigned short IDTLimit;
    unsigned short LowIDTbase;
    unsigned short HiIDTbase;
}

IDT의 주소는 위 구조체에서 확인할 수 있듯이, 하위 주소와 상위 주소가 나누어져 있다. 이를 통해 IDT의 주소를 확인할 수가 있는데, 그렇다면 IDT에는 어떠한 내용이 포함되어 있는지 아래의 그림을 보자. 크게 중요한 내용은 바로 위 시스템 콜에서 볼 수 있었던 0x2E와 키보드 인터럽트인 0x93이 존재하고 있다. 0x93의 경우 키보드 메시지 후킹과 관련된 내용이므로 자세히 다루지 않고 0x2E를 위주로 살펴보자.


INT 0x2E


응용 프로그램이 운영체제가 제공하는 API를 호출하면 NTDLL.DLL은 EAX 레지스터에 해당 시스템 함수의 번호를 로드하고 EDX 레지스터에는 그 시스템 함수로 전달되는 인자가 저장된 사용자 영역의 스택 주소를 로드한다. 그리고 INT 0x2E 명령을 수행하여 인터럽트를 발생시킨다. 이 인터럽트에 의해 유저 모드에서 커널 모드로 전환된다. 따라서 INT 0x2E 명령을 수행하여 인터럽트가 발생한 다음, IDT로부터 ISR의 주소를 얻어 실행하고자 할 때 바로 ISR의 주소를 바꾸어 IDT를 후킹 할 수가 있는 것이다. 이를 표현하면 아래와 같다.

IDT를 후킹 하는 데 있어 동작하고 있는 중 그 값이 변경되면 오류를 일으킬 수 있기 때문에 'cli' 명령을 통해 인터럽트 처리를 정지시킨 후, 해당 값이나 주소를 변경해야 한다. 모든 수정이 완료된 이후에는 다시 인터럽트 처리를 활성화시켜야 하기 때문에 'sti' 명령을 사용하여야만 한다. 이를 위한 코드는 아래와 같다.

_asm {
     sidt idt_info;
}

idt_entries = (*IDTENTRY) MAKELONG (idt_info.LowIDTbase, idt_info.HiIDTbase);
int2e_entry = &(idt_entries[0x2E])    //IDT에서 0x2E번째 엔트리 주소

_asm {
     cli;                                                                // 인터럽트 Disable
     lea eax, HookKiSystemService;             // EAX에 후킹 함수의 주소를 저장
     mov ebx, int2e_entry;                             // IDT에서 0x2E번째 엔트리의 주소 전달
     mov [ebx], ax;                                            // 후킹 함수 하위 16비트 주소를 IDT엔트리에 기록
     shr eax, 16;                                                 
     mov [ebx+6], ax;                                        // 후킹 함수 상위 16비트 주소 또한 기록
     sti;                                                                // 인터럽트 Enable
}


MSR Hooking

그렇다면 SYSENTER의 경우는 어떻게 될까? 윈도우 XP 이상에서는 INT 0x2E가 아닌 SYSENTER를 사용하는데, 시스템 콜이 요청되면 NTDLL은 EAX에 해당 시스템 콜의 번호를 로드하고 EDX 레지스터에서는 포인터 레지스터인 ESP를 로드한다. 그다음 NTDLL은 SYSENTER 명령을 실행한다. SYSENTER 명령이 실행되면 커널 영역에 들어가게 된다. 이때 그냥 커널로 들어가는 것이 아니라 실행되어질 커널의 주소인 MSR 0x176 레지스터(KiFastCallEntry) 내부에 있는 값으로 이동하게 된다. 그 후 KiFastCallEntry는 EAX에 저장되어 있는 시스템 콜 번호를 가지고 SSDT에서 Nt함수 주소로 가져오고 해당 함수를 호출한다. SSDT에 대해선 뒤에서 더 자세히 알아볼 것이다.

위 그림과 같이 나타낼 수 있으며, 바로 MSR 0x176의 값을 변경하므로 이를 수정하는 것이다. 이를 변경하기 위해서는 rdmsr 명령어와 wrmsr 명령어를 사용하여 msr의 값을 읽고 조작하여야 한다. 이를 위한 코드는 아래와 같다.

_asm {
    mov ecx, 0x176            // MSR 0x176 지정
    rdmsr                             // 어떠한 주소가 있지 읽음
    mov  OrigFunc, eax    // 기존에 있던 값을 저장
    mov eax, HookFunc  
    wrmsr                            // MSR 0x176에 HookFunc의 주소를 기록함
}

이렇게 후킹 작업을 완료한 다음, SYSENTER 명령으로 인해 MSR 0x176에 존재하고 있는 KiFastCallEntry의 주소가 아닌 훅 함수의 주소가 호출되므로 인해 KiFastCallEntry가 아닌 HookFunc()이 먼저 호출된 다음 KiFastCallEntry가 호출된다. 실제로 이러한 후킹 작업이 성공적으로 이루어졌다면 훅 함수에서는 EAX 레지스터에 있는 값을 통해 어떠한 시스템 콜을 요청했는지 확인할 수가 있다. 이는 다시 말해 특정한 시스템 콜을 지정하여 해당 시스템 콜은 거부되도록 할 수 있다.


단, 시스템 콜은 빈번하게 이루어지는 작업이기 때문에 과도한 조건문을 걸어놓는 것은 시스템의 성능을 크게 하락시키므로 사용자가 속도의 변화를 체감할 수도 있다. 따라서 빈번하게 호출되는 만큼 최적화된 내용만을 후킹 함수에 넣어야 한다.


SSDT Hooking

시스템 서비스 디스패치 테이블은 시스템 콜을 처리하기 위한 함수를 찾을 때 사용된다. 위에서 프로그램이 시스템 콜을 호출하는 두 가지 방법(INT 0x2E, SYSENTER)에 대하여 알아보았다. 이 두 명령어를 통해 결국 KiSystemService 함수(시스템 서비스 디스패처)를 호출하게 되는데, 이 함수는 EAX 레지스터에서 시스템 콜 번호를 읽어 SSDT에서 해당 시스템 콜의 루틴을 찾는다. 


또한 KiSystemService는 시스템 콜의 인자를 유저 모드 스택에서 커널 모드 스택으로 복사하는데, 이때 EDX 레지스터가 시스템 콜에 사용할 인자를 가리키고 있다(몇몇 루트킷은 이런 시스템 콜 처리 과정에서 시스템 콜의 인자를 변경하거나 시스템 콜의 수행 루틴을 변경시키기도 한다). 시스템 콜 번호에 맞게 KeServiceDescriptorTable을 참조하여 Native API를 호출한다. 그 후 시스템 콜을 종료하고 각각 IRET나 SYSEXIT 명령어를 사용하여 유저 모드로 복귀한다. 아래의 그림은 이를 종합적으로 표현한 내용이다.

결국 SSDT에서 서비스 호출 번호에 맞는 주소를 얻은 다음 이를 호출하는 형태로 진행되는 것이다. 그렇다면 SSDT Hooking은 어느 부분에서 후킹 해야 하는 것일까? 바로 GetFuncAddress 과정에서 인덱스 번호에 맞는 함수의 주소를 SSDT에서 가지고 올 때이다. 예를 들어, 0xAD 서비스 함수(EAX=0xAD) 시스템 콜이 발생하면 SSDT에서 0xAD번째에 위치한 함수의 주소를 가지고 오게 된다. 그리고 해당 함수를 호출하므로 진행되는 방식인데, 만약 SSDT를 변조하므로 0xAD번째에 위치한 함수의 주소를 HookFunc의 주소로 대체하면 해당 시스템 콜이 발생할 때마다 HookFunc가 호출된다.

위 예를 좀 더 구체적으로 제시하자면 0xAD 시스템 콜의 루틴이 SSDT에서 0xCCCCCCCC로 존재하고 있는 상황에 이 주소를 Hook함수의 주소인 0xDDDDDDDD로 대체하게 되면 0xAD 시스템 콜이 발생할 때마다 0xDDDDDDDD의 함수가 호출되게 된다. 대개 이러한 후킹 함수는 정상적인 루틴 0xCCCCCCCC를 조작하는데, 어떠한 인자가 오느냐에 따라 이를 진행하지 않거나 원래 함수의 결과가 특정 값일 경우 이를 변조하여 반환하는 등의 조작을 가할 수가 있다.


하지만 SSDT는 Read Only로 설정되어 있기 때문에 SSDT Hooking을 진행하기 위해서는 쓰기 권한이 필요하다. 이를 우회하기 위한 방법 중 하나인 CR0 Register에 대하여 알아보자. CR0 레지스터는 WP(Write Protect) Bit를 포함하고 있는데 이 값이 0 이면 메모리 보호 기능이 해제되고 1 이면 메모리 보호가 활성화된다. 따라서 메모리 보호 기능을 비활성화하므로 쓰기 권한을 얻을 수가 있다. 해당 코드는 다음과 같다.

__asm {    // 메모리 보호 기능 비활성화
    push eax
    mov eax, CR0
    and eax, 0xFFFEFFFF
    mov CR0, eax
    pop eax}

__asm {    // 메모리 보호 기능 활성화
    push eax
    mov eax, CR0
    or eax, NOT 0xFFFEFFFF
    mov CR0, eax
    pop eax}

이러한 후킹은 목적은 대개 흔적을 남기지 않는 것이 중요하므로, SSDT를 후킹 한 다음에는 다시 CR0를 조작하여 메모리 보호 기능을 활성화해주어야 한다. 이는 비활성화와는 반대로 or eax, NOT 0xFFFEFFFF 연산을 해주어야 한다. 이제 SSDT에 값을 기록할 수 있으므로 어떻게 변조할 것인가에 대하여 알아보자. SSDT Hooking에는 유용하게 사용되는 몇 가지 매크로가 존재한다. 


"SYSTEMSERVICE" 매크로는 ntoskrnl.exe에서 제공하는 Zw* 함수의 주소를 입력받아 그에 상응하는 Nt* 함수의 주소를 SSDT에서 구할 때 사용한다. "SYSCALL_INDEX" 매크로는 Zw* 함수의 주소를 입력받아서 SSDT에서 해당 함수의 인덱스 번호를 구하는 데 사용할 수 있다. 이렇게 "SYSTEMSERVICE"와 "SYSCALL_INDEX" 매크로가 해당 함수의 시작 부분(_func)에 +1을 하는 이유는 opcode에 따라 [mov eax, 인덱스 값] 이므로 해당 인덱스 값은 함수의 시작점에서 mov eax의 opcode 값 다음에 오므로 +1을 더해 해당 값을 알 수 있기 때문이다. 이 두 매크로를 통해 각각 Nt* 함수의 주소와 인덱스 번호를 구할 수가 있다.

#define SYSTEMSERVICE(_func)  KeServiceDescriptorTable.ServiceTableBase[ *(PULONG)                                                                                                              ((PUCHAR)_func+1)]
#define SYSCALL_INDEX (_func) *(PULONG) ((PUCHAR) _func+1)


HOOK_SYSCALL과 UNHOOK_SYSCALL 매크로는 후킹을 수행할 Zw* 함수의 주소와 SSDT에서의 인덱스, 그리고 새로운 HookFunc 함수의 주소를 이용해 SSDT 안에서의 주소를 변경해준다. 밑의 코드 중에서 "InterlockedExchange"가 있는데 이는 두 개의 인자 값이 일치하지 않을 경우 첫 번째 인자가 지정하는 메모리의 값을 두 번째 인자로 바꾸는 함수이다.

#define HOOK_SYSCALL (_func, _Hook, _Orig)\
               _Orig=(PVOID) InterlockedExchange((PLONG)\                &MappedSystemCallTable[SYSCALL_INDEX(_func)],(LONG) _Hook)
#define UNHOOK_SYSCALL(_func, _Hook, _Orig)\
               InterlockedExchange((PLONG)\
               &MappedSystemCallTable[SYSCALL_INDEX(_func)], (LONG) _Orig)

이러한 과정을 통해 SSDT에 존재하고 있는 서비스 루틴을 훅 함수로 대체할 수가 있고, 다시 원래대로 되돌릴 수가 있다. 


Reference


http://luckyyowu.tistory.com/133

http://hackerspace.tistory.com/entry/시스템-호출-처리순서

http://bbolmin.tistory.com/158

http://luckey.tistory.com/86

http://securityfactory.tistory.com/158

https://en.wikipedia.org/wiki/Interrupt_descriptor_table

http://blog.naver.com/wwwkasa/80167132015

http://ezbeat.tistory.com/279

SSDT(System Service Descriptor Table) Hooking.pdf - Written by 백구