본문 바로가기
WebBook/윈도우 구조

윈도우 구조 - 커널 진입 - Ntdll.dll 세부 분석

by 올엠 2022. 4. 2.
반응형

우리가 편지를 쓴다면, 보내는 사람은 “나”라는 이름으로 편지를 보내고, 받는 사람 “너”라는 사람이 편지 내용을 읽고 회신하게 된다. 이 과정 속에 실질적인 편지 배달은 우체국에서 진행하게 된다. 우체국은 중간에서 편지를 각 지역별로 구분한 후, 공통된 배달 방식에 맞게 구분하여 편지를 배달해 주는 것이다. 이와 같은 원리는 시스템에서도 볼 수 있다. 편지와 비교해 본다면, “나”는 응용프로그램으로 사용자의 입력이나, 처리 요청을 받아 개발 의도에 맞게 가공하여 커널에 처리를 요청하게 된다. 그리고 커널은 편지에서 “너”라고 할 수 있다.

이 둘간의 전달 역할을 해주는 것이 서브시스템Ntdll.dll로써, 서브시스템은 바로 우편배달부처럼 각 지역 요청들을 수집하여 우체국에 전달하고 이를 우체국과 유사한 기능을 하는 Ntdll.dll 받아 처리한다. 정리하면 사용자들의 커널 이용을 위한 요청을 우편배달부(서브시스템)가 우체국(Ntdll.dll)을 통해서 커널로 진입 시키는 것이다.

Ntdll.dll은 유저 모드에서 동작 중이던 프로그램들을 위해 커널 모드의 요청을 대신 처리한 후 결과 값을 반환해 주는 역할을 한다. 
커널 모드 이용 권한이 필요한 시스템 명령들을 System Service API(Native API라고도 한다)라 부르는데, 서브시스템들은 필요에 맞게 구분되어 사용자의 응용프로그램 혹은 서비스 실행을 돕고, 실질적인 커널 요청은 Ntdll.dll을 통해 진행하게 된다. 즉 Ntdll.dll은 서브시스템과 커널 모드를 연결하는 중간 다리 역할을 해준다. 운영체제는 Ntdll.dll과 같은 별도의 통일화된 인터페이스를 제공하여 시스템 자원의 관리와 호환성을 높일 수 있기 때문이다. 그럼 Ntdll.dll은 어떻게 커널 모드로 진입하는 것일까? Ntdll.dll은 커널 모드 진입을 위해 명령 int 0x2e(혹은 Sysenter)를 통해 커널에 필요한 요청을 하게 된다. 이렇게 커널에 진입하여 처리를 요청할 때 디스패치(DPC)를 거친다.
아래 그림은 Ollydbg를 이용하여 Ntdll.dll이 커널로 진입하는 내용을 확인한 것이다.

Ollydbg 로 확인한 Readfile API 호출 순서

만약 응용프로그램이 파일 읽기를 요청하였다면, 요청을 처리하기 위한 흐름은 다음과 같다(굵게 표시한 부분이 Ntdll.dll 이후 커널에서 처리되는 부분이다).

BOOL WINAPI ReadFile() > kernel32.ReadFile() > ntdll.ZwReadFile() > ntdll.KiFastSystemCall() > SYSENTER > nt!NtReadFile() > 처리 > 처리 결과 리턴

 

이 요청 처리 흐름을 그림으로 표현하면 다음과 같다.

Ntdll.dll 과 커널의 관계

앞서 파일 읽기의 요청 순서를 보면, ZwReadFile, NtReadFile , ReadFile라는 동일한 작업을 하는 API 앞에 짧은 문자들이 추가로 붙는 것을 확인할 수 있다. 앞에 붙은 Zw, Ki, NT는 윈도우 시스템에는 함수에 의미별로 나누어 놓은 접두어로써 각 접두어가 나타내는 의미는 아래 표로 정리하였다.

접두어 설명
Alpc 고급 로컬 인터-프로세스 통신
Cc 공통 캐시
Cm 구성 관리자
dbgk 유저 모드를 위한 디버깅 프레임워크
Em 정오 관리자
Etw 윈도우 이벤트 트레이싱
Ex 익스큐티브 지원 루틴
FsRtl 파일 시스템 드라이버 런타임 라이브러리
Hal 하드웨어 추상화 계층
Hvl 하이퍼바이저 라이브러리
Io I/O 관리자
Kd 커널 디버거
Ke 커널
Lsa 로컬 보안 권한
Mm 메모리 관리자
Nt NT 시스템 서비스
Ob 객체 관리자
Pf 프리패처
Po 파워 관리자
Pp 플래그앤플레이 관리자
Ps 프로세스 지원
Rtl 런타임 라이브러리
Se 보안
Tm 트랜잭션 관리자
Vf 베리파이어
Whea 원도우 하드웨어 오류 아키택처
Wmi 윈도우 관리 도구
Wdi 윈도우 진단 인프라
Zw 시스템 서비스를 위한 미러 엔트리 포인트

 

이중 자주 보는 접두어는 당연히 Nt, Zw이다. Nt의 미러 포인트로 사용되는 Zw는 유저 모드에서 요청한 경우 처리 전 입력한 인자에 대한 검증(int 2e, Sysenter)을 진행한다. 이를 통해 권한이나 인자 검증과 같은 처리를 진행하게 된다. 커널 내에서 사용되는 Nt로 시작하는 API는 이와 같은 검증을 진행하지 않는다. 하지만 Zw를 호출하는 경우 유저 모드에서 호출한 것 같이 Nt 관련 API로 포워드하며 검증을 진행하게 된다(단 이전 엑세스 모드(Previous mode)를 커널 모드로 설정하여 인자 검증은 하지 않도록 한다).
요약하면, 유저 모드에서는 Nt, Zw 모두 검증을 진행하고, 커널 모드에서는 Zw만이 일부 검증을 진행하게 된다. 이를 통해 불필요한 검증을 생략해 처리 성능을 개선한 것이다.
그럼 Windbg를 이용하여 처리 흐름을 확인해 보자.

// 먼저 NTDLL의 ZwReadFile 호출을 확인해 보자.
kd> u nt!zwreadfile
nt!ZwReadFile:
804df508 b8b7000000      mov     eax,0B7h
804df50d 8d542404        lea     edx,[esp+4]
804df511 9c              pushfd
804df512 6a08            push    8
804df514 e818110000      call    nt!KiSystemService (804e0631)
804df519 c22400          ret     24h

// nt!KiSystemService는 어떻게 처리되는지 어셈블리 명령을 통해 100개의 라인을 확인해 보자.
kd> u 804e0631 L100
nt!KiSystemService:
804e0631 6a00            push    0  Exception Frame 구성
804e0633 55              push    ebp
804e0634 53              push    ebx
804e0635 56              push    esi
804e0636 57              push    edi
804e0637 0fa0            push    fs
804e0639 bb30000000      mov     ebx,30h
804e063e 8ee3            mov     fs,bx
804e0640 ff3500f0dfff    push    dword ptr ds:[0FFDFF000h]
804e0646 c70500f0dfffffffffff mov dword ptr ds:[0FFDFF000h],0FFFFFFFFh  _KPCR 포인터 (2부에서 다룬다.)
804e0650 8b3524f1dfff    mov     esi,dword ptr ds:[0FFDFF124h]  _KTHREAD 포인터 (2부에서 다룬다.)
804e0656 ffb640010000    push    dword ptr [esi+140h]
804e065c 83ec48          sub     esp,48h
804e065f 8b5c246c        mov     ebx,dword ptr [esp+6Ch]
804e0663 83e301          and     ebx,1
804e0666 889e40010000    mov     byte ptr [esi+140h],bl
804e066c 8bec            mov     ebp,esp
804e066e 8b9e34010000    mov     ebx,dword ptr [esi+134h]  _KTHREAD포인터 + 0x134 = _KTRAP_FRAME 포인터 (2부에서 다룬다.)
804e0674 895d3c          mov     dword ptr [ebp+3Ch],ebx 
804e0677 89ae34010000    mov     dword ptr [esi+134h],ebp 
804e067d fc              cld
804e067e 8b5d60          mov     ebx,dword ptr [ebp+60h]
804e0681 8b7d68          mov     edi,dword ptr [ebp+68h]
804e0684 89550c          mov     dword ptr [ebp+0Ch],edx
804e0687 c74508000ddbba  mov     dword ptr [ebp+8],0BADB0D00h
804e068e 895d00          mov     dword ptr [ebp],ebx
804e0691 897d04          mov     dword ptr [ebp+4],edi
804e0694 f6462cff        test    byte ptr [esi+2Ch],0FFh
804e0698 0f858efeffff    jne     nt!Dr_kss_a (804e052c)
804e069e fb              sti
804e069f e9dd000000      jmp     nt!KiFastCallEntry+0x8d (804e0781)  804e0781로 분기
...중략 
804e0781 8bf8            mov     edi,eax
804e0783 c1ef08          shr     edi,8
804e0786 83e730          and     edi,30h
804e0789 8bcf            mov     ecx,edi
804e078b 03bee0000000    add     edi,dword ptr [esi+0E0h]
804e0791 8bd8            mov     ebx,eax
804e0793 25ff0f0000      and     eax,0FFFh
804e0798 3b4708          cmp     eax,dword ptr [edi+8]
804e079b 0f8341fdffff    jae     nt!KiBBTUnexpectedRange (804e04e2)
804e07a1 83f910          cmp     ecx,10h
804e07a4 751a            jne     nt!KiFastCallEntry+0xcc (804e07c0)
804e07a6 8b0d18f0dfff    mov     ecx,dword ptr ds:[0FFDFF018h]
804e07ac 33db            xor     ebx,ebx
804e07ae 0b99700f0000    or      ebx,dword ptr [ecx+0F70h]
804e07b4 740a            je      nt!KiFastCallEntry+0xcc (804e07c0)  804e07c0로 분기
...중략
804e07c0 ff0538f6dfff    inc     dword ptr ds:[0FFDFF638h]
804e07c6 8bf2            mov     esi,edx
804e07c8 8b5f0c          mov     ebx,dword ptr [edi+0Ch]
804e07cb 33c9            xor     ecx,ecx
804e07cd 8a0c18          mov     cl,byte ptr [eax+ebx]
804e07d0 8b3f            mov     edi,dword ptr [edi]  SSDT 주소값을 edi에 저장 한다.
804e07d2 8b1c87          mov     ebx,dword ptr [edi+eax*4]  SSDT 엔트리중 실제 호출 함수 위치인 nt!NtReadFile 가르킨다.
804e07d5 2be1            sub     esp,ecx
804e07d7 c1e902          shr     ecx,2
804e07da 8bfc            mov     edi,esp
804e07dc 3b35d41b5680    cmp     esi,dword ptr [nt!MmUserProbeAddress (80561bd4)]
804e07e2 0f83a8010000    jae     nt!KiSystemCallExit2+0x9f (804e0990)
804e07e8 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
804e07ea ffd3            call    ebx  실제 서비스 함수 nt!NtReadFile 실행
804e07ec 8be5            mov     esp,ebp
804e07ee 8b0d24f1dfff    mov     ecx,dword ptr ds:[0FFDFF124h]
804e07f4 8b553c          mov     edx,dword ptr [ebp+3Ch]
804e07f7 899134010000    mov     dword ptr [ecx+134h],edx

// 그럼 위 SSDT 엔트리의 실제 함수 호출 위치에 브레이크 포인트를 설정하여 확인해 보자.
kd> bp 804e07d2
// 브레이크 포인트가 잘 설정되었다면 운영체제를 실행하도록 하자.
kd> g
Breakpoint 0 hit
nt!KiFastCallEntry+0xde:
804e07d2 8b1c87          mov     ebx,dword ptr [edi+eax*4]
// 위와 같이 804e07d2에 접근하였을 때 커널 내 시스템 서비스가 호출되는 주소를 확인할 수 있다. 그럼 현재의 레지스터를 확인을 통해 어떤 시스템 서비스를 호출하였는지 알아보자.
kd> r
eax=000000b7 ebx=80512088 ecx=00000024 edx=f866f94c esi=f866f94c edi=804e46a8
eip=804e07d2 esp=f866f8d4 ebp=f866f8d4 iopl=0         nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246
nt!KiFastCallEntry+0xde:
804e07d2 8b1c87          mov     ebx,dword ptr [edi+eax*4] ds:0023:804e4984={nt!NtReadFile (80576117)}
// edi 주소값을 확인해 보면 SSDT 엔트리 포인트임을 확인할 수 있다.
kd> dds 804e46a8
804e46a8  80591df5 nt!NtAcceptConnectPort
804e46ac  8057b0f1 nt!NtAccessCheck
804e46b0  80589999 nt!NtAccessCheckAndAuditAlarm
804e46b4  80593130 nt!NtAccessCheckByType
804e46b8  8058fa83 nt!NtAccessCheckByTypeAndAuditAlarm
804e46bc  8063a07e nt!NtAccessCheckByTypeResultList
804e46c0  8063c207 nt!NtAccessCheckByTypeResultListAndAuditAlarm
804e46c4  8063c250 nt!NtAccessCheckByTypeResultListAndAuditAlarmByHandle
804e46c8  8057c6e4 nt!NtAddAtom
804e46cc  8064b047 nt!NtQueryBootOptions
804e46d0  80639835 nt!NtAdjustGroupsToken
804e46d4  8058f0a1 nt!NtAdjustPrivilegesToken
804e46d8  8063197c nt!NtAlertResumeThread
804e46dc  8057cbcd nt!NtAlertThread
804e46e0  8058a928 nt!NtAllocateLocallyUniqueId
804e46e4  806288ff nt!NtAllocateUserPhysicalPages
804e46e8  805df3c9 nt!NtAllocateUuids
804e46ec  8056afc3 nt!NtAllocateVirtualMemory
804e46f0  805db767 nt!NtAreMappedFilesTheSame
804e46f4  805a44ba nt!NtAssignProcessToJobObject
804e46f8  804e4cb4 nt!NtCallbackReturn
804e46fc  8064b05b nt!NtModifyBootEntry
804e4700  805cbb06 nt!NtCancelIoFile
804e4704  804eefac nt!NtCancelTimer
804e4708  8056b66f nt!NtClearEvent
804e470c  805698dd nt!NtClose
804e4710  8058f50f nt!NtCloseObjectAuditAlarm
804e4714  8065093c nt!NtCompactKeys
804e4718  8058b718 nt!NtCompareTokens
804e471c  80592b3d nt!NtCompleteConnectPort
804e4720  80650ba9 nt!NtCompressKey
804e4724  805899eb nt!NtConnectPort
반응형