IT

Syscall - implement

4sterisk 2026. 6. 10. 22:10

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 명령어를 실행합니다.

동작원리:

  1. NTDLL.dll 에서 대상 함수 (예: NtAllocateVirtualMemory)의 주소를 가져옴
  2. 함수 시작부의 바이트를 스캔하여 mov rax, <syscall number>의 번호 추출
  3. 추출한 번호를 사용하여 직접 syscall 명령어 실행
  4. 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