보안

시큐어 코딩

hjjummy 2025. 4. 20. 01:23

안전한 암호화 연산 (Secure Cryptographic Operations)

🔸1. 개요

암호화는 데이터를 보호하기 위해 필수적인 과정이지만, 잘못된 방식으로 구현하면 오히려 보안 취약점을 유발할 수 있다.
따라서 안전한 암호화 연산이란, 단순히 암호 알고리즘을 사용하는 것이 아니라 안전한 사용 방식올바른 구현 패턴을 따르는 것을 의미한다.


🔸2. 암호화 알고리즘 사용 시 주의사항

🔹 ① 안전하지 않은 암호화 알고리즘 사용 금지

위험한 알고리즘 설명

 

DES 키 길이가 짧아 브루트포스에 취약
MD5, SHA-1 빠른 해시 → 사전 공격, 충돌 공격에 취약
RC4 초기 바이트 편향성 존재, 보안 취약

권장 알고리즘:

  • AES (128/192/256bit)
  • RSA, ECC (비대칭 암호화)
  • SHA-256, SHA-3 (해시)
  • HMAC-SHA256 (무결성 확인)

🔹 ② 암호화 키 관리 미흡

  • 암호 알고리즘보다 키의 보관, 유출, 주기적 변경이 더 중요할 수 있음
  • 키를 코드에 하드코딩하거나 평문으로 저장하는 것은 금지
  • 운영 환경에서는 보안 모듈(KMS, HSM, 환경변수 등)을 활용해 키 관리

🔹 ③ IV(Initialization Vector) 오용

잘못된 사용위험성
IV를 고정값으로 설정 같은 평문 → 같은 암호문 생성 → 패턴 노출
IV를 재사용 암호 안전성 손상 → CPA(Ciphertext Pattern Attack) 위험

IV는 반드시 난수(random)로 생성하고, 암호문과 함께 저장 (하지만 암호키와는 분리!)

 


🔸5. 암호화 관련 보안 권고 사항

항목권고 사항
키 관리 하드코딩 금지, 보안 저장소 사용
알고리즘 선택 AES, SHA-256, RSA 등 안전한 알고리즘 사용
IV 처리 매번 새로 생성, 암호문과 함께 저장 가능
무결성 암호화와 함께 HMAC 등으로 위조 탐지 필요
복호화 전 검증 복호화 시 IV, 길이, 포맷 등 검증 수행

🔸정리 요약표

구분안전하지 않은 방식 ❌안전한 방식 ✅
코드에 하드코딩 난수 생성 후 보안 저장소 사용
IV 고정값, 재사용 암호화마다 난수로 새로 생성
알고리즘 DES, MD5, SHA-1 AES, RSA, SHA-256, HMAC
무결성 없음 HMAC, MAC 추가 적용
키 관리 파일이나 코드에 저장 환경변수, HSM, KMS 활용

🔸핵심 포인트

  • 암호화는 안전한 알고리즘 + 안전한 사용 방식 + 키/IV 관리가 함께 중요
  • IV는 재사용하면 안 되며, 키는 노출되어선 안 됨
  • 해시 함수만으로는 보안성이 부족할 수 있음 (무결성만 검증, 암호화 기능은 없음)

안전한 난수 생성기 사용


🔸1. 개요

  • 난수(Random Number)는 세션 키, 토큰, 인증코드, 임시 비밀번호 등 다양한 보안 요소에 활용됨
  • 예측 가능한 난수는 공격자가 의도적으로 값을 추측할 수 있어 심각한 보안 위협이 됨
  • 따라서 보안 목적으로 사용하는 난수는 반드시 **암호학적으로 안전한 난수 생성기(CSPRNG)**를 사용해야 함

❌ 안전하지 않은 난수 생성 방식

 ① JAVA – 고정된 Seed 사용

public static int getRandomValue(int maxValue) {
    Random random = new Random(100);  // 고정된 seed
    return random.nextInt(maxValue);
}
  • new Random(100) 처럼 seed가 고정된 경우, 항상 동일한 난수 시퀀스 생성
  • 인증키, 비밀번호, 세션값 등에 사용 시 예측 공격 가능

 ② JAVA – 일반 Random 사용

public static String getAuthKey() {
    Random random = new Random();  // 시스템 시간 기반 seed
    String authKey = Integer.toString(random.nextInt());
}
  • seed는 매번 바뀌지만, Random은 암호학적으로 안전하지 않음
  • Random 클래스는 내부 상태를 역추적해 다음 값을 예측 가능

③ C – 시드 없이 rand() 사용

void foo() {
    for (int i = 0; i < 20; i++)
        printf("%d", rand());  // 동일한 값 반복 출력
}
  • rand()는 초기 seed 설정 없이 호출 시 항상 동일한 시퀀스 출력
  • 예측 공격의 위험

✅  안전한 난수 생성 방식

① JAVA 예시 – SecureRandom 사용

import java.security.SecureRandom;
import java.security.MessageDigest;

public static String getAuthKey() {
    SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
    secureRandom.setSeed(secureRandom.generateSeed(128));  // 동적으로 seed 생성

    long randVal = secureRandom.nextLong();

    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    String authKey = new String(digest.digest(Long.toString(randVal).getBytes()));

    return authKey;
}

보안 포인트

  • SecureRandom은 암호학적으로 안전한 난수 생성기(CSPRNG)
  • generateSeed()를 통해 매번 새로운 시드로 초기화
  • SHA-256으로 해시하여 출력값을 암호학적으로 난독화

② C 예시 – random() + 시드 설정

void foo() {
    srandom(time(NULL));  // 시간 기반 seed 설정
    for (int i = 0; i < 20; i++)
        printf("%ld", random());
}
  • srandom()으로 시드 설정하여 난수 다양화
  • 단, 짧은 시간 간격의 호출은 여전히 예측 가능 → 암호용으로는 미흡

🔸문제점 요약


문제 코드 취약점 설명
Random(100) 고정된 시드로 항상 동일한 난수 시퀀스 생성
new Random() 시드가 시간이지만 암호학적으로 안전하지 않음
rand() 초기화하지 않으면 항상 동일한 결과 출력
srandom(time(NULL)) 시드 설정은 되지만 예측 가능성 존재

 

❌ 문제 코드 ✅ 대응되는 올바른 코드
java<br>Random random = new Random(100);<br>return random.nextInt(maxValue);<br>
📌 고정된 seed 사용 → 항상 같은 난수 생성
java<br>SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");<br>sr.setSeed(sr.generateSeed(128));<br>return sr.nextInt();<br>
📌 암호학적으로 안전한 난수 생성
java<br>Random random = new Random();<br>String key = Integer.toString(random.nextInt());<br>
📌 Random은 예측 가능 → 보안용으로 부적합
java<br>SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");<br>long val = sr.nextLong();<br>MessageDigest sha = MessageDigest.getInstance("SHA-256");<br>String key = new String(sha.digest(Long.toString(val).getBytes()));<br>
📌 CSPRNG + 해시 적용
c<br>for (int i = 0; i < 20; i++)<br> printf("%d", rand());<br>
📌 시드 설정 없이 호출 → 항상 동일 결과
c<br>srandom(time(NULL));<br>for (int i = 0; i < 20; i++)<br> printf("%ld", random());<br>
📌 시간 기반 시드 설정 (단, 암호용으로는 제한적)

 


🔸보안 권장사항 요약

항목권장  방식
난수 목적 인증, 토큰, 비밀번호 등 보안 관련 데이터
사용 클래스 (Java) SecureRandom (SHA1PRNG 등)
사용 함수 (C) random() + 난수 시드 설정, 혹은 외부 보안 라이브러리 사용
Seed 설정 generateSeed() 또는 시스템 이벤트 기반 랜덤 값
출력 처리 SHA-256, HMAC, Base64 등과 함께 사용하여 안정성 확보

🔸핵심 요약

  • 보안 용도의 난수는 반드시 SecureRandom 등 CSPRNG 사용
  • Random이나 rand()는 보안용으로 절대 사용 금지
  • 고정된 시드(seed) 사용은 예측 가능성 때문에 취약
  • Java에서 SecureRandom.getInstance("SHA1PRNG")는 많이 나오는 방식

인증서 유효성 검증 (Certificate Validation)


🔸 1. 개요

인증서는 서버의 신원을 증명하고 데이터 암호화 통신의 기반이 되는 요소이다.

인증서 검증을 제대로 수행하지 않을 경우, **중간자 공격(MITM)**에 노출될 수 있다.

따라서 SSL/TLS 통신에서 인증서를 받을 때는 다음 항목들에 대한 유효성 확인이 필요하다.


🔸2. 인증서 유효성 검증 항목

항목설명
✅ CN (Common Name) 검증 인증서의 도메인(CN)과 실제 접속 호스트명이 일치하는지 확인
✅ CA 서명 검증 인증서가 신뢰된 인증기관(CA)에 의해 발급되었는지 검증
✅ 유효기간 확인 인증서의 시작일/만료일을 확인
✅ 암호화 알고리즘 확인 안전하지 않은 알고리즘(SHA-1 등) 사용 시 거부
✅ 인증서 해지 여부 확인 인증서가 CRL 또는 OCSP를 통해 폐기되지 않았는지 확인

❌ 잘못된 구현 예 (C 코드)

if ((cert = SSL_get_peer_certificate(ssl)) && host)
    foo = SSL_get_verify_result(ssl);

if ((X509_V_OK == foo)) {
    ...
}

문제점

  • X509_V_OK만 체크하여 단순히 성공 여부만 확인
  • 실제로는 **자체 서명된 인증서(Self-signed)**도 통과될 수 있음
  • Common Name 불일치, 해지된 인증서, 만료된 인증서 등을 검증하지 않음
  • MITM 공격 가능

✅ 안전한 구현 예 (JAVA)

private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) {
  if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) //호스트 인증서와 CA인증서의 DN일치 여부확인 
      return false;

  try {
    toVerify.verify(signingCert.getPublicKey()); // CA 인증서로 서명 검증
    toVerify.checkValidity();                   // 유효기간 확인
    return true;
  } catch (GeneralSecurityException verifyFailed) {
    return false;
  }
}

 

보안적으로 수행하는 검증 절차

절차설명
발급자 DN 비교 toVerify.getIssuerDN()과 signingCert.getSubjectDN()이 일치하는지 확인
서명 검증 인증서가 signingCert의 공개키로 서명되었는지 검증
유효기간 확인 checkValidity()를 통해 현재 시점이 유효기간 안에 있는지 확인

🔸정리 요약

검증 필요 이유 SSL 통신 시 신뢰되지 않은 서버와 통신 방지
필수 검증 요소 CN 일치 여부, CA 신뢰성, 유효기간, 해지 여부 등
잘못된 구현 X509_V_OK만으로 판단할 경우, 자체 서명 인증서도 통과 가능
올바른 구현 발급자 검증 + 서명 검증 + 유효기간 확인 + 추가 검증 수행 필요

🔸 핵심 포인트

  • 인증서 검증이 빠지면 MITM 공격이 가능해진다
  • SSL_get_verify_result()는 최소 수준의 결과만 제공하므로 보완이 필요하다
  • Java에서는 공개키 기반 검증 + 유효기간 검증을 함께 수행해야 한다
  • 신뢰되지 않은 발급자 또는 Common Name 불일치는 명확히 차단해야 한다

 


에러처리 (Error Handling) 정리


🔸1. 개요

  • 에러처리 취약점이란, 프로그램이 오류 발생 시 적절한 대응을 하지 않거나, 민감한 정보를 포함한 에러 메시지를 외부에 노출할 때 발생하는 보안 약점이다.

주요 취약 사례 및 대응법

오류 메시지 정보 노출

❌ 문제 예 (JAVA)

try {
  rd = new BufferedReader(new FileReader(new File(filename)));
} catch(IOException e) {
  System.err.print(e.getMessage());
  e.printStackTrace();  // Stack trace를 외부에 노출
}

✅ 대응 예 (Secure Coding)

try {
  rd = new BufferedReader(new FileReader(new File(filename)));
} catch(IOException e) {
  logger.error("ERROR-01: 파일 열기 실패"); // 사용자에게 노출할 메시지는 정적 메시지 사용
}
 

➡ Stack trace는 절대 사용자에게 출력하지 말고 로그에만 기록


오류 상황 대응 부재

❌ 문제 예 (JAVA)

try {
  username = s.getParser().getRawParameter(USERNAME);
  password = s.getParser().getRawParameter(PASSWORD);
  if (!"webgoat".equals(username) || !password.equals("webgoat")) {
    s.setMessage("Invalid username and password entered.");
    return makeLogin(s);
  }
} catch (NullPointerException e) {
  // 처리 없이 방치
}

✅ 대응 예

} catch (NullPointerException e) {
  s.setMessage("입력 오류가 발생했습니다.");  // 사용자 메시지 설정
  return makeLogin(s);  // 로그인 화면으로 다시 이동
}

➡ 예외가 발생해도 프로그램 흐름을 제어할 수 있어야 함


부적절한 예외 처리

❌ 문제 예 (JAVA)

try {
  ...
  Date date = format.parse(line);
} catch (Exception e) {
  System.err.println("Exception: " + e.getMessage());
}
  • 모든 오류를 Exception 하나로 처리 → 어떤 문제가 발생했는지 불분명

✅ 대응 예

try {
  ...
  Date date = format.parse(line);
} catch (MalformedURLException e) {
  System.err.println("URL 형식 오류: " + e.getMessage());
} catch (IOException e) {
  System.err.println("입출력 오류: " + e.getMessage());
} catch (ParseException e) {
  System.err.println("날짜 형식 오류: " + e.getMessage());
}

➡ 발생 가능한 예외 유형을 명확히 구분하여 처리


🔸정리 요약


 

항목 잘못된 예  보안 코딩 방식
오류 메시지 e.printStackTrace() 사용자에게 노출하지 않음, 로그로만 기록
예외 처리 부재 catch { } 비워두거나 무시 사용자에게 적절한 안내 제공
예외 분기 없음 모든 에러를 Exception으로 처리 예외 유형별로 명시적 처리
에러 발생 후 흐름 없음 프로그램 비정상 종료 정상적인 흐름으로 유도 (예: 로그인 화면 복귀 등)

 

  • ❗ 오류 메시지는 내부 정보가 노출되지 않도록 직접 작성
  • ❗ 예외 상황은 catch 블록에서 의미 있는 처리 필요
  • ❗ 다양한 예외 유형은 분리하여 개별 처리
  • ❗ 사용자에게는 간결하고 안전한 메시지만 제공

 


시간 및 상태(Time and Status) 정리


🔸 1. 개요

  • 병렬 시스템 또는 다중 프로세스 환경에서
    시간(시점) 또는 상태(자원, 변수 등) 를 부적절하게 다룰 경우 발생하는 보안 취약점을 의미함.
  • 예: 경쟁 조건, TOCTOU, 무한 루프 등

🔸 2. 대표적인 취약 사례

TOCTOU (Time-Of-Check to Time-Of-Use)

검사 시점(TOC)과 사용 시점(TOU)이 분리되어 있을 때 발생하는 경쟁 조건(Race Condition) 취약점

📌 예시 시나리오

  1. 프로세스 A: 파일의 존재를 확인 (TOC)
  2. 프로세스 B: 그 파일을 삭제함
  3. 프로세스 A: 파일이 존재한다고 믿고 접근을 시도 (TOU) → 실패하거나 악용될 수 있음

❗문제점

  • 자원이 존재하는지 확인한 후 사용하는 구조는, 중간에 자원이 변경될 수 있음을 고려하지 않으면 취약

❌ 안전하지 않은 코드 예 (C)

static volatile double account;

void deposit(int amount) {
    account += amount;
}

void withdraw(int amount) {
    account -= amount;
}

➡ 여러 프로세스가 동시에 접근하면 값 손실, 무결성 오류 발생 가능


✅ Secure Coding 예 (C)

static volatile double account;
static mtx_t account_lock;

void deposit(int amount) {
    mutex_lock(&account_lock);
    account += amount;
    mutex_unlock(&account_lock);
}

void withdraw(int amount) {
    mutex_lock(&account_lock);
    account -= amount;
    mutex_unlock(&account_lock);
}

mutex로 동기화하여 경쟁 조건 방지

 

② 종료되지 않는 반복문 또는 재귀함수

❗문제점

  • 재귀 종료 조건(Base Case)이 없거나 잘못된 경우 → 무한 재귀 → 메모리 고갈 → 서비스 거부(DOS)

 안전하지 않은 코드 예 (C)

int factorial(int i) {
    return i * factorial(i - 1);
}

➡ 0, -1, -2... → Stack Overflow (Segmentation Fault) 발생 가능


✅ Secure Coding 예 (C)

int factorial(int i) {
    if (i <= 1) {
        return 1;
    }
    return i * factorial(i - 1);
}

종료 조건 추가로 안전하게 재귀 종료


🔸정리 요약표

 

취약점 유형설명 대응책
TOCTOU 검사 시점과 사용 시점 사이의 자원 변경으로 인한 경쟁 조건 자원 접근 시 락(Mutex, Lock) 사용
무한 루프 재귀 종료 조건 없는 재귀 호출로 인한 무한 실행 Base Case 명확히 지정

🔸핵심 요약

  • TOCTOU: 다중 프로세스에서 파일, 변수 등의 상태가 바뀔 수 있음을 고려
  • 재귀함수: 종료 조건 없으면 스택 고갈 위험
  • 보안 코딩 핵심: 항상 자원은 락으로 보호, 반복문/재귀는 종료 조건 필수