안전한 암호화 연산 (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) 취약점
📌 예시 시나리오
- 프로세스 A: 파일의 존재를 확인 (TOC)
- 프로세스 B: 그 파일을 삭제함
- 프로세스 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: 다중 프로세스에서 파일, 변수 등의 상태가 바뀔 수 있음을 고려
- 재귀함수: 종료 조건 없으면 스택 고갈 위험
- 보안 코딩 핵심: 항상 자원은 락으로 보호, 반복문/재귀는 종료 조건 필수
'보안' 카테고리의 다른 글
위협 모델링 (0) | 2025.04.23 |
---|---|
SDLC의 각 단계 및 모델링 기법의 특징 (0) | 2025.04.22 |
네트워크 보안: 정보 목록화(Footprinting & Scanning) (0) | 2025.04.21 |
네트워크 보안: 스니핑(Sniffing) (0) | 2025.04.21 |
네트워크 보안 : 스푸핑(Spoofing) (0) | 2025.04.21 |