Skip to main content

0x05 소프트웨어 보안 1 (Software Security - Introduction)

1. 시스템 보안 정의

컴퓨터 시스템 보안:
컴퓨터 시스템 내의 정보와 자원을 보호하는
원칙과 방법론

1.1 현실의 시스템 보안

현실적 도전:
• 지속적으로 진화하는 위협
• 창의적인 공격에 의한 우회
• 새로운 시스템과 새로운 공격 벡터

시스템 보안 연구자의 역할:
1. 새로운 공격에 대한 방어 지속 개선
2. 공격자 관점에서 사고
3. 새로운 분야의 보안 이슈 탐구

2. 소프트웨어 보안 개요

2.1 메모리에서의 영원한 전쟁

Software Security: "Eternal War in Memory"
메모리 관리와 관련된 지속적인 보안 투쟁

2.2 현대 소프트웨어의 복잡성

소프트웨어 규모

현대 소프트웨어 코드 라인 수 (MLOC = Million Lines of Code):
• Google Chrome Browser: ≥ 6.7 MLOC
• Android OS: ≥ 12~15 MLOC
• Mac OS X 10.4: ≥ 86 MLOC
• Linux Kernel: ≥ 20 MLOC

복잡성의 문제

현대 소프트웨어 = 매우 복잡함
복잡성 ∝ 버그 발생 가능성 증가

2.3 대규모 보안 사고

주요 취약점 사례

HeartBleed (2014):
• OpenSSL 버퍼 오버리드 취약점
• 서버 메모리의 TLS 비밀정보 노출 (개인키 등)

ShellShock (2014):
• bash 셸 버그
• 웹을 통한 임의 명령 실행 가능

WannaCry (2017):
• 랜섬웨어
• Windows SMB 파일 공유 취약점 악용
• 400,000대 감염

0-Day 익스플로잇

정의: 대상 소프트웨어에 패치가 없는 신규 발견 취약점
특징: 매우 비쌈 (경제적 가치 높음)

2.4 보안 취약점의 경제적 가치

보안 취약점 = $$$
• 제로데이 익스플로잇 시장 존재
• 연구자들의 버그 바운티 프로그램 참여
• 국가 차원의 사이버 무기 개발

3. 소프트웨어 버그와 익스플로잇

3.1 소프트웨어 버그

정의: 소프트웨어의 실수와 허점

결과:
• 프로그램이 잘못된 작업 수행
• 확률적으로 "크래시" 발생
• 성공적으로 수행할 수 없는 명령 실행

예시:
• Segmentation Fault (메모리 접근 오류)
• Invalid Opcode (코드가 임의 위치로 점프)

3.2 소프트웨어 익스플로잇

익스플로잇 프로세스:
Input → [BUG] → 악의적으로 버그 트리거 → Access Secret

정의: 버그를 악의적으로 트리거하여 프로그램 동작 제어

조건: 버그가 익스플로잇 가능한 경우 → 취약점(Vulnerability)

4. 소프트웨어 보안 정의와 메모리 안전성

4.1 소프트웨어 개발의 현실

질문들:
• 첫 컴파일에서 에러 없이 성공하는 빈도? → 가끔 (무서운 일 😊)
• 첫 실행에서 완벽하게 동작하는 빈도? → 아직 버그를 찾지 못했을 뿐 😊

4.2 저수준 언어와 보안 문제

C/C++의 특성

저수준 언어 (C/C++)의 특징:
• 성능을 위해 타입 안전성과 메모리 안전성 포기
• 프로그래머에게 책임 전가
• 레거시 소프트웨어와 성능 중심 소프트웨어에 광범위 사용
• 수동으로 찾고 수정하기에는 너무 많은 버그

안전한 언어 vs C 언어 비교

안전한 언어의 메모리 관리:
1. 타입 안전한 방식으로 객체 생성 및 초기화
2. 수명 동안 객체 손상 불가능
3. 타입 안전한 방식으로 객체 소멸 및 메모리 회수

C 언어의 문제점:
1. 타입 안전하지 않은 힙 값 생성
2. 캐스팅, 배열 접근, 할당 해제로 인한 메모리 손상
3. 안전하지 않은 할당 해제 → 댕글링 포인터

4.3 메모리 안전성 정의

메모리 안전성 (Memory Safety):
• 모든 메모리 접근이 소스 프로그래밍 언어의 의미론을 준수하는 속성
• 프로그램의 모든 가능한 실행이 메모리 안전하면 메모리 안전한 프로그램

4.4 공간적 메모리 안전성

정의

공간적 메모리 안전성 (Spatial Memory Safety):
모든 메모리 역참조가 포인터의 유효한 객체 경계 내에 있음을 보장

핵심 개념:
• 객체 경계는 객체 할당 시 정의
• 계산된 포인터는 원본 객체의 경계 상속
• 연관 객체 외부를 가리키는 포인터는 역참조 금지

공간적 메모리 손상 예시

char array[10];  // 10개 문자 배열
array[10] = 'a'; // 경계 초과 접근!

// array[0] ~ array[9]까지만 유효
// array[10]은 범위 초과

4.5 시간적 메모리 안전성

정의

시간적 메모리 안전성 (Temporal Memory Safety):
모든 메모리 역참조가 역참조 시점에 유효함을 보장

문제: 포인터가 가리키는 객체가 역참조 시점에 유효하지 않음
예시: 해제된 객체에 대한 역참조

시간적 메모리 손상 예시

단일 스레드 예시:

char* ptr = (char*)malloc(SIZE);
if (err) {
abrt = 1;
free(ptr); // 메모리 해제
}
...
if (abrt) {
logError("operation aborted before commit", ptr); // 해제된 메모리 접근!
}

멀티스레드 예시:

[Thread 1]                [Thread 3]
MyObj* obj = obj->studentName; // Use-After-Free!
malloc(sizeof(MyObj));
...

[Thread 2]
free(obj); // 메모리 해제

Use-After-Free: 가장 일반적인 시간적 메모리 손상 유형

4.6 기타 보안 개념

로직 버그

로직 버그:
• 프로그램별 특성, 프로그래머가 정확성 판단
• 로직 버그 탐지 명세 정의 어려움

타입 안전성

정의:

타입 안전성 (Type Safety):
타입 시스템 규칙을 위반하지 않는 연산만 허용

타입 안전성 위반 예시:

struct ObjA {          struct ObjB {
int a; int a;
int b; int b;
int c; }
}

ObjA_ptr = (struct ObjA*)&ObjB_instance;
ObjA_ptr->c; // C에서는 합법이지만 타입 안전성 위반

문제점: C/C++는 설계상 타입 안전성을 제공하지 않음

5. 버그 탐지 방법

5.1 해결 방안 개요

현재 가능한 것:
• 정적 분석으로 버그 탐지
• 동적 분석으로 버그 탐지
• (하드웨어 지원) 소프트웨어 공격 방어

장기적 방안:
• 안전한 코딩 교육
• 안전한 시스템 언어로 전환

5.2 버그 탐지 방법

주요 방법:
• 형식 검증 (Formal Verification)
• 정적 분석 (Static Analysis)
• 퍼징 (Fuzzing)

5.3 형식 검증 (Formal Verification)

개념

형식 검증:
• 형식적 명세를 사용하여 시스템의 정확성을 증명/반증
• 수학적 모델을 사용한 프로그램 동작 증명
• 모델(소프트웨어)과 명세 정의

접근법: 프로그램의 정확성을 수학적으로 증명

적용 범위

부적합한 경우:
• 크고 복잡한 소프트웨어
• 자주 업데이트되는 소프트웨어
• (사용하는 대부분의 소프트웨어)

적합한 경우:
• 상대적으로 작은 소프트웨어
• 거의 업데이트되지 않는 소프트웨어
• 우주선, 군용 항공기 등의 소프트웨어

5.4 정적 분석 (Static Analysis)

개념

정적 분석:
• 프로그램을 실행하지 않고 분석
• 분석 대상: 소스 코드, IR/기계 코드
• 용도: 버그 탐지, 취약점 발견에 널리 사용

분석 과정

분석 파이프라인:
Source Code → AST → Compiler IR (LLVM IR) → 보안 분석 도구

IR 기반 분석:
• 컴파일러 최적화 패스와 유사한 방식으로 보안 분석 도구 구축
• LLVM IR 등 중간 표현 활용

5.5 동적 분석 (Dynamic Analysis)

기본 개념

동적 분석:
• 계측된 실행 환경에서 프로그램 실행
• 계측 방법: 바이너리 번역기, 정적 계측, 에뮬레이터

탐지 대상:
• 무효한 메모리 사용
• 경쟁 조건 (Race Conditions)
• 널 포인터 역참조 등

예시: ASan, Valgrind, OS 예외 핸들러

소프트웨어 새니타이저

개념:

소프트웨어 새니타이저:
• 소프트웨어 버그를 탐지하는 특별한 컴파일러 계측
• 메모리 접근에 검사 코드 삽입으로 버그 실시간 탐지

AddressSanitizer (ASAN):

Google 개발 ASAN:
• 각 객체에 대한 버퍼 오버플로우 방지
• Shadow Bytes로 "redzone" 주소 저장
• 모든 load/store 명령어 계측 필요
• 200% 이상 CPU 오버헤드

동작 원리:
*a = ...;
↓ 계측
char *shadow = (a>>3)+Offset;
if (*shadow)
ReportAndCrash();

Shadow Byte:
• 각 shadow byte가 8바이트 블록의 유효성 기록
• 이 메타데이터로 접근 유효성 검사

퍼징 (Fuzzing)

기본 개념:

퍼징:
• (반)무작위 입력으로 코드를 실행하는 자동화된 테스트
• 프로그램을 크래시시키는 입력을 기록
• 크래시 리포트를 분석하여 익스플로잇 가능성 확인
• 보통 새니타이저와 함께 사용

사용자:
• 소프트웨어 회사
• 취약점을 찾는 해커들
• 학계에서 활발히 연구

ClusterFuzz:

Google의 ClusterFuzz:
• 확장 가능한 클라우드 기반 퍼징 프레임워크
• "비트코인 버그 마이닝"과 유사한 개념

퍼징 입력 생성 방법:

두 가지 주요 방법:
• Mutation Based - "Dumb Fuzzing"
• Generation Based - "Smart Fuzzing"

Mutation Based Fuzzing:

특징:
• 입력 구조에 대한 지식 거의/전혀 없음
• 기존 유효한 입력에 변칙 추가
• 완전 무작위 또는 휴리스틱 기반 변칙

장점:
• 설정과 자동화 매우 쉬움
• 프로토콜 지식 거의 불필요

단점:
• 초기 코퍼스에 의해 제한
• 체크섬, 챌린지-응답 프로토콜에서 실패 가능

예시: PDF 뷰어 퍼징
1. .pdf 파일 수집 (구글에서 10억 개 결과)
2. 페이지 크롤링으로 코퍼스 구축
3. 파일 선택 → 변조 → 프로그램 입력 → 크래시 기록

Generation Based Fuzzing:

특징:
• RFC, 문서 등 형식 설명으로부터 테스트 케이스 생성
• 입력의 각 가능한 지점에 변칙 추가
• 프로토콜 지식으로 무작위 퍼징보다 나은 결과

장점:
• 완전성
• 체크섬 등 복잡한 종속성 처리 가능

단점:
• 프로토콜 명세 필요
• 복잡한 프로토콜의 생성기 작성 노동 집약적
• 명세 ≠ 코드

PNG 예시:
//png.spk에서 헤더, IHDR 청크 등 정의

RustSan:

Rust 최적화 ASan:
• 빠른 퍼징 성능을 위해 선택적 검사 삽입
• 최대 57.08% 성능 향상

6. 하드웨어 기반 익스플로잇 완화

6.1 Intel CET

Intel Control-flow Enforcement Technology:
• 하드웨어 수준에서 제어 흐름 무결성 보장
• ROP/JOP 공격 방어

6.2 ARM Pointer Authentication

ARMv8.3-A (2017) 새 기능:
• Pointer Authentication Code (PAC) 생성
• 포인터 값의 무결성 보장
• 하드웨어 수준에서 포인터 변조 탐지

7. 안전한 프로그래밍 언어

7.1 미래의 해결책

장기적 방안:
• 안전한 언어로 전환
• 젊은 컴퓨터 과학자들 교육

7.2 안전한 프로그래밍 언어 개요

현대 고수준 언어의 특징:
• 메모리 안전성 제공
• 타입 안전성 제공

메모리 안전하고 강타입 언어:
• Python
• Java
• Rust
• 기타...

7.3 Rust 언어

Rust 개요

Rust:
• Mozilla 재단이 2015년 개발한 안전 우선 시스템 프로그래밍 언어

시스템 프로그래밍 언어 특징:
• C/C++와 비교 가능한 성능
• 저수준 프로그램 개발 가능 (OS 커널 등)

Rust의 메모리 안전성 메커니즘

보장 메커니즘:

Rust의 메모리 안전성:
• 소유권 (Ownership)
• 정적/동적 경계 검사
• 명시적 unsafe 블록 선언

소유권 시스템:

소유권 규칙:
• 모든 값은 정확히 하나의 소유자를 가짐
• 소유자에 대한 참조는 값의 소유자보다 오래 살 수 없음
• 값은 최대 하나의 mutable 참조를 가질 수 있음

공유 빌림 vs 가변 빌림:
• 읽기 전용 참조 여러 개 OR 쓰기 가능 참조 하나
• 동시에 둘 다는 불가능

효과:
• Use-After-Free 방지
• Double Free 방지
• Data Race 방지

경계 검사:

런타임 경계 검사:
• 컴파일 타임에 객체 경계를 알 수 없는 경우
• Rust가 자동으로 런타임 경계 검사 삽입
• 공간적 메모리 안전성 보장

unsafe 블록:

unsafe {
// 위험한 작업들
}

허용되는 위험한 작업:
• 원시 포인터 접근
• 소유권 규칙 무시
• 외부 함수 호출 (C/C++)

목적: 성능이나 저수준 제어가 필요한 경우에만 제한적 사용

8. 안전한 코딩 교육

8.1 교육의 중요성 [Slide #65, 80]

핵심 필요사항:
• CS 학생과 개발자들에게 안전한 코딩 교육
• OWASP (Open Worldwide Application Security Project) 표준 가이드라인 제공
• MITRE CWE (Common Weakness Enumeration) 활용

8.2 Power of Ten 규칙

Rule 1: Simple Control Flow:

• goto 문 사용 금지
• 재귀 방지
• setjmp/longjmp 방지
• 목적: 검증과 분석 단순화

Rule 2: Bounded Loops:

• 모든 루프는 정적 상한 보유
• 정적 분석으로 증명 가능
• 무한 루프 방지

나쁜 예:
while (str[i] != target) { // 보장된 경계 없음
i++;
}

Rule 3: No Dynamic Memory Allocation:

• 초기화 후 고정 메모리만 사용
• 메모리 누수 방지
• 예측 불가능한 동작 방지

Rule 4: Short Functions:

• 함수당 ~60줄로 제한
• 가독성과 유지보수성
• 논리적 명확성과 쉬운 디버깅

Rule 5: High Assertion Density:

• 함수당 최소 2개 어설션
• 비정상 조건 탐지
• 오류 처리와 디버깅 개선

Rule 6: Minimal Scope of Data Objects:

• 가능한 가장 작은 범위에서 변수 선언
• 부작용 감소
• 디버깅 단순화

Rule 7: Check Return Values and Parameters:

• 함수 반환값 항상 검증
• 입력 매개변수 엄격히 검증
• 견고성 향상

나쁜 예:
FILE *file = fopen("data.txt", "r");
fgets(buffer, 100, file); // fopen 실패 검사 없음
fclose(file);

Rule 8: Limited Preprocessor Use:

• 헤더 포함/간단한 매크로로 제한
• 복잡한 매크로나 조건부 컴파일 금지
• 명확성 향상

Rule 9: Restricted Pointer Use:

• 1레벨 포인터 역참조만
• 함수 포인터 사용 금지
• 정적 분석 단순화

Rule 10: Mandatory Compiler & Static Analyzer Checks:

• 모든 컴파일러 경고 활성화
• 일일 정적 코드 분석
• 오류 조기 탐지 보장

8.3 실습 퀴즈

퀴즈 1: 위험한 함수 사용

char comment[16];
gets(comment); // 위험한 함수!

문제점:
• gets는 위험하고 deprecated된 함수
• CWE-242: Use of Inherently Dangerous Function

해결책:
fgets(comment, 16, stdin);
buf[strcspn(comment, "\n")] = 0;

컴파일러 경고:
gcc -Wall -Werror // 모든 경고를 오류로 처리

퀴즈 2: 포맷 스트링 공격

printf(argv[1]);  // 위험!

문제점:
• CWE-134: Use of Externally-Controlled Format String
• 사용자 입력을 포맷 스트링으로 직접 사용

공격 예시:
./example "Hello World %p %p %p %p %p %p"
→ 스택 메모리 내용 노출

해결책:
printf("%s", argv[1]); // 포맷 스트링 고정

퀴즈 3: 널 종료 문제

char a[16];
strncpy(a, "0123456789abcdef", sizeof(a));

문제점:
• strncpy는 자동으로 널 종료하지 않음
• 결과 문자열이 널 종료되지 않을 수 있음
• CWE-170: Improper Null Termination

퀴즈 4: 정수 오버플로우

// OpenSSH 3.3 취약점
nresp = packet_get_int();
if (nresp > 0) {
response = xmalloc(nresp * sizeof(char*)); // 정수 오버플로우!
for (i = 0; i < nresp; i++)
response[i] = packet_get_string(NULL);
}

문제점:
• nresp가 매우 클 때 nresp * sizeof(char*) 오버플로우
• 작은 메모리 할당 후 버퍼 오버플로우 발생

예상 시험문제

1. 메모리 안전성 유형 분석

문제: 공간적 메모리 안전성과 시간적 메모리 안전성의 차이를 설명하고, 각각의 위반 사례를 코드로 작성하시오.

모범답안:

공간적 메모리 안전성 (Spatial Memory Safety):

  • 정의: 모든 메모리 역참조가 포인터의 유효한 객체 경계 내에 있음을 보장
  • 위반 사례:
char buffer[10];
buffer[15] = 'A'; // 배열 경계 초과

시간적 메모리 안전성 (Temporal Memory Safety):

  • 정의: 모든 메모리 역참조가 역참조 시점에 유효함을 보장
  • 위반 사례:
char *ptr = malloc(100);
free(ptr);
*ptr = 'A'; // Use-After-Free

차이점:

  • 공간적: 메모리 위치의 유효성
  • 시간적: 메모리 시점의 유효성

2. 정적 분석 vs 동적 분석 비교

문제: 정적 분석과 동적 분석의 장단점을 비교하고, 각각이 적합한 사용 사례를 제시하시오.

모범답안:

정적 분석:

  • 장점:
    • 모든 실행 경로 분석 가능
    • 실행 환경 불필요
    • 코드 검토 단계에서 버그 발견
  • 단점:
    • 거짓 양성률 높음
    • 런타임 정보 부족
    • 복잡한 로직 분석 한계
  • 적합한 사례: 코드 리뷰, CI/CD 파이프라인, 규정 준수

동적 분석:

  • 장점:
    • 실제 실행에서 발생하는 버그 탐지
    • 낮은 거짓 양성률
    • 런타임 환경 정보 활용
  • 단점:
    • 실행된 경로만 분석
    • 성능 오버헤드
    • 테스트 케이스 품질에 의존
  • 적합한 사례: 퍼징, 회귀 테스트, 프로덕션 모니터링

3. Rust 소유권 시스템

문제: Rust의 소유권 시스템이 어떻게 메모리 안전성을 보장하는지 설명하고, 다음 코드의 문제점을 분석하시오.

let mut data = vec![1, 2, 3];
let ptr1 = &mut data;
let ptr2 = &mut data; // 컴파일 에러!

모범답안:

Rust 소유권 규칙:

  1. 모든 값은 정확히 하나의 소유자를 가짐
  2. 소유자에 대한 참조는 값의 소유자보다 오래 살 수 없음
  3. 값은 최대 하나의 mutable 참조를 가질 수 있음

메모리 안전성 보장 방법:

  • Use-After-Free 방지: 소유권 이전 시 원본 변수 무효화
  • Double-Free 방지: 소유권이 하나뿐이므로 중복 해제 불가능
  • Data Race 방지: mutable 참조는 하나만 허용

코드 문제점:

let mut data = vec![1, 2, 3];
let ptr1 = &mut data; // 첫 번째 mutable 참조
let ptr2 = &mut data; // 두 번째 mutable 참조 - 위반!
  • 동시에 여러 mutable 참조 생성 시도
  • 컴파일러가 컴파일 타임에 에러 발생시킴
  • 데이터 레이스 방지를 위한 설계

4. 퍼징 기법 비교

문제: Mutation-based 퍼징과 Generation-based 퍼징의 차이점을 설명하고, 각각의 장단점과 적용 사례를 제시하시오.

모범답안:

Mutation-based Fuzzing (Dumb Fuzzing):

  • 방법: 기존 유효한 입력에 무작위 변조 적용
  • 장점:
    • 설정과 자동화 매우 쉬움
    • 프로토콜 지식 불필요
    • 빠른 시작 가능
  • 단점:
    • 초기 코퍼스 품질에 의존
    • 체크섬이나 복잡한 프로토콜에서 제한적
    • 깊은 코드 경로 도달 어려움
  • 적용 사례: 파일 포맷 퍼징 (PDF, PNG, MP3 등)

Generation-based Fuzzing (Smart Fuzzing):

  • 방법: 프로토콜 명세로부터 테스트 케이스 생성
  • 장점:
    • 프로토콜 구조 이해 기반
    • 체크섬 등 복잡한 제약 조건 처리
    • 더 깊은 코드 경로 탐색
  • 단점:
    • 프로토콜 명세 필요
    • 생성기 개발 비용 높음
    • 명세와 실제 구현의 차이
  • 적용 사례: 네트워크 프로토콜 (HTTP, TLS, DNS 등)

5. Power of Ten 규칙 적용

문제: Power of Ten 규칙 중 3가지를 선택하여 설명하고, 각 규칙을 위반하는 코드 예시와 올바른 수정 방법을 제시하시오.

모범답안:

Rule 2: Bounded Loops

  • 위반 코드:
while (str[i] != target) {  // 상한 없음
i++;
}
  • 수정:
for (i = 0; i < MAX_LEN && str[i] != target; i++) {
// 명확한 상한 설정
}

Rule 7: Check Return Values

  • 위반 코드:
FILE *file = fopen("data.txt", "r");
fgets(buffer, 100, file); // fopen 결과 검사 없음
  • 수정:
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
// 에러 처리
return -1;
}
fgets(buffer, 100, file);

Rule 10: Compiler Checks

  • 적용:
gcc -Wall -Werror -Wextra source.c
# 모든 경고를 에러로 처리하여 코드 품질 강제

6. 소프트웨어 새니타이저 분석

문제: AddressSanitizer(ASan)의 동작 원리를 설명하고, Shadow Memory 기법의 장단점을 분석하시오.

모범답안:

ASan 동작 원리:

  1. 계측: 모든 메모리 접근 명령어에 검사 코드 삽입
  2. Shadow Memory: 실제 메모리와 별도로 메타데이터 저장
  3. 검사: 메모리 접근 시 shadow memory 확인

Shadow Memory 구조:

실제 메모리: [8바이트 블록]
Shadow Memory: [1바이트] - 해당 블록의 유효성 정보

계측 코드 예시:

// 원본
*a = value;

// 계측 후
char *shadow = (a >> 3) + SHADOW_OFFSET;
if (*shadow) {
ReportAndCrash();
}
*a = value;

장점:

  • 정확한 버퍼 오버플로우 탐지
  • Use-After-Free 탐지
  • 실시간 오류 리포팅

단점:

  • 200% 이상 성능 오버헤드
  • 메모리 사용량 증가 (shadow memory)
  • 프로덕션 환경 적용 어려움

핵심 요약

  • 현대 소프트웨어는 복잡성 증가로 인해 버그와 취약점이 불가피
  • 메모리 안전성은 공간적(경계)과 시간적(수명) 측면으로 구분
  • C/C++ 언어의 한계를 보완하기 위해 다양한 분석 도구와 기법 발전
  • 정적 분석은 실행 전 코드 분석, 동적 분석은 실행 중 모니터링
  • 퍼징은 자동화된 테스트로 버그 발견에 효과적
  • Rust 등 안전한 언어가 메모리 안전성 문제의 근본적 해결책
  • 안전한 코딩 교육표준화된 가이드라인 준수가 중요
  • 하드웨어 지원 보안 기능이 소프트웨어 보안을 보완

💡 중요: 소프트웨어 보안은 단일 해결책이 없으며, 언어 선택, 개발 프로세스, 도구 활용, 교육 등의 종합적 접근이 필요합니다. 특히 개발 단계에서의 보안 고려가 사후 패치보다 훨씬 효과적입니다.