랜섬웨어 분석 보고서

Direwolf 랜섬웨어 분석 보고서

geonwoo9643 2026. 2. 9. 19:31

1. 개요 (Overview)


1.1 분석 배경

Direwolf 랜섬웨어는 2025년 5월 처음 발견된 Golang 기반 폐쇄형(Closed-Group) 랜섬웨어로, Curve25519 기반 Diffie-Hellman 키 교환 및 ChaCha20 스트림 암호화를 통해 파일을 암호화합니다.
파일 확장자는 .direwolf로 변경되며, 데이터 암호화 + 유출 협박(이중 갈취, double extortion)을 수행합니다.


1.2 핵심 요약

  • 암호화 체인 : ECDH (Curve25519) + SHA-256 (2회) + ChaCha20
  • 키 생성 : 파일별 독립적인 임시 ECDH 키쌍 (RtlGenRandom)
  • 최적화 : 1MB 초과 파일은 첫 1MB만 암호화
  • 푸터 구조 : 38바이트 (임시 공개키 32B + 시그니처 6B)

 

암호화 메커니즘

 

  • Curve25519 기반 ECDH로 파일별 공유 비밀 생성
  • 공유 비밀 → SHA-256 해시 → 세션 키 도출
  • 도출된 세션 키로 ChaCha20 스트림 암호화 수행

 

 

암호화 워크플로우

// Step 1: 임시 ECDH 키쌍 생성 (파일별)
temp_privkey = RtlGenRandom(32 bytes)
temp_pubkey = Curve25519.ScalarBaseMult(temp_privkey)

// Step 2: 공유 비밀 생성
shared_secret = Curve25519.ScalarMult(temp_privkey, attacker_pubkey)

// Step 3: 키 파생 (이중 SHA-256)
hash1 = SHA256(shared_secret)          // ChaCha20 키
hash2 = SHA256(hash1)                  // Nonce 추출용

chacha20_key = hash1[0:32]
chacha20_nonce = hash2[10:22]          // 12바이트

// Step 4: 파일 암호화
ciphertext = ChaCha20(plaintext, chacha20_key, chacha20_nonce)

// Step 5: 메타데이터 추가
footer = temp_pubkey (32B) + signature (6B: 0xABBCCDDEEFF0)

 

2. 식별 정보 (Identification)


항목
Malware Family Direwolf Ransomware
Filetype PE64 (Windows 64-bit Executable, Golang)
Hash (SHA256) 7f877830ebafb0b809b96bac7baf4435e235ab7835f695006ff779e6178c3638
Hash (MD5) bc6912c853be5907438b4978f6c49e43
Size 2,096,128 bytes (0x1ffc00)
Extension .direwolf
Target Windows 기반 시스템
Ransom Note README_TO_DECRYPT.txt

2.1 패킹 분석 (Packing Analysis)

항목
Packer UPX 3.96 (Ultimate Packer for eXecutables)
Original Size ~6.2 MB (언패킹 전 추정)
Packed Size 2,096,128 bytes (0x1ffc00)
압축률 약 66.2%
Entropy 7.4+ (높은 엔트로피 - 패킹 지표)

 

2.1.1 UPX 탐지 시그니처

- PE 섹션명 : UPX0, UPX1

- Import Table : 최소화됨

- Entry Point : UPX stub

 

2.1.2 언패킹 절차

1. UPX 탐지 : 'upx -t sample.exe'

2. 자동 언패킹 : 'upx -d sample.exe -o unpacked.exe'

3. IDA 분석 : 언패킹된 바이너리 로드

 

2.2 최초 실행 검사

  • Mutex : Global\direwolfAppMutex
  • 탐지 마커 파일 : C:\runfinish.exe (이미 암호화된 시스템을 체크)
    ⇒ 둘 중 하나가 존재하면 자기 삭제 + 종료.

 

3. 분석 환경 및 도구 (Tools)


구분 도구명 (Tool) 용도 (Purpose)
정적 분석 IDA Pro Golang 바이너리 디컴파일 및 암호화 로직 분석
동적 분석 API Monitor RtlGenRandom API 후킹 및 키 추출
행위 분석 Process Monitor 파일 I/O 및 암호화 행위 추적
암호 분석 Python + cryptography ChaCha20 복호화 스크립트 개발
라이브러리 golang.org/x/crypto Curve25519, ChaCha20 구현 확인

 

4. 암호화 기술 분석 (Technical Analysis)


4.1 전체 암호화 워크플로우

[실행 단계]

1. 초기화
   - Windows CSP 획득
   - RtlGenRandom 초기화
   - 공격자 공개키 로드 (0x5E12E0)
   ↓

2. 파일 탐색
   - 재귀적 디렉터리 스캔
   - .direwolf, .exe, .dll 제외
   - 고루틴으로 병렬 처리
   ↓

3. 파일별 암호화
   [Step 1: 임시 키쌍 생성]
   temp_privkey = RtlGenRandom(32 bytes)
   temp_pubkey = Curve25519
   ↓
   
   [Step 2: ECDH 공유 비밀]
   shared_secret = Curve25519.ScalarMult(temp_privkey, attacker_pubkey)
   ↓
   
   [Step 3: 키 파생]
   hash1 = SHA256(shared_secret)
   hash2 = SHA256(hash1)
   chacha20_key = hash1[0:32]
   chacha20_nonce = hash2[10:22]
   ↓
   
   [Step 4: ChaCha20 암호화]
   if file_size > 1MB:
       encrypt_size = 1MB
   else:
       encrypt_size = file_size
   
   ciphertext = ChaCha20(plaintext, chacha20_key, chacha20_nonce)
   ↓
   
   [Step 5: 푸터 추가]
   footer = temp_pubkey (32B) + signature (0xABBCCDDEEFF0, 6B)
   ↓

4. 파일명 변경
   - 원본.확장자 → 원본.확장자.direwolf
   - README_TO_DECRYPT.txt 생성

4.2 키 생성 및 관리 (Key Generation)

4.2.1 임시 키쌍 생성 (Per-File)

v185 = runtime_makeslice(&RTYPE_uint8, 32, 32, ...);
io_ReadAtLeast(qword_5FDF30, qword_5FDF38, v185, 32, 32, 32, ...);
// → RtlGenRandom으로 32바이트 생성 (임시 개인키)

 

특징:

  • Windows RtlGenRandom (SystemFunction036) API 사용
  • CSPRNG (Cryptographically Secure PRNG)
  • 각 파일마다 독립적인 32바이트 개인키

 

4.2.2 공격자 공개키

// 임시 공개키 생성
golang_org_x_crypto_curve25519_scalarBaseMult(
    v176,           // output: 임시 공개키 (32 bytes)
    v177,           // input: 임시 개인키 (32 bytes)
    ...
);

// 공유 비밀 생성
golang_org_x_crypto_curve25519_scalarMult(
    v174,           // output: 공유 비밀 (32 bytes)
    v177,           // input: 임시 개인키 (32 bytes)
    &unk_5E12E0,    // input: 공격자 공개키 (32 bytes)
    ...
);
수학적 원리:

G: Curve25519 생성점

임시 공개키 = G × 임시_개인키
공유 비밀 = 공격자_공개키 × 임시_개인키
= (G × 공격자_개인키) × 임시_개인키
= G × (공격자_개인키 × 임시_개인키)

4.4 키 파생 (Key Derivation)

4.4.1 이중 SHA-256

// 첫 번째 SHA-256
crypto_sha256_Sum256(v174, 32, 32, ...);  // input: 공유 비밀
*(_OWORD *)v179 = v120;                    // output: hash1

// 두 번째 SHA-256
crypto_sha256_Sum256(v179, 32, 32, ...);  // input: hash1
v178[0] = v121;                            // output: hash2
v178[1] = v146;

 

키 파생 과정:

hash1 = SHA256(shared_secret)      # 32 bytes → ChaCha20 Key
hash2 = SHA256(hash1)               # 32 bytes → Nonce 추출용

chacha20_key = hash1[0:32]          # 전체 32바이트
chacha20_nonce = hash2[10:22]       # 오프셋 10부터 12바이트

 

왜 두 번 해시하는가?

  • 키 분리 (Key Separation) : 암호화 키와 Nonce를 독립적으로 파생
  • 추가 엔트로피 : 보안성 강화
  • HKDF 대체 : 간단한 구현

4.5 ChaCha20 암호화

4.5.1 ChaCha20 초기화

golang_org_x_crypto_chacha20_newUnauthenticatedCipher(
    &v181,              // cipher 객체
    v179,               // 키: hash1 (32 bytes)
    32,
    32,
    v178 + 10,          // Nonce: hash2[10:22] (12 bytes)
    12,
    ...
);
파라미터:
Algorithm: ChaCha20 (20 rounds)
Key Size:  256 bits (32 bytes)
Nonce Size: 96 bits (12 bytes)
Counter:   32 bits (4 bytes, 초기값 0)

 

4.5.2 파일 크기 기반 최적화

v76 = v172;                    // v172 = 원본 파일 크기
if (v172 > 0x100000)          // 1MB = 1,048,576 bytes
    v76 = 0x100000;           // 1MB로 제한
v171 = v76;                   // 실제 암호화 크기
파일 크기 암호화 범위
≤ 1MB 전체 파일
> 1MB 첫 1MB만

4.6 파일 처리 아키텍처

4.6.1 암호화된 파일 구조

영역 크기 설명
암호화된 데이터 원본 크기 (max 1MB) ChaCha20으로 암호화
Footer (38 bytes)    
└─ 임시 공개키 32 bytes 복호화 시 필요
└─ 시그니처 6 bytes 0xABBCCDDEEFF0 (식별용)

 

4.6.2 메타데이터 추가

// 1. 임시 공개키 쓰기
os__ptr_File_WriteAt(
    v183,           // file handle
    v176,           // 임시 공개키 (32 bytes)
    32, 32,
    v172,           // offset: 파일 크기
    ...
);

// 2. 시그니처 쓰기
v169 = -556942165;  // 0xDECDBCAB (LE) → 0xABBCCDDE
v170 = -3857;       // 0xF0EF (LE) → 0xEFF0

os__ptr_File_WriteAt(
    v183,
    &v169,          // 시그니처 (6 bytes)
    6, 6,
    v172 + 32,      // offset: 파일 크기 + 32
    ...
);

4.7 정적 분석

runtime.main 함수

  • nanotime 초기화 (프로그램 시작 시간 기록)

 

  • 런타임 초기화

 

  • 가비지 컬렉터 활성화

 

  • main.main 함수 호출 (랜섬웨어 실행 시작점)

 

  • 프로그램 종료 전 정리 작업

 

  • 프로세스 종료

 

main.main 함수

  • 명령줄 인자 파싱 (-p 옵션 : 프로세스 경로)

 

  • 중복 실행 체크

 

  • 완료 마커 파일 존재 여부 확인 (C:\BGDKecuqlc2d9akd.exe)

 

  • 뮤텍스 열기 시도 (Global\BGDKecuqlc2d9akd)

 

  • 고루틴 시작

Close_Service : 백업/보안 서비스 종료
Close_Log : 로그 서비스 종료
Clear_Recover : 섀도우 카피/백업 삭제
Close_Process : DB/백업 프로세스 종료
main.main.func1 -> TraverseDisk : 디스크 순회 및 암호화
main.main.func2 : 암호화 진행 상황 모니터링 - 주기적 로깅

 

  • 2초 대기 (서비스 종료 시간 확보)

 

  • WaitGroup 생성 (병렬 암호화 동기화용)

 

  • 시스템 강제 재부팅 (10초 후)

 

  • 랜섬웨어 실행 파일 삭제 (3초 후)

 

  • 뮤텍스 해제

 

Close_Service 함수 (백업/보안 서비스 종료)

 

Close_Log 함수 (로그 서비스 종료)

 

Clear_Recover 함수 (섀도우 카피/백업 삭제)

 

Close_Process 함수 (DB/백업 프로세스 종료)

 

main.main.func1 함수 (Traverse_Disk 함수 호출)

 

TraverseDisk 함수 (디스크 순회 및 암호화)

 

main.Traverse_Files.func1 함수 (파일/디렉터리 방문)

 

  • 암호화 제외

 

  • 랜섬노트 내용

 

Check_Pass 함수 (암호화 제외 확장자 필터링)

 

main.main.func2 함수 (암호화 진행 상황 모니터링 - 주기적 로깅)

 

main.main.func3 함수 (암호화 함수 호출)

 

encryptFile (파일 암호화)

  • 푸터 시그니처 생성

 

  • RtlGenRandom 호출 (32바이트 임시 개인키 생성)

 

  • Curve25519 연산 (G * 임시 개인키) -> 32바이트 임시 공개키 생성

 

  • Curve25519 연산 (임시 개인키, 공격자 공개키) -> ECDH 공유 비밀 생성

 

  • 공격자 Curve25519 공개키 (32바이트)

 

  • .direwolf 확장자 추가

 

  • 파일 열기 (쓰기 권한)

 

  • SHA-256 Hash (ECDH 공유 비밀) -> ChaCha20 암호화 키 (32바이트)

 

  • SHA-256 Hash (ChaCha20 암호화 키) -> ChaCha20 Nonce 추출용 (32바이트)

 

  • ChaCha20 스트림 암호 초기화 (32바이트 키, 12바이트 Nonce)

 

  • 파일 크기 검사 (1MB 초과 시, 첫 1MB만 암호화) -> 부분 암호화

 

  • ChaCha20 KeyStream XOR 연산 (파일 데이터 암호화)

 

  • 푸터 쓰기 (임시 공개키 32바이트 + 시그니처 6바이트)

 

5. 복호화 기술 분석 (Decryption Strategy)


5.1 복호화 가능성 분석

5.1.1 복호화 가능 조건

요소 획득 방법
공격자 공개키 정적 분석
임시 개인키 (파일별) RtlGenRandom API 후킹
암호화 알고리즘 정적 분석

 

5.1.2 복호화 가능 시나리오

시나리오 1: RtlGenRandom 후킹 (권장)
───────────────────────────
방법: 암호화 진행 중 API Monitor로 32바이트 페이로드 캡처

시나리오 2: 메모리 덤프
───────────────────────────
방법: 암호화 실행 중 프로세스 메모리 덤프
대상: RtlGenRandom 호출 직후 32바이트 패턴

시나리오 3: 브루트포스
───────────────────────────
방법: 후킹 로그의 모든 32바이트 후보 시도
시간 복잡도: O(n), n = RtlGenRandom 호출 횟수

5.2 복호화 체인

5.2.1 전체 복호화 프로세스

[입력 데이터]
1. 암호화된 파일 (.direwolf)
2. RtlGenRandom 후킹 로그 (JSON)
3. 공격자 공개키

↓

[Step 1: 푸터 파싱]
ciphertext = file[:-38]
temp_pubkey = file[-38:-6]
signature = file[-6:]

↓

[Step 2: ECDH 공유 비밀 재생성]
for each candidate_privkey in hooking_log:
    shared_secret = Curve25519.ScalarMult(candidate_privkey, attacker_pubkey)
    
↓

[Step 3: 키 파생]
hash1 = SHA256(shared_secret)
hash2 = SHA256(hash1)
chacha20_key = hash1[0:32]
chacha20_nonce = hash2[10:22]

↓

[Step 4: ChaCha20 복호화]
plaintext = ChaCha20(ciphertext, chacha20_key, chacha20_nonce)

↓

[Step 5: 파일 시그니처 검증]
if plaintext.startswith(JPEG_MAGIC):  # 0xFFD8FF
    extension = '.jpg'
elif plaintext.startswith(PDF_MAGIC):  # 0x25504446
    extension = '.pdf'

5.3 자동화 복호화 스크립트

5.3.1 핵심 복호화 로직

import json
import hashlib
from cryptography.hazmat.primitives.asymmetric import x25519
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat

ONE_MB = 0x100000  # 1MB 기준 (부분 암호화 구간)

def decrypt_direwolf(encrypted_file, hooking_json, attacker_pubkey):
    """
    Direwolf 복호화
    1) footer에서 temp_pubkey 추출
    2) RtlGenRandom(32B) 후보 수집
    3) pubkey 일치하는 키만 사용
    4) ChaCha20 복호화 (부분 암호화 반영)
    """

    # 파일 읽기 및 footer 분리
    with open(encrypted_file, 'rb') as f:
        data = f.read()

    if len(data) < 38:
        return None

    body = data[:-38]
    temp_pubkey_from_file = data[-38:-6]
    signature = data[-6:]

    # 시그니처 확인
    if signature != bytes.fromhex("abbccddeeff0"):
        return None

    # 부분 암호화 길이 계산 (앞 1MB만 암호화된 경우 대비)
    enc_len = min(len(body), ONE_MB)

    # Hooking 로그에서 32바이트 난수 후보 수집
    with open(hooking_json, 'r') as f:
        log = json.load(f)

    candidates = [
        e.get('payload_hex')
        for e in log.get('events', [])
        if e.get('api') == 'RtlGenRandom'
        and e.get('length_bytes') == 32
        and e.get('payload_hex')
    ]

    attacker_pk = x25519.X25519PublicKey.from_public_bytes(
        bytes.fromhex(attacker_pubkey)
    )

    for temp_privkey_hex in candidates:
        try:
            temp_sk = x25519.X25519PrivateKey.from_private_bytes(
                bytes.fromhex(temp_privkey_hex)
            )

            # footer의 temp_pubkey와 일치하는 후보만 사용
            cand_pub = temp_sk.public_key().public_bytes(
                Encoding.Raw, PublicFormat.Raw
            )
            if cand_pub != temp_pubkey_from_file:
                continue

            # ECDH → 키/nonce 파생
            shared_secret = temp_sk.exchange(attacker_pk)
            key = hashlib.sha256(shared_secret).digest()
            nonce = hashlib.sha256(key).digest()[10:22]

            # 앞 enc_len만 복호화 (부분 암호화 대응)
            dec_head = chacha20_decrypt(key, nonce, body[:enc_len])
            plaintext = dec_head + body[enc_len:]

            # 복호화 성공 확인
            if check_file_signature(plaintext):
                return plaintext

        except (ValueError, TypeError):
            continue

    return None

 

5.3.2 ChaCha20 구현

def chacha20_block(key, counter, nonce):
    """ChaCha20 블록 생성"""
    constants = b"expand 32-byte k"
    
    state = [0] * 16
    state[0:4] = struct.unpack("<4I", constants)
    state[4:12] = struct.unpack("<8I", key)
    state[12] = counter
    state[13:16] = struct.unpack("<3I", nonce)
    
    working = state[:]
    
    # 20 rounds
    for _ in range(10):
        # Column rounds
        quarter_round(working, 0, 4, 8, 12)
        quarter_round(working, 1, 5, 9, 13)
        quarter_round(working, 2, 6, 10, 14)
        quarter_round(working, 3, 7, 11, 15)
        # Diagonal rounds
        quarter_round(working, 0, 5, 10, 15)
        quarter_round(working, 1, 6, 11, 12)
        quarter_round(working, 2, 7, 8, 13)
        quarter_round(working, 3, 4, 9, 14)
    
    output = [(working[i] + state[i]) & 0xffffffff for i in range(16)]
    return b"".join(struct.pack("<I", w) for w in output)

5.4 무결성 검증

5.4.1 파일 시그니처 테이블

파일 유형 매직 넘버 (Hex)
JPEG FF D8 FF E0/E1/DB
PNG 89 50 4E 47 0D 0A 1A 0A
PDF 25 50 44 46
ZIP/DOCX 50 4B 03 04
EXE 4D 5A

 

5.4.2 검증 로직

def check_file_signature(data):
    """복호화된 데이터 검증"""
    signatures = {
        b'\\xFF\\xD8\\xFF': ".jpg",
        b'\\x89PNG': ".png",
        b'%PDF': ".pdf",
        b'PK\\x03\\x04': ".zip",
        b'MZ': ".exe"
    }
    
    for sig, ext in signatures.items():
        if data.startswith(sig):
            return ext
    
    return None

 

6. 요약 및 결론 (Conclusion)


6.1 최종 평가

Direwolf 랜섬웨어는 ECDH (Curve25519) + SHA-256 (2회) + ChaCha20을 사용하는 현대적인 하이브리드 암호화 구조를 가지고 있습니다. Golang으로 개발되어 크로스 플랫폼 지원이 가능하며, 파일별 독립 키 생성과 1MB 임계값 기반 부분 암호화로 성능을 최적화했습니다. RtlGenRandom API 후킹을 통해 임시 개인키를 획득하면 완전한 복호화가 가능합니다. 테스트 결과 모든 파일에서 매직 바이트가 검증되었으며, 파일 무결성이 확인되었습니다.


6.2 대응 가이드

6.2.1 예방적 조치

 

API 모니터링

  • RtlGenRandom 과도한 호출 탐지
  • 32바이트 요청 패턴 감지
  • 실시간 API 후킹 로깅

 

행위 기반 탐지

  • 대량 파일 접근 패턴
  • .direwolf 확장자 추가 차단
  • 고루틴 생성 패턴 탐지

 

네트워크 방어

  • Tor 네트워크 접근 차단
  • C&C 통신 탐지
  • DNS 필터링

 

백업 전략

  • 실시간 오프라인 백업
  • VSS (Volume Shadow Copy) 보호
  • Immutable 백업 스토리지

 

6.2.2 사후 대응

  1. 초동 조치
    • 감염 시스템 격리
    • 즉시 RtlGenRandom API 후킹 시작 (시스템 종료 금지!)
    • 프로세스 메모리 덤프
  2. 키 복구
    • API Monitor 로그에서 32바이트 후보 추출
    • 메모리 덤프 분석 (높은 엔트로피 32바이트 패턴)
  3. 복호화 수행
    • 자동화 스크립트 실행
    • 브루트포스 (후보 키 시도)
    • 매직 바이트 검증
  4. 시스템 복구
    • 복호화 성공 후 시스템 재구축
    • 초기 침투 경로 분석
    • 보안 패치 적용

6.3 기술적 특징 요약

항목 세부 내용
키 교환 ECDH (Curve25519, 256-bit)
키 파생 SHA-256 (2회)
대칭 암호 ChaCha20 (256-bit key, 96-bit nonce)
키 생성 RtlGenRandom (CSPRNG)
최적화 1MB 초과 시 부분 암호화
푸터 38 bytes (임시 공개키 + 시그니처)
확장자 .direwolf
랜섬 노트 README_TO_DECRYPT.txt
언어 Golang 1.x

6.4 결론

Direwolf 랜섬웨어는 강력한 암호학적 설계를 가지고 있으나, RtlGenRandom API가 노출된 취약점으로 인해 동적 분석을 통한 복호화가 가능합니다. 공격자의 공개키가 바이너리에 하드코딩되어 있고, 파일별 임시 개인키가 Windows API를 통해 생성되므로 API 후킹으로 완전한 키 체인을 복구할 수 있습니다.

  • 암호학 알고리즘 : Curve25519 + ChaCha20
  • 구현 취약점 : RtlGenRandom 후킹 가능

 

부록


YARA 룰

1) Direwolf 랜섬웨어 바이너리 탐지(PE)

rule Ransomware_Direwolf_Golang_UPX_AttackerPubkey
{
  strings:
    // attacker Curve25519 pubkey (32B) from report
    $attacker_pk = { c5 9e 3a d3 3a 79 dd 6e 0f 1b f2 28 63 6b a4 76 ba ff 99 3e 82 d1 1c 7e 51 f2 71 2c fd 23 0c 1f }

    // common artifacts
    $ext  = ".direwolf" ascii wide
    $note = "README_TO_DECRYPT.txt" ascii wide

    // UPX section names are often present in packed samples
    $upx0 = "UPX0" ascii
    $upx1 = "UPX1" ascii

  condition:
    uint16(0) == 0x5A4D and          // MZ
    filesize < 15MB and
    $attacker_pk and
    (1 of ($ext, $note)) and
    (1 of ($upx0, $upx1))
}

 

2) 암호화된 파일(.direwolf) 아티팩트 탐지 (footer 시그니처)

rule Ransomware_Direwolf_EncryptedFile_FooterSignature
{
  strings:
    $sig = { AB BC CD DE EF F0 }  // footer signature (6B)

  condition:
    filesize > 38 and
    $sig at (filesize - 6)
}