posted by 블르샤이닝 2011. 6. 2. 18:00
728x90

CATEGORY

전체 (154)
Security News (14)
이달의 광장인 (1)
Security 공지사항 (1)
광장스터디 (88)
세인트광장 (50)
TAGS

시스템 감염 상세한 설명 정보 약탈 내 손 야식 공모전 영화 야식 테러 사람인 문화와 영어 참여의식 문화탐방 발표 루트킷 형언 약지 야식테러 니들 상대방 입장 쏘다 메세지 서비스 온실 로키 호프 내다 영어 표현 Windows API 기업 친척 방문 셀폰

rss
2009/04/20 SSDT Hooking 의 이해 (2)

SSDT 후킹은 윈도우즈 API 가 커널 모드에서 서비스를 받기 위해 필요한 SSDT(System Service Descriptor Table) 의 내용을 조작하는 커널모드 후킹 방법 중에 하나 이다. SSDT 는 프로세스에 독립적이고 커널 주소 공간에 전역적으로 올라와 있으므로, 특별한 조작을 가하지 않는다면 모든 프로세스가 같은 SSDT 를 가지고 있다. 커널모드에서 전역적으로 윈도우즈 서비스 함수를 가로챌 필요가 있을때 주로 SSDT 후킹을 많이 사용한다. SSDT 를 후킹 하기에 앞서 윈도우즈 API 의 호출이 커널레벨까지 어떻게 흘러가는지 일련의 흐름에 대한 이해가 필요하다. 윈도우 환경 개발자라면 흔히 사용하는 CreateFile, CreateThread 같은 함수들은 kernel32.dll 에서 제공해주는 유저레벨 API 이다. 하지만 이 함수들은 걷껍데기 일뿐 실제 윈도우 API 들은 커널모드에서 구현되어 동작한다. 그렇다면 어플리케이션에서 CreateFile 함수를 호출할 경우 어떤 과정 거쳐 커널레벨의 CreateFile 함수 실제 코드를 실행할까? windbg 를 사용하여 그 과정에 대해 알아보자. 디버기로 사용된 PC 의 OS 는 Windows XP SP3 이다.

<목록1> CreateFileA 함수 디스어셈블


kd> u kernel32!CreateFileA

kernel32!CreateFileA:
7c801a28 8bff            mov     edi,edi
7c801a2a 55              push    ebp
7c801a2b 8bec            mov     ebp,esp
7c801a2d ff7508          push    dword ptr [ebp+8]
7c801a30 e8cfc60000      call    kernel32!Basep8BitStringToStaticUnicodeString (7c80e104)
7c801a35 85c0            test    eax,eax
7c801a37 741e            je      kernel32!CreateFileA+0x11 (7c801a57)
...
7c801a4e e89ded0000      call    kernel32!CreateFileW (7c8107f0)
7c801a53 5d              pop     ebp
7c801a54 c21c00          ret     1Ch
7c801a57 83c8ff          or      eax,0FFFFFFFFh
7c801a5a ebf7            jmp     kernel32!CreateFileA+0x30 (7c801a53)

 CreateFIle 함수는 멀티바이트와 유니코드용 함수로 나뉘어 kernel32.dll 에서 각각 CreateFileA 함수와 CreateFileW 함수로 export 되어 있다. <목록1> 을 보면 첫번째 인자로 들어오는 pFileName 을 유니코드로 변환하여 CreateFileW 함수를 호출하게 된다. 이 같은 사실로, 다른 윈도우 API 도 마찬가지로 접미사로 A 가 붙은 함수는 넘겨진 인자들 중 문자열은 모두 유니코드로 변환하여 접미사 W 가 붙은 함수를 호출함을 알수 있었다. 그럼 CreateFileW 함수를 한번 살펴보자. 함수 내에서 디스어셈블한 내용중 call 명령만 추려서 살펴보았다.

<목록2> CreateFileW 함수 call 명령 정보


kd> uf -c kernel32!CreateFileW
Flow analysis was incomplete, some code may be missing
kernel32!CreateFileW (7c8107f0)
  kernel32!CreateFileW+0x67 (7c810821):
    call to ntdll!RtlInitUnicodeString (7c931295)
  kernel32!CreateFileW+0x97 (7c81084e):
    call to kernel32!BaseIsThisAConsoleName (7c810a08)
  kernel32!CreateFileW+0xea (7c810865):
    call to ntdll!RtlDosPathNameToNtPathName_U (7c9442d5)
  kernel32!CreateFileW+0x359 (7c8109a0):
   call to ntdll!ZwCreateFile (7c93d090)
  kernel32!CreateFileW+0x372 (7c8109b9):
    unresolvable call: call    esi
  kernel32!CreateFileW+0x384 (7c8109cb):
    unresolvable call: call    esi
  kernel32!CreateFileW+0x3dd (7c8109ea):
    call to kernel32!SetLastError (7c809342)

CreateFileW 함수에서는 일련의 과정을 거쳐 <목록 2> 에서 볼수 있듯이 ntdll.dll 의 ZwCreateFile 함수를 호출한다. ZwCreateFile 함수와 같이 앞에 Zw 나 Nt 같은 접두사가 붙은 함수를 Native API 라고 부른다. Natvie API(http://en.wikipedia.org/wiki/Native_API)  란 윈도우 내부에서 사용되는 즉 커널모드 서비스를 제공하는 함수를 말한다. Native API 는 함수이름에 2-3글자 정도의 접두사가 붙는데, 시스템 콜을 하는 API 는 Nt 와 Zw 접미사가 붙는다. ZwCreateFile 은 파일에 엑세스하는 시스템콜을 담당 하기 때문에 Zw 접두사가 붙는다. 그리고 Zw 접두사가 붙은 함수는 쌍으로 똑같은 이름의 Nt 접두사가 붙은 함수도 존재한다. 아래 <그림 1> 에서 확인해 볼수 있다.

<그림 1> Dependecy Walker 로 ntdll.dll 안에 export 함수 목록

사용자 삽입 이미지

사용자 삽입 이미지
 
여기서 눈여겨 볼 부분은 NtCreateFIle(), ZwCreateFile() 함수 둘다 entry point 로 0x0000D0AE 값을 가지고 있다. 똑같은 entry point 를 가르키고 있다는건 동일한 함수라는 것을 의미한다. <목록 3> 에 ZwCreateFile(), NtCreateFile() 함수를 디스어셈블한 내용을 보면 더욱 확실해 진다. 둘다 7c93d090 를 함수 주소를 갖고 있고 코드 또한 같다. 현재 디버깅 하는 시스템은 Nt* 함수와 Zw* 함수 모두 Zw* 함수를 호출 함을 볼수 있는데, 다른 시스템에서는 Nt* 함수를 호출 하기도 한다. 유저모드 API 에서 Nt* 함수와 Zw* 함수중 어떤 함수를 호출 할지는 설치된 윈도우 종류와 버전에 따라 다른것 같다.

<목록 3> ZwCreateFile, NtCreateFile


kd> uf ntdll!ZwCreateFile
ntdll!ZwCreateFile:
7c93d090 b825000000      mov     eax,25h
7c93d095 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c93d09a ff12            call    dword ptr [edx]
7c93d09c c22c00          ret     2Ch

kd> uf ntdll!NtCreateFile
ntdll!ZwCreateFile:
7c93d090 b825000000      mov     eax,25h
7c93d095 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c93d09a ff12            call    dword ptr [edx]
7c93d09c c22c00          ret     2Ch

디스어셈블한 내용을 보면 eax 에 0x25 를 저장한 후, 7ffe0300 주소가 가르키는 위치로 호출한다. 여기서 0x25 는 SSDT 상에 ZwCreateFile 함수의 인덱스를 의미 한다. 그리고 7ffe0300 주소는 <목록 4> 에 덤프 뜬 내용과 같이 KiFastSystemCall 함수를 가르킨다. KiFastSystemCall 함수는 현재 스택 프레임을 edx 레지스터에 저장하고 sysenter 명령을 호출 한다.

<목록 4> SharedUserData!SystemCallStub


kd> dd 7ffe0300
7ffe0300  7c93e4f0 7c93e4f4 00000000 00000000

kd> uf 7c93e4f0
ntdll!KiFastSystemCall:
7c93e4f0 8bd4            mov     edx,esp
7c93e4f2 0f34            sysenter
7c93e4f4 c3              ret

sysenter 명령은 유저모드에서 사용되는 명령어로, 현재 스레드가 유저모드에서 커널모드로 진입 시켜주는 역할을 한다. 인텔 메뉴얼에 보면 SYSENTER_EIP_MSR 가 가르키는 위치를 eip 레지스터에 넣어 실행 한다고 나와 있는데, 이 값은 MSRs(MODEL-SPECIFIC REGISTERS) 의 0x176 주소에서 가져온다. MSRs 는 rdmsr 명령으로 읽어올수 있다.

<목록 5> sysenter
kd> rdmsr 0x176
msr[176] = 00000000`8053f540

kd> u 8053f540
nt!KiFastCallEntry:
8053f540 b923000000      mov     ecx,23h
8053f545 6a30            push    30h
8053f547 0fa1            pop     fs
8053f549 8ed9            mov     ds,cx
8053f54b 8ec1            mov     es,cx
8053f54d 8b0d40f0dfff    mov     ecx,dword ptr ds:[0FFDFF040h]
8053f553 8b6104          mov     esp,dword ptr [ecx+4]
8053f556 6a23            push    23h

sysenter 명령은 nt!KiFastCallEntry 함수를 호출하고, 여기서부터 커널모드에서 코드가 실행된다.  nt!KiFastCallEntry 함수에서는 SSDT 주소를 현재 스레드의 ETHREAD 구조체의 ServiceTable 필드값으로 부터 얻어오고, ZwCreateFile 함수의 서비스 번호가 저장된 eax 값을 참조하여  SSDT 에서 NtCreateFile 함수가 위치한 곳을 찾아 호출한다. 여태까지의 과정을 <그림 2> 를 통해 정리해 보자.

<그림 2> 윈도우즈 API 호출 흐름
 
사용자 삽입 이미지

이제 SSDT 에 대해 살펴보도록 하자. 윈도우즈 커널에는 기본적으로 2개의 서비스 테이블이 존재 한다. 하나는 Native API 정보를 가지고 있는 KeServiceDescriptorTable 이고, 나머지 하나는 Native API 정보와 더불어 GUI 함수 정보를 가지고 있는 KeServiceDescriptorTableShadow 이다. KeServiceDescriptorTable 은 일반적인 스레드에서 주로 사용하고, GUI 스레드일 경우는 KeServiceDescriptorTableShadow 를 사용한다. 두 서비스 테이블은 아래의 SERVICE_DESCRIPTOR_TABLE 구조체의 모습을 하고 있다. SERVICE_DESCRIPTOR_TABLE 구조체의 win32k 필드는 KeServiceDescriptorTable 에서는 사용하지 않고, KeServiceDescriptorTableShadow 에서만 사용한다.

<목록 6> 서비스 테이블 구조체 정의


typedef struct _SERVICE_DESCRIPTOR_ENTRY {
   unsigned int *ServiceTableBase;               // 함수 테이블 주소
   unsigned int *ServiceCounterTableBase;    // 함수 호출 개수를 저장하는 테이블 주소  

   unsigned int NumberOfServices;                // 함수 테이블에 저장된 함수의 개수
   unsigned char  *ParamTableBase;             // 각 함수의 인자 크기를 저장하는 테이블 주소
} SERVICE_DESCRIPTOR_ENTRY , *PSERVICE_DESCRIPTOR_ENTRY ;

typedef struct _SERVICE_DESCRIPTOR_TABLE {
   SERVICE_DESCRIPTOR_ENTRY ntoskrnl;          // Native API 테이블

   SERVICE_DESCRIPTOR_ENTRY win32k;           // GUI 서브시스템 테이블
   SERVICE_DESCRIPTOR_ENTRY reserved[2];   // 예악된 테이블. 사용안함.
 } SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;


그렇다면 우리가 후킹을 하기 위해선 NtCreateFile 함수주소가 저장되어 있는 KeServiceDescriptorTable 주소를 알아야 한다. 이 주소는 ntoskrnl.exe 에서 export 해주고, 있으므로 후킹 코드에서 KeServiceDescriptorTable 심볼을 import 해오면 바로 알수있다. 이제 windbg 를 이용해서  KeServiceDescriptorTable 구조체 내부를 살펴보고, NtCreateFile 함수가 위치한 곳을 찾아 보도록 하겠다.

<목록 7> KeServiceDescriptorTable


kd> dd KeServiceDescriptorTable
80554fa0  80503b8c 00000000 0000011c 80504000
80554fb0  00000000 00000000 00000000 00000000

kd> dd 80503b8c
80503b8c  8059b948 805e8db6 805ec5fc 805e8de8
80503b9c  805ec636 805e8e1e 805ec67a 805ec6be

kd> dd 80503b8c + 0x25 * 4
80503c20  8057027c 8056fc5a 805cd888 805cd5c0
80503c30  8061c286 8057038a 8060f7be 805702b6

kd> u 8057027c
nt!NtCreateFile:
8057027c 8bff            mov     edi,edi
8057027e 55              push    ebp
8057027f 8bec            mov     ebp,esp
80570281 33c0            xor     eax,eax


 KeServiceDescriptorTable 을 덤프한 내용을 살펴보면, 함수 테이블 주소는 80503b8c, 함수 테이블의 함수 개수는 0x11c 개, 각 함수의 인자 크기를 저장하는 테이블은 80504000 을 가르킴을 확인할수 있다. 이어서 NtCreateFile 함수를 찾기 위해80503b8c 을 살펴보니 커널 공간 주소를 가지고 있는 함수들이 함수포인터 배열 형태로 나열되고 있다. 여기서 NtCreateFile 함수의 위치를 찾기 위해선 위에서 봤던 서비스 번호 0x25 를 가지고 함수 주소의 크기가 4 바이트 이므로, 함수 테이블 주소를 기준으로 0x25 * 4 만큼 떨어진곳에 NtCreateFile 함수가 있는곳을 찾을수 있다. 찾은 위치인 8057027c 로 디스어셈블 해보면 NtCreateFile 함수 내용을 확인 할수 있다.  NtCreateFile 함수 주소 8057027c 위치를 우리가 작성한 함수 주소로 덮어쓰고 사용자 어플리케이션에서 CreateFile 함수를 호출하면, 커널모드에서 우리가 작성한 함수 주소가 CreateFile 함수의 서비스 함수인줄 알고 호출하게 되는 것이다. 여기까지가 SSDT 후킹의 기본 개념이고, 다음에 SSDT 후킹이 실제 코드로 어떻게 구현이 되는지 살펴보도록 하겠다.

728x90