본문으로 건너뛰기

안동민 개발노트

본문 시작

보안 위협과 방어

버퍼 오버플로와 악성 코드의 공격 경로를 살펴보고 메모리 보호·SELinux·AppArmor·방화벽의 계층적 방어를 구성합니다.

보호 메커니즘이 완벽하더라도 공격자는 소프트웨어의 취약점을 파고듭니다.

OS 수준의 보안 위협과 방어 기법을 이해하면, 안전한 코드를 작성하고 시스템을 견고하게 구성하는 능력이 생깁니다.

보안은 단일 기술이 아니라 계층적 방어(Defense in Depth)입니다.


버퍼 오버플로우

버퍼 오버플로우(Buffer Overflow)는 오래되었지만 여전히 대표적인 취약점 중 하나입니다.

1988년 모리스 웜부터 현재까지 계속 발견됩니다.

배열의 범위를 넘어서 데이터를 쓰면, 인접한 메모리 영역(리턴 주소, 함수 포인터 등)을 덮어쓸 수 있습니다.

vulnerable_code.c
#include <string.h>
#include <stdio.h>

/* 취약한 코드 — 절대 이렇게 작성하지 마세요 */
void vulnerable(char *input) {
    char buffer[64];
    strcpy(buffer, input);  /* 입력이 64바이트를 초과하면 오버플로우! */
    printf("Buffer: %s\n", buffer);
}

/* 경계 검사를 추가한 코드 */
void safe(const char *input) {
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%s", input);  /* 길이를 넘으면 잘라서 널 종단 */
    printf("Buffer: %s\n", buffer);
}

스택 스매싱 공격

경계 밖 쓰기를 막지 못하면 각 방어층은 우회 비용만 높인다

스택 버퍼를 넘은 바이트는 인접 데이터를 훼손하고, 제어 데이터까지 닿으면 흐름 탈취로 이어질 수 있습니다.

  1. 1
    개념적 프레임

    낮은 주소 → 높은 주소 buffer[64] 외부 입력의 목적지 canary 함수 종료 전 변조 검사 saved state 프레임·레지스터 저장 값 return target 반환 제어 지점의 한 예 입력 길이 > 목적지 용량 → 인접 영역으로 손상 확산

  2. 2
    결함에서 악용까지

    각 단계에서 차단 1 범위 검사 누락 고정 버퍼에 제한 없는 쓰기 2 메모리 손상 카나리·포인터·제어 데이터 변조 3 제어 전환 시도 코드 주입 또는 ROP/JOP 재사용

  3. 3
    1

    범위 검사 누락 고정 버퍼에 제한 없는 쓰기

  4. 4
    2

    메모리 손상 카나리·포인터·제어 데이터 변조

  5. 5
    3

    제어 전환 시도 코드 주입 또는 ROP/JOP 재사용

  6. 6
    경계 인지 설계

    · PREVENT 경계 인지 설계 메모리 안전 언어, 길이 검증, 크기 제한 API

  7. 7
    빌드·테스트 탐지

    · FIND 빌드·테스트 탐지 경고·SAST·퍼징·AddressSanitizer

  8. 8
    런타임 변조 감지

    · DETECT 런타임 변조 감지 FORTIFY_SOURCE · stack-protector-strong

  9. 9
    악용 표면 축소

    · CONTAIN 악용 표면 축소 NX · ASLR+PIE · RELRO · CFI/CET

공격자가 리턴 주소를 조작하면 함수 반환 시 제어 흐름이 공격자가 의도한 위치로 이동할 수 있습니다.

현대 시스템에서는 직접 shellcode 실행뿐 아니라 ROP 같은 코드 재사용 공격도 함께 고려해야 합니다.

다계층 방어

defense_demo.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    /* 1. 안전한 함수 사용 */
    char buf[32];
    /* strcpy → strncpy, sprintf → snprintf, gets → fgets */
    if (fgets(buf, sizeof(buf), stdin) == NULL) return 1;

    /* 2. 컴파일러 방어 (-fstack-protector) */
    /* 스택 카나리: 리턴 주소 앞에 랜덤 값 삽입 */
    /* 함수 반환 시 카나리가 변조되었으면 __stack_chk_fail() 호출 */

    /* 3. OS 방어 (ASLR) */
    printf("스택 변수 주소: %p\n", (void *)buf);
    printf("힙 주소:       %p\n", (void *)malloc(1));
    /* 매 실행마다 주소가 달라짐 → 공격자의 주소 예측을 어렵게 함 */

    return 0;
}
security_compile.sh
# 주요 보안 옵션을 켜고 컴파일
gcc -O2 \
    -fstack-protector-all \
    -D_FORTIFY_SOURCE=2 \
    -Wformat -Wformat-security \
    -pie -fPIE \
    -Wl,-z,relro,-z,now \
    -Wl,-z,noexecstack \
    -o program program.c

# -fstack-protector-all:  모든 함수에 스택 카나리
# -D_FORTIFY_SOURCE=2:    최적화 옵션과 함께 버퍼 크기 검사 강화
# -pie -fPIE:             위치 독립 실행 파일 (ASLR 효과 극대화)
# -Wl,-z,relro,-z,now:    GOT 보호 (Full RELRO)
# -Wl,-z,noexecstack:     스택 실행 금지 표시
방어 기법대상동작
스택 카나리스택 오버플로우리턴 주소 변조 탐지
ASLR주소 예측메모리 레이아웃 무작위화
DEP/NX코드 주입데이터 영역 실행 금지
PIE코드 영역 예측코드 섹션도 무작위 배치
RELROGOT 변조GOT 영역 읽기 전용화
CFI간접 호출 변조제어 흐름 무결성 검증
버퍼 오버플로우 방어

취약한 함수 하나를 고치는 것만으로는 부족합니다. 스택 카나리, ASLR, NX, PIE, RELRO, CFI가 각기 다른 공격 단계를 막습니다.

  1. 1
    경계 검사

    Safe API 경계 검사 strcpy 대신 snprintf , fgets 를 사용합니다.

  2. 2
    리턴 주소 변조 탐지

    Canary 리턴 주소 변조 탐지 반환 전 랜덤 값이 바뀌었는지 확인합니다.

  3. 3
    주소 예측 방해

    ASLR 주소 예측 방해 스택, 힙, 라이브러리 위치를 실행마다 바꿉니다.

  4. 4
    데이터 실행 차단

    NX 데이터 실행 차단 스택과 힙에 넣은 코드를 바로 실행하지 못하게 합니다.

  5. 5
    GOT 보호

    RELRO GOT 보호 동적 링크 테이블 변조를 어렵게 만듭니다.

  6. 6
    간접 호출 검증

    CFI 간접 호출 검증 허용된 제어 흐름 밖으로 점프하지 못하게 합니다.


악성 코드 분류

유형전파 방식특징예시
바이러스숙주 프로그램에 삽입실행 시 활성화, 자기 복제CIH, Melissa
네트워크 자동 전파독립 실행, 취약점 이용Morris, WannaCry
트로이 목마유용한 프로그램 위장백도어 설치Zeus, Emotet
랜섬웨어다양 (이메일, 웹)파일 암호화 후 몸값 요구WannaCry, Ryuk
루트킷기존 침입 후 설치커널 수준 은닉Sony Rootkit
키로거트로이 목마/물리적키 입력 기록 전송-
악성 코드 관찰 축

바이러스와 웜은 퍼지는 방식이 다르고, 루트킷과 랜섬웨어는 시스템에 남기는 흔적과 우선 조치가 다르다.

  1. 침투와 전파

    웜 네트워크 취약점을 이용해 독립적으로 이동하므로 패치와 방화벽 범위가 중요하다. 트로이 목마 정상 도구처럼 보이는 실행 흐름을 만들기 때문에 출처와 서명 검증이 필요하다.

  2. 은닉과 피해

    루트킷 프로세스와 파일 표시를 숨기므로 해시 비교와 오프라인 검사가 효과적이다. 랜섬웨어 파일을 빠르게 암호화하므로 쓰기 권한 최소화와 복구 가능한 백업이 핵심이다.

실무 방어

integrity_check.py
import hashlib
import os

def file_hash(path):
    """파일의 SHA-256 해시 계산"""
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(8192), b""):
            h.update(chunk)
    return h.hexdigest()

# 중요 시스템 파일의 해시를 저장하고 주기적으로 비교
critical_files = ["/usr/bin/sudo", "/usr/bin/ssh", "/usr/sbin/sshd"]
for f in critical_files:
    if os.path.exists(f):
        print(f"{f}: {file_hash(f)}")

# 해시가 변경되었으면 → 파일이 변조된 것 (루트킷 의심)
# 실무에서는 AIDE, Tripwire, OSSEC 같은 무결성 검사 도구 사용

강제 접근 제어 (MAC)

일반적인 Unix 권한(DAC, Discretionary Access Control)은 파일 소유자가 권한을 마음대로 설정할 수 있습니다.

root가 탈취되면 모든 보호가 무력화됩니다.

MAC(Mandatory Access Control)은 시스템 관리자가 정의한 정책에 따라 접근을 제어합니다.

정책이 enforcing 상태라면, root 권한 프로세스도 허용되지 않은 접근은 제한됩니다.

SELinux

NSA가 개발한 MAC 구현입니다.

프로세스마다 보안 컨텍스트(타입)를 부여하고, 정책에 명시적으로 허용된 접근만 가능합니다.

selinux_demo.sh
# SELinux 상태 확인
getenforce          # Enforcing, Permissive, Disabled
sestatus            # 상세 상태

# 파일의 보안 컨텍스트 확인
ls -Z /var/www/html/index.html
# -rw-r--r--. root root system_u:object_r:httpd_sys_content_t:s0

# httpd 프로세스는 httpd_sys_content_t 타입의 파일만 읽을 수 있음
# /home/user/ 디렉토리는 user_home_t → httpd가 접근 불가

# 컨텍스트 복원
restorecon -Rv /var/www/html/

웹 서버 프로세스가 침해되더라도 confined 도메인에 갇혀 있다면, SELinux 정책에 의해 /etc/shadow 읽기나 임의 포트 변경 같은 행동이 차단될 수 있습니다.

SELinux 타입 정책

DAC 권한이 허용하더라도 enforcing 상태의 MAC 정책이 막으면 접근은 실패한다. root 프로세스도 confined 도메인에서는 정책 안에 갇힌다.

  1. 주체

    httpd 프로세스 httpd_t

  2. 객체

    /var/www/html/index.html httpd_sys_content_t

  3. 연산

    파일 read, 디렉토리 search, 소켓 bind 같은 권한 단위

  4. 정책

    allow httpd_t httpd_sys_content_t:file read

  5. AVC denial 또는 접근 허용

    허용 규칙이 있으면 접근, 없으면 AVC denial 기록

AppArmor

프로그램별로 접근 가능한 파일, 네트워크, 커패빌리티를 프로파일로 정의합니다.

SELinux보다 설정이 간단합니다.

apparmor_profile.sh
# /etc/apparmor.d/usr.sbin.nginx 프로파일 예시
# /usr/sbin/nginx {
#   /var/www/** r,          # 웹 콘텐츠 읽기만
#   /var/log/nginx/** w,    # 로그 쓰기만
#   /run/nginx.pid rw,      # PID 파일
#   network inet tcp,       # TCP 네트워크 허용
#   deny /etc/shadow r,     # shadow 파일 읽기 명시적 거부
# }

# AppArmor 상태 확인
aa-status

# 프로파일을 enforce 모드로 설정
aa-enforce /etc/apparmor.d/usr.sbin.nginx

네트워크 보안 기초

방화벽 (iptables/nftables)

firewall_basics.sh
# iptables 기본 규칙: 필요한 것만 허용, 나머지 차단
iptables -P INPUT DROP           # 기본 정책: 모든 입력 차단
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPT        # 나가는 트래픽은 허용

# 기존 연결 유지
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# SSH (포트 22) 허용
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# HTTP/HTTPS 허용
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT

# 로컬 루프백 허용
iptables -A INPUT -i lo -j ACCEPT

보안 모니터링

security_monitoring.sh
# 실패한 로그인 시도 확인
grep "Failed password" /var/log/auth.log | tail -20

# 현재 열린 포트 확인
ss -tlnp

# 최근 sudo 사용 기록
grep sudo /var/log/auth.log | tail -10

# 비정상 프로세스 확인
ps aux --sort=-pcpu | head -20

보안 계층 정리

계층방어 수단
네트워크방화벽, IDS/IPS, VPN
운영체제MAC(SELinux/AppArmor), 패치 관리, 로깅
애플리케이션입력 검증, 안전한 함수, 코드 분석
데이터암호화, 해싱, 백업
물리잠금장치, BIOS 패스워드, TPM
취약점 하나가 곧 침해가 되지 않도록 여러 계층에서 막는다

버퍼 오버플로우, 악성 코드, 권한 탈취는 서로 다른 경로로 들어오지만 입력 검증, 실행 방지, 강제 접근 제어, 감시가 함께 작동해야 한다.

  1. 경계 검사
    메모리 공격 방어

    경계 검사 문자열 복사는 버퍼 크기를 기준으로 제한하고, 널 종료 여부까지 확인한다. 스택 카나리 반환 주소 앞의 임의 값을 확인해 스택 변조를 함수 반환 전에 탐지한다. 주소 무작위화 스택, 힙, 코드 영역 위치를 예측하기 어렵게 만들어 코드 재사용 공격 비용을 높인다.

  2. 강제 접근
    침해 이후 확산 제한

    강제 접근 보안 컨텍스트와 프로파일이 루트 권한 프로세스의 허용되지 않은 파일 접근도 차단한다. 방화벽 기본 입력 차단 뒤 기존 연결, 관리 포트, 서비스 포트만 명시적으로 허용한다. 무결성 검사 중요 실행 파일의 해시 변화를 감시해 루트킷이나 파일 변조 징후를 찾는다.

보안은 OS의 한 기능이 아니라, 물리적 접근부터 애플리케이션 코드까지 전 계층에 걸친 노력입니다.

다음 장에서는 현대 인프라의 핵심인 가상화와 컨테이너를 다루겠습니다.

방어는 공격 이름이 아니라 권한 전이 지점에 건다

침해는 진입 한 번으로 끝나지 않는다. 실행 주체의 자격 증명과 허용 범위가 바뀌고, 그 권한으로 지속성·유출·자원 고갈이 이어질 때 비로소 피해가 커진다.

  1. 1
    실행 경로 확보

    ENTRY 실행 경로 확보 노출 서비스, 악성 파일, 탈취 계정으로 user process가 시작된다. auth·service·exec 기록

  2. 2
    권한 전이 시도

    BOUNDARY 권한 전이 시도 setuid, file capability, sudo 정책 또는 커널 결함을 노린다. UID·capability 변화

  3. 3
    고권한 동작

    CONTROL 고권한 동작 보호 파일·프로세스·네트워크 설정에 접근 범위를 넓힌다. syscall·LSM 거부/허용

  4. 4
    지속·유출·고갈

    IMPACT 지속·유출·고갈 unit·cron 변경, secret 읽기, CPU·메모리 독점으로 피해를 유지한다. 파일 변경·자원 압력