우리가 편지를 쓴다면, 보내는 사람은 “나”라는 이름으로 편지를 보내고, 받는 사람 “너”라는 사람이 편지 내용을 읽고 회신하게 된다. 이 과정 속에 실질적인 편지 배달은 우체국에서 진행하게 된다. 우체국은 중간에서 편지를 각 지역별로 구분한 후, 공통된 배달 방식에 맞게 구분하여 편지를 배달해 주는 것이다. 이와 같은 원리는 시스템에서도 볼 수 있다. 편지와 비교해 본다면, “나”는 응용프로그램으로 사용자의 입력이나, 처리 요청을 받아 개발 의도에 맞게 가공하여 커널에 처리를 요청하게 된다. 그리고 커널은 편지에서 “너”라고 할 수 있다.
이 둘간의 전달 역할을 해주는 것이 서브시스템과 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이 커널로 진입하는 내용을 확인한 것이다.
만약 응용프로그램이 파일 읽기를 요청하였다면, 요청을 처리하기 위한 흐름은 다음과 같다(굵게 표시한 부분이 Ntdll.dll 이후 커널에서 처리되는 부분이다).
BOOL WINAPI ReadFile() > kernel32.ReadFile() > ntdll.ZwReadFile() > ntdll.KiFastSystemCall() > SYSENTER > nt!NtReadFile() > 처리 > 처리 결과 리턴
이 요청 처리 흐름을 그림으로 표현하면 다음과 같다.
앞서 파일 읽기의 요청 순서를 보면, 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
'WebBook > 윈도우 구조' 카테고리의 다른 글
윈도우/리눅스 – 가상 메모리 관리(Paging, Swap) (0) | 2024.02.23 |
---|---|
윈도우 예외 처리 이해 하기 - KeBugCheckEx (0) | 2022.09.13 |
시스템 프로세스(Windows Startup Process) - 자동 실행 - Userinit.exe (0) | 2022.03.23 |
윈도우 - 서비스 계정 (0) | 2022.03.23 |
시스템 프로세스(Windows Startup Process) - 서비스 관리자 Services.exe (0) | 2022.03.14 |