암호화 키 관리 방식에 취약점이 확인된 Green Blood 랜섬웨어
■ 개요
2026년 2월 랜섬웨어 피해 사례 수는 지난 1월(766건) 대비 소폭 상승한 770건으로 집계됐다.
2026년 2월 17일 폴란드 당국은 Phobos 랜섬웨어와 연루된 47세 남성을 체포했다. 이번 체포는 유럽경찰청이 주도한 Phobos 랜섬웨어 그룹 대상 국제 공조 수사인 Operation Aether의 일환으로 이루어졌다. 체포 과정에서 서버 접근 정보와 로그인 자격 증명이 확인됐으며, Phobos 랜섬웨어 관련 인물들과 메시지를 주고받은 정황도 드러났다.
Storm-2603 그룹은 SmarterMail에서 발견된 인증 우회 취약점(CVE-2026-23760)을 악용하여 시스템에 침투한 뒤 Warlock 랜섬웨어를 실행했다. 해당 취약점은 관리자 비밀번호를 재설정하는 API의 인증을 검증하는 과정이 미흡하여 발생한 것으로, 인증되지 않은 공격자도 관리자 계정의 비밀번호를 임의로 변경할 수 있는 허점이 있었다. 공격자는 이를 악용하여 관리자 권한을 확보하고 원격 명령 실행 권한을 획득한 뒤, 서버에서 Warlock 랜섬웨어를 실행해 파일을 암호화했다.
2026년 2월 한 달간 국내 산업계를 겨냥한 공격은 총 6건으로 집계됐다. 제조업을 필두로 제약 및 IT 컨설팅 등 산업 전반에 걸쳐 침해 사례가 확인됐으며, 특히 Beast 그룹의 집중 공세와 변칙적인 협박 기법이 두드러졌다.
가장 많은 공격 사례를 게시한 Beast 그룹은 2월 5일 국내 제조 기업을 타깃으로 삼아 계약서와 내부 핵심 자료를 탈취한 뒤 다크웹 유출 사이트에 공개했다. 이어 24일에는 국내 제약 업체와 자전거 부품 제조업체의 내부 자료까지 추가로 유출하며 이달 국내 침해 사고의 큰 비중을 차지했다.
전술적 측면에서 주목해야 할 사례는 Gentlemen 그룹이다. 2월 21일 IT 컨설팅 회사를 공격한 이들은 일반적인 유출 방식과는 다른 변칙적인 협박 기법을 구사하고 있다. 협상 기한이 종료됐음에도 데이터를 즉시 공개하는 대신, TOX ID를 통해 직접적인 구매 문의를 유도하는 안내 문구를 게시했다. 이는 탈취한 데이터를 일종의 판매 상품으로 전환하여 피해 기업을 지속적으로 압박하고 수익을 극대화하려는 전략으로 보인다.
또한, 2월 13일에는 Anubis 그룹이 플라스틱 제조업체를 공격해 내부 자료와 계약 문서를 유출했으며, 27일에는 Morpheus 그룹이 도금 업체의 회사 개요 및 매출 관련 민감 데이터를 다크웹에 게시했다. 이러한 일련의 사례들은 국내 제조업을 향한 위협이 지속되고 있음을 뒷받침한다.
■ 랜섬웨어 뉴스
그림 1. 랜섬웨어 동향
■ 랜섬웨어 위협
그림 2. 2026년 2월 랜섬웨어 위협 현황
• 새로운 위협
2026년 2월에는 신규 랜섬웨어 그룹 6개가 등장했다. 확인된 신규 랜섬웨어 그룹들은 모두 다크웹 유출 사이트를 보유하고 있으나, 현재 CipherForce 그룹과 ShadowByt3$의 다크웹 유출 사이트는 비활성화된 상태로 확인된다.
그림 3. Reynolds의 다크웹 유출 사이트
2026년 2월에 등장한 Reynolds 그룹은 현재까지 총 1건의 피해자를 게시했다. 해당 그룹은 탈취 데이터를 일괄 공개하는 방식이 아닌, 유출 분량을 단계적으로 늘려가는 협박 방식을 사용하는 것이 특징이다. 실제로 게시글에는 3일 내 100GB, 7일 내 200GB, 14일 내 전체 파일을 모두 공개하겠다는 내용이 포함되어 있어, 시간 경과에 따라 압박 수위를 높이는 전략을 사용하는 것으로 확인된다.
그림 4. KittyKatKrew의 다크웹 유출 사이트(좌)와 텔레그램 채널(우)
2026년 2월에 등장한 KittyKatKrew 그룹은 현재까지 총 1건의 피해자를 게시했다. 이들은 다크웹 유출 사이트에서 Healthcare, Financial Services 등 산업군별로 피해자를 분류하는 특징을 보였다. 또한, 별도로 운영 중인 텔레그램 채널에서는 미국식 억양으로 IT 담당자를 사칭하여 전화를 수행할 인력을 모집하거나 계열사 및 신뢰할 수 있는 IAB 1 를 모집하려는 내용이 텔레그램 채널에 게시된 정황이 드러났다.
• Top5 랜섬웨어
그림 5. 산업/국가별 주요 랜섬웨어 공격 현황
2026년 2월은 기존 그룹들의 지속성과 신흥 그룹의 가파른 성장세가 맞물리며 770건의 피해 사례가 확인됐다. 특히 2022년 7월 'Agenda'라는 명칭으로 활동을 시작한 Qilin 그룹은 2025년 2분기를 기점으로 세력을 급격히 확장해 왔다. 지난 1월 110건의 피해를 발생시킨 데 이어, 2월에는 소폭 증가한 122건을 기록하며 매달 세 자릿수 이상의 피해 건수를 유지하고 있다. 이들의 행보는 현재 랜섬웨어 생태계에서 가장 위협적인 그룹으로 평가되는 만큼 철저한 대비 방안이 필요하다. Qilin 그룹의 세부 공격 전략과 대응 방안은 SK쉴더스 KARA 랜섬웨어 동향 보고서 2025 3Q에서 자세하게 확인할 수 있다.
한편, Gentleman 그룹의 활동도 두드러진다. 1월 48건이던 피해 건수는 2월 90건으로 늘어나며 두 배 가까이 증가했다. 이들은 Go 언어 기반의 랜섬웨어를 제작해 사용하며, X25519 알고리즘으로 키를 생성하고 XChaCha20으로 파일을 암호화하는 하이브리드 암호화 기법을 사용하는 것으로 확인된다. 아울러 계열사 운영 정책에서도 이들의 배경을 짐작할 수 있는 정황이 드러난다. 특히 계열사 모집 공고에서 독립연합국가(CIS)을 공격 대상에서 제외한 점은 이들이 러시아와 밀접한 연관이 있을 가능성을 시사한다.
초기 침투 전략이 두드러진 Akira 그룹은 2월 한 달간 47건의 피해를 기록했다. 이들은 2023년 3월 발견된 이후 MFA(다중 인증)가 적용되지 않은 Cisco, SonicWall 등 VPN 침투 장비의 취약점을 초기 침투 경로로 적극 활용하고 있다. 이후에는 RDP, PowerShell, PsExec 등 정상 관리 도구를 악용해 내부망 이동과 권한 확장을 수행하는 양상을 보인다.
NightSpire 그룹은 1월 20건에서 2월 43건으로 피해 사례가 증가했다. 이들은 주로 의료 분야를 집중적으로 노리며 미국의 의료 소프트웨어 기업인 Md Charts의 소스코드를 유출하거나 이스라엘 Abrahamsom Center의 환자 개인정보를 공개하는 등 민감 정보를 공개하며 협박을 이어가고 있다.
마지막으로 2022년부터 장기간 활동해 온 Play 그룹은 2월 40건의 피해를 기록했다. 이들은 미국의 Spring Brook Country Club을 공격해 고객 개인정보와 신분증 자료를 유출했으며, 이를 통해 기업뿐 아니라 민간 서비스 영역까지 공격 범위를 확장하고 있는 것으로 나타났다.
■ 랜섬웨어 집중 포커스
그림 6. Green Blood 그룹의 다크웹 유출 사이트
2026년 1월부터 활동을 시작한 Green Blood 랜섬웨어 그룹은 현재까지 총 2건의 피해자를 게시했다. 피해자별로 탈취한 파일의 종류와 게시 일자를 기록한 뒤, 협상에 실패하거나 일정 기간이 경과하면 해당 데이터를 모든 사용자가 접근할 수 있도록 다크웹에 공개하고 있다. 그러나 현재 기준으로 Green Blood가 운영하던 다크웹 유출 사이트는 접근이 불가능한 상태이며, 이후 추가적인 활동은 확인되지 않고 있다.
한편, Green Blood 랜섬웨어를 분석한 결과 암호화 과정에서 구조적인 취약점이 확인됐다. 특히 암호화 키가 Machine ID와의 XOR 연산을 기반으로 관리되는 구조적 특성으로 인해, 특정 조건에서 암호화 키를 재구성할 수 있는 가능성이 존재한다. 본 보고서에서는 해당 랜섬웨어의 분석 결과와 취약점을 공유하여 향후 발생할 수 있는 랜섬웨어 위협에 보다 효과적으로 대비할 수 있도록 하고자 한다.
그림 7. 랜섬웨어 개요
• 랜섬웨어 전략
그림 8. 랜섬웨어 공격 전략
Green Blood 랜섬웨어는 별도의 실행 인자 없이 동작하도록 설계되어 있으며, 중복 실행을 방지하기 위해 랜섬웨어 내부에 포함되어 있는 “GREENBLOOD_ENCRYPTOR_MUTEX_2A3B4C5D” 문자열을 사용해 뮤텍스 2 를 생성한다.
이후 시스템 전체 드라이브를 순차적으로 탐색하여 암호화 대상을 확인한다. 이 과정에서 특정 폴더명, 확장자, 파일명 등 일부 항목은 암호화 대상에서 제외하도록 설정되어 있다. 확인된 예외 대상은 아래 표와 같다.
| 폴더명 | 확장자 및 파일명 |
|
Windows, Program Files, Program Files (x86), $RECYCLE.BIN, ProgramData, System Volume Information, $Recycle.Bin, Boot, Public, Default, PerfLogs, |
.exe, .dll, .sys, .drv, .ocx, .cpl, .scr, .msi, .msu, .cab, .mui, .mun, .edb, .jrs, .log, .tmp,temp, .lnk, .url, .pif, .tgbg, |
표1. 암호화 예외 대상
랜섬웨어는 분석 방해 및 탐지 회피를 목적으로 Windows Defender 기능을 무력화하고 방화벽을 비활성화한다. 또한 복구를 방해하기 위해 VSS(Volume Shadow Copy Service)를 통해 생성된 백업 데이터를 삭제한다. 아래는 해당 목적을 위해 사용된 명령어 목록이다.
| VSS 및 VSC 삭제 |
| vssadmin delete shadows /all /quiet |
| wmic shadowcopy delete |
| Windows Backup 카탈로그 삭제 |
| wbadmin delete catalog -quiet |
| 부팅 및 복구 옵션 무력화 |
| bcdedit /set {default} recoveryenabled no |
| bcdedit /set {default} bootstatuspolicy ignoreallfailures |
| Windows Defender 실시간 보호 비활성화 |
| reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection" /v DisableRealtimeMonitoring /t REG_DWORD /d 1 /f |
| 방화벽 비활성화 |
| netsh advfirewall set allprofiles state off |
Green Blood 랜섬웨어는 먼저 32바이트 길이의 암호화 키를 1회 생성한 후, 해당 키를 사용해 모든 암호화 대상 파일을 AES-256(CTR) 알고리즘으로 암호화한다. 암호화 키와 같이 무작위로 생성된 IV 3 값을 사용하며, 추후 복호화를 위해 암호화된 파일의 시작 부분에 저장한다.
또한 시스템에서 획득한 Machine ID와 생성된 암호화 키를 XOR 연산하여 Recovery ID를 생성한다. 이후 공격자는 Machine ID와 Recovery ID를 다시 XOR 연산함으로써 암호화에 사용된 키를 복원할 수 있다. 이는 암호화 키가 Machine ID와의 XOR 연산에만 의존하여 보호되는 구조로, 추가적인 보호 절차가 적용되지 않아 두 값을 확보할 경우 파일 복호화가 가능하며, 상세 내용은 뒤에서 다룰 ‘Green Blood 복호화’에서 살펴볼 수 있다.
그림 9. 암호화 방식
이때 랜섬웨어의 파일 암호화 범위는 파일 크기에 따라 다르게 적용된다. 파일 크기가 50GB 미만인 경우 전체를 암호화하며, 파일 크기가 50GB 초과할 경우에는 암호화를 수행하지 않는다.
그림 10. Green Blood의 랜섬노트
파일 암호화가 끝나면, Green Blood 랜섬웨어는 각 암호화 대상 경로에 랜섬노트를 생성한다. 랜섬노트에는 시스템의 Machine ID와 Recovery ID가 포함되며, 피해 시스템을 식별하고 복호화 절차를 안내하기 위한 정보로 활용된다.
이후 랜섬노트 생성이 완료되면 랜섬웨어는 분석을 방해하기 위해 자가 삭제를 수행한다. 먼저 랜섬웨어 실행 파일의 경로를 확인한 뒤 cleanup_greenblood.bat라는 이름의 배치 파일을 생성하여 실행한다. 해당 파일은 일정 시간 대기한 후 랜섬웨어 파일 삭제를 시도하며, 삭제가 완료되면 배치 파일 또한 삭제된다. 이때 확인되는 배치 파일 스크립트는 아래와 같다.
| 자가 삭제 |
| @echo off && timeout /t 5 /nobreak >nul && :try && del /f /q "%s" && if exist "%s" goto try && del /f /q "%~f0" |
• 랜섬웨어 대응방안
그림 11. 랜섬웨어 대응 방안
Green Blood 랜섬웨어는 실행 시 명령 프롬프트를 활용하여 시스템 내 VSS(Volume Shadow Copy)를 삭제하고, Windows Defender 설정을 변경하는 명령을 연속적으로 실행한다. 이에 따라 ASR 4 규칙을 활성화하여 비정상적인 프로세스를 차단함으로써 악성 행위를 방지할 수 있다.
또한 EDR 솔루션을 도입하고 최신 보안 패치를 적용하여 취약점을 악용한 침투나 비정상적인 행위를 신속히 식별하고 차단해야 한다. 더불어 백업 복사본을 별도의 네트워크 구간이나 외부 저장소, 오프라인 매체에 주기적으로 분산 백업해 두면 시스템이 암호화되더라도 데이터 복구가 가능하다. 이때 백업 장치 접근 권한을 최소화하고, 정기적인 복구 테스트를 수행해 백업 데이터의 무결성을 지속적으로 검증해야 한다.
• Green Blood 복호화
앞서 Green Blood 랜섬웨어를 분석한 결과 해당 랜섬웨어는 32바이트 암호화 키를 1회 생성한 뒤, 암호화 키와 시스템의 Machine ID를 XOR 연산하여 Recovery ID를 생성한다. 이후 파일 암호화가 완료되면 랜섬노트에 Machine ID와 Recovery ID를 함께 기록하는데, 이 두 값을 다시 XOR 연산함으로써 암호화에 사용된 키를 복원할 수 있다.
또한 Green Blood는 단일 키를 사용해 모든 암호화 대상 파일을 AES-256(CTR) 알고리즘으로 암호화하며, 파일마다 랜덤하게 생성한 16바이트 크기의 IV값은 암호화된 파일의 첫 시작 부분에 저장된다.
이와 같은 구조적 특성으로 인해 키와 IV값을 확보할 수 있어 파일 복호화가 가능한 사례로, 분석한 샘플과 같은 구조일 경우 적용 가능하다. 단, 복호화 시도 전에 백업과 테스트를 통해 복호화 여부를 확인할 필요가 있다.
| Green Blood 복호화 스크립트 |
|---|
|
#!/usr/bin/env python3 import argparse import re from pathlib import Path from Crypto.Cipher import AES ## Requires separate installation via pip (pycryptodome). RECOVERY_PREFIX = "GREEN-BLOOD-" WARNING_LINES = ( "1. This tool only applies to files encrypted with .tgbg extension.", "2. Back up your encrypted files before attempting decryption.", "3. This script is provided as-is, without any warranty. The author is not liable for any issues, damages, or data loss.", "4. Decryption may fail if Recovery ID or Machine ID values from the ransom note are altered or entered incorrectly.", ) def parse_recovery_id(recovery_id: str) -> bytes: token = recovery_id.strip() if token.upper().startswith(RECOVERY_PREFIX): token = token[len(RECOVERY_PREFIX):] token = token.strip() try: return bytes.fromhex(token) except ValueError as exc: raise SystemExit("Recovery ID hex format is invalid.") from exc def parse_machine_id(machine_id: str) -> bytes: token = machine_id.strip() machine = token.encode("ascii", errors="strict") if len(machine) != 32: raise SystemExit(f"Machine ID must be 32 ASCII bytes. Current length: {len(machine)}") return machine def parse_ids_from_ransom_note(note_text: str) -> tuple[str, str]: recovery_id = None machine_id = None recovery_match = re.search( r"(?im)\bRecovery\s*ID\b\s*[:=]\s*([A-Za-z0-9\-]+)", note_text, ) if recovery_match: recovery_id = recovery_match.group(1).strip() else: fallback = re.search(r"(?i)\bGREEN-BLOOD-[0-9A-F]{64}\b", note_text) if fallback: recovery_id = fallback.group(0).strip() machine_match = re.search( r"(?im)\bMachine\s*ID\b\s*[:=]\s*([^\r\n]+)", note_text, ) if machine_match: machine_id = machine_match.group(1).strip().strip("\"'[]()") if not recovery_id: raise SystemExit("Could not parse Recovery ID from ransom note.") if not machine_id: raise SystemExit("Could not parse Machine ID from ransom note.") return recovery_id, machine_id def load_ids_from_note_path(note_path_raw: str) -> tuple[str, str, Path]: normalized = note_path_raw.strip().strip("\"'") note_path = Path(normalized).expanduser() if not note_path.is_file(): raise SystemExit(f"Ransom note file not found: {note_path}") note_text = note_path.read_text(encoding="utf-8", errors="replace") note_recovery_id, note_machine_id = parse_ids_from_ransom_note(note_text) return note_recovery_id, note_machine_id, note_path def xor_recover_key(recovery_bytes: bytes, machine_bytes: bytes) -> bytes: if len(recovery_bytes) != 32: raise SystemExit(f"Recovery ID payload length is not 32 bytes: {len(recovery_bytes)}") if len(machine_bytes) != 32: raise SystemExit(f"Machine ID length is not 32 bytes: {len(machine_bytes)}") return bytes(recovery_bytes[i] ^ machine_bytes[i] for i in range(32)) def decrypt_aes_ctr_file(in_path: Path, out_path: Path, key: bytes) -> None: with in_path.open("rb") as f: iv = f.read(16) ciphertext = f.read() if len(iv) != 16: raise SystemExit("Input file is too short. Could not read the 16-byte IV.") initial_value = int.from_bytes(iv, byteorder="big") cipher = AES.new(key, AES.MODE_CTR, nonce=b"", initial_value=initial_value) plaintext = cipher.decrypt(ciphertext) with out_path.open("wb") as f: f.write(plaintext) def output_path_from_encrypted(path: Path, extension: str) -> Path: if path.name.lower().endswith(extension.lower()): return path.with_name(path.name[: -len(extension)]) return path.with_suffix(path.suffix + ".decrypted") def decrypt_folder(folder: Path, key: bytes, extension: str, recursive: bool) -> tuple[int, int]: pattern = f"*{extension}" files = sorted(folder.rglob(pattern) if recursive else folder.glob(pattern)) ok = 0 fail = 0 for enc_file in files: out_file = output_path_from_encrypted(enc_file, extension) try: decrypt_aes_ctr_file(enc_file, out_file, key) ok += 1 print(f"[OK ] {enc_file} -> {out_file}") except Exception as exc: fail += 1 print(f"[ERR] {enc_file}: {exc}") return ok, fail def prompt_nonempty(message: str) -> str: while True: value = input(message).strip() if value: return value print("[!] Empty input is not allowed.") def main() -> None: print("[WARNING]") for line in WARNING_LINES: print(line) parser = argparse.ArgumentParser( description="Recover AES-256 key from Recovery ID + Machine ID, then decrypt AES-CTR file(s)." ) parser.add_argument("-r", "--recovery-id", help="Example: GREEN-BLOOD-<64hex>") parser.add_argument( "-m", "--machine-id", help="32-byte ASCII string (example: 6F707172737475767778797A7B7C7D7E)", ) parser.add_argument("--note", help="Ransom note file path for auto-parsing Recovery ID and Machine ID") target = parser.add_mutually_exclusive_group(required=False) target.add_argument("-i", "--input", help="Encrypted file path") target.add_argument("-d", "--dir", help="Folder path for batch decryption") parser.add_argument("-o", "--output", help="Output file path for single-file decryption") parser.add_argument("--ext", default=".tgbg", help="Target extension for batch decryption (default: .tgbg)") parser.add_argument("--recursive", action="store_true", help="Process subfolders recursively") args = parser.parse_args() if args.note: note_recovery_id, note_machine_id, note_path = load_ids_from_note_path(args.note) if not args.recovery_id: args.recovery_id = note_recovery_id if not args.machine_id: args.machine_id = note_machine_id print(f"[+] Parsed IDs from ransom note: {note_path}") elif not args.recovery_id and not args.machine_id: note_prompt = input("Ransom note path (press Enter to skip): ").strip() if note_prompt: note_recovery_id, note_machine_id, note_path = load_ids_from_note_path(note_prompt) args.recovery_id = note_recovery_id args.machine_id = note_machine_id print(f"[+] Parsed IDs from ransom note: {note_path}") if not args.recovery_id: args.recovery_id = prompt_nonempty("Enter Recovery ID (GREEN-BLOOD-...): ") if not args.machine_id: args.machine_id = prompt_nonempty("Enter Machine ID (32 ASCII bytes): ") if not args.input and not args.dir: target_path = Path(prompt_nonempty("Enter target path (file or folder): ")) if target_path.is_dir(): args.dir = str(target_path) if not args.recursive: recursive_answer = input("Process subfolders as well? [y/N]: ").strip().lower() args.recursive = recursive_answer in ("y", "yes") elif target_path.is_file(): args.input = str(target_path) if not args.output: args.output = prompt_nonempty("Output file path: ") else: raise SystemExit(f"Path not found: {target_path}") recovery_bytes = parse_recovery_id(args.recovery_id) machine_bytes = parse_machine_id(args.machine_id) key = xor_recover_key(recovery_bytes, machine_bytes) print(f"[+] Recovered key (hex): {key.hex().upper()}") if args.input: if not args.output: raise SystemExit("--output (-o) is required in single-file mode (-i).") decrypt_aes_ctr_file(Path(args.input), Path(args.output), key) print(f"[+] Decrypted: {args.output}") return folder = Path(args.dir) if not folder.is_dir(): raise SystemExit(f"Folder not found: {folder}") ok, fail = decrypt_folder(folder, key, args.ext, args.recursive) print(f"[+] Done. success={ok}, failed={fail}") if __name__ == "__main__": main() |
• IoCs
| Hash(SHA-256) |
| 12BBA7161D07EFCB1B14D30054901AC9FFE5202972437B0C47C88D71E45C7176 |
| 05294C9970F365C92E0B0F1250DB678DC356DBF418DBA27BDD5EEB68487A7199 |
■ 참고 사이트
cyberscoop.com(https://cyberscoop.com/phobos-ransomware-affiliate-arrested-poland/#:~:text=The%2047,programs%20used%20to%20conduct%20cyberattacks) reliaquest.com(https://reliaquest.com/blog/threat-spotlight-storm-2603-exploits-CVE-2026-23760-to-stage-warlock-ransomware/#:~:text=,facing%20systems)
1 IAB(Initial Access Broker): 기업이나 기관의 네트워크에 대한 초기 침투 권한을 확보한 뒤 이를 랜섬웨어 조직 등 다른 위협 행위자에게 판매하는 중개자
2 뮤텍스(Mutex): 하나의 자원에 여러 스레드 혹은 프로세스가 동시에 접근하지 못하도록 하는 동기화 매커니즘으로, 랜섬웨어에서는 흔히 중복 실행 방지를 위해 사용한다.
3 IV(Initialization Vector): 암호화 과정에서 사용되는 초기값으로, 동일한 키를 사용하더라도 각 파일이 서로 다른 암호문을 갖도록 하기 위해 사용된다.
4 ASR (Attack Surface Reduction): 공격자가 사용하는 특정 프로세스와 실행 가능한 프로세스를 차단하는 보호 기능