랜섬웨어 분석 보고서

Beast 랜섬웨어 암호 및 복호화 기술 분석 보고서

zeusec 2025. 12. 19. 01:29

1. 랜섬웨어 개요

  • Beast는 2022년 3월에 등장한 Monster랜섬웨어에서 진화한 RaaS의 일종으로 2025년 2월 Monster의 후속 그룹으로 공식 등장했다.
  • Beast 랜섬웨어의 초기 침해 방법은 피싱 이메일과 취약한 원격 데스크톱 프로토콜(RDP) 접속을 통해 침입하고 2024년 이메일 저작권 위반 통지나 이력서로 위장해 배포하는 사례가 있었으며 SMB 포트를 탐색하여 같은 네트워크 내 취약한 공유 폴더를 찾아 네트워크 내에 확산시키는 기능이 확인됐다.
  • 25년 7월에는 Tor기반 데이터 유출 사이트를 만들어 미국 유럽 아시아 라틴 아메리카 16개 피해 조직을 공개했으며 Beast와 코드가 거의 동일한 Boramae 변종이 발생하여 OpenSSL을 정적으로 링크해 기능 수와 문자열 암호화가 더 복잡해졌다.

2. 식별 정보

  • Malware Family: Beast
  • Sample Hash
    • MD5: 059ac4569026c1b74e541d98b6240574
    • SHA1: 2a9c036ed1f2a86bec63ead2f2d2e6412faf6ada
    • SHA256: 479d0947816467d562bf6d24b295bf50512176a2d3d955b8f4d932aea2378227
  • Collection Path: Malware Bazaar(Sample Link)
  • Target: Window 기반 시스템

3. 분석 환경

  • 사용 분석 도구: IDA, xdbg64, VMware(Window 11)

4. 암호화 기술 분석

  • ida에서 Beast 랜섬웨어가 AV의 문자열 패턴 매칭으로 악성코드 탐지를 회피하기 위해 rdata가 0x55 XOR 연산으로 난독화 돼 있음을 확인

[그림 1] 난독화된 문자열 (sub_4082A5)

 

4.1 암호화 실행 환경 CSP(Crypto Service Provider) 확인 함수

  • Windows XP ~ 11 여러 환경에서 CSP가 존재하는지 확인하여 원활한 암호화 작업을 수행하기 위한 함수가 존재함
  • 만약 CSP가 존재하지 않아 CryptGenRandom을 사용할 수 없다면 하드 코딩된 MT19937 난수 생성기를 이용해 난수 생성

[그림 2] CSP확인하여 암호화 핸들 설정 함수(sub_4082A5)
[그림 3] MIT19937 난수 생성기 초기 셋팅 설정 (sub_408423)
[그림 4] MIT19937 난수 생성 코드(sub_408519)

4.2 default.key 파일 생성(공개키 파일)

  • CryptGenRandom으로 생성한 32byte 난수값과 default.key 공개키 파일 32byte를 사용해 Curve25519 계산하기 위해 default.key 파일 생성
  • default.key 파일 내용 생성 과정은 CryptoGenRandom(32) → ECDH로 key 가공 SHA-512로 64byte 해싱 → ChaCha20 암호화 수행하여 default.key 파일에 쓰기

[그림 5] v14에 난독화된 default.key 문자열 사용 확인 (sub_40283B)
[그림 6] default.key 파일 내용 생성과정(sub_40283B)

  • 만약 CryptoGenRandom을 통해 32byte 키가 만들어지지 않았다면 원할한 암호화를 위해 MT19937 난수 생성기를 사용해서 32byte 난수값 생성

[그림 7] CryptoGenRandom을 이용해 32byte 난수값 생성(sub_4048D1)
[그림 8] Curve25519로 난수값 가공 (sub_4055D8)
[그림 9] SHA512 을 이용해 32byte 난수값 64byte 해싱 (sub_40688B)
[그림 10] byte_4018A0 SHA-512 IV 메모리 값 (sub_40688B)

  • ChaCha20 state 구성 방식 확인 가능 (sub_4027EC → sub_4026F8)

[그림 12] 실제 생성된 default.key 생성 파일

4.3 실제 파일 암호화 수행

  • 암호화할 파일 별로 ChaCha키를 생성한 뒤 파일의 크기에 따라 파일 데이터 암호화를 수행한다.

[그림 13] 현재 시스템 코어 개수 * 6개 만큼의 암호화 스레드 생성(sub_40901E)

  • byte_412E98에 저장된 default.key의 공격자 공개키 32바이트를 CryptGenRandom으로 생성한 32바이트 난수값(v128)과 Curve25519 ECDH 계산하고, 그 결과(v127)와 v128로 생성한 임시 공개키(v119)를 연결하여 SHA-512 해싱한 64바이트 값의 첫 32바이트는 ChaCha20 키로, 그 다음 12바이트는 Nonce로 사용하여 키스트림을 생성하고 카운터는 0부터 시작됨

[그림 14] 암호화에 사용하는 실제 키 생성 로직(sub_402C17)
[그림 15] default.key에서 Load되는 32byte 값
[그림 16] 파일 크기별 분기점(sub_402C17)

  • sub_40273E에서 생성된 KeyStream과 XOR로 암호화(a1=ChaCha20 상태, a2=데이터 포인터, a3=길이)

[그림 17] 대용량(1GB 이상) 파일 암호화 로직(sub_402C17)

  • sub_40273E에서 생성된 KeyStream과 XOR로 암호화 (a1=ChaCha20 상태, a2=데이터 포인터, a3=길이)

[그림 17] 소용량(1GB 이하) 파일 암호화 로직(sub_402C17)
[그림 18] ChaCha20 XOR 암호화 함수(sub_40273E)

4.4 Key 생성 및 스트림 동적 분석

  • xdbg64를 이용해 암호화된 파일(dog.png)을 사용해서 검증

[그림 19] 검증에 사용할 default.key 파일

21 7B 62 43 CF E8 03 E5 70 A4 DC 43 28 EC 4C 3B
41 88 AE E6 DC 38 57 1A 50 E1 97 DF B0 31 9C 39
  • 상세 동적 분석 및 검증의 경우 부록 7.1 참고

1. default.key 32byte 값과 CryptGenRandom 값을 Curve25519

  • CryptoGenRandom
  • 27305B15D5B43462A2B13795400F90FF20DA7D295919FCD3F61BDDC9905BEA47
  • default.key 32byte
  • 217B6243CFE803E570A4DC4328EC4C3B4188AEE6DC38571A50E197DFB0319C3
  • 검증 파이썬 코드
더보기
# Python (requires cryptography)
from cryptography.hazmat.primitives.asymmetric import x25519

# hex으로 주어진 예시: priv(32)와 pub(32)
priv_hex = "DC004E20BA3DE112EBEC620E4DB89EE0F4C460F528B86603A52738C354647F4B"  # 예: 너가 사용한 값(32B)
pub_hex  = "C02D7FCC5EC0AE6BA33F78A54B4A9BE3BCAAD9AF1F01B12B0877BBE48D2EB26A"  # a3에 들어간 32바이트 공개값

priv = bytes.fromhex(priv_hex)
pub  = bytes.fromhex(pub_hex)

# 라이브러리는 내부적으로 클램프를 적용하므로 raw 32바이트를 그대로 넣으면 됨
sk = x25519.X25519PrivateKey.from_private_bytes(priv)
pk = x25519.X25519PublicKey.from_public_bytes(pub)
shared = sk.exchange(pk)
print(shared.hex())

출력값: 6a641fc7b1f5c453aac969b07f54d4e775a429442b202e02efd47d3d47e3d66d

 

2. 이 값을 sha512 해싱(앞의 32바이트를 ChaCha20 키로, 그 뒤 16바이트를 IV)

  • 검증 스크립트
더보기
powershell -command "[BitConverter]::ToString((New-Object Security.Cryptography.SHA512Managed).ComputeHash([byte[]]('1e14bc33ff07526ebc5a7d64e34862e82bc910003fdf1a109f549d5f208f923f' -split '(..)' | ? {$_} | % { [Convert]::ToByte($_,16) }))).Replace('-','').ToLower()"

출력값: ce52f2f2d57f2875699e2a0bb73805bfae44cd9b84aa0d4f65d95dd4802e31efec7c3afb76622f43e6d1b065e649ba63a17b5df13f3069a438adbaeca2092029

  • 키: CE52F2F2D57F2875699E2A0BB73805BFAE44CD9B84AA0D4F65D95DD4802E31EF
  • IV: EC7C3AFB76622F43E6D1B065

3. 예상 키스트림

268284D81AE0AE876DBD2C37BD40C9940E01CF68F9A022A9EF8C52091AFC62B344D2BEDE64FF2AE3DB0AB872F735A1CAE924D62A0A32580BE9414CEF545020EC

 

[그림 20] 검증 성공 xdbg 메모리 덤프

4.5 암호화 흐름도

[그림 21] 암호화 흐름도

5. 복호화 기술 분석

5.1 공격자 공개키 및 CryptGenRandom 로딩

  • default.key 파일 구조에서 0x20 ~ 0x3F(32byte) 공격자의 Curve25519 공개키 파싱
pub_key_bytes = raw_key_bytes[PUBLIC_KEY_OFFSET : PUBLIC_KEY_OFFSET + PUBLIC_KEY_SIZE]
pk = x25519.X25519PublicKey.from_public_bytes(pub_key_bytes)
  • 후킹 로그에서 CryptGenRandom 값 파싱
candidates = [
    event['payload_hex']
    for event in log_data.get('events', [])
    if event.get('type') == 'api_call' 
       and event.get('api') == 'CryptGenRandom' 
       and event.get('payload_hex')
]
priv_key_candidates = [c for c in candidates if len(c) == 64]

5.2 공유 비밀 생성

  • default.key에서 파싱한 32byte값과 CryptGenRandom 32byte를 이용해서 Cuver25519를 이용해 공유 비밀 생성
priv_key_candidate = bytes.fromhex(priv_key_hex_candidate)
sk = x25519.X25519PrivateKey.from_private_bytes(priv_key_candidate) # Curve25519 글램핑
shared_secret = sk.exchange(pk)  # 32바이트 공유 비밀

5.3 키 파생 로직(KDF)

  • 생성된 공유 비밀을 SHA-512 KDF를 사용해 ChaCha20에 필요한 키 IV 파생
derived_key = hashlib.sha512(shared_secret).digest()  # 64바이트
  • SHA-512를 통해서 확장된 64byte에서 32byte / 12byte 파싱하여 각각 ChaCha20의 Key와 IV로 사용
chacha_key = derived_key[:32]
chacha_iv = derived_key[32:32+12]

5.4 ChaCha20 파일 암호화를 위한 키스트림 생성

  • 표준 ChaCha20(20 round)를 직접 구현
    • 20 round = (10 round + 대각선 round) 10번 반복
    • state + working_state를 더해 little-endian으로 정렬해 64byte 키스트림 블록 새성
keystream_block = chacha20_block(chacha_key, counter, chacha_iv)

5.5 복호화 시도

  • 생성된 KeyStream을 이용해 암호문과 XOR을 통해 복호화
    • Block Counter는 0부터 시작(counter = i)
for i in range(num_blocks):
    start = i * BLOCK_SIZE
    end = min(start + BLOCK_SIZE, len(ciphertext))
    keystream_block = chacha20_block(chacha_key, i, chacha_iv)
    for j in range(end - start):
        decrypted_data[start + j] = ciphertext[start + j] ^ keystream_block[j]

5.5.1 Beast 메타 데이터 제거

  • 암호화 파일의 메타데이터 마지막 160바이트를 제거하고 복호화 시도
METADATA_SIZE = 160
ciphertext = encrypted_data[:-METADATA_SIZE]

6. 요약 및 결론

[그림 22] 암호화 워크플로우 요약

  • 분석된 전체 암호화 과정은 위 도식과 같이 난수값 생성(CryptGenRandom)부터 Encrypted File(암호화)까지 순차적으로 진행되며, 각 단계별 상세 동작은 다음과 같다.
    • Step 1. 키 교환 준비로 각 파일별로 독립된 대칭키를 만들기 위해 CryptGenRandom(32byte)를 사용해 임시 개인키 후보를 생성하고 default.key 파일에서 공격자의 고정 공개키 32byte를 로드하여 임시 개인키와 X22519 ECDH 연산을 수행해 32byte 공유 비밀을 생성한다.
    • Step 2. KDF(키 파생) SHA 512를 이용해 공유 비밀을 64byte로 확장하고 ChaCha20 암호화에 사용할 Key와 IV를 각각 32byte 12byte씩 파싱한다.
      • ChaCha20 Key (32 bytes) = SHA-512(shared_secret)[0:32]
      • Nonce (12 bytes) = SHA-512(shared_secret)[32:44]
    • Step 3. ChaCha20 키스트림 생성단계에서 Counter를 0으로 초기화 하고 위에서 설정한 Key와 Nonce를 가지고 64byte 단위의 키스트림 블록을 순차적으로 생성한다.
    • Step 4. 파일 암호화 진행 생성된 ChaCha20 KeyStrema과 원본의 파일 데이터를 XOR하는 방식으로 파일 암호화를 수행한다.

6.1 최종 평가 및 결론

  • Beast 랜섬웨어의 경우 백신 AV의 탐지 회피와 모든 WIndow 환경에서 원할한 암호화 진행을 위해 CryptoGenRandom API를 호출하지 못하는 환경의 경우 MT19937 난수 생성기를 이용하는 방식을 사용하고 있으며 파일 크기(1GB)기준으로 암호화 방식이 달라진다.생성된 공유 비밀은 SHA-512를 이용해 64byte로 확장되며 ChaCha20에 사용되는 Key(32byte)와 Nonce(12byte)로 이용하여 KeyStream을 생성하여 평문과 XOR을 통해 파일 암호화를 수행한다.
  • Beast 랜섬웨어는 ECDH에 사용되는 공격자의 공개키를 실행 파일 내에 직접 하드코딩하지 않고 런타임 환경에 default.key 파일을 생성하여 활용하는 설계는 분석 및 복호화를 어렵게 만들기 위한 의도로 보이며 해당 랜섬웨어 체계에서 가장 인상적인 특징을 가지고 있다.
  • 키 교환 방식은 Curve25519을 사용하는데 여기에는 하드 코딩된 공개키가 아닌 Beast 랜섬웨어가 실행될 때 default.key 파일의 32byte와 생성된 난수값을 사용하여 32byte의 공유 비밀을 생성한다.
  • 복호화 코드는 부록7.2 참고

7. 부록

7.1 Raw한 동적 분석 과정

  • 상세 동적 분석과정
더보기

중단점 후보

 

412E98이 마스터키

 

CryptGenRandom 쓰는 함수 사용 확인

SHA-512 기반 키 파생 함수 호출 확인

여기서 ecx의 값을 확인하여 암호화 하는 함수(sub_40273E) 호출되는 확인

1GB 이하 파일 암호화 시 sub_40273E (키스트림 XOR) 호출하는 부분

 

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 21 7B 62 43 CF E8 03 E5 70 A4 DC 43 28 EC 4C 3B 41 88 AE E6 DC 38 57 1A 50 E1 97 DF B0 31 9C 39 62 08 1B 55 E4 97 DD EE 61 4B 9A E7 BD 79 18 CC 6E 27 03 2B 9B 8F DB 44 18 AD 2F 07 B9 69 24 DC A9 31 8C 0B 45 C6 A7 28 2B 8F 69 A2 1D 6A A2 87 04 6E 4D 55 65 E0 49 7A 47 6F E7 F1 74 48 4E 3A 

 

edi에 default.key 파일의 0x00을 제외한 앞의 32byte 값 Load하는 것을 확인

두번째 4055D8로 생성된 결괏값을 SHA512 해싱하여 64byte 값이 생기는 것 확인

 

아래의 .ps1 스크립트로 검증

powershell -command "[BitConverter]::ToString((New-Object Security.Cryptography.SHA512Managed).ComputeHash([byte[]]('24638B4925727F746BBD7A6389BD3BF818BBF9133B824B8267AD9AB9810BA635' -split '(..)' | ? {$_} | % { [Convert]::ToByte($_,16) }))).Replace('-','').ToLower()"
8784c5eb0218bff2fa39a65c3119ff792ed4dee198576509d0cbab2ae763c4619902f4c4d4ff17075cb8fd5b1aad5f99fb28f4037df2bc883fef17a5a853f2a0

 

중단점 재 설정

CryptGenRandom 값이랑 default.key 32바이트를 다음 스크립트로 커브 돌렸을 때 결괏값이 맞는지 확인하기

# Python (requires cryptography)
from cryptography.hazmat.primitives.asymmetric import x25519

# hex으로 주어진 예시: priv(32)와 pub(32)
priv_hex = "DC004E20BA3DE112EBEC620E4DB89EE0F4C460F528B86603A52738C354647F4B"  # 예: 너가 사용한 값(32B)
pub_hex  = "C02D7FCC5EC0AE6BA33F78A54B4A9BE3BCAAD9AF1F01B12B0877BBE48D2EB26A"  # a3에 들어간 32바이트 공개값

priv = bytes.fromhex(priv_hex)
pub  = bytes.fromhex(pub_hex)

# 라이브러리는 내부적으로 클램프를 적용하므로 raw 32바이트를 그대로 넣으면 됨
sk = x25519.X25519PrivateKey.from_private_bytes(priv)
pk = x25519.X25519PublicKey.from_public_bytes(pub)
shared = sk.exchange(pk)
print(shared.hex())

위 스크립트 결과값: 1e14bc33ff07526ebc5a7d64e34862e82bc910003fdf1a109f549d5f208f923f

검증 확인 완료

 

이제 키스트림 로직만 확인하면 되는데

일단 지금까지 확인된 거:

  1. default.key 값과 크젠랜 값을 curve 돌리기
# Python (requires cryptography)
from cryptography.hazmat.primitives.asymmetric import x25519

# hex으로 주어진 예시: priv(32)와 pub(32)
priv_hex = "DC004E20BA3DE112EBEC620E4DB89EE0F4C460F528B86603A52738C354647F4B"  # 예: 너가 사용한 값(32B)
pub_hex  = "C02D7FCC5EC0AE6BA33F78A54B4A9BE3BCAAD9AF1F01B12B0877BBE48D2EB26A"  # a3에 들어간 32바이트 공개값

priv = bytes.fromhex(priv_hex)
pub  = bytes.fromhex(pub_hex)

# 라이브러리는 내부적으로 클램프를 적용하므로 raw 32바이트를 그대로 넣으면 됨
sk = x25519.X25519PrivateKey.from_private_bytes(priv)
pk = x25519.X25519PublicKey.from_public_bytes(pub)
shared = sk.exchange(pk)
print(shared.hex())

 

1e14bc33ff07526ebc5a7d64e34862e82bc910003fdf1a109f549d5f208f923f

2. 이 값을 sha512 해싱

powershell -command "[BitConverter]::ToString((New-Object Security.Cryptography.SHA512Managed).ComputeHash([byte[]]('1e14bc33ff07526ebc5a7d64e34862e82bc910003fdf1a109f549d5f208f923f' -split '(..)' | ? {$_} | % { [Convert]::ToByte($_,16) }))).Replace('-','').ToLower()"

2659d23c90c34cad109fd7ea189f381f5344c50fbf6cb571b0e47de012525813237dd03aee4d33b7fcc9f88e3fa6ec7b05c9abb618c828a87e73f49e837ba30c

 

3.앞의 32바이트를 ChaCha20 키로, 그 뒤 16바이트를 IV로 사용하여 키스트림 생성

  • 키: 2659D23C90C34CAD109FD7EA189F381F5344C50FBF6CB571B0E47DE012525813
  • IV: 237DD03AEE4D33B7FCC9F88E3FA6EC7B

→ 첫 번째 키스트림: 82AB9073447B7D132CF702DF655BBD55D910FA5CF472B0FD4BFE2E3F67198DD8D96E7A7704872CD6145F5CF77DDB2163D7CA064A0B6E585507C4D8938A184529

dog.jpg

  1. 크젠랜: 27305B15D5B43462A2B13795400F90FF20DA7D295919FCD3F61BDDC9905BEA47
  2. curve: 6a641fc7b1f5c453aac969b07f54d4e775a429442b202e02efd47d3d47e3d66d
  3. 해시: ce52f2f2d57f2875699e2a0bb73805bfae44cd9b84aa0d4f65d95dd4802e31efec7c3afb76622f43e6d1b065e649ba63a17b5df13f3069a438adbaeca2092029
  4. 키: CE52F2F2D57F2875699E2A0BB73805BFAE44CD9B84AA0D4F65D95DD4802E31EF IV: EC7C3AFB76622F43E6D1B065
  5. 키스트림 예상: 268284D81AE0AE876DBD2C37BD40C9940E01CF68F9A022A9EF8C52091AFC62B344D2BEDE64FF2AE3DB0AB872F735A1CAE924D62A0A32580BE9414CEF545020EC

7.2 최종 복호화 코드

  • 사용 방법
    • 복호화 Python 코드와 default.key를 같은 경로에 넣어 사용
    • python decrypt.py -f {암호화된 파일} -j {후킹 로그(json)}
더보기
# -*- coding: utf-8 -*-
import argparse
import json
import os
import struct
import hashlib
from typing import List, Optional

# cryptography 라이브러리 (pip install cryptography 필요)
try:
    from cryptography.hazmat.primitives.asymmetric import x25519
except ImportError:
    print("오류: 'cryptography' 라이브러리가 설치되어 있지 않습니다.")
    print("pip install cryptography 를 실행하여 설치해 주세요.")
    exit(1)

# ==============================================================================
# 1. ChaCha20 구현 
# ==============================================================================

def rotl32(v: int, n: int) -> int:
    """32비트 정수 좌측 회전"""
    return ((v << n) & 0xffffffff) | (v >> (32 - n))

def quarter_round(s: List[int], a: int, b: int, c: int, d: int):
    """ChaCha20 쿼터 라운드 함수"""
    s[a] = (s[a] + s[b]) & 0xffffffff
    s[d] ^= s[a]; s[d] = rotl32(s[d], 16)
    s[c] = (s[c] + s[d]) & 0xffffffff
    s[b] ^= s[c]; s[b] = rotl32(s[b], 12)
    s[a] = (s[a] + s[b]) & 0xffffffff
    s[d] ^= s[a]; s[d] = rotl32(s[d], 8)
    s[c] = (s[c] + s[d]) & 0xffffffff
    s[b] ^= s[c]; s[b] = rotl32(s[b], 7)

def chacha20_block(key_bytes: bytes, counter: int, nonce_bytes: bytes) -> bytes:
    """
    ChaCha20 (20라운드) 블록 생성 함수.
    64바이트의 키스트림 블록을 반환합니다.
    """
    if len(key_bytes) != 32:
        raise ValueError("키는 32바이트여야 합니다.")
    if len(nonce_bytes) != 12:
        raise ValueError("IV/Nonce는 12바이트여야 합니다.")
        
    # 상수 "expand 32-byte k"
    constants = b"expa" + b"nd 3" + b"2-by" + b"te k"
    
    # 초기 상태 (16개의 32비트 워드) 구성
    state = [0] * 16
    
    # 0-3: 상수
    for i in range(4):
        state[i] = struct.unpack("<I", constants[i*4:(i+1)*4])[0]
        
    # 4-11: 키 (32바이트)
    for i in range(8):
        state[4+i] = struct.unpack("<I", key_bytes[i*4:(i+1)*4])[0]
        
    # 12: 카운터
    state[12] = counter & 0xffffffff
    
    # 13-15: IV/Nonce (12바이트)
    for i in range(3):
        state[13+i] = struct.unpack("<I", nonce_bytes[i*4:(i+1)*4])[0]

    working = state.copy()
    for _ in range(10): # 20라운드 = 10회 반복
        # 열 라운드
        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)
        # 대각선 라운드
        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)
            
    # 초기 상태와 XOR
    out_state = [(working[i] + state[i]) & 0xffffffff for i in range(16)]
    
    # 바이트 시퀀스로 변환 (Little-endian)
    out_bytes = b"".join(struct.pack("<I", w) for w in out_state)
    return out_bytes

# ==============================================================================
# 2. 파일 시그니처 체크 함수 (확장자 반환하도록 수정)
# ==============================================================================

def check_file_signature(data: bytes) -> Optional[str]:
    """
    복호화된 데이터의 파일 시그니처를 확인하여 성공 시 예상 확장자를 반환합니다.
    """
    if len(data) < 8:
        return None
        
    # 시그니처: (바이트 시그니처, (확장자, 파일 유형 이름))
    signatures = {
        b'\xFF\xD8\xFF': (".jpg", "JPEG"),
        b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A': (".png", "PNG"),
        b'\x47\x49\x46\x38': (".gif", "GIF"),
        b'\x25\x50\x44\x46': (".pdf", "PDF"),
        b'\x50\x4B\x03\x04': (".zip", "ZIP/Office"), # ZIP, DOCX, XLSX, PPTX
        b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1': (".doc", "OLE2/Old Office"),
        b'\x4D\x5A': (".exe", "MZ/Executable") # DOS/Windows Executable
    }

    for sig, (ext, file_type) in signatures.items():
        if data.startswith(sig):
            print(f"[성공] 파일 시그니처 일치: {file_type}")
            return ext
            
    print("[실패] 알려진 파일 시그니처와 불일치.")
    return None

# ==============================================================================
# 3. 메인 복호화 로직
# ==============================================================================

def attempt_decryption(
    encrypted_filepath: str, 
    pub_key_bytes: bytes, 
    priv_key_hex_candidate: str, 
    output_dir: str
) -> bool:
    """
    CryptGenRandom 페이로드 후보를 사용하여 복호화를 시도합니다.
    """
    try:
        # 2. Curve25519 공유 키 생성
        priv_key_candidate = bytes.fromhex(priv_key_hex_candidate)
        
        # 개인 키 객체 생성 (라이브러리 내부적으로 클램프 적용)
        sk = x25519.X25519PrivateKey.from_private_bytes(priv_key_candidate)
        # 공개 키 객체 생성
        pk = x25519.X25519PublicKey.from_public_bytes(pub_key_bytes)
        
        # 공유 비밀 키 (32바이트) 교환
        shared_secret = sk.exchange(pk)
        
        print(f"  -> 공유 비밀 키 (32B): {shared_secret.hex().upper()}...")
        
        # 3. SHA-512 해싱을 통한 키 파생 (64바이트)
        derived_key = hashlib.sha512(shared_secret).digest()
        
        # 4. ChaCha20 키 (32B) 및 IV (12B) 추출
        chacha_key = derived_key[:32]
        # 사용자 로직에 따라 IV는 파생 키의 32바이트 다음부터 12바이트
        chacha_iv = derived_key[32:32+12] 
        
        print(f"  -> ChaCha20 키 (32B): {chacha_key.hex().upper()}")
        print(f"  -> ChaCha20 IV (12B): {chacha_iv.hex().upper()}")
        
        # 암호화된 파일 로드
        with open(encrypted_filepath, 'rb') as f:
            encrypted_data = f.read()

        # 5. 암호화된 파일에서 메타데이터 (160B) 제거 및 복호화
        METADATA_SIZE = 160
        if len(encrypted_data) <= METADATA_SIZE:
            print("  [실패] 파일 크기가 메타데이터 크기(160B)보다 작거나 같습니다.")
            return False
            
        ciphertext = encrypted_data[:-METADATA_SIZE]
        decrypted_data = bytearray(len(ciphertext))
        
        BLOCK_SIZE = 64 # ChaCha20 블록 크기 (64바이트)
        num_blocks = (len(ciphertext) + BLOCK_SIZE - 1) // BLOCK_SIZE
        
        print(f"  -> 복호화 블록 수: {num_blocks}")
        
        for i in range(num_blocks):
            start = i * BLOCK_SIZE
            end = min(start + BLOCK_SIZE, len(ciphertext))
            
            # 카운터를 증가시키며 키스트림 블록 생성
            keystream_block = chacha20_block(chacha_key, i, chacha_iv)
            
            # XOR 연산
            for j in range(end - start):
                decrypted_data[start + j] = ciphertext[start + j] ^ keystream_block[j]
        
        final_decrypted_data = bytes(decrypted_data)
        
        # 6. 복호화 성공 판단 (파일 시그니처 기반)
        suggested_extension = check_file_signature(final_decrypted_data)
        
        if suggested_extension:
            # 7. 복호화 성공 시 파일 저장
            os.makedirs(output_dir, exist_ok=True)
            
            encrypted_filename = os.path.basename(encrypted_filepath)
            
            # --- 파일 이름 변경 로직: 원본파일명_decrypted.확장자 (수정됨) ---
            
            # 1. 원본 파일명 추출 (사용자 요청: 첫 번째 '.'까지만 추출)
            # 예: "11.jpg.{...}.Cooseagroup" -> "11"
            try:
                # 파일 이름을 '.' 기준으로 나누고 첫 번째 요소만 가져옵니다.
                original_base_name = encrypted_filename.split('.')[0]
            except Exception as e:
                print(f"  [경고] 파일명 분할 중 오류 발생: {e}. 파일 전체 이름을 기본 이름으로 사용합니다.")
                original_base_name = encrypted_filename

            # 2. 새 파일 이름 생성: 원본파일명_decrypted.확장자
            new_filename = f"{original_base_name}_decrypted{suggested_extension}"
            output_filepath = os.path.join(output_dir, new_filename)
            
            # 3. 파일명 중복 방지 로직 (이미 파일이 존재하면 이름 뒤에 숫자를 붙입니다.)
            counter = 1
            base, ext = os.path.splitext(new_filename)
            while os.path.exists(output_filepath):
                new_filename = f"{base}_{counter}{ext}"
                output_filepath = os.path.join(output_dir, new_filename)
                counter += 1
            
            # 4. 파일 쓰기
            with open(output_filepath, 'wb') as f:
                f.write(final_decrypted_data)
                
            print(f"  [성공] 복호화 성공! -> {output_filepath}에 저장되었습니다. (새 파일명: {new_filename})")
            return True
        else:
            print("  [실패] 파일 시그니처 검사 실패. 다음 키로 이동합니다.")
            return False
            
    except Exception as e:
        print(f"  [오류] 복호화 중 예외 발생: {e}")
        return False

# ==============================================================================
# 4. 파일 로드 및 메인 실행
# ==============================================================================

def main():
    parser = argparse.ArgumentParser(
        description="후킹 로그의 CryptGenRandom 값들을 브루트포싱하여 암호화된 파일을 복호화합니다.",
        formatter_class=argparse.RawTextHelpFormatter
    )
    parser.add_argument('-f', '--file', required=True, help='암호화된 파일 경로')
    parser.add_argument('-j', '--json', required=True, help='후킹 로그 JSON 파일 경로')
    args = parser.parse_args()

    # 파일 경로 및 디렉토리 설정
    encrypted_filepath = args.file
    log_filepath = args.json
    script_dir = os.path.dirname(os.path.abspath(__file__)) if os.path.abspath(__file__) else os.getcwd()
    default_key_filepath = os.path.join(script_dir, 'default.key')
    output_dir = os.path.join(script_dir, 'decrypted')

    # 1. default.key 로드 및 공개 키 추출
    try:
        # 인코딩 오류 해결: 파일을 바이너리('rb')로 읽어와서 바이트(bytes) 객체로 처리합니다.
        with open(default_key_filepath, 'rb') as f:
            raw_key_bytes = f.read()

        # default.key 파일 구조: [32바이트 패딩/키] + [32바이트 공개 키] + [나머지]
        PUBLIC_KEY_OFFSET = 32
        PUBLIC_KEY_SIZE = 32
        
        if len(raw_key_bytes) < PUBLIC_KEY_OFFSET + PUBLIC_KEY_SIZE:
            raise ValueError(f"default.key 파일이 너무 짧습니다. 최소 {PUBLIC_KEY_OFFSET + PUBLIC_KEY_SIZE} 바이트가 필요합니다.")

        # 32바이트 공개 키 추출
        pub_key_bytes = raw_key_bytes[PUBLIC_KEY_OFFSET : PUBLIC_KEY_OFFSET + PUBLIC_KEY_SIZE]
        
        # 로그를 위해 16진수 문자열로 변환
        pub_key_hex = pub_key_bytes.hex()
        
        print(f"--- default.key 로드 완료 (바이너리 모드) ---")
        print(f"default.key 경로: {default_key_filepath}")
        print(f"추출된 공개 키 (32B): {pub_key_hex}")
        print("-" * 30)

    except FileNotFoundError:
        print(f"오류: default.key 파일 ({default_key_filepath})을 찾을 수 없습니다.")
        return
    except Exception as e:
        print(f"오류: default.key 처리 중 오류 발생: {e}")
        return

    # 2. 후킹 로그 JSON 파일 로드 및 후보 키 추출
    try:
        with open(log_filepath, 'r', encoding='utf-8') as f:
            log_data = json.load(f)
            
        # CryptGenRandom API 호출의 payload_hex 값만 필터링
        candidates = [
            event['payload_hex']
            for event in log_data.get('events', [])
            if event.get('type') == 'api_call' and event.get('api') == 'CryptGenRandom' and event.get('payload_hex')
        ]
        
        # 32바이트 길이의 키만 사용 (16진수 문자열은 64자리)
        priv_key_candidates = [c for c in candidates if len(c) == 64]
        
        print(f"--- 후킹 로그 분석 완료 ---")
        print(f"로그 파일 경로: {log_filepath}")
        print(f"CryptGenRandom 페이로드 후보 (32B) 수: {len(priv_key_candidates)}")
        print("-" * 30)

        if not priv_key_candidates:
            print("경고: 유효한 CryptGenRandom 페이로드 (32B)를 찾을 수 없습니다. 스크립트를 종료합니다.")
            return

    except FileNotFoundError:
        print(f"오류: JSON 로그 파일 ({log_filepath})을 찾을 수 없습니다.")
        return
    except json.JSONDecodeError:
        print(f"오류: JSON 로그 파일 ({log_filepath}) 형식이 잘못되었습니다.")
        return
    except Exception as e:
        print(f"오류: JSON 로그 처리 중 오류 발생: {e}")
        return

    # 3. 브루트포싱 복호화 시도
    decrypted_success = False
    
    for i, priv_key_hex in enumerate(priv_key_candidates):
        print(f"\n[{i+1}/{len(priv_key_candidates)}] 복호화 시도: Private Key 후보 {priv_key_hex}")
        
        if attempt_decryption(encrypted_filepath, pub_key_bytes, priv_key_hex, output_dir):
            decrypted_success = True
            break
            
    if not decrypted_success:
        print("\n=== 복호화 최종 결과 ===")
        print("모든 CryptGenRandom 페이로드 후보로 복호화를 시도했지만 성공하지 못했습니다.")
        print("원본 파일이 복구되지 않았습니다.")
    else:
        print("\n=== 복호화 최종 결과 ===")
        print("성공적으로 복호화된 파일을 'decrypted' 폴더에서 확인해 주세요.")

if __name__ == "__main__":
    main()