Syscall은 사용자 프로그램(User mode, Ring 3)이 운영체제의 핵심 기능(Kernel mode, Ring 0)에 접근하기 위한 공식적인 진입점 입니다. 운영체제의 커널은 보호된 영역에서 동작합니다. 애플리케이션이 파일 I/O, 메모리 할당, 프로세스 생성, 네트워킹 등의 커널 기능을 사용하려면 특수한 CPU 명령어를 통해 Ring3 -> Ring0로 전환해야 하는데, 이 전환 메커니즘이 바로 Syscall입니다.

Linux에서는 syscall/sysenter 명령어로 직접 커널 테이블을 참조하지만, Windows에서는 Native API 라는 계층화된 구조를 통해 접근합니다.
Windows 의 API 계층 구조
Windows는 운영체제 기능을 노출하는데 세 가지 주요 계층을 사용합니다. 각 계층은 아래 계층을 호출하며, 최종적으로 커널 모드로 진입합니다.
1. Win32 API (kernel32.dll, user32.dll, gdi32.dll 등)
Win32 API는 개발자가 가장 흔히 사용하는 상위 수준 API 입니다. kernel32.dll, user32.dll, gdi32.dll 등에 구현되어 있습니다.
| Win32 함수 | 역할 |
| CreateFile() | 파일 열기 / 생성 |
| VirtualAlloc() | 메모리 할당 |
| CreateProcess() | 프로세스 생성 |
| CreateThread() | 스레드 생성 |
| ReadFile() | 파일 읽기 |
Win32 API는 편의성에 초점을 맞춥니다. 매개변수 검증, 기본값 설정, 에러 코드 변환, UNICODE/ANSI 오버로딩(예: CreateFileA / CreateFileW) 등을 처리합니다. Win32 API 함수들은 내부적으로 Native API를 호출합니다.
2. Native API (NTDLL.dll)
Native API는 NTDLL.dll 에 구현되어 있으며, Win32 API의 아래 계층에 위치합니다. 모든 Nt, Zw 접두사로 시작하는 함수들이 여기에 속합니다.
| Win32 API | Native API |
| CreateFIle() | NtCreateFile() |
| VirtualAlloc() | NtAllocateVirtualMemory() |
| CreateProcess() | NtCreateProcess() |
| ReadFile() | NtReadFile() |
3. 커널 모드 (ntoskrnl.exe)
ntoskrnl.exe는 Windows NT커널의 핵심 이미지입니다. 이름의 기원은 "NT OS on RISC"이지만, 현재 x86/x64모두에서 동작합니다.
Syscall이 커널 모드에서 진입하면, 커널은 syscall번호를 이용해 SERVICE_TABLE_ENTRY 디스패치 테이블을 탐색하고 해당하는 커널 함수를 호출합니다.
User mode -> Kernel mode 전환 메커니즘
Windows는 아키텍처(x86/x64)와 Windows 버전에 따라 다른 CPU 명령어를 사용하여 User mode -> Kernel mode 전환을 수행합니다.
1. x86: INT 2Eh -> SYSENTER
초기 Windows NT(x86 32bit)는 INT 2Eh(시스템 호출 인터럽트)를 사용했습니다. 이는 전통적인 x86인터럽트 메커니즘을 통했지만 속도가 느렸습니다.
Pentium III부터 도입된 SYSENTER 명령어(0F 34)가 등장하면서 Windows XP 이후 x86시스템은 SYSENTER를 기본 전환 명령어로 사용합니다. SYSENTER는 인터럽트 디스패치를 거치지 않고 MSR(Model Specific Register)에 저장된 커널 진입점(IA32_SYSENTER_EIP)으로 직접 점프하므로 훨씬 빠릅니다.
; inline assembly
; x86 SYSENTER를 사용한 syscall 예시 (NtAllocateVirtualMemory, syscall #0x5D)
; x86 stdcall 컨벤션: 스택 기반 매개변수 전달
_NtAllocateVirtualMemory@24:
mov eax, 5Dh ; syscall 번호를 EAX에 저장
mov edx, 7 ; SSDT 섹터 번호 (x86)
mov dx, [fs:0x24] ; 현재 프로세서 번호 읽기 (KEPROCESSOR_NUMBER)
sysenter ; Ring 3 -> Ring 0 전환
2. x64: SYSCALL 명령어
x64 아키텍처(AMD64)에서는 SYSCALL 명령어(0F 05)가 사용됩니다. Intel은 SYSENTER를, AMD는 SYSCALL를 각각 소개했으나, x64 아키텍처는 AMD의 SYSCALL를 표준으로 채택했습니다. 따라서 x64 Windows는 항상 SYSCALL명령어를 사용합니다.
x64 Windows 레지스터 컨벤션:
| 역할 | 레지스터 | 설명 |
| syscall 번호 | RAX | 호출할 syscall 번호 |
| 1번째 인자 | RCX | 첫 번째 매개변수 |
| 2번째 인자 | RDX | 두 번째 매개변수 |
| 3번째 인자 | R8 | 세 번째 매개변수 |
| 4번째 인자 | R9 | 네 번째 매개변수 |
| 5번째 이후 인자 | 스택 | 5개 이상은 스택으로 전달 |
그러나 x64 Windows syscall에는 하나의 특별한 규칙이 있습니다. 커널모드에서는 첫 번째 인자를 RCX 가 아닌 R10에서 읽어옵니다.
따라서 NTDLL.dll의 syscall stub은 SYSCALL 명령어 직전에 mov r10, rcx를 실행합니다.
; x64 syscall stub (NTDLL.dll 내부에서 실제 수행하는 동작)
; NtAllocateVirtualMemory(HANDLE, PVOID*, ULONG, PSIZE_T, ULONG)
NtAllocateVirtualMemory:
mov r10, rcx ; * 커널이 1번 인자를 R10에서 읽음
mov rax, 00000018h ; NtAllocateVirtualMemory syscall 번호
syscall ; 0F 05 — Ring 3 → Ring 0
ret
mov r10, rcx 의 중요성
이 패턴은 x64 Windows syscall의 "서명(signature)" 과 같습니다. EDR 솔루션들이 syscalls 를 탐지할 때 가장 많이 찾는 패턴 중 하나입니다. 인라인 syscall을 작성할 때 이 패턴이 없으면 커널이 첫 번째 인자를 제대로 읽지 못합니다.
3. Fast Syscall 패턴 (0x0C 0x0F)
일반적으로 syscall 명령어는 0F 05 (2byte)로 인코딩 됩니다. NTDLL.dll 내부에서 mov r10, rcx (41 89 CA) 와 syscall (0F 05)가 연속으로 배치되어 있습니다.
; ntdll.dll의 NtAllocateVirtualMemory 실제 기계어
41 89 CA ; mov r10, rcx
B8 18 00 00 00 ; mov rax, 18h (syscall 번호)
0F 05 ; syscall
C3 ; ret
리버싱 엔지니어들은 이 41 89 CA ... 0F 05 패턴(또는 그 일부인 0C 0F byte subsequence)을 NTDLL.dll 이미지에서 스캔하여 syscall stub 위치와 momv rax, <number> 에서 syscall 번호를 자동으로 추출합니다. 이 기법은 Inline Syscall Generator의 핵심 원리입니다.
4. AWC (Abstract Walk of Contents)
Windows 11 22H2(빌드 22621+)에서 Microsoft는 NTDLL.dll의 syscall 메커니즘을 크게 변경했습니다.
- 이전: 각 NTDLL.dll 함수가 직접 mov rax, <number> + syscall 를 실행
- AWC 이후: NTDLL.dll stub들이 공유된 공통 dispatcher를 호출
- 이전 처럼 각 함수마다 mov rax, N + syscall 패턴이 직접 봉지 않음
- syscall 번호가 다른 위치(데이터 섹션)에 저장됨
AWC의 영향
AWC 도입으로 기존 "바이트 패턴 스캔으로 syscall번호 추출" 방식의 Inline syscall generator 들이 동작하지 않게 되었습니다. 새로운 기법 (PEB Scan, API set 변경 감시, 데이터 섹션 파싱 등)이 필요합니다. 하지만 수동으로 syscall번호를 하드코딩 하면 여전히 직접호출이 가능합니다.
System Call Number 와 Dispatch Table
각 syscall은 고유한 정수 번호(System Call Number)를 가집니다. 커널은 User Mode에서 전달한 syscall 번호(RAX)를 인덱스로 사용하여 SERVICE_TABLE_ENTRY 디스패치 테이블을 탐색하고, 해당 함수 포인터를 호출합니다.
// 커널 내부의 SERVICE_TABLE_ENTRY 구조
typedef struct _SERVICE_TABLE_ENTRY {
PVOID ServiceTable; // Nt* 함수 포인터들의 배열
PULONG CounterTable; // 호출 카운트 (디버깅용)
ULONG TableSize; // 테이블 크기
} SERVICE_TABLE_ENTRY;
Windows 커널은 여러 서비스 테이블을 가지고 있습니다:
| 테이블 | 역할 |
| NT Table | 주요 syscall (파일, 메모리, 프로세스, 스레드 등) |
| ALPC Table | ALPC (Atomic Lightweight Process Communication) |
| RTM Table | Transaction (MSR - Memory Serialization) |
| UMP Table | User Mode Power 관리 |
주요 syscall 번호 (x64, Windows 10/11 기준):
| 함수명 | Syscall# | 역할 |
| NtClose | 0x0E (14) | 핸들 닫기 |
| NtDelayExecution | 0x01 (1) | 스레드 지연 (Sleep) |
| NtAllocateVirtualMemory | 0x18 (24) | 가상 메모리 할당 |
| NtFreeVirtualMemory | 0x1A (26) | 가상 메모리 해제 |
| ... | ... | ... |
버전 주의
syscall 번호는 Windows 버전마다 다릅니다. Windows 7, 10, 11, Server 2016/2019/2022 마다 syscall 번호가 상이할 수 있습니다. https://www.vergiliusproject.com 에서 각 버전의 syscall 번호를 확인할 수 있습니다.
Nt* 와 Zw* 접두사의 의미
| 접두사 | 의미 | 사용 위치 |
| Nt* | Native 호출 (User Mode에서 호출 시 사용) | 가장 일반적으로 사용됨 |
| Zw* | Kernel 호출 (커널 내부에서도 호출 가능) | 커널 드라이버, 커널 내부 함수 호출 |
실제로 NTDLL.dll 에서 NtCreateFile 과 ZwCreateFile은 동일한 심볼을 가리키며, 동일한 syscall 번호를 사용합니다. 차이점은 커널 내부에서 발생합니다. Zw 버전은 추가적인 커널 내부 처리(예: 커널 디버거 브레이크 포인트 폴링)를 거칩니다. User Mode 에서는 거의 항상 Nt 버전을 사용합니다.
마무리
이번엔 syscall 에 대해 상세히 다뤄봤습니다.
다음에는 syscall의 사용(C 언어)을 알아보고, 실습하는 시간을 가져보도록 하겠습니다.
'IT' 카테고리의 다른 글
| Syscall - implement (1) | 2026.06.10 |
|---|---|
| IAT Hiding (0) | 2026.05.30 |
| Classic APC Injection (0) | 2026.05.25 |
| Process, Thread Enumeration (0) | 2026.05.25 |
| API Hooking with Custom structure (0) | 2026.05.24 |