시스템 콜 (System Call)

사용자 공간과 커널 공간을 잇는 핵심 인터페이스인 시스템 콜을 다룹니다. x86_64 SYSCALL/SYSRET 호출 규약, entry_SYSCALL_64 진입 경로, sys_call_table, SYSCALL_DEFINE 매크로, vDSO, seccomp-bpf, 호환성 계층까지 심층적으로 분석합니다.

관련 표준: POSIX.1-2017 (시스템 콜 시맨틱), System V AMD64 ABI (x86-64 호출 규약), Intel SDM (SYSCALL/SYSRET 명령어) — 시스템 콜 인터페이스의 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 커널 아키텍처(특권 레벨 Ring 0/3, 레지스터 개념)를 먼저 읽으세요.
일상 비유: 시스템 콜은 은행 창구와 같습니다. 일반 고객(사용자 프로그램)은 금고(하드웨어)에 직접 접근할 수 없고, 반드시 창구 직원(커널)을 통해 요청해야 합니다. 창구 번호표가 시스템 콜 번호이고, 신청서에 적는 내용이 인자(arguments)입니다. SYSCALL 명령어는 "번호표를 뽑고 창구로 가는 행위"에 해당합니다.

핵심 요약

  • 시스템 콜 — 사용자 프로그램이 커널 서비스를 요청하는 유일한 공식 인터페이스입니다.
  • SYSCALL/SYSRET — x86_64에서 사용자↔커널 전환을 수행하는 CPU 명령어입니다.
  • sys_call_table — 시스템 콜 번호를 실제 커널 함수에 매핑하는 함수 포인터 테이블입니다.
  • vDSO — 커널 진입 없이 사용자 공간에서 실행되는 가상 동적 공유 객체 (gettimeofday 등).
  • seccomp-BPF — 프로세스가 사용할 수 있는 시스템 콜을 BPF 필터로 제한하는 보안 기능입니다.

단계별 이해

  1. 호출 흐름 — 사용자 프로그램 → glibc 래퍼 → SYSCALL 명령어 → 커널 진입 → 핸들러 실행 → 결과 반환.

    이 전체 과정이 수백 나노초 안에 완료됩니다.

  2. strace로 관찰strace ls를 실행하면 ls가 호출하는 모든 시스템 콜을 볼 수 있습니다.

    open(), read(), write() 등 실제 시스템 콜과 그 인자/반환값을 확인합니다.

  3. 커널 내부SYSCALL_DEFINE3(read, ...) 매크로가 sys_read() 함수를 정의합니다.

    시스템 콜 번호는 arch/x86/entry/syscalls/syscall_64.tbl에 정의되어 있습니다.

  4. 보안 — seccomp-BPF로 컨테이너나 샌드박스에서 허용할 시스템 콜을 화이트리스트로 제한합니다.

    Docker, Chrome 등이 seccomp을 사용하여 공격 표면을 줄입니다.

시스템 콜 개요

시스템 콜(system call)은 사용자 공간 프로그램이 커널의 서비스를 요청하기 위한 프로그래밍 인터페이스입니다. 프로세스 생성, 파일 읽기/쓰기, 네트워크 통신, 메모리 할당 등 하드웨어 자원에 접근하는 모든 작업은 시스템 콜을 통해야 합니다. Linux 커널은 약 450개 이상의 시스템 콜을 제공합니다.

보호 링과 특권 수준

x86 아키텍처는 4개의 보호 링(Ring 0~3)을 제공하지만, Linux는 Ring 0(커널 모드)과 Ring 3(사용자 모드) 두 가지만 사용합니다. 시스템 콜은 사용자 모드에서 커널 모드로의 제어된 전환을 수행하는 유일한 합법적 경로입니다.

특권 수준 Ring CPL 접근 가능 영역
커널 모드 Ring 0 0 모든 메모리, I/O 포트, 특권 명령어
사용자 모드 Ring 3 3 사용자 공간 메모리만 (가상 주소 하위 영역)
CPL (Current Privilege Level): CS 레지스터의 하위 2비트에 저장되는 현재 특권 수준입니다. SYSCALL 명령어 실행 시 하드웨어가 자동으로 CPL을 0으로 전환합니다.

int 0x80에서 SYSCALL까지의 변천

x86 시스템 콜 호출 메커니즘은 성능 향상을 위해 발전해 왔습니다.

메커니즘 아키텍처 도입 시기 특징
int 0x80 i386 초기 소프트웨어 인터럽트, IDT 조회 필요 → 느림
sysenter/sysexit i386 (Pentium II+) 2000년대 Intel 전용 빠른 진입, MSR 기반
SYSCALL/SYSRET x86_64 AMD64 64비트 표준, MSR_LSTAR 기반, 가장 빠름

int 0x80은 IDT(Interrupt Descriptor Table)를 조회하고 스택 전환을 수행하므로 수백 사이클이 소요됩니다. SYSCALL은 IDT를 우회하고 MSR에 미리 저장된 진입점으로 직접 점프하므로 훨씬 빠릅니다.

/* int 0x80 (레거시 32비트) - 느린 경로 */
mov    $__NR_write, %eax     /* 시스템 콜 번호 */
mov    $1, %ebx              /* fd = stdout */
mov    $msg, %ecx            /* buffer */
mov    $len, %edx            /* count */
int    $0x80                 /* 소프트웨어 인터럽트 */

/* SYSCALL (x86_64) - 빠른 경로 */
mov    $1, %rax              /* __NR_write */
mov    $1, %rdi              /* fd = stdout */
mov    $msg, %rsi            /* buffer */
mov    $len, %rdx            /* count */
syscall                       /* 빠른 시스템 콜 진입 */

시스템 콜 아키텍처

x86_64 레지스터 규약

x86_64에서 SYSCALL 명령어를 사용할 때의 레지스터 규약은 다음과 같습니다. 이는 C 함수 호출 규약(System V AMD64 ABI)과 유사하지만 rcx 대신 r10을 사용하는 점이 다릅니다.

레지스터 용도 비고
rax 시스템 콜 번호 / 반환값 호출 전: 번호, 반환 후: 결과 또는 -errno
rdi 1번째 인자
rsi 2번째 인자
rdx 3번째 인자
r10 4번째 인자 C ABI에서는 rcx이지만, SYSCALL이 rcx를 덮어쓰므로 r10 사용
r8 5번째 인자
r9 6번째 인자
rcx 복귀 주소 (RIP) SYSCALL이 자동으로 RIP → RCX 저장
r11 RFLAGS 백업 SYSCALL이 자동으로 RFLAGS → R11 저장
rcx와 r11 파괴: SYSCALL 명령어는 하드웨어적으로 rcx에 복귀 주소(RIP)를, r11에 RFLAGS를 저장합니다. 따라서 이 두 레지스터의 원래 값은 시스템 콜 수행 후 복원되지 않습니다. glibc의 syscall wrapper는 이를 고려하여 r10으로 4번째 인자를 전달합니다.

시스템 콜 흐름 다이어그램

User Space (Ring 3) Kernel Space (Ring 0) Application glibc wrapper mov $NR, %rax SYSCALL instruction RCX=RIP, R11=RFLAGS vDSO (optional) gettimeofday 등 call syscall fast path entry_SYSCALL_64 swapgs, save pt_regs do_syscall_64() sys_call_table[nr](args) 실제 시스템 콜 핸들러 MSR_LSTAR entry_SYSCALL_64 주소 MSR_STAR CS/SS 셀렉터 MSR_LSTAR SYSRET

시스템 콜 테이블

sys_call_table은 시스템 콜 번호를 해당 핸들러 함수 포인터에 매핑하는 배열입니다. x86_64에서는 arch/x86/entry/syscall_64.c에 정의됩니다.

/* arch/x86/entry/syscall_64.c */
#include <asm/syscalls_64.h>

#define __SYSCALL(nr, sym) [nr] = __x64_##sym,

const sys_call_ptr_t sys_call_table[] __ro_after_init = {
    [0 ... __NR_syscall_max] = __x64_sys_ni_syscall,
    #include <asm/syscalls_64.h>
};

이 테이블은 __ro_after_init으로 선언되어 부팅 후에는 읽기 전용이 됩니다. 이는 루트킷이 시스템 콜 테이블을 변조하는 것을 방지하는 보안 장치입니다.

시스템 콜 번호 할당

시스템 콜 번호는 arch/x86/entry/syscalls/syscall_64.tbl 테이블 파일에서 관리됩니다. 빌드 시 이 파일로부터 헤더가 자동 생성됩니다.

# arch/x86/entry/syscalls/syscall_64.tbl
# <번호>  <ABI>   <이름>          <진입점>
0       common  read            sys_read
1       common  write           sys_write
2       common  open            sys_open
3       common  close           sys_close
...
56      common  clone           sys_clone
57      common  fork            sys_fork
59      common  execve          sys_execve
...
435     common  clone3          sys_clone3

주요 시스템 콜 번호를 확인하려면:

# 사용자 공간에서 시스템 콜 번호 헤더 확인
$ grep -E '__NR_(read|write|open|close|fork|clone|execve) ' \
    /usr/include/asm/unistd_64.h
#define __NR_read    0
#define __NR_write   1
#define __NR_open    2
#define __NR_close   3
새로운 시스템 콜 번호: 번호는 절대 재사용되지 않으며, 항상 테이블 끝에 새 번호가 추가됩니다. 삭제된 시스템 콜은 sys_ni_syscall(not implemented)로 매핑되어 -ENOSYS를 반환합니다.

SYSCALL_DEFINE 매크로

DEFINE0~6 패밀리

커널 시스템 콜 핸들러는 SYSCALL_DEFINEN 매크로로 정의됩니다. N은 인자 개수(0~6)를 나타냅니다. 이 매크로는 함수 프로토타입 생성, 인자 타입 검증, 추적(tracing) 지원, CVE 방지용 타입 캐스팅을 자동으로 처리합니다.

/* include/linux/syscalls.h */
#define SYSCALL_DEFINE0(name)  ...
#define SYSCALL_DEFINE1(name, type1, arg1)  ...
#define SYSCALL_DEFINE2(name, type1, arg1, type2, arg2)  ...
/* ... SYSCALL_DEFINE6까지 */

/* 내부적으로 확장되는 구조 */
#define SYSCALL_DEFINE3(name, ...) \
    SYSCALL_METADATA(name, 3, __VA_ARGS__)  \
    __SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

SYSCALL_METADATA는 ftrace의 syscall tracepoint에 사용되며, __SYSCALL_DEFINEx는 실제 함수 본체를 정의합니다. 인자를 long으로 받아 올바른 타입으로 캐스팅하여 레지스터 상위 비트 오염 공격(CVE-2009-0029)을 방지합니다.

실제 커널 코드 예제

/* fs/read_write.c - read 시스템 콜 */
SYSCALL_DEFINE3(read,
    unsigned int, fd,
    char __user *, buf,
    size_t, count)
{
    return ksys_read(fd, buf, count);
}

/* kernel/fork.c - getpid (인자 없음) */
SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current);
}

/* mm/mmap.c - mmap (인자 6개, 최대) */
SYSCALL_DEFINE6(mmap,
    unsigned long, addr,
    unsigned long, len,
    unsigned long, prot,
    unsigned long, flags,
    unsigned long, fd,
    unsigned long, off)
{
    return ksys_mmap_pgoff(addr, len, prot, flags, fd,
                            off >> PAGE_SHIFT);
}
인자 6개 제한: x86_64 SYSCALL 규약에서 사용 가능한 인자 전달 레지스터가 rdi, rsi, rdx, r10, r8, r9의 6개이므로, 시스템 콜 인자는 최대 6개로 제한됩니다. 더 많은 데이터가 필요한 경우 구조체 포인터를 전달합니다.

시스템 콜 진입 경로 상세

entry_SYSCALL_64 분석

entry_SYSCALL_64는 x86_64에서 모든 64비트 시스템 콜의 커널 진입점입니다. MSR_LSTAR에 이 함수의 주소가 저장되어 있어, SYSCALL 명령어 실행 시 하드웨어가 이 주소로 점프합니다. 어셈블리 종합 페이지와 MSR 레지스터 페이지에서 관련 세부 사항을 확인할 수 있습니다.

/* arch/x86/entry/entry_64.S (간략화) */
SYM_CODE_START(entry_SYSCALL_64)
    /* SYSCALL 직후: RCX=사용자RIP, R11=사용자RFLAGS
     * RSP는 아직 사용자 스택! */

    swapgs                        /* GS base를 커널용으로 전환 */

    /* 사용자 RSP를 per-CPU 영역에 임시 저장 */
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)

    /* 커널 스택으로 전환 (per-CPU에서 로드) */
    movq    PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp

    /* pt_regs 구조체 형태로 레지스터 저장 */
    pushq   $__USER_DS              /* pt_regs->ss */
    pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2)  /* pt_regs->sp */
    pushq   %r11                    /* pt_regs->flags */
    pushq   $__USER_CS              /* pt_regs->cs */
    pushq   %rcx                    /* pt_regs->ip */
    pushq   %rax                    /* pt_regs->orig_ax (syscall nr) */

    /* 나머지 범용 레지스터 저장 */
    PUSH_AND_CLEAR_REGS rax=$-ENOSYS

    /* C 핸들러 호출 */
    movq    %rsp, %rdi             /* 1번째 인자: pt_regs 포인터 */
    movslq  %eax, %rsi             /* 2번째 인자: syscall 번호 */
    call    do_syscall_64

    /* 반환 경로: 레지스터 복원 후 SYSRET */
    ...
    swapgs
    sysretq
SYM_CODE_END(entry_SYSCALL_64)
swapgs 보안: swapgs는 사용자 GS base와 커널 GS base를 교환합니다. 이 명령어가 빠지면 커널이 사용자 제어 GS base를 사용하게 되어 보안 취약점이 발생합니다. Spectre v1 변종 공격에서 swapgs 경계가 표적이 된 사례(CVE-2019-1125)가 있습니다.

pt_regs 구조체

struct pt_regs는 시스템 콜(또는 인터럽트/예외) 진입 시 저장되는 사용자 레지스터 상태를 담는 구조체입니다. 커널 스택에 이 구조체 형태로 레지스터를 push합니다.

/* arch/x86/include/asm/ptrace.h */
struct pt_regs {
    unsigned long r15, r14, r13, r12;
    unsigned long bp, bx;
    unsigned long r11, r10, r9, r8;
    unsigned long ax, cx, dx, si, di;
    unsigned long orig_ax;      /* 시스템 콜 번호 */
    unsigned long ip;           /* 사용자 RIP (RCX에서) */
    unsigned long cs;
    unsigned long flags;        /* 사용자 RFLAGS (R11에서) */
    unsigned long sp;           /* 사용자 RSP */
    unsigned long ss;
};

do_syscall_64()

do_syscall_64()는 어셈블리 진입점에서 호출되는 C 함수로, 시스템 콜 번호 검증과 디스패치를 수행합니다.

/* arch/x86/entry/common.c */
__visible noinstr void do_syscall_64(
    struct pt_regs *regs, int nr)
{
    add_random_kstack_offset();
    nr = syscall_enter_from_user_mode(regs, nr);

    instrumentation_begin();

    if (!do_syscall_x64(regs, nr))
        do_syscall_x32(regs, nr);  /* x32 ABI 폴백 */

    instrumentation_end();
    syscall_exit_to_user_mode(regs);
}

static __always_inline bool do_syscall_x64(
    struct pt_regs *regs, int nr)
{
    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
        return true;
    }
    return false;
}
add_random_kstack_offset(): 시스템 콜마다 커널 스택 오프셋을 랜덤화하여, 스택 기반 공격(스택 스프레이 등)을 어렵게 만드는 보안 강화 기법입니다. array_index_nospec()는 Spectre v1 방어를 위한 투기적 실행 차단 함수입니다.

진입 경로 상세 다이어그램

SYSCALL (하드웨어) swapgs + 커널 스택 전환 pt_regs 저장 (push regs) syscall_enter_from_user_mode() seccomp 검사 audit, ptrace, sys_call_table[nr](regs) 실제 핸들러 실행 syscall_exit_to_user_mode() 시그널 처리, 스케줄링 체크 pt_regs 복원 (pop regs) swapgs + SYSRET (하드웨어)

새로운 시스템 콜 추가

단계별 가이드

리눅스 커널에 새로운 시스템 콜을 추가하는 과정을 단계별로 설명합니다. 아래는 sys_hello라는 예시 시스템 콜을 추가하는 과정입니다.

1단계: 시스템 콜 테이블에 번호 등록

# arch/x86/entry/syscalls/syscall_64.tbl 끝에 추가
# 번호는 마지막 항목 + 1
451     common  hello           sys_hello

2단계: 시스템 콜 핸들러 구현

/* kernel/sys_hello.c */
#include <linux/syscalls.h>
#include <linux/uaccess.h>

SYSCALL_DEFINE1(hello, char __user *, buf)
{
    const char msg[] = "Hello from kernel!\n";

    if (copy_to_user(buf, msg, sizeof(msg)))
        return -EFAULT;

    return sizeof(msg);
}

3단계: 헤더에 프로토타입 선언

/* include/linux/syscalls.h 에 추가 */
asmlinkage long sys_hello(char __user *buf);

4단계: Makefile에 오브젝트 추가

# kernel/Makefile
obj-y += sys_hello.o

5단계: 사용자 공간에서 호출

/* userspace test program */
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>

#define __NR_hello 451

int main(void)
{
    char buf[64];
    long ret = syscall(__NR_hello, buf);
    if (ret > 0)
        printf("%s", buf);
    return 0;
}
시스템 콜 ABI 안정성: 리눅스 커널은 시스템 콜 ABI의 하위 호환성을 절대적으로 보장합니다. 한 번 번호가 할당된 시스템 콜은 영구히 유지되며, 인터페이스 변경은 새로운 시스템 콜을 추가하는 방식으로만 이루어집니다 (예: cloneclone3, epoll_createepoll_create1).

호환성 계층

compat_sys_* 래퍼

64비트 커널에서 32비트 사용자 프로그램을 실행할 때, 시스템 콜 인자의 크기가 다릅니다 (예: long이 4바이트 vs 8바이트). compat_sys_* 래퍼는 32비트 인자를 64비트로 안전하게 변환합니다.

/* fs/read_write.c */
COMPAT_SYSCALL_DEFINE3(read,
    unsigned int, fd,
    char __user *, buf,
    compat_size_t, count)
{
    return ksys_read(fd, buf, (size_t)count);
}

/* 타입 변환이 필요한 구조체 예시 */
struct compat_stat {
    compat_dev_t   st_dev;     /* 32비트 dev_t */
    compat_ino_t   st_ino;     /* 32비트 ino_t */
    compat_off_t   st_size;    /* 32비트 off_t */
    /* ... */
};

32비트 호환 모드 (IA-32 Emulation)

x86_64 Linux 커널은 CONFIG_IA32_EMULATION 옵션으로 32비트 프로그램의 int 0x80sysenter 호출을 지원합니다. 32비트 시스템 콜은 별도의 테이블(ia32_sys_call_table)을 사용하며, 번호 체계도 다릅니다.

/* arch/x86/entry/entry_64_compat.S */
SYM_CODE_START(entry_SYSENTER_compat)
    /* 32비트 sysenter 진입점 */
    swapgs
    /* 32비트 레지스터에서 인자 추출 */
    movl    %ebp, %r10d    /* ebp → r10 (4번째 인자) */
    ...
    call    do_SYSENTER_32
SYM_CODE_END(entry_SYSENTER_compat)
항목 64비트 (native) 32비트 (compat)
호출 명령어 SYSCALL int 0x80 / sysenter
시스템 콜 테이블 sys_call_table ia32_sys_call_table
번호 헤더 unistd_64.h unistd_32.h
write 번호 1 4
인자 전달 rdi, rsi, rdx, r10, r8, r9 ebx, ecx, edx, esi, edi, ebp

vDSO (virtual Dynamic Shared Object)

개념과 목적

vDSO는 커널이 사용자 공간 프로세스의 주소 공간에 매핑하는 작은 공유 라이브러리입니다. 커널 모드 전환 없이 특정 시스템 콜을 사용자 공간에서 직접 실행할 수 있게 합니다. 이를 통해 gettimeofday(), clock_gettime(), getcpu() 등 자주 호출되는 시스템 콜의 오버헤드를 제거합니다.

# vDSO 매핑 확인
$ cat /proc/self/maps | grep vdso
7ffd3c5fe000-7ffd3c600000 r-xp 00000000 00:00 0  [vdso]

# vDSO에서 제공하는 함수 목록
$ objdump -T /proc/self/root/$(readlink /proc/self/exe) 2>/dev/null || \
  LD_SHOW_AUXV=1 /bin/true | grep VDSO
$ vdso=$(dd if=/proc/self/mem bs=1 skip=$((0x7ffd3c5fe000)) count=8192 2>/dev/null | file -)

vDSO가 가속하는 함수들:

작동 원리: 커널은 시간 데이터를 담고 있는 vvar 페이지(읽기 전용)를 사용자 공간에 매핑합니다. 타이머 인터럽트마다 커널이 이 페이지를 갱신하고, vDSO 함수는 이 데이터를 직접 읽어 반환합니다. 자세한 내용은 ktime / Clock 심화 페이지를 참조하세요.

구현

/* arch/x86/entry/vdso/vclock_gettime.c (간략화) */
notrace int __vdso_clock_gettime(clockid_t clock,
                                  struct timespec *ts)
{
    /* 커널 진입 없이 vvar 페이지에서 시간 데이터 읽기 */
    const struct vdso_data *vd = __arch_get_vdso_data();

    if (vd->clock_mode != VDSO_CLOCKMODE_NONE) {
        /* TSC 기반 고속 경로 */
        u64 cycles = __arch_get_hw_counter(vd->clock_mode, vd);
        ns = (cycles - vd->cycle_last) * vd->mult;
        ns >>= vd->shift;
        ts->tv_sec  = vd->basetime[clock].sec + ns / NSEC_PER_SEC;
        ts->tv_nsec = ns % NSEC_PER_SEC;
        return 0;
    }

    /* TSC 불안정 시 실제 시스템 콜로 폴백 */
    return clock_gettime_fallback(clock, ts);
}

vDSO vs vsyscall

항목 vsyscall (레거시) vDSO (현재)
주소 고정 (0xFFFFFFFFFF600000) ASLR 적용 (매 실행마다 변경)
크기 1 페이지 2~4 페이지
보안 ROP gadget 표적 (고정 주소) ASLR로 보호
에뮬레이션 모드 vsyscall=emulate (기본) 해당 없음
함수 수 3개 (time, gettimeofday, getcpu) 5개+
상태 레거시 (호환용만 유지) 표준
vsyscall=emulate: 현재 커널은 기본적으로 vsyscall 페이지를 에뮬레이트 모드로 설정합니다. 접근 시 실제로는 page fault를 발생시킨 후 커널이 결과를 반환합니다. 이는 레거시 바이너리 호환성은 유지하면서 고정 주소 ROP 공격을 방지합니다. 부트 파라미터 vsyscall=none으로 완전히 비활성화할 수도 있습니다.

Seccomp (Secure Computing Mode)

seccomp-bpf 필터링

Seccomp은 프로세스가 호출할 수 있는 시스템 콜을 제한하는 리눅스 커널 보안 기능입니다. 원래 모드(strict)는 read, write, exit, sigreturn 4개만 허용했으나, seccomp-bpf(filter 모드)에서는 BPF 프로그램으로 세밀한 필터링이 가능합니다.

/* seccomp-bpf 필터 예제: write만 허용하는 필터 */
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <sys/prctl.h>

struct sock_filter filter[] = {
    /* 아키텍처 검증 */
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            offsetof(struct seccomp_data, arch)),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64,
            1, 0),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),

    /* 시스템 콜 번호 로드 */
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS,
            offsetof(struct seccomp_data, nr)),

    /* write(1) 허용 */
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

    /* exit_group(231) 허용 */
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),

    /* 나머지 모두 거부 */
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS),
};

struct sock_fprog prog = {
    .len    = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
    .filter = filter,
};

/* 필터 설치 */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

seccomp 필터의 반환값(action):

반환값 동작
SECCOMP_RET_ALLOW 시스템 콜 허용
SECCOMP_RET_KILL_PROCESS 프로세스 종료 (SIGSYS)
SECCOMP_RET_KILL_THREAD 해당 스레드만 종료
SECCOMP_RET_TRAP SIGSYS 시그널 전송 (핸들링 가능)
SECCOMP_RET_ERRNO 지정한 errno 반환
SECCOMP_RET_TRACE ptrace tracer에 통지
SECCOMP_RET_LOG 허용하되 로그 기록
SECCOMP_RET_USER_NOTIF 사용자 공간 알림 fd로 전달

컨테이너 보안과 seccomp

컨테이너 런타임(Docker, containerd 등)은 seccomp 프로파일을 사용하여 컨테이너 내부에서 위험한 시스템 콜을 차단합니다. Docker의 기본 프로파일은 약 44개의 시스템 콜을 차단합니다. 자세한 컨테이너 보안 설정은 Linux Containers 심화 페이지를 참조하세요.

# Docker 기본 seccomp 프로파일에서 차단되는 주요 시스템 콜
# (보안 위험이 높은 시스템 콜)
$ docker run --rm alpine cat /proc/1/status | grep Seccomp
Seccomp:	2      # 2 = SECCOMP_MODE_FILTER

# 차단되는 예시:
# - mount, umount2     : 파일시스템 조작
# - reboot             : 호스트 재부팅
# - kexec_load         : 커널 교체
# - init_module        : 커널 모듈 로드
# - bpf                : BPF 프로그램 로드
# - userfaultfd        : 사용자 페이지 폴트 핸들링 (exploit primitive)

# 커스텀 seccomp 프로파일로 컨테이너 실행
$ docker run --security-opt seccomp=my-profile.json alpine sh
seccomp 필터 체이닝: seccomp 필터는 누적됩니다. fork()/clone()으로 생성된 자식 프로세스는 부모의 필터를 상속하며, 추가 필터를 설치하면 기존 필터에 AND 조건으로 결합됩니다. 따라서 필터는 점점 더 제한적으로만 변경 가능합니다.

시스템 콜 추적과 디버깅

strace

strace는 프로세스의 시스템 콜 호출을 추적하는 사용자 공간 도구입니다. 내부적으로 ptrace() 시스템 콜을 사용하여 대상 프로세스의 syscall 진입/반환을 가로챕니다.

# 기본 추적: 시스템 콜과 반환값 출력
$ strace ls /tmp
execve("/usr/bin/ls", ["ls", "/tmp"], ...) = 0
openat(AT_FDCWD, "/tmp", O_RDONLY|O_DIRECTORY) = 3
getdents64(3, ..., 32768) = 240
write(1, "file1.txt\nfile2.log\n", 20) = 20
close(3)                                = 0
exit_group(0)                           = ?

# 시간 측정: 각 시스템 콜의 소요 시간
$ strace -T -e trace=read,write cat /dev/null
read(3, "", 131072)                     = 0 <0.000008>

# 통계 요약: 시스템 콜별 호출 횟수와 시간
$ strace -c ls /tmp
% time     calls  syscall
------  --------  --------
 25.00        10  mmap
 18.75         5  openat
 12.50         5  close
  6.25         3  read
  6.25         1  write

# 특정 시스템 콜만 추적
$ strace -e trace=network nginx          # 네트워크 관련만
$ strace -e trace=%file ls               # 파일 관련만
$ strace -e trace=%process bash           # 프로세스 관련만

ftrace 시스템 콜 tracepoint

ftrace의 시스템 콜 tracepoint는 ptrace보다 훨씬 가벼운 커널 내부 추적 메커니즘입니다. SYSCALL_METADATA 매크로가 생성한 tracepoint를 활용합니다.

# 시스템 콜 tracepoint 활성화
$ cd /sys/kernel/tracing

# 사용 가능한 시스템 콜 이벤트 확인
$ ls events/syscalls/ | head
sys_enter_read
sys_exit_read
sys_enter_write
sys_exit_write

# read 시스템 콜 진입/반환 추적
$ echo 1 > events/syscalls/sys_enter_read/enable
$ echo 1 > events/syscalls/sys_exit_read/enable
$ cat trace
           <...>-1234  [002] .... 12345.678: sys_read(fd: 3, buf: 7ffd..., count: 4096)
           <...>-1234  [002] .... 12345.679: sys_read -> 0x100

# 필터 적용: 특정 PID의 시스템 콜만
$ echo 'common_pid == 1234' > events/syscalls/sys_enter_write/filter

audit 서브시스템

Linux audit 프레임워크는 보안 감사를 위해 시스템 콜을 기록합니다. 시스템 콜 진입 경로의 syscall_enter_from_user_mode()에서 audit 검사가 수행됩니다.

# 파일 삭제 관련 시스템 콜 감사 규칙 추가
$ auditctl -a always,exit -F arch=b64 -S unlink,unlinkat,rename,renameat \
    -F auid>=1000 -k file_delete

# 감사 로그 검색
$ ausearch -k file_delete -ts recent
type=SYSCALL msg=audit(1707000000.123:456): arch=c000003e syscall=263 \
  success=yes exit=0 a0=ffffff9c a1=7ffd... auid=1000 uid=1000 \
  comm="rm" exe="/usr/bin/rm"

# 실행 파일 실행 감사
$ auditctl -a always,exit -F arch=b64 -S execve -k exec_log

주요 시스템 콜 분류

Linux 시스템 콜은 기능에 따라 다음과 같이 분류됩니다. 관련 문서 페이지로의 교차 참조를 포함합니다.

카테고리 대표 시스템 콜 관련 페이지
프로세스 fork, clone, clone3, execve, exit, wait4, getpid 프로세스 관리
파일 I/O open, read, write, close, lseek, pread64, pwrite64 VFS, inode
메모리 mmap, munmap, mprotect, brk, madvise, mlock 메모리 관리
네트워크 socket, bind, listen, accept, connect, sendto, recvfrom 네트워크 스택
IPC pipe, shmget, semget, msgget, eventfd, signalfd
시그널 kill, rt_sigaction, rt_sigprocmask, sigaltstack 프로세스 관리
시간 clock_gettime, nanosleep, timer_create, timerfd_create ktime / Clock, 타이머
네임스페이스 unshare, setns, clone (CLONE_NEW*) 네임스페이스
보안 prctl, seccomp, setuid, setgid, capset Linux Containers
비동기 I/O io_uring_setup, io_uring_enter, epoll_create1, select, poll 성능 최적화
모듈/드라이버 init_module, finit_module, delete_module, ioctl 커널 모듈, 디바이스 드라이버
BPF bpf, perf_event_open BPF/XDP
시스템 콜 전체 목록: man 2 syscalls 명령어로 현재 시스템에서 지원하는 전체 시스템 콜 목록을 확인할 수 있습니다. 커널 소스의 include/linux/syscalls.h에 모든 시스템 콜의 프로토타입이 선언되어 있습니다.