Nt vs Zw - Clearing confusion on the Native API
2007/10/16 14:16

Preface

원문(OSR)의 글을 요약, 이해 정리한 글이다.

아래의 4가지 시나리오를 통해 NtXxx 함수와 ZwXxx함수에 대해 알아보자.
UserMode에서 NtXxx(NTDLL.DLL) 함수 호출하기
UserMode에서 ZwXxx(NTDLL.DLL) 함수 호출하기
KernelMode에서 NtXxx(NTOSKRNL.EXE) 함수 호출하기
KernelMode에서 ZwXxx(NTOSKRNL.EXE) 함수 호출하기

Calling From User Mode

NTDLL.DLL에서 NtXxx의 함수 NtReadFile을 Windbg에서 U 명령어를 통해 disassemble 한 결과는 아래와 같다.

0: kd> u ntdll!NtReadFile
ntdll!NtReadFile:
77f761e8 b8b7000000      mov     eax,0xb7
77f761ed ba0003fe7f       mov     edx,0x7ffe0300
77f761f2 ffd2                 call    edx
77f761f4 c22400             ret     0x24


0: kd> u ntdll!ZwReadFile
ntdll!NtReadFile:
77f761e8 b8b7000000      mov     eax,0xb7
77f761ed ba0003fe7f       mov     edx,0x7ffe0300
77f761f2 ffd2                  call    edx
77f761f4 c22400             ret     0x24


NtReadFile과 ZwReadFile은 0x77f761e8의 동일주소를 가르킨다.
CALL EDX의 0x7ffe0300의 주소를 ln(list nearest symblos) 명령을 통해 심볼을 본다.

0: kd> ln 0x7ffe0300(7ffe0300)   SharedUserData!SystemCallStub  
Exact matches:
    SharedUserData!SystemCallStub

0: kd> u SharedUserData!SystemCallStub
SharedUserData!SystemCallStub:
7ffe0300 8bd4             mov     edx,esp
7ffe0302 0f34              sysenter
7ffe0304 c3                ret



SystemCallStub이란 함수에서 SYSENTER(윈도우2000에서는 INT 2Eh)를 호출한다.

SYSENTER(INTEL document)란?

 thread into Kernel Mode and executes the routine pointed to by the SYSENTER_EIP_MSR, which is MSR 0x176.

커널로 전환하며 SYSENTER_EIP_MSR 레지스터가 가르키는 포인터 번지를 실행한다.

rdmsr(read msr, msr은 model specific register) 명령어로 SYSENTER_EIP_MSR를 살펴보면

0: kd> rdmsr 176
msr[176] = 00000000:8053a270

0: kd> ln 8053a270
(8053a270)   nt!KiFastCallEntry   |  (8053a2fb)   nt!KiSystemService
Exact matches:
    nt!KiFastCallEntry


결국 SYSENTER KiFastCallEntry를 호출한다는걸 알수있다.

053a2f9 eb5c jmp     nt!KiSystemService+0x5c (8053a357)

KiFastCallEntry의 마지막(?)을 보면 KiSystemService를 호출한다.

아래는 OSR에서 제공한 windbg용 확장 DLL을 설치(windbg내 winext폴더에 복사)하여 !osrexts.sst를 사용하여 System Service Descriptor Table의 내용을 살펴보고 실제 주소를 disassemble한 예제이다.

0: kd> !osrexts.sst
0: 0x805912c2  (nt!NtAcceptConnectPort)
1: 0x805d87b0  (nt!NtAccessCheck)
2: 0x805dc3e4  (nt!NtAccessCheckAndAuditAlarm)
...
b7: 0x8056b2ec  (nt!NtReadFile)
...

0: kd> u nt!NtReadFile
nt!NtReadFile:
8056b2ec 6a58              push    0x58
8056b2ee 6858044e80     push    0x804e0458
8056b2f3 e8e09ffcff         call    nt!_SEH_prolog (805352d8)
8056b2f8 33ff                 xor     edi,edi
8056b2fa 897de4           mov     [ebp-0x1c],edi
8056b2fd 897de0           mov     [ebp-0x20],edi
8056b300 897dd8           mov     [ebp-0x28],edi

요약

User mode에서  NtXxx , ZwXxx 호출은 결국 같은 루틴이다.
EAX에 index, EDX에 argument pointer 설정
SystemCallStub 를 호출하여 SYSENTER 호출
SYSENTER 는 인터럽트 방지하고 커널모드로 thread를 변경  SYSENTER_EIP_MSR (XP SP1에서는 KiFastCallEntry)를 호출
KiFastCallEntry 는 새로운 trap frame 형성, 인터럽트 사용가능후 KiSystemService 호출
KiSystemService 는 EDX를 통해 파라미터를 전달받고 EAX의 인덱스에 따라 KiServiceTable[EAX] 함수 호출

Calling From Kernel Mode 

NTOSKRNL.EXE에서 NtXxx의 함수 NtReadFile을 Windbg에서 U 명령어를 통해 disassemble 한 결과는 아래와 같다.
(NTOSKRNL.EXE 모듈의 심볼명은 nt 이다. 이는 CPU 에 따라 NTOSKRNL.EXE의 이름이 변경되지만 심볼명은 nt 하나로 통일하기 위한것이 아닐까 라고 추정 - 이재홍님)

0: kd> u nt!NtReadFile
nt!NtReadFile:
8056b2ec 6a58                push    0x58
8056b2ee 6858044e80       push    0x804e0458
8056b2f3 e8e09ffcff          call    nt!_SEH_prolog (805352d8)
8056b2f8 33ff                   xor     edi,edi

NtReadFile은 원래 함수라고 보여진다.

0: kd> u nt!ZwReadFile
nt!ZwReadFile:
80504d4c b8b7000000     mov     eax,0xb7
80504d51 8d542404         lea     edx,[esp+0x4]
80504d55 9c                        pushfd
80504d56 6a08                   push    0x8
80504d58 e89e550300     call    nt!KiSystemService (8053a2fb)
80504d5d c22400               ret     0x24

ZwReadFile은 이전에 보았던 코드와 유사하다.
EAX에 index 번호 0xb7을 넣고 EDX에 argument pointer를 설정하고 PUSHFD(PUSH EFLAGS)후 KiSystemService 바로 호출

KernelMode이기 때문에 KernelMode로 전환하기 위한 SYSENTER와 KiFastCallEntry 생략되었다.

아래그림은 EFLAGSeflags.jpg

요약

Case A:
Kernel Mode 에서 NtXxx 함수 호출
PreviousMode 변경이 없다.
Case B:
Kernel Mode 에서  ZwXxxx 함수 호출
EAX에 index 번호 0xb7을 넣고 EDX에 argument pointer를 설정하고 PUSHFD(PUSH EFLAGS)후 KiSystemService 바로 호출
PreviousMode KernelMode로 설정
CALL KiServiceTable[EAX]

중요한 차이는 KiSysstemService를 통한 과정은 previous mode가 커널모드로 설정하는것이다.
Nt를 바로 호출하는게 overhead가 없지만 Zw호출은 previousmode를 변경한다.

Previous Mode

Previous mode란 시스템이 시스템 서비스를 호출하는 곳의 모드를 결정하기 위한 indicator로 사용된다.
Previous mode가  User mode로 설정되면 시스템 서비스 처리 루틴은 호출은 User mode에서 호출되는것으로 인식하고 루틴으로 전달된 파라미터를 사용하기전에 유효성 검사를 필요로 한다.
하지만 커널모드는 그러한 세밀한 조사를 하지 않고 유효하다고 가정한다.

Previous Mode를 구하는 API

 KPROCESSOR_MODE  ExGetPreviousMode( VOID );

ZwXxx, NtXxx 호출시 실패하는 경우  

NtXxx 함수를 직접 호출하면 previous mode를 변경되지 않기때문에 호출된 NtXxx 커널 시스템 서비스 루틴이 previous mode가 user mode로 설정된채 임의 user stack상에서 실행될수 있다. 이러한 호출은 Parameters의 유효성 확인 시도를 알지 못하기 때문에 호출실패가 발생할수 있다.
또다른 문제점은 ProbeForRead, ProbeForWrite를 통해 메모리가 Usermode의 메모리인지를 검사할때 Previous mode가 user mode로 설정된 상태에서 시스템 서비스 NtXxx 함수 호출시 커널모드 버퍼를 인자로 넘겨준다면 STATUS_ACCESS_VIOLATION이 발생할수 있다.

Sample Code

Sample code는 드라이버에서 c: 드라이버에 파일로 로그를 남기는 코드
InitializeObjectAttribute함수를 통해 c: 드라이버에 파일에 대한 핸들생성

(Language : c)
  1. switch (operation)
  2. {
  3.    case IOCTL_OSR_ENABLE_LOGGING:
  4.       if (!devExt->LoggingEnabled)
  5.       {
  6.          RtlInitUnicodeString(&logFileName, OSRNTAPI_LOGFILE);
  7. #ifdef USER_HANDLE
  8.          InitializeObjectAttributes(&oa, &logFileName, OBJ_CASE_INSENSITIVE,
  9.                       NULL, NULL);
  10. #else
  11.          InitializeObjectAttributes(&oa, &logFileName,
  12.             OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE, NULL, NULL);
  13. #endif
  14.          code = ZwCreateFile(&devExt->LogFileHandle,
  15.             GENERIC_WRITE | SYNCHRONIZE,
  16.             &oa, &iosb, …

위의 예제에서 볼수 있듯이 핸들의 2가지가 생성될수 있다.

첫번째는 User mode 상에서 생성된 핸들(같은 프로세스의 context상에서만 접근가능, 다른 프로세스 접근 불가능)과 OBJ_KERNEL_HANDLE의 옵션을 주고 생성한 커널핸들(모든 프로세스, 드라이버에서 접근가능)이 있다.

응용프로그램에서 ReadFile 호출

드라이버 IRP_MJ_READ dispatch 함수, 드라이버에서 log write 시도

(Language : c)
  1. PreviousMode = UserMode
  2. code = NtWriteFile(devExt->LogFileHandle,
  3.       NULL,
  4.       NULL,
  5.       NULL,
  6.       &iosb,
  7.       (PVOID)"OsrRead: NtWriteFile\r\n",
  8.       strlen("OsrRead: NtWriteFile\r\n"),
  9.       NULL,
  10.       NULL);
  11. #ifdef USER_HANDLE
  12.    ASSERT(code == STATUS_ACCESS_VIOLATION);
  13. #else
  14. // OBJ_KERNEL_HANDLE
  15.    ASSERT(code == STATUS_INVALID_HANDLE);
  16. #endif
  17.  

NtWriteFile 호출의 경우

#ifdef USER_HANDLE 정의시 (OBJ_KERNEL_HANDLE를 사용하지 않았을경우)STATUS_ACCESS_VIOLATION 에러발생
넘겨진 파라미터는 커널버퍼이지만 PreviousMode가 User 이므로 ProbeForRead/Write 를 통해 유효성 검사에서 실패하여 발생

#ifdef USER_HANDLE 미정의시(OBJ_KERNEL_HANDLE를 사용했을경우)STATUS_INVALID_HANDLE 에러발생
핸들생성시 커널핸들로 생성했지만 PreviousMode가 User 이므로 User모드 핸들에서는 커널핸들을 참조(찾을수 없다)할수없어 invalid 한 핸들에러 발생

(Language : c)
  1. PreviousMode = KernelMode (KiSystemService에서 change)
  2. code = ZwWriteFile(devExt->LogFileHandle,
  3.          NULL, NULL, NULL,
  4.          &iosb,
  5.          (PVOID)"OsrRead: ZwWriteFile\r\n",
  6.          strlen("OsrRead: ZwWriteFile\r\n"),
  7.          NULL, NULL);
  8. ASSERT(code == STATUS_SUCCESS);

ZwWriteFile 호출의 경우
어떤 경우에서도 성공

Conclusion  

User mode에서는 어떤것을 사용하여도 무방하나 kernel mode에서는 ZwXxx를 사용하여 previous mode가 커널로 설정되어 유효성 검사를 거쳐 시스템 서비스 함수들이 호출되도록 하여야 한다.

ETC

MSDN에 따르면 Zw로 시작하는 해당함수를 호출한 프로세스의 대한 접근권한 체크를 하지 않습니다.
http://msdn2.microsoft.com/en-us/library/ms804352.aspx 

이 글은 스프링노트에서 작성되었습니다.

2007/10/16 14:16 2007/10/16 14:16
Trackback Address :: 이 글에는 트랙백을 보낼 수 없습니다

  • 아이작 2007/12/15 01:42  댓글주소  수정/삭제  댓글쓰기
    무섭다.
    너 완전 기술자구나?
    영어 못한다더니 영어 잘하네.
    얼레? 산수도 잘하네.
    다양한 색상폰트... 미술도??
  • seyool 2007/12/15 12:14  댓글주소  수정/삭제  댓글쓰기
    평소에 둘의 차이가 뭘까 궁금했는데.. 이기회에 잘 알게되었습니다.
    감사합니다.