랜섬웨어 암호화 메커니즘 분석
사용 시기
- 랜섬웨어 감염이 발생했고, 복구를 위해 사용된 암호화 방식을 이해해야 할 때
- 구현 결함, 알려진 복호화 도구 등을 통해 몸값을 지불하지 않고 복호화가 가능한지 평가할 때
- 랜섬웨어를 역공학하여 암호화 알고리즘, 키 유도 방식, 키 저장 메커니즘을 식별할 때
- 랜섬웨어의 암호화 구현에서 취약점이 발견되어 복호화 도구를 개발할 때
- 암호화 접근 방식으로 랜섬웨어 샘플을 분류하여 알려진 패밀리에 귀속시킬 때
사용하지 말아야 할 경우: 프로덕션 데이터 복구 작업에서 복호화 방법을 암호화된 파일의 테스트 복사본에서 먼저 검증하지 않은 경우
사전 요구 사항
- Ghidra 또는 IDA Pro: 랜섬웨어 바이너리 역공학용
- Python 3.8+ 및
pycryptodome라이브러리: 암/복호화 루틴 테스트용 - 암호화된 샘플 파일과 해당 평문 원본(알려진 평문 쌍)
- 랜섬웨어 바이너리(필요시 언패킹된 상태)
- 대칭 키(AES, ChaCha20) 및 비대칭 키(RSA) 암호 알고리즘에 대한 이해
- NoMoreRansom.org 데이터베이스: 기존 무료 복호화 도구 확인용
워크플로우
1단계: 암호화 알고리즘 식별
랜섬웨어가 사용하는 암호화 알고리즘을 확인합니다:
# Windows Crypto API 사용 여부를 임포트에서 확인
import pefile
pe = pefile.PE("ransomware.exe")
crypto_apis = {
"CryptAcquireContextA": "Windows CryptoAPI",
"CryptAcquireContextW": "Windows CryptoAPI",
"CryptGenKey": "Windows CryptoAPI 키 생성",
"CryptEncrypt": "Windows CryptoAPI 암호화",
"CryptImportKey": "Windows CryptoAPI 키 임포트",
"BCryptOpenAlgorithmProvider": "Windows CNG (최신 암호화)",
"BCryptEncrypt": "Windows CNG 암호화",
"BCryptGenerateKeyPair": "Windows CNG 비대칭 키 생성",
}
print("Crypto API 임포트:")
for entry in pe.DIRECTORY_ENTRY_IMPORT:
for imp in entry.imports:
if imp.name and imp.name.decode() in crypto_apis:
print(f" {entry.dll.decode()} -> {imp.name.decode()}: {crypto_apis[imp.name.decode()]}")
일반적인 랜섬웨어 암호화 방식:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AES-256-CBC + RSA-2048: 가장 흔한 하이브리드 방식 (LockBit, REvil, Conti)
AES-256-CTR + RSA-4096: 스트림 암호 모드 변형 (BlackCat/ALPHV)
ChaCha20 + RSA-4096: 최신 스트림 암호 (Hive, Royal)
Salsa20 + ECDH: Curve25519 키 교환 (Babuk)
AES-128-ECB: 약한 모드 - 알려진 평문 공격으로 복호화 가능
XOR 전용: 단순 암호화 - 항상 복구 가능
사용자 정의 알고리즘: 종종 구현 결함 포함
2단계: 키 생성 및 관리 분석
암호화 키가 어떻게 생성되고 저장되는지 역공학합니다:
랜섬웨어의 키 관리 패턴:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 강력함 (키 없이 복구 불가능):
- CryptGenRandom으로 파일별 AES 키 생성
- AES 키를 내장된 RSA 공개 키로 암호화
- 암호화된 키를 각 파일에 추가하거나 별도 저장
- RSA 개인 키는 공격자의 C2 서버에만 존재
2. 약함 (복구 가능성 있음):
- 예측 가능한 시드(타임스탬프, PID)에서 AES 키 유도
- 모든 파일에 동일한 AES 키 사용 (단일 키 탈취 = 전체 복구)
- 암호화 시작 전 키가 C2로 전송됨 (PCAP에 키 포함 가능)
- 짧은 반복 키로 XOR (무차별 대입 가능)
- GetTickCount() 또는 time()으로 시드된 PRNG (제한된 키 공간)
3. 결함 있는 구현:
- ECB 모드 (평문 패턴 유지)
- 파일 간 초기화 벡터(IV) 재사용
- 메모리에 평문으로 키 저장 (메모리 덤프에서 복구 가능)
- 부분 암호화 (처음 N 바이트만 암호화)
3단계: 파일 암호화 루틴 검토
파일 처리 로직을 역공학합니다:
// 일반적인 랜섬웨어 파일 암호화 흐름 (Ghidra 역컴파일 의사 코드):
void encrypt_file(char *filepath) {
// 1. 대상 확장자 목록 확인
if (!is_target_extension(filepath)) return;
// 2. 파일별 AES 키 생성 (AES-256의 경우 32바이트)
BYTE aes_key[32];
CryptGenRandom(hProv, 32, aes_key);
// 3. 랜덤 IV 생성 (16바이트)
BYTE iv[16];
CryptGenRandom(hProv, 16, iv);
// 4. 파일 내용 읽기
HANDLE hFile = CreateFile(filepath, GENERIC_READ, ...);
BYTE *plaintext = read_entire_file(hFile);
// 5. AES-256-CBC로 암호화
aes_cbc_encrypt(plaintext, file_size, aes_key, iv);
// 6. AES 키를 RSA 공개 키로 암호화
BYTE encrypted_key[256]; // RSA-2048 출력
rsa_encrypt(aes_key, 32, rsa_pubkey, encrypted_key);
// 7. 파일에 쓰기: 암호화된 데이터 + 암호화된 키 + IV
write_file(filepath, encrypted_data, encrypted_key, iv);
// 8. 랜섬웨어 확장자로 파일 이름 변경
rename_file(filepath, strcat(filepath, ".locked"));
}
4단계: 암호화 취약점 확인
구현에서 악용 가능한 결함을 테스트합니다:
from Crypto.Cipher import AES
import os
import struct
# 테스트 1: 여러 파일에 동일한 키가 사용되는지 확인
# 알려진 파일의 암호화된 버전 비교
def check_key_reuse(file1_enc, file2_enc):
with open(file1_enc, "rb") as f:
data1 = f.read()
with open(file2_enc, "rb") as f:
data2 = f.read()
# IV 추출 (위치는 랜섬웨어 패밀리에 따라 다름)
# IV가 같고 파일이 동일한 암호화 블록을 공유하면 같은 키
iv1 = data1[-16:] # 예: IV가 끝에 위치
iv2 = data2[-16:]
if iv1 == iv2:
print("[!] 동일한 IV 탐지 - 키 재사용 가능성 높음")
# 테스트 2: 예측 가능한 키 유도 확인
# 키가 타임스탬프에서 유도된 경우, 가능한 값 반복
def brute_force_timestamp_key(encrypted_file, known_header, timestamp_range):
with open(encrypted_file, "rb") as f:
encrypted_data = f.read()
for ts in timestamp_range:
# 랜섬웨어와 동일한 방식으로 키 유도
import hashlib
key = hashlib.sha256(str(ts).encode()).digest()
iv = encrypted_data[-16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(encrypted_data[:16])
if decrypted[:len(known_header)] == known_header:
print(f"[!] 키 발견! 타임스탬프: {ts}")
return key
return None
# 테스트 3: ECB 모드 확인 (패턴 유지)
def check_ecb_mode(encrypted_file):
with open(encrypted_file, "rb") as f:
data = f.read()
# ECB는 동일한 평문 블록에 대해 동일한 암호문 생성
blocks = [data[i:i+16] for i in range(0, len(data), 16)]
unique = len(set(blocks))
total = len(blocks)
if unique < total * 0.95:
print(f"[!] ECB 모드 가능성 높음: 총 {total}개 중 {total-unique}개 중복 블록")
5단계: 키 복구 시도
식별된 취약점을 이용한 키 복구:
# 복구 방법 1: 메모리 덤프에서 키 추출
# AES 키 스케줄 스캔용 Volatility 플러그인
# vol3 -f memory.dmp windows.yarascan --yara-rule "aes_key_schedule"
# 복구 방법 2: 알려진 평문 공격 (약한 알고리즘)
def xor_key_recovery(encrypted_file, known_plaintext):
"""알려진 평문-암호문 쌍에서 XOR 키 복구"""
with open(encrypted_file, "rb") as f:
ciphertext = f.read()
key = bytes(c ^ p for c, p in zip(ciphertext, known_plaintext))
# 반복 키 길이 찾기
for key_len in range(1, 256):
candidate = key[:key_len]
if all(key[i] == candidate[i % key_len] for i in range(min(len(key), key_len * 4))):
print(f"XOR 키 (길이 {key_len}): {candidate.hex()}")
return candidate
return None
# 복구 방법 3: NoMoreRansom에서 기존 복호화 도구 확인
# https://www.nomoreransom.org/en/decryption-tools.html
6단계: 암호화 분석 문서화
분석 결과를 구조화된 보고서로 정리:
분석에는 다음 사항을 문서화해야 함:
- 식별된 알고리즘 (AES, RSA, ChaCha20, 사용자 정의)
- 키 크기 및 동작 모드 (CBC, CTR, ECB, GCM)
- 키 생성 방법 (CSPRNG, 예측 가능한 시드, 정적 키)
- 키 저장 위치 (파일에 추가, 레지스트리, C2 전송)
- 파일 수정 패턴 (전체 암호화, 부분 암호화, 헤더만)
- 대상 파일 확장자
- 랜섬노트 형식 및 결제 인프라
- 복호화 가능성 평가 (가능/불가능/부분 가능)
- 권장 복구 접근 방식
주요 개념
| 용어 | 정의 |
|---|---|
| 하이브리드 암호화 | 빠른 파일 암호화를 위한 대칭 키(AES)와 안전한 키 래핑을 위한 비대칭 키(RSA)를 결합한 표준 랜섬웨어 접근 방식 |
| 키 래핑 | 파일별 대칭 키를 공격자의 RSA 공개 키로 암호화하여 공격자의 개인 키만 복호화할 수 있게 하는 기술 |
| ECB 모드 | 전자 코드북 모드로 각 블록을 독립적으로 암호화하여 평문 패턴을 유지하며, 부분 복구가 가능한 중요한 취약점 |
| 알려진 평문 공격 | 알려진 원본 파일과 암호화된 버전을 사용하여 암호화 키를 유도하는 공격으로, XOR 및 약한 스트림 암호에 효과적 |
| 키 스케줄 | 메모리 내 AES 키의 확장된 형태로, 삭제되기 전에 메모리 덤프에서 스캔하여 키를 복구할 수 있음 |
| CSPRNG | 암호학적으로 안전한 의사 난수 생성기로, CryptGenRandom을 사용하는 랜섬웨어는 예측 불가능한 키를 생성함 |
| 부분 암호화 | 일부 랜섬웨어는 속도를 위해 처음 N 바이트 또는 N번째 블록만 암호화하여, 암호화되지 않은 부분이 복구에 도움이 될 수 있음 |
도구 및 시스템
- Ghidra: 어셈블리 수준에서 랜섬웨어 암호화 루틴을 분석하기 위한 역공학 스위트
- PyCryptodome: 복호화 루틴 구현 및 테스트를 위한 Python 암호화 라이브러리
- NoMoreRansom.org: Europol과 보안 벤더가 유지 관리하는 알려진 랜섬웨어 패밀리용 무료 복호화 도구 저장소
- Volatility: 감염된 시스템의 RAM 덤프에서 암호화 키를 추출하기 위한 메모리 포렌식 프레임워크
- CryptoTester: 상수 및 코드 패턴을 기반으로 암호화 알고리즘을 식별하는 도구
일반적인 시나리오
시나리오: 랜섬웨어 사고에 대한 복호화 가능성 평가
상황: 조직이 파일 서버를 암호화하는 랜섬웨어에 감염되었습니다. 경영진은 복구 결정을 내리기 전에 몸값을 지불하지 않고 복호화가 가능한지 알아야 합니다.
접근 방식:
- 랜섬노트, 파일 확장자 및 샘플 해시로 랜섬웨어 패밀리 식별 (ID Ransomware 확인)
- NoMoreRansom.org에서 이 패밀리에 대한 기존 무료 복호화 도구 확인
- Ghidra에서 암호화 루틴을 역공학하여 알고리즘 및 키 관리 식별
- 구현 취약점 테스트 (키 재사용, 예측 가능한 시드, ECB 모드)
- 사고 당시 PCAP에서 C2로의 키 전송이 캡처되었는지 확인 (암호화 전에 키가 전송된 경우)
- 영향을 받은 시스템의 메모리 덤프에서 RAM 내 AES 키 스케줄 스캔
- 결과 보고: 복호화 가능/불가능 및 구체적인 기술적 근거 제시
주의사항:
- 암호화된 파일의 유일한 복사본에서 복호화 방법을 테스트하는 경우 (항상 복사본에서 작업)
- 확인 없이 모든 파일이 동일한 키를 사용한다고 가정하는 경우 (일부 랜섬웨어는 파일별 키 사용)
- 랜섬웨어가 삭제에 실패한 볼륨 섀도 복사본(vssadmin)을 확인하지 않는 경우
- 보고서에서 파일 암호화 알고리즘과 키 래핑 알고리즘을 혼동하는 경우
출력 형식
랜섬웨어 암호화 분석
================================
샘플: lockbit3.exe
패밀리: LockBit 3.0 / LockBit Black
SHA-256: abc123def456...
암호화 방식
파일 암호: AES-256-CTR (파일별 고유 키)
키 래핑: RSA-2048 (바이너리에 내장된 공개 키)
키 생성: CryptGenRandom (CSPRNG - 예측 불가능)
IV 생성: 파일당 랜덤 16바이트
파일 구조: [암호화된 데이터][RSA 암호화 키(256B)][IV(16B)][매직(8B)]
대상 확장자
총계: 412개 확장자 대상
범주: 문서(.doc, .xls, .pdf), 데이터베이스(.sql, .mdb),
아카이브(.zip, .7z), 소스 코드(.py, .java, .cs)
제외: .exe, .dll, .sys, .lnk (시스템 파일 보존)
구현 분석
키 강도: 강력함 - 파일별 랜덤 키, 재사용 없음
모드 보안: 강력함 - 고유 nonces 사용 CTR 모드
키 저장: RSA 암호화 키가 각 파일에 추가
섀도 복사본: vssadmin 및 WMI를 통해 삭제됨
복호화 가능성
키 없이: 불가능
- 식별된 구현 결함 없음
- RSA-2048 키 래핑이 무차별 대입 방지
- CSPRNG가 키 예측 방지
- 기존 무료 복호화 도구 없음
복구 옵션
1. 오프라인 백업에서 복원 (권장)
2. 볼륨 섀도 복사본 확인 (낮은 가능성 - 랜섬웨어가 삭제함)
3. 시스템이 재부팅되지 않은 경우 메모리 포렌식 (키가 RAM에 남아 있을 수 있음)
4. 공격자와 협상 (최후의 수단 - 복호화 보장 없음)