본문 바로가기

보안 위협과 방어

버퍼 오버플로우가 복귀 주소를 덮어써 제어 흐름을 빼앗는 과정을 스택 프레임 모형으로 따라가고, 스택 카나리·ASLR·NX·RELRO·CFI 같은 완화 기법이 각각 무엇을 막는지 비교합니다. 악성 코드의 종류와 무결성 검사, SELinux·AppArmor의 강제 접근 제어, 방화벽까지 여러 겹의 방어를 정리합니다.

앞의 두 글에서 본 보호 장치가 아무리 잘 짜여 있어도, 공격자는 소프트웨어의 결함을 파고들어 그 장치를 우회합니다. 권한이 높은 프로그램에 결함이 있으면 공격자는 그 프로그램의 권한으로 무엇이든 할 수 있게 됩니다. 이 글에서는 대표적인 결함인 버퍼 오버플로우와 그에 맞서는 방어 기법, 악성 코드의 종류, 그리고 운영체제와 네트워크 수준의 방어를 살펴봅니다.

보안은 기술 하나로 이루어지지 않습니다. 한 겹의 방어가 뚫려도 다음 겹이 막도록 여러 겹을 쌓는 것을 심층 방어(defense in depth)라고 하며, 이 글에서 보는 기법들도 각자 다른 지점을 막는 겹들입니다.

버퍼 오버플로우는 어떻게 제어 흐름을 빼앗나

버퍼 오버플로우(buffer overflow)는 배열의 크기를 넘어서 데이터를 쓰는 결함입니다. 1988년 인터넷을 마비시킨 모리스 웜이 fingerd의 버퍼 오버플로우를 이용한 이래, 지금까지도 끊임없이 발견되는 취약점입니다. C와 C++은 배열에 쓸 때 크기를 확인하지 않으므로, 프로그래머가 확인하지 않으면 배열 뒤에 있는 메모리가 그대로 덮어써집니다.

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

void vulnerable(const char *input)
{
    char buffer[64];
    strcpy(buffer, input);          /* 입력이 63바이트보다 길면 buffer 뒤를 덮어쓴다. */
    printf("%s\n", buffer);
}

int safer(const char *input)
{
    char buffer[64];
    int n = snprintf(buffer, sizeof buffer, "%s", input);
    if (n < 0 || (size_t) n >= sizeof buffer)
        return -1;                  /* 잘렸거나 실패했다면 거부한다. */
    printf("%s\n", buffer);
    return 0;
}

buffer[64]에 담을 수 있는 문자열은 끝의 널 문자까지 64바이트, 곧 내용 63바이트까지입니다. strcpy()는 입력의 길이를 확인하지 않으므로 그보다 긴 입력이 들어오면 buffer 뒤를 덮어쓰며, 이는 C 표준에서 정의되지 않은 동작입니다. snprintf()는 버퍼 크기를 넘지 않게 자르고, 원래 필요했던 길이를 돌려주므로 잘렸는지 알 수 있습니다. 잘린 값을 그대로 써도 되는지는 프로그램이 정해야 하므로, 위 코드는 잘리면 거부하도록 했습니다.

스택 프레임에서 일어나는 일

보안 · 경계와 완화책

경계 검사와 완화책은 서로 다른 지점을 보호합니다

buffer[64]에는 문자열의 널 종료 문자까지 들어가야 합니다. 범위를 벗어난 쓰기는 C의 정의되지 않은 동작이며, 아래는 가능한 인접 영역 훼손의 개념 경로입니다. 카나리·CFI·ASLR·NX가 모든 범위 밖 쓰기를 막는다는 뜻은 아닙니다.

가능한 인접 훼손 경로와 서로 다른 완화 지점널 종료 문자까지 포함하는 buffer64 영역에서 범위 밖 쓰기가 인접 저장 영역이나 제어 값을 훼손할 수 있는 개념 경로다. 화살표는 가능한 훼손이며 필연적 실행 경로가 아니다. 경계 검사는 쓰기 전에 차단하고 카나리는 존재와 검사의 조건 아래 일부 손상을 탐지한다. CFI는 제어 전환 범위를 제한하고 ASLR·PIE는 주소 예측, NX는 금지 영역 실행을 다룬다. 실제 배치는 ABI와 컴파일러에 따라 달라지며 완화책이 모든 쓰기나 악용을 막는다는 뜻은 아니다.buffer[64]널 문자 포함경계 검사는쓰기 전 차단인접 저장 영역카나리가 있을 수 있음검사는 일부손상을 탐지제어에 쓰이는 값복귀 주소 등CFI는 허용된제어 전환을 제한가능한 훼손가능한 훼손배치와 별개의 완화 역할ASLR·PIE주소 예측을 어렵게NX금지 영역 실행 차단개념 배치 · 악용 성공이나항상 차단을 보장하지 않음
배열 밖 쓰기와 보호 지점제한 없는 쓰기는 버퍼를 넘어 인접 저장 영역과 제어 데이터를 훼손할 수 있습니다. 길이 검사, 카나리, CFI, ASLR과 NX는 서로 다른 지점을 다룹니다.buffer[64]널 문자까지 용량에 포함인접 저장 영역카나리가 있을 수 있음제어에 쓰이는 값복귀 주소 등경계 검사쓰기 전 차단카나리 검사일부 변조 탐지CFI 등제어 전환 제한ASLR·PIE: 주소 예측을 어렵게NX: 실행 금지 영역의 실행 차단

스택 프레임의 개념 배치이며 실제 순서·간격·카나리 유무는 ABI·컴파일러·옵션에 따라 다릅니다. 길이 검사는 범위 밖 쓰기를 막고, 완화책은 일부 손상을 탐지하거나 악용을 어렵게 합니다.

지역 배열은 스택 프레임에 놓이고, 그 근처에는 함수가 끝나면 돌아갈 복귀 주소(return address)가 있습니다. 배열을 넘친 데이터가 복귀 주소를 덮어쓰면, 함수가 돌아갈 때 공격자가 적어 넣은 주소로 실행이 넘어갑니다. 이것이 스택 스매싱(stack smashing)입니다. 컴파일러는 이를 막으려고 배열과 복귀 주소 사이에 스택 카나리(stack canary)라는 무작위 값을 두고, 함수가 돌아가기 직전에 그 값이 그대로인지 확인합니다. 카나리가 바뀌었다면 복귀 주소도 망가졌을 가능성이 크므로 프로그램을 바로 중단시킵니다.

다음 코드는 버퍼, 카나리, 복귀 주소가 차례로 놓인 프레임을 바이트 배열로 흉내 냅니다. 실제 배치는 ABI와 컴파일러에 따라 다르며, 이것은 개념을 보이는 모형입니다.

import struct

BUF = 8                                         # 버퍼 크기
CANARY = 0x5EED1234                             # 프로그램이 시작할 때 정한 무작위 값이라고 하자

def make_frame():
    # [버퍼 8바이트][카나리 4바이트][복귀 주소 4바이트] 순서로 놓인 스택 프레임 모형
    return bytearray(BUF) + struct.pack("<II", CANARY, 0x401000)

def unchecked_copy(frame, data):                # strcpy처럼 길이를 확인하지 않는다
    frame[0:len(data)] = data

def checked_copy(frame, data):                  # 버퍼 크기를 넘는 부분은 버린다
    frame[0:BUF] = data[:BUF].ljust(BUF, b"\0")

evil = struct.pack("<I", 0xDEADBEEF)            # 공격자가 원하는 복귀 주소
cases = [
    ("검사 없는 복사", unchecked_copy, b"A" * 8 + b"XXXX" + evil),
    ("카나리를 알아낸 공격", unchecked_copy, b"A" * 8 + struct.pack("<I", CANARY) + evil),
    ("크기를 지킨 복사", checked_copy, b"A" * 8 + b"XXXX" + evil),
]
for name, copy, data in cases:
    frame = make_frame()
    copy(frame, data)
    canary, ret = struct.unpack("<II", frame[BUF:])
    verdict = "카나리 그대로" if canary == CANARY else "카나리 변조 -> 돌아가기 전에 중단"
    print(f"{name}: 복귀 주소 {ret:#x}, {verdict}")
검사 없는 복사: 복귀 주소 0xdeadbeef, 카나리 변조 -> 돌아가기 전에 중단
카나리를 알아낸 공격: 복귀 주소 0xdeadbeef, 카나리 그대로
크기를 지킨 복사: 복귀 주소 0x401000, 카나리 그대로

검사 없는 복사는 복귀 주소를 공격자의 값으로 바꾸었지만, 카나리도 함께 망가뜨려 함수가 돌아가기 전에 들킵니다. 그러나 공격자가 다른 결함으로 카나리 값을 먼저 알아냈다면, 그 자리에 같은 값을 써 넣어 검사를 통과합니다. 카나리는 공격을 어렵게 할 뿐 막아 주지는 못하며, 결함을 없애는 것은 크기를 지킨 복사뿐입니다.

여러 겹의 완화 기법

카나리를 통과했더라도 공격자가 원하는 일을 하려면 몇 가지 장벽을 더 넘어야 합니다. 운영체제와 컴파일러는 다음과 같은 완화 기법(mitigation)을 둡니다.

기법막으려는 것동작
스택 카나리복귀 주소를 덮어쓰는 스택 오버플로우함수가 돌아가기 전에 카나리 값이 바뀌었는지 확인한다
NX(DEP)스택이나 힙에 넣은 코드의 실행데이터 영역의 페이지를 실행할 수 없게 표시한다
ASLR공격에 쓸 주소의 예측스택, 힙, 라이브러리의 위치를 실행할 때마다 무작위로 바꾼다
PIE실행 파일 코드의 주소 예측실행 파일도 아무 주소에나 올릴 수 있게 만들어 ASLR이 옮길 수 있게 한다
RELRO라이브러리 함수 주소표(GOT)의 변조로딩이 끝난 뒤 주소표를 읽기 전용으로 바꾼다
CFI간접 호출과 복귀의 변조허락된 곳으로만 실행이 넘어가는지 확인한다

NX가 스택에 넣은 코드를 실행하지 못하게 하자, 공격자들은 이미 프로그램 안에 있는 코드 조각들의 주소를 이어 붙여 원하는 일을 하는 ROP(return-oriented programming)를 쓰기 시작했습니다. ASLR은 그 코드 조각들의 주소를 숨기고, CFI는 실행이 엉뚱한 곳으로 넘어가는 것 자체를 막습니다. 최근의 Intel과 AMD CPU는 복귀 주소의 사본을 따로 보관해 대조하는 섀도 스택(shadow stack)을 하드웨어로 지원합니다.

Linux에서 ASLR의 설정은 /proc/sys/kernel/randomize_va_space로 확인합니다. 커널 문서에 따르면 0은 끈 상태, 1은 스택과 mmap 영역, 공유 라이브러리를 무작위로 배치하는 상태, 2는 힙까지 무작위로 배치하는 상태입니다. 컴파일할 때는 다음과 같은 옵션으로 완화 기법을 켭니다.

cat /proc/sys/kernel/randomize_va_space       # 보통 2

gcc -O2 \
    -D_FORTIFY_SOURCE=3 \
    -fstack-protector-strong \
    -fstack-clash-protection \
    -fcf-protection=full \
    -fPIE -pie \
    -Wl,-z,relro,-z,now \
    -Wl,-z,noexecstack \
    -o program program.c

-D_FORTIFY_SOURCE는 컴파일러가 크기를 아는 버퍼에 대해 memcpy, strcpy 같은 함수를 크기를 확인하는 버전으로 바꾸며, 최적화를 켜야 동작합니다. -fstack-protector-strong은 배열이나 지역 변수의 주소를 쓰는 함수에 카나리를 넣고, -fcf-protection=full은 x86에서 CPU의 제어 흐름 보호 기능을 씁니다. GCC 문서에 따르면 GCC 14부터는 -fhardened 하나로 이런 권장 옵션 묶음을 켤 수 있습니다. 대부분의 배포판은 패키지를 이미 이런 옵션으로 빌드합니다.

이 모든 완화 기법은 결함을 악용하기 어렵게 만들 뿐, 결함 자체를 없애지는 못합니다. 마이크로소프트와 크롬 팀은 자기 제품의 심각한 보안 취약점 가운데 약 70%가 메모리 안전성 문제라고 밝힌 바 있으며, 이 때문에 Rust처럼 컴파일러가 메모리 안전성을 보장하는 언어로 새 코드를 쓰려는 움직임이 커지고 있습니다. Linux 커널도 일부 드라이버를 Rust로 쓸 수 있게 되었습니다.

더 깊이: Linux에서 사용자 프로그램의 섀도 스택 켜기

Linux의 섀도 스택 문서에 따르면 x86의 제어 흐름 보호 기술(control-flow enforcement technology, CET)에서 섀도 스택은 응용 프로그램이 직접 고칠 수 없는 두 번째 스택입니다. CALL은 복귀 주소를 보통의 스택과 섀도 스택에 함께 넣고, RET은 두 사본을 비교해 다르면 제어 보호 예외(control-protection fault)를 일으킵니다. 그래서 스택 오버플로우로 복귀 주소를 덮어써도 섀도 스택의 사본은 그대로 남아 있으므로, 덮어쓴 주소로 돌아가는 대신 예외가 납니다.

이 기능은 하드웨어와 커널, 프로그램이 모두 준비되어야 켜집니다. 커널은 X86_USER_SHADOW_STACK 설정으로 빌드해야 하고, 부팅할 때 nousershstk 매개변수로 끌 수 있습니다. 프로그램이 섀도 스택을 지원하는지는 ELF 노트에 표시되며 readelf -n의 출력에서 SHSTK를 찾아 확인합니다. 커널은 이 표시를 직접 읽지 않고, glibc 같은 로더가 읽어 arch_prctl()로 스레드마다 켭니다. 켜진 기능은 /proc/<PID>/status의 x86_Thread_features 줄에 나오며, 64비트 커널이 지원하는 것은 사용자 프로그램의 섀도 스택과 커널 자신의 간접 분기 추적(IBT)뿐입니다.

악성 코드

악성 코드(malware)는 퍼지는 방식과 하는 일에 따라 다음과 같이 나눕니다.

종류퍼지는 방식특징예
바이러스다른 프로그램이나 문서에 붙는다숙주가 실행될 때 활동하고 스스로를 복제한다CIH, Melissa
웜네트워크로 스스로 퍼진다독립된 프로그램으로, 취약점을 이용해 다른 컴퓨터로 옮겨 간다모리스 웜, WannaCry
트로이 목마쓸모 있는 프로그램으로 위장한다사용자가 직접 설치하게 만든 뒤 뒷문을 연다Zeus, Emotet
랜섬웨어이메일, 웹, 취약점 등 다양하다파일을 암호화하고 풀어 주는 대가를 요구한다WannaCry, Ryuk
루트킷침입한 뒤 설치된다커널이나 시스템 도구를 고쳐 자신과 다른 악성 코드를 숨긴다소니 BMG 루트킷

분류는 서로 겹칩니다. 2017년의 WannaCry는 Windows SMB의 취약점으로 웜처럼 퍼지면서 파일을 암호화한 랜섬웨어였습니다. 그래서 이름보다 어떤 경로로 들어오고 무엇을 바꾸는지를 보고 방어를 정해야 합니다.

파일이 바뀌었는지 확인하기

침입자는 흔적을 숨기려고 sshd나 ps 같은 시스템 파일을 고쳐 놓곤 합니다. 무결성 검사는 믿을 수 있을 때 중요한 파일의 해시를 기록해 두었다가, 나중에 다시 계산해 비교하는 방법입니다. 해시는 길이에 상관없이 내용을 정해진 길이의 값으로 요약한 지문이며, 암호에 쓰는 해시 함수의 성질은 네트워크 교재의 암호화의 기초에서 다룹니다. 다음 코드는 임시 디렉터리에서 이 과정을 흉내 냅니다.

import hashlib
import os
import tempfile

def sha256(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(65536), b""):
            h.update(chunk)
    return h.hexdigest()

with tempfile.TemporaryDirectory() as folder:
    paths = {name: os.path.join(folder, name) for name in ("sshd", "sudo")}
    for name, path in paths.items():
        with open(path, "wb") as f:
            f.write(f"{name} binary v1".encode())
    baseline = {name: sha256(path) for name, path in paths.items()}   # 믿을 수 있을 때 기록한다

    with open(paths["sshd"], "ab") as f:
        f.write(b" + backdoor")                                       # 누군가 몰래 고쳤다

    for name, path in paths.items():
        state = "그대로" if sha256(path) == baseline[name] else "바뀜"
        print(f"{name}: {state} (기준값 {baseline[name][:16]}...)")
sshd: 바뀜 (기준값 37017c7b82252081...)
sudo: 그대로 (기준값 05c4c3815493829d...)

내용이 한 바이트만 바뀌어도 SHA-256 값은 완전히 달라지므로 변조를 알아챌 수 있습니다. 다만 해시가 바뀌었다는 것은 패키지 업데이트일 수도 있으므로, 변경 기록과 맞춰 보아야 합니다. 더 중요한 것은 기준값과 검사 도구를 믿을 수 있어야 한다는 점입니다. 기준값이 공격자가 고칠 수 있는 곳에 있거나, 루트킷이 커널을 고쳐 파일을 읽는 결과 자체를 속인다면 검사는 의미가 없습니다. 그래서 기준값은 읽기 전용 매체나 다른 서버에 보관하고, 의심스러울 때는 믿을 수 있는 매체로 부팅해 검사합니다. 실무에서는 AIDE 같은 무결성 검사 도구를 쓰고, 패키지 관리자의 rpm -Va나 dpkg --verify로 설치된 파일을 패키지의 기록과 비교할 수도 있습니다.

강제 접근 제어

지금까지 본 Unix의 권한은 임의 접근 제어(discretionary access control, DAC)입니다. 파일의 소유자가 권한을 마음대로 정할 수 있고, root는 그 검사를 대부분 건너뜁니다. 그래서 root로 실행되는 웹 서버가 장악되면, DAC만으로는 공격자가 /etc/shadow를 읽는 것을 막을 수 없습니다.

강제 접근 제어(mandatory access control, MAC)는 관리자가 정한 시스템 전체의 정책에 따라 접근을 판정합니다. 파일의 소유자도 이 정책을 바꿀 수 없고, 정책이 강제 모드로 동작하는 동안에는 root인 프로세스도 정책이 허락하지 않은 접근은 거부됩니다. Linux에서는 SELinux와 AppArmor가 대표적인 구현입니다.

SELinux

SELinux는 미국 NSA가 처음 개발해 커널에 들어간 MAC 구현으로, Red Hat 계열 배포판이 기본으로 씁니다. 모든 프로세스와 파일에 보안 컨텍스트(security context)라는 꼬리표를 붙이고, 그 가운데 타입(type)끼리 어떤 접근이 허락되는지 정책에 적습니다. 예를 들어 웹 서버 프로세스는 httpd_t 타입으로 실행되고, 웹 문서는 httpd_sys_content_t 타입이 붙어 있으며, 정책은 httpd_t가 웹 문서는 읽되 /etc/shadow에 붙은 shadow_t는 읽지 못하게 합니다.

getenforce                       # Enforcing, Permissive, Disabled 가운데 하나
ls -Z /var/www/html/index.html   # 파일의 보안 컨텍스트
ps -eZ | grep httpd              # 프로세스의 보안 컨텍스트
restorecon -Rv /var/www/html     # 정책에 맞는 기본 컨텍스트로 되돌리기
ausearch -m avc -ts recent       # 최근에 SELinux가 거부한 기록

Enforcing은 정책을 강제하는 모드이고, Permissive는 거부할 접근을 기록만 하고 허락하는 모드입니다. 다른 디렉터리에서 파일을 옮겨 오면 원래의 컨텍스트가 따라와 웹 서버가 읽지 못하는 일이 흔한데, 이때 SELinux를 끄지 말고 restorecon으로 컨텍스트를 바로잡는 것이 올바른 해결입니다.

AppArmor

AppArmor는 Ubuntu와 Debian이 기본으로 쓰는 MAC 구현으로, 프로그램마다 접근할 수 있는 파일 경로, 네트워크, 커패빌리티를 프로파일(profile)로 적습니다. 꼬리표 대신 경로로 규칙을 적기 때문에 읽고 쓰기가 비교적 쉽습니다. 다음은 nginx 프로파일의 일부를 보인 것으로, 실제로 쓰려면 라이브러리와 설정 파일 같은 접근도 모두 허락해야 합니다.

/usr/sbin/nginx {
  /var/www/** r,              # 웹 문서는 읽기만
  /var/log/nginx/** w,        # 로그는 쓰기만
  network inet tcp,           # TCP 통신
  deny /etc/shadow r,         # 명시적으로 거부
}
sudo aa-status                                   # 적재된 프로파일과 모드
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx  # 기록만 하는 모드로
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx   # 강제 모드로

새 프로파일은 complain 모드에서 프로그램을 충분히 돌려 보며 기록을 보고 다듬은 뒤, enforce 모드로 바꾸는 것이 안전합니다.

네트워크에서 막기

방화벽

방화벽은 들어오고 나가는 패킷을 규칙에 따라 허락하거나 버립니다. 규칙을 적는 순서와 기본 차단(default deny) 같은 방화벽 설정의 원칙은 네트워크 교재의 네트워크 보안 위협과 방어에서 다룹니다. Linux에서는 커널의 netfilter가 패킷을 걸러 내고, 요즘은 그 규칙을 nftables로 적습니다. 다음은 서버에서 SSH와 웹 포트만 열어 두는 최소한의 규칙입니다.

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept       # 이미 맺어진 연결의 응답
    iif "lo" accept                           # 자기 자신과의 통신
    meta l4proto { icmp, ipv6-icmp } accept   # ping과 IPv6에 필요한 ICMP
    tcp dport { 22, 80, 443 } accept          # SSH, HTTP, HTTPS
  }
}

inet 테이블은 IPv4와 IPv6에 함께 적용되고, policy drop은 규칙에 걸리지 않은 패킷을 모두 버린다는 뜻입니다. 원격 서버에 이런 규칙을 적용할 때는 SSH 포트를 열어 두는 규칙이 맞는지 먼저 확인해야 합니다. 그렇지 않으면 규칙을 적용하는 순간 자신의 접속도 끊깁니다. 규칙 파일은 sudo nft -f로 적용하고 sudo nft list ruleset으로 확인합니다.

이상한 낌새 살피기

공격은 흔적을 남깁니다. 로그인 실패가 쏟아지거나, 모르는 포트가 열려 있거나, CPU를 많이 쓰는 낯선 프로세스가 있다면 살펴봐야 합니다.

journalctl -u ssh --since today | grep "Failed password"   # 오늘의 SSH 로그인 실패(서비스 이름은 배포판에 따라 sshd)
ss -tlnp                                                   # 연결을 기다리는 TCP 포트와 프로세스
last -n 20                                                 # 최근 로그인 기록
ps aux --sort=-pcpu | head                                 # CPU를 많이 쓰는 프로세스

정리

  • 버퍼 오버플로우는 배열의 크기를 넘어 쓰는 결함으로, 스택에서는 복귀 주소를 덮어써 실행 흐름을 공격자에게 넘길 수 있습니다. 근본적인 해결은 크기를 확인하는 것입니다.
  • 스택 카나리, NX, ASLR, PIE, RELRO, CFI는 각자 다른 단계를 막아 악용을 어렵게 하지만, 결함 자체를 없애지는 못합니다.
  • 악성 코드는 바이러스, 웜, 트로이 목마, 랜섬웨어, 루트킷 등으로 나뉘며, 무결성 검사는 믿을 수 있는 기준값과 도구가 있어야 의미가 있습니다.
  • DAC에서는 root가 대부분의 검사를 건너뛰지만, SELinux와 AppArmor 같은 MAC은 root인 프로세스도 정책으로 묶어 둡니다.
  • 방화벽, 로그 감시, 패치, 백업까지 여러 겹을 쌓아, 한 겹이 뚫려도 다음 겹이 막도록 하는 것이 심층 방어입니다.

연습 문제

1. 잘린 문자열

64바이트 버퍼에 snprintf(buf, sizeof buf, "%s", input)으로 80바이트짜리 ASCII 문자열을 넣었습니다. 반환값, 버퍼에 저장되는 내용의 길이, 전체를 담는 데 필요했던 크기를 구하세요. 이 값이 계정 이름이라면, 잘린 값을 그대로 써도 될까요?

해설 보기

snprintf()는 잘리기 전에 쓰려던 길이를 돌려주므로 반환값은 80입니다. 버퍼에는 내용 63바이트와 널 문자가 들어가고, 전체를 담으려면 널 문자까지 81바이트가 필요했습니다. 반환값이 버퍼 크기 64 이상이면 잘렸다는 뜻입니다. 메모리를 넘어서 쓰지는 않았지만, 잘린 계정 이름이 우연히 다른 사용자의 이름과 같아질 수 있으므로 그대로 쓰면 안 됩니다. 메모리를 안전하게 다룬 것과 값의 의미를 지킨 것은 별개이므로, 이런 값은 거부하고 다시 입력받아야 합니다.

2. 카나리가 뚫렸을 때

본문 모형의 두 번째 경우처럼, 공격자가 다른 결함으로 카나리 값을 알아내 복귀 주소만 바꾸는 데 성공했습니다. 이 공격이 실제로 원하는 코드를 실행하려면 어떤 장벽을 더 넘어야 하나요? 각 장벽을 넘는 대표적인 방법도 설명하세요.

해설 보기

첫째, 스택에 넣은 기계어로 돌아가게 하려 해도 NX 때문에 스택 페이지는 실행되지 않습니다. 공격자는 이미 있는 코드 조각을 이어 붙이는 ROP로 이를 우회합니다. 둘째, ROP에 쓸 코드 조각의 주소를 알아야 하는데 ASLR과 PIE가 실행할 때마다 주소를 바꿉니다. 공격자는 주소를 흘리는 또 다른 결함을 찾아 이를 우회합니다. 셋째, 섀도 스택이나 CFI가 있다면 바뀐 복귀 주소가 저장된 사본과 다르거나 허락되지 않은 곳이어서 실행이 멈춥니다. 공격이 성공하려면 여러 결함이 함께 필요해지므로 비용이 크게 늘지만, 처음의 오버플로우를 고치지 않는 한 위험은 남아 있습니다.

3. root로 장악된 웹 서버

root 권한으로 실행되던 웹 서버가 장악되었습니다. SELinux가 꺼져 있을 때와, Enforcing 모드에서 웹 서버가 httpd_t 타입으로 실행되고 있을 때 공격자가 /etc/shadow를 읽을 수 있는지 비교하세요. Permissive 모드라면 어떻게 되나요?

해설 보기

SELinux가 꺼져 있으면 DAC만 남는데, root는 파일 권한 검사를 건너뛰므로 공격자는 /etc/shadow를 읽을 수 있습니다. Enforcing 모드에서는 DAC 검사를 통과하더라도 SELinux 정책을 한 번 더 확인하며, 정책이 httpd_t에게 shadow_t 타입의 파일을 읽는 것을 허락하지 않으므로 거부됩니다. 이 거부는 감사 로그에 남아 침입을 알아채는 단서도 됩니다. Permissive 모드에서는 같은 접근을 거부해야 했다는 기록만 남기고 실제로는 허락하므로, 공격자는 파일을 읽을 수 있습니다. MAC은 root가 장악되었을 때도 피해를 정책의 범위로 묶어 두지만, 정책이 강제되고 있어야 그렇습니다.

다음 글 Linux 권한 실습과 파일 속성에서는 전용 임시 디렉터리를 만들어 파일 쓰기와 이름 삭제, umask, 불변 속성의 규칙을 직접 확인합니다.