C 언어에서 syscall 호출하는 방법을 알아보겠습니다.
방법 1: NTDLL.dll EXPORT 함수 직접 호출
가장 간단한 방법입니다. NTDLL.dll 이 export 하는 Nt*/Zw* 함수를 직접 선언하고 호출합니다.
NTDLL.dll 은 프로세스 로드 시 자동으로 메모리에 매핑되므로 별도의 LoadLibrary가 필요 없습니다.
#include <stdio.h>
#include <windows.h>
#define NTSTATUS LONG
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
typedef NTSTATUS (fnNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect), *pNtAllocateMemory;
typedef NTSTATUS (fnNtReadVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
SIZE_T BufferSize,
PSIZE_T ReturnSize), *pNtReadVirtualMemory;
int main()
{
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
pNtAllocateVirtualMemory NtAllocateVirtualMemory =
(pNtAllocateVirtualMemory)GetProcAddress(ntdll, "NtAllocateVirtualMemory");
pNtReadVirtualMemory NtReadVirtualMemory =
(pNtReadVirtualMemory)GetProcAddress(ntdll, "NtReadVirtualMemory");
PVOID base = NULL;
SIZE_T size = 4096;
NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(),
&base,
0,
&size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (status == STATUS_SUCCESS) {
printf("Memory Allocation Success: %p (size: %zu)\n", base, size);
char* msg = "Hello from Syscall!";
RtlCopyMemory(base, msg, strlen(msg) + 1);
char buf[64] = { 0 };
SIZE_T read = 0;
status = NtReadVirtualMemory(
GetCurrentProcess(),
base,
buf,
64,
&read);
if (status == STATUS_SUCCESS) {
printf("Memory Read Success: %s\n", buf);
}
}
GetChar();
return 0;
}
typedef NTSTATUS (fnNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect), *pNtAllocateMemory;
함수 포인터를 선언합니다. Argument가 호출하려는 함수와 형식이 맞아야 합니다. 이름은 없어도 상관없습니다.
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
NTDLL.dll을 로드합니다. LoadLibrary는 사용하지 않아도 됩니다.
이미 프로세스 로드 시에 로드되어 있기 때문입니다.
NTSTATUS status = NtAllocateVirtualMemory(
GetCurrentProcess(),
&base,
0,
&size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
NtAllocateVirtualMemory를 사용하여 size 만큼의 메모리를 PAGE_READWRITE 권한으로 할당합니다.
status = NtReadVirtualMemory(
GetCurrentProcess(),
base,
buf,
64,
&read);
NtReadVirtualMemory를 호출하여 base Address 에 있는 데이터를 읽어 buf에 저장합니다.
장점:
가장 간단하고 가독성 좋음, 컴파일러가 호출규칙을 자동으로 처리.
단점:
NTDLL.dll 을 경유하므로 EDR/AV가 API HOOK으로 감지 가능
방법 2: Inline Assembly (MSVC x86)
MSVC의 x86 컴파일러는 __asm 블록을 지원하여 인라인 어셈블리를 사용할 수 있습니다.
하지만 x64 MSVC에서는 __asm 블록이 제한됩니다. (mostly macro-only)
NTSTATUS Syscall_AllocateMemory(PVOID* base, SIZE_T* size)
{
NTSTATUS status;
HANDLE hProc = (HANDLE)0xFFFFFFFFFFFFFFFF;
__asm {
push PAGE_READWRITE
push MEM_COMMIT | MEM_RESERVE
push size
push 0
push base
push hProc
mov eax, 5Dh
mov dx, [fs:0x24]
sysenter
mov status, eax
add esp, 24
}
return status;
}
mov eax, 5Dh
mov dx, [fs:0x24]
sysenter
NtAllocateVirtualMemory syscall입니다.
앞선 포스팅에 적어놨듯이 x86은 SYSENTER를 사용합니다.
mov status, eax
NTSTATUS 에 결과를 저장합니다. 함수의 결과는 eax 레지스터에 반환됩니다.
방법 3: GCC/Clang Inline Assembly (x64)
GCC와 Clang은 x64 에서도 인라인 어셈블리를 지원합니다. __asm__ volatile 를 사용하여 syscall을 직접 호출할 수 있습니다.
#include <stdint.h>
typedef int32_t NTSTATUS;
typedef void* HANDLE;
typedef uint64_t SIZE_T;
NTSTATUS NtAllocateVirtualMemory(
HANDLE ProcessHandle,
void** BaseAddress,
uint32_t ZeroBits,
SIZE_T* RegionSize,
uint32_t AllocationType,
uint32_t Protect)
{
NTSTATUS status;
__asm__ volatile(
"mov %%rcx, %%r10;\n"
"mov %1, %%rax;\n"
"syscall;\n"
"mov %%rax, %0;\n"
:"=m"(status)
:"r"(0x18),
"rcx"(ProcessHandle),
"rdx"(BaseAddress),
"r8" (ZeroBits),
"r9" (RegionSize)
:"r10", "rax", "memory", "cc"
);
return status;
}
"mov %%rcx, %%r10;\n"
"mov %1, %%rax;\n"
"syscall;\n"
커널이 1번 인자를 R10에서 읽어옵니다.
syscall 번호를 rax에 저장한 수 syscall을 호출합니다.
:"r"(0x18),
syscall 번호입니다.
이 뒤로는 1번인자부터 순서대로 입력합니다.
NTDLL.dll 을 경유하지 않고 직접 커널에 진입하므로 EDR API Hook 을 bypass 할 수 있습니다. 하지만 mov r10, rcx 패턴이 어셈블리 내에 존재하므로 코드 스캔형 EDR에는 여전히 탐지될 수 있습니다.
방법 4: Inline Syscall (x64 - NTDLL bypass)
Inline syscall 은 리버싱 커뮤니티 (특히 EAC/BE 회피 및 Rootkit 개발) 에서 가장 많이 사용하는 기법입니다.
NTDLL.dll 내부의 syscall stub에서 syscall 번호만 추출하여 직접 syscall 명령어를 실행합니다.
동작원리:
- NTDLL.dll 에서 대상 함수 (예: NtAllocateVirtualMemory)의 주소를 가져옴
- 함수 시작부의 바이트를 스캔하여 mov rax, <syscall number>의 번호 추출
- 추출한 번호를 사용하여 직접 syscall 명령어 실행
- NTDLL.dll의 실제 함수 코드를 경유하지 않으므로 API Hook이 무효화됨
#include <stdio.h>
#include <windows.h>
typedef long NTSTATUS;
DWORD GetSyscallNumber(HMODULE ntdll, const char* functionName)
{
FARPROC addr = GetProcAddress(ntdll, functionName);
UINT8* code = (UINT8*)addr;
for (int i = 0; i < 16; i++) {
if (code[i] == 0xB8 && (code[i + 5] == 0x05 || code[i + 5] == 0x0F)) {
return * (DWORD*)(code + i + 1);
}
}
return 0;
}
NTSTATUS InlineSyscall(DWORD syscallNum,
HANDLE proc, void** base,
ULONG zeroBits, SIZE_T* regionSize,
ULONG allocType, ULONG protect)
{
NTSTATUS status;
__asm__ volatile(
"mov %%rcx, %%r10;\n"
"mov %1, %%rax;\n"
"syscall;\n"
"mov %%rax, %0:\n"
:"=m"(status)
:"r"(syscallNum),
"rcx"(proc),
"rdx"(base),
"r8" (zeroBits),
"r9" (regionSize)
:"r10", "rax", "memory", "cc"
);
return status;
}
int main()
{
HMODULE ntdll = GetModuleHandleA("ntdll.dll");
DWORD syscall_NtAlloc = GetSyscallNumber(ntdll, "NtAllocateVirtualMemory");
printf("NtAllocateVirtualMemory syscall #: 0x%X\n", syscall_NtAlloc);
PVOID base = NULL;
SIZE_T size = 4096;
NTSTATUS st = InlineSyscall(
syscall_NtAlloc,
GetCurrentProcess(),
&base,
0,
&size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (st == 0) {
printf("Inline Syscall Success! Memory Address: %p\n", base);
char* msg = "Hello via Inline Syscall!";
RtlCopyMemory(base, msg, strlen(msg) + 1);
printf("Memory: %s\n", (char*)base);
}
getchar();
return 0;
}
for (int i = 0; i < 16; i++) {
if (code[i] == 0xB8 && (code[i + 5] == 0x05 || code[i + 5] == 0x0F)) {
return * (DWORD*)(code + i + 1);
}
}
mov rax, imm32 패턴: B8 XX XX XX XX
NTDLL의 syscall stub은 보통 mov r10, rcx (41 89 CA) 후
mov rax, N (B8 N 00 00 00) 패턴을 가짐
NTSTATUS st = InlineSyscall(
syscall_NtAlloc,
GetCurrentProcess(),
&base,
0,
&size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
인라인 syscall로 메모리 할당
보안: Syscall 감지 및 EDR 회피
Syscall은 EDR 및 AV 솔루션의 주요 감지 대상입니다. 주요 감지 기법과 대응 전략을 살펴보겠습니다.
EDR의 syscall 감지 기법
| 감지 기법 | 원리 |
| API Hooking | NTDLL.dll의 IAT/EAT를 hook 하여 Win32 -> Native 호출을 감지. Detours, Minhook, EAC/BE Inline hook 등 |
| ETW(Event Tracing) | TraceEvent, KernelTrace 이벤트를 모니터링하여 프로세스/스레드/메모리 활동을 감지 |
| Kernel Callback | CmRegisterCallback, PsSetCreateProcessNotifyRoutine 을 이용해 커널 이벤트를 실시간 감지 |
| SSDT Monitoring | Syscall 디스패치 테이블(SSDT)의 접근을 모니터링 |
| Code Scan | 프로세스 메모리에서 mov r10, rcx + syscall 패턴 스캔 |
대응 전략
| 전략 | 설명 |
| Inline Syscall | NTDLL.dll을 경유하지 않고 직접 커널 진입, API Hooking을 우회 |
| Syscall 번호 동적 추출 | 하드코딩 대신 NTDLL.dll에서 런타임에 syscall 번호를 추출하여 버전 호환성과 가독성 확보 |
| 코드 변형 | mov r10, rcx 를 xchg rcx, r10 또는 다른 등가 명령어로 변조하여 패턴 스캔 회피 |
| Unhooked NTDLL | 디스크에서 fresh NTDLL.dll을 로드하여 Hook 되지 않은 상태로 사용 |
| Syswhispers | Well-known inline syscall library. NTDLL stub을 스캔하여 syscall 번호를 추출하고 직접 호출 |
John Turner 가 개발한 Syswhisper는 가장 유명한 inline syscall library입니다. NTDLL.dll 의 PE 구조를 파싱하여 Syscall 번호를 자동 추출하고, 직접 syscall을 호출합니다. Syswhispers3는 AWC 이후에도 동작하도록 개선되었습니다.
'IT' 카테고리의 다른 글
| Syscall - 커널의 소통 창구 (0) | 2026.06.03 |
|---|---|
| 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 |