악성코드 분석 2026-08-13
| 위협 유형 | 서비스형 랜섬웨어(RaaS), 2025년 12월 러시아어권 사이버범죄 포럼에서 최초 확인 |
| 지원 플랫폼 | Windows, Linux, VMware ESXi (VECT 2.0, 2026년 2월 공개) |
| 암호화 방식 | ChaCha20-IETF, 실행 파일 내장 32바이트 고정 키 + 12바이트 난스(nonce) |
| 감염 흔적 | .vect 확장자, !!!_READ_ME_!!!.txt 랜섬노트, dvm3_wall.bmp 바탕화면 |
| 확산 방식 | 네트워크 공유 연결, Active Directory 조회 후 WMI/CIM 원격 실행 |
| 복구 가능성 | 128 KiB 초과 파일은 난스 N1~N3가 기록되지 않아 원본 완전 복구 불가 |
VECT는 2025년 12월 러시아어권 사이버범죄 포럼에서 처음 확인된 서비스형 랜섬웨어(RaaS) 조직이다. 제휴자에게는 악성코드 제작 도구와 함께 피해자 협상 창구, 유출 자료 공개 사이트를 묶어 제공한다. 2026년 2월에는 Windows와 Linux, VMware ESXi를 지원하는 VECT 2.0을 공개했고, 3월에는 TeamPCP와의 협력을 발표하면서 BreachForums 등록 사용자를 제휴자로 받는 공개 가입 모델을 운영했다.
분석한 VECT 2.0은 관리자 권한을 확인한 뒤 복구 기능을 방해하고 서비스와 프로세스를 종료한다. 이어서 네트워크 공유와 Active Directory에 등록된 컴퓨터를 탐색해, Windows 원격 관리 기능인 WMI와 CIM으로 다른 시스템에서도 실행을 시도한다. 암호화를 마치면 랜섬노트를 남겨 자료를 이미 확보했다고 주장하지만, 주 실행 흐름에는 파일을 수집하거나 외부로 보내는 단계가 없다.
특히 128 KiB를 넘는 파일을 암호화하는 과정에서 특이점이 확인됐다. 각 파일을 네 구간으로 나눠 암호화하며 구간마다 새 일회용 난수값(난스, nonce)을 만드는데, 이 값을 담아 두는 공간이 하나뿐이라 마지막 난스인 N4만 파일 끝에 남는다. 파일을 정상적으로 복구하려면 N1~N3가 필요하지만 파일 어디에도 남지 않으므로, 고정 키와 N4로는 네 번째 구간만 복구할 수 있다.
분석한 VECT 2.0은 기본 설정을 세우고 명령행 옵션을 적용한 뒤, 중복 실행 표시 파일을 확인한다. 이전 실행이 남긴 파일이 열리면 종료하고, 열리지 않으면 관리자 권한과 안전 모드 상태를 확인한다.
이어지는 단계는 확인한 조건에 따라 갈린다. 관리자 권한이 있으면 복구 기능을 방해한 뒤 암호화 대상 경로를 수집하고, 권한이 없으면 방해 단계를 건너뛴다. 경로 탐색과 파일 암호화는 별도 스레드에서 병행되며, 탐색 과정에서 랜섬노트를 생성한다. 로컬 암호화가 끝나면 시작 경로에 C: 또는 c:가 있을 때만 바탕화면 이미지를 만들고, 상세 출력(-v)이 켜져 있으면 최종 통계를 표시한다.
마지막으로 표시 파일을 기록한 뒤, --gpo가 켜져 있으면 원격 실행을 시도한다. 이후 흔적 삭제와 --stealth 조건에 따른 자기 삭제를 거쳐 종료한다.
< 그림 1 > VECT 2.0 전체 실행 흐름
| 항목 | 분석 결과 |
|---|---|
| 식별명 | VECT 2.0 |
| 형식 | PE32+ 실행 파일, Windows AMD64 |
| 크기 | 1,453,056 bytes |
| MD5 | 207b1a60f803d348c795d382f5aed9c3 |
| SHA-256 | 8ee4ec425bc0d8db050d13bbff98f483fff020050d49f40c5055ca2b9f6b1c4d |
| 내부 표시 문자열 | VECT 2.0 |
| 생성 파일 | .vect / !!!_READ_ME_!!!.txt / dvm3_wall.bmp |
< 표 1 > 분석 대상 VECT 2.0 파일 정보
프로그램은 실행 옵션을 담아 두는 메모리 구조의 +336(0x150)부터 +339(0x153)까지 네 바이트를 모두 1로 채운 뒤, 이 영역에서 세 값을 읽어 기본 동작을 결정한다. +336은 --mount, +338은 --stealth, +339는 --gpo에 해당하며 세 옵션 모두 켜진 상태로 동작한다. 따라서 별도 지정 없이 실행해도 네트워크 공유 연결과 원격 실행, 자기 삭제 단계를 수행한다. 나머지 +337은 대응하는 옵션이 없으며 이후 어디에서도 참조하지 않는다.
< 그림 2 > 기본 옵션 4바이트 초기값
< 그림 3 > 기본 설정 활성화
지원하는 옵션은 도움말, 상세 출력, 대상 경로, 자격 증명, 공유 연결, 원격 실행, 자기 삭제, 안전 모드 설정이다.
이 가운데 --gpo와 --mount, --stealth는 기본으로 켜져 있어 --no-gpo, --no-mount, --no-stealth로 꺼야 한다.
경로 옵션(-p, --path)에 값을 주면 자동으로 찾은 드라이브 목록 대신 지정한 경로만 암호화한다. 하지만 네트워크 공유 연결은 암호화 경로를 정하기 전에 실행되기 때문에 관리자 권한에서 --mount가 켜져 있으면 경로를 지정해도 공유 연결을 시도한다.
< 그림 4 > 명령행 인자 설정
< 그림 5 > 도움말에 표시되는 전체 옵션
자격 증명은 파일 안에 Base64로 들어 있으며, 디코딩하면 <도메인>\<사용자명>:<암호> 형식이라 이를 셋으로 나눠 사용하지만 -c 옵션으로 다른 값을 넣을 수도 있다.
이 자격 증명이 쓰이는 곳은 WMI와 원격 관리 규격인 CIM을 이용한 원격 실행뿐이며, 파일 복사와 공유 연결은 현재 로그인 계정 권한으로 시도한다.
< 그림 6 > Base64 문자열에서 도메인, 계정, 암호 분리
중복 실행 여부는 C:\ProgramData\.vect를 읽기 전용으로 열 수 있는지만 보고 판단하며, 파일이 열리면 이전 실행이 남긴 표시로 보고 종료한다.
표시 파일은 실행 초기에 확인하지만, 로컬 암호화와 랜섬노트, 바탕화면 처리가 모두 끝난 뒤에야 같은 경로의 파일에 문자 ‘1’을 기록하고, 상세 출력(-v)이 켜져 있으면 기록 직전에 최종 통계를 표시한다. 확인과 기록 사이가 이렇게 벌어져 있어, 두 프로세스가 거의 동시에 시작하면 양쪽 모두 표시 파일이 없다고 판단해 함께 실행될 수 있다.
< 그림 7 > 표시 파일을 이용한 중복 실행 확인
< 그림 8 > 로컬 처리 후 최종 통계, 표시 파일 기록
Windows 토큰 정보를 읽어 현재 프로세스가 관리자 권한인지 확인한다. 다만 권한이 부족해도 스스로 끌어올리려 시도하지 않고, 확인한 권한 수준에 맞춰 이후 동작만 나눈다.
< 그림 9 > Windows 토큰 정보로 관리자 권한 확인
안전 모드 여부는 GetSystemMetrics로 부팅 상태(SM_CLEANBOOT)를 조회하며, 반환값이 0이 아니면 안전 모드로 판단한다. --force-safemode가 켜지면 bcdedit를 창 없이 실행해 다음 부팅을 최소 안전 모드로 설정하고 최대 5초간 기다린 뒤, 재부팅하지 않고 다음 단계로 넘어간다.
관리자 권한으로 안전 모드에서 실행된 경우, 종료 단계에서 부팅 구성 데이터(BCD)의 safeboot 설정을 지운다. SafeBoot와 Run 레지스트리 값은 그대로 남아 다음 부팅에도 적용된다.
< 그림 10 > bcdedit 안전 모드 설정 시도
관리자 권한이면 --force-safemode 사용 여부와 무관하게 현재 실행 파일 경로를 SafeBoot\Minimal과 SafeBoot\Network, HKLM\Run에 등록한다. SafeBoot와 HKLM\Run에 쓰는 등록값 이름은 실행 파일명에서 처음 나오는 소문자 ‘.exe’ 앞부분이고, 소문자 ‘.exe’가 없으면 파일명 전체를 쓴다.
이어서 PowerShell의 Set-MpPreference로 실시간 감시와 동작 감시, 다운로드 파일 검사(IOAV), 스크립트 검사를 끄도록 요청하고 DisableTaskMgr=1을 기록해 작업 관리자 실행을 막는다. 종료 단계에서 되돌리는 항목은 BCD의 safeboot 설정 하나뿐이다. SafeBoot와 Run 등록, Defender 설정, 작업 관리자 차단은 실행이 끝난 뒤에도 남는다.
< 그림 11 > SafeBoot와 Run 레지스트리 등록
< 그림 12 > Defender 보호 설정 변경과 작업 관리자 차단
관리자 권한이면 Windows 서비스 제어 관리자(SCM)를 통해 28개 서비스 이름을 차례로 조회하고, 실행 중인 항목에 중지 명령을 보낸다. 프로세스는 실행 파일명이 8개 대상 이름 중 하나와 대소문자 구분 없이 정확히 일치할 때 종료 코드 9를 지정해 강제 종료를 요청한다.
< 그림 13 > Windows 서비스 제어 관리자를 이용한 중지 요청
< 그림 14 > 대상 파일명 일치 시 프로세스 종료 요청
같은 관리자 권한 조건에서 vssadmin delete shadows /all /quiet를 창 없이 실행하고 최대 30초를 기다린다. 30초 안에 종료되지 않으면 vssadmin 프로세스를 강제로 종료하지 않은 채 다음 단계로 넘어간다. 따라서 해당 프로세스가 백그라운드에서 계속 실행될 수 있다.
< 그림 15 > vssadmin을 이용한 볼륨 섀도 복사본 전체 삭제 시도
아래 표는 설정에서 확인된 종료 대상을 정리한 것이다. 서비스 28개, 프로세스 8개이며 백업, 데이터베이스, 문서 편집기, 메일 클라이언트 관련 항목이 함께 들어 있다.
| 구분 | 대상 이름 | 처리 방식 |
|---|---|---|
| 서비스 |
vss, sql, svc$, memtas, mepocs, sophos, veeam, backup, GxVss, GxBlr, GxFWD, GxCVD, GxCIMgr, DefWatch, ccEvtMgr, ccSetMgr, SavRoam, RTVscan, QBFCService, QBIDPService, Intuit.QuickBooks.FCS, QBCFMonitorService, YooBackup, YooIT, MSSQLSERVER, MySQL57, MySQL80, wbengine |
SCM에서 대상 서비스를 이름으로 조회한 뒤, 실행 중인 경우 중지 명령을 보내고 다음 항목으로 넘어간다. |
| 프로세스 |
sql.exe, oracle.exe, excel.exe, outlook.exe, winword.exe, mysqld.exe, firefox.exe, thunderbird.exe | 실행 파일명을 대소문자 구분 없이 정확히 비교하고, 대상 이름과 일치하는 프로세스에 종료를 요청한다. |
< 표 2 > 종료 대상 서비스와 프로세스
관리자 권한에서 --mount가 켜져 있으면 드라이브 문자가 없는 볼륨을 찾아 X:\부터 C:\ 방향으로 사용 가능한 드라이브 문자를 할당한다. 이 단계는 --path 적용보다 앞에 있어, 경로를 지정해도 그대로 실행된다.
암호화 대상 경로는 이동식 디스크와 고정 디스크, 원격 드라이브, RAM 디스크, 그리고 Windows가 인식한 네트워크 공유에서 모으며 CD-ROM은 제외한다.
< 그림 16 > 볼륨 GUID와 드라이브 문자 할당 시도
< 그림 17 > Windows API를 이용한 로컬 드라이브 목록 수집
공유 연결도 관리자 권한에서 --mount가 켜져 있을 때 동작한다. 현재 로그인 계정 권한으로 Z:에서 M: 방향으로 사용 가능한 드라이브 문자를 찾아 연결을 시도한다. 앞서 본 X:~C: 방향의 로컬 볼륨 문자 할당과는 별개 단계다.
< 그림 18 > 현재 로그인 계정 권한을 이용한 네트워크 공유 연결
수집한 경로 중 같은 네트워크 공유를 가리키는 UNC 경로가 여러 번 확인되면 하나만 남기고 나머지를 제거한다. 드라이브 문자로 이미 매핑된 공유는 목록에 추가하지 않으며, 매핑되지 않은 UNC 경로만 암호화 대상에 넣는다. 이 과정을 거친 뒤에도 대상 경로가 하나도 없으면 C:\를 사용한다.
< 그림 19 > 드라이브 문자에 매핑된 UNC 경로 중복 제거
제외 판정은 디렉터리와 파일을 따로 한다. 디렉터리는 이름이 비어 있거나 $로 시작하는 경우, 또는 시스템, 부팅, 프로그램 관련 12개 제외 대상 이름과 일치하는 경우 대소문자를 구분하지 않고 제외한다.
파일 쪽은 부팅 관련 파일명과 .exe, .dll, .sys 확장자를 제외한다. 랜섬노트는 !!!_READ_ME_!!!.txt와 정확히 일치할 때만 제외하므로 대소문자가 다르면 암호화 대상에 들어간다. 파일명이 .vect로 끝나고 .vect 앞에 한 글자 이상 있으면 이미 암호화된 파일로 보고 제외한다. 반면 파일명이 정확히 .vect이면 제외 조건에 해당하지 않는다. 설치 패키지(.msi)를 걸러내는 조건은 없다. 심볼릭 링크와 디렉터리 연결 지점을 일반 디렉터리처럼 처리하므로, 연결된 경로까지 탐색할 수 있다.
< 그림 20 > 파일 암호화 제외 조건
탐색과 암호화는 서로 다른 스레드가 맡는다. 경로 탐색 스레드가 시작 경로 아래를 훑으면서 조건을 통과한 파일을 대기 목록에 넣으면, 암호화 스레드가 목록에서 하나씩 꺼내 처리한다. 랜섬노트는 탐색한 디렉터리에 같은 파일이 없을 때만 만든다.
처리 건수는 암호화 함수를 호출한 직후 올라간다. 실패한 파일도 여기에 포함된다.
< 그림 21 > 조건을 통과한 파일을 암호화 대기 목록에 추가
< 그림 22 > 암호화 호출 직후 증가하는 처리 건수
작업 스레드 수는 논리 프로세서 수에 따라 정해진다. 프로세서가 2개 이하면 16개, 3~4개면 8배, 5~8개면 6배, 9개 이상이면 4배이며 상한은 256개다. 이 중 약 1/8은 경로 탐색에, 나머지는 파일 암호화에 배정한다. 계산 결과가 적더라도 경로 탐색은 최소 4개, 암호화는 최소 12개를 만든다.
문제는 이렇게 만든 스레드가 모두 끝나기를 기다리는 단계에서 나타난다. 여러 스레드의 종료를 한 번에 기다리는 Windows API는 최대 64개까지만 처리하는데, 논리 프로세서가 19개 이상이면 암호화 스레드가 이 한도를 넘는다. 코드는 스레드 목록을 나누지 않고 한 번에 전달하므로, 고사양 환경에서는 종료를 기다리는 단계가 실패할 수 있다.
< 그림 23 > 암호화 스레드 대기 요청
암호화 키는 실행 파일 안에 32바이트 고정값으로 저장돼 있다. 구간마다 Windows 난수 생성기로 12바이트 난스를 새로 만들어 ChaCha20-IETF로 내용을 암호화한다. 위변조를 확인하는 인증값(태그)은 붙이지 않는다.
128 KiB를 넘는 파일은 네 위치에서 데이터를 읽고, 읽기에 성공한 구간마다 새 난스를 생성해 암호화한다. 네 구간이 모두 처리되면 N1부터 N4까지 차례로 만들어진다.
< 그림 24 > 난스 생성과 ChaCha20-IETF 암호화
파일 크기를 N이라 하면, 128 KiB를 넘는 파일은 0, N/4, 2N/4, 3N/4 위치에서 각각 최대 32 KiB를 암호화한다. 해당 위치에서 1바이트라도 읽혀야 그 구간을 처리한다. 네 구간이 모두 32 KiB씩 처리되더라도 실제로 바뀌는 데이터는 최대 128 KiB이고, 구간 사이 영역은 원본 그대로 남는다.
< 그림 25 > 128 KiB 초과 파일의 네 암호화 위치와 최대 처리 범위
< 그림 26 > 네 구간에 각각 적용되는 12바이트 난스 N1~N4
암호화 함수는 N1부터 N4까지 만들면서 같은 12바이트 공간을 계속 덮어쓴다. 난스를 담아 둘 자리가 하나뿐이라, 네 구간을 모두 처리하고 나면 그 자리에는 N4만 남는다. 그래서 고정 키와 N4로 되돌릴 수 있는 범위는 네 번째 구간뿐이다. 앞의 세 구간을 복호화하려면 N1~N3가 필요한데 파일 어디에도 남지 않아, 원본 상태로 복구하려면 이 값들이 덮어써지기 전에 메모리에서 확보했거나 별도 원본 백업이 있어야 한다.
< 그림 27 > 대용량 파일 끝에 N4만 남는 저장 구조
암호화보다 이름 변경이 먼저 일어나며, <원본>.vect로 바꾸는 데 성공하면 그 파일을 열어 크기에 맞는 방식을 고른다. 0바이트 파일은 이름만 바뀌고 끝난다.
| 파일 크기 | 암호화 방식 | 추가 특징 및 복구 영향 |
|---|---|---|
| 공통 | <원본>.vect로 이름을 변경한 뒤 암호화를 시작 | 이름 변경에 성공한 파일만 다음 단계로 넘어간다. |
| 128 KiB 이하 | 32 KiB 버퍼에 파일 전체를 한 번 읽어 암호화 |
파일 끝에 12바이트 난스 하나를 기록하며, 고정 키와 해당 난스로 복호화할 수 있다. 32 KiB를 넘는 파일은 읽기 길이가 버퍼 크기를 넘어선다. |
| 128 KiB 초과 | 0, N/4, 2N/4, 3N/4 위치에서 각각 최대 32 KiB 암호화 |
네 구간이 모두 처리돼도 N4만 파일 끝에 기록된다. 고정 키와 N4로는 네 번째 구간만 복호화할 수 있으며, 원본과 동일한 복구에는 N1~N3 또는 백업이 필요하다. |
< 표 3 > 파일 크기별 암호화 방식과 복구 영향
< 그림 28 > 암호화 전에 .vect로 변경되는 파일명
< 그림 29 > 파일 끝에 마지막 12바이트 난스 기록
경로 탐색 스레드는 탐색한 각 디렉터리와 바탕화면에 !!!_READ_ME_!!!.txt를 만든다. 같은 이름이 이미 있으면 손대지 않는다. 처음 만들 때는 암호화 시도 건수와 원본 크기 합계를 모두 0으로 기록하고, 각 시작 경로의 탐색이 끝나면 누적값으로 갱신한다.
랜섬노트 경로를 만들 때는 시작 경로 문자열 끝에 백슬래시를 붙이지 않고 파일명을 그대로 이어 붙인다. 시작 경로가 D:\data라면 생성 경로는 D:\data!!!_READ_ME_!!!.txt가 된다.
노트 본문에는 Tor 주소와 qTox 연락처, 감염 시스템마다 다른 피해자 ID가 들어간다. 여기에 자료를 이미 확보했다는 주장도 함께 적혀 있다.
< 그림 30 > 랜섬노트에 피해자 ID 삽입
< 그림 31 > 경로 탐색 완료 후 랜섬노트 통계 갱신
< 그림 32 > 생성된 랜섬노트 실제 출력 화면
1920×1080 크기의 24비트 BMP를 %TEMP%\dvm3_wall.bmp에 만들고 VECT 2.0과 YOUR FILES HAVE BEEN ENCRYPTED, 피해자 ID를 그려 넣는다. 단, 암호화를 시작한 경로에 C: 또는 c:가 들어 있어야 이미지 생성과 바탕화면 변경으로 넘어간다.
이때 경로 전체를 따지지 않고 해당 문자가 들어 있는지만 확인하므로, 경로 중간에 있어도 조건을 통과한다. 조건을 통과하면 BMP를 만든 직후 Windows 바탕화면 변경 함수를 호출한다.
< 그림 33 > 1920×1080 24비트 BMP 생성 구조
< 그림 34 > BMP 생성 후 바탕화면 설정 호출
< 그림 35 > 통제 환경에서 확인한 VECT 2.0 바탕화면 이미지
상세 출력 옵션(-v)을 주면 실행 중 진행 상황을 콘솔에 출력한다. 약 500ms 간격으로 암호화 시도 건수를 표시하고, 로컬 암호화가 끝난 뒤 표시 파일 기록과 --gpo 원격 실행 사이에 최종 통계를 한 번 출력한다.
최종 통계에는 Files encrypted와 Data encrypted 두 값이 나온다. 앞의 값은 암호화 함수를 호출한 직후 증가하므로 이름 변경이나 파일 열기에 실패한 파일, 이름만 바뀐 0바이트 파일도 함께 집계된다. 뒤의 값은 대상 파일의 원본 크기를 그대로 더한 값이라, 128 KiB를 넘는 파일이 실제로는 최대 128 KiB만 바뀌어도 전체 크기가 합계에 반영된다.
< 그림 36 > 약 500ms 간격의 암호화 시도 건수 출력
--gpo가 켜져 있으면 원격 실행을 담당하는 스레드 세 개를 만든다. 각 스레드는 Active Directory에서 조회한 컴퓨터의 C$\ProgramData에 실행 파일 복사를 시도하고, 복사에 성공한 대상에서 서로 다른 방식으로 실행을 시도한다.
< 그림 37 > 원격 실행 스레드 생성
원격 실행 스레드가 끝나면 흔적을 지운다. 관리자 권한이면 Application과 Security, System, Windows PowerShell 이벤트 로그 삭제를 시도하고, PSReadLine 명령 기록과 Recent 폴더 항목은 권한과 무관하게 지운다. 다만 Recent는 하위 폴더까지 들어가지 않고 바로 아래 항목만 처리한다.
흔적을 지운 뒤, --stealth가 켜져 있으면 자기 삭제를 시도한다. 현재 실행 파일 경로를 삭제 명령에 넣고, ping으로 시간을 지연시킨 뒤 실행한다.
< 그림 38 > PSReadLine과 Recent 사용 기록 삭제
< 그림 39 > --stealth 기반 지연 자기 삭제 명령
분석한 VECT 2.0은 관리자 권한에서 Defender 설정을 바꾸고 서비스와 프로세스를 종료한 뒤, 볼륨 섀도 복사본과 이벤트 로그를 지운다. 로컬과 네트워크 경로의 파일명을 .vect로 바꿔 암호화하며, WMI와 CIM을 이용한 원격 실행도 시도한다.
가장 큰 특징은 파일 크기에 따라 갈리는 암호화 처리다. 128 KiB를 넘는 파일에는 앞 세 구간을 되돌릴 N1~N3가 남지 않아, 이 값이나 원본 백업이 없으면 원본과 동일하게 복구할 수 없다. 128 KiB 이하 파일도 32 KiB 버퍼에 파일 전체를 읽어 들이므로, 32 KiB를 넘는 파일에서는 읽기 길이가 버퍼 크기를 넘어선다.
8ee4ec425bc0d8db050d13bbff98f483fff020050d49f40c5055ca2b9f6b1c4d
| 플랫폼 | SHA-256 |
|---|---|
| Windows | 9c745f95a09b37bc0486bf0f92aad4a3d5548a939c086b93d6235d34648e683f |
| Windows | e512d22d2bd989f35ebaccb63615434870dc0642b0f60e6d4bda0bb89adee27a |
| Linux | e1fc59c7ece6e9a7fb262fc8529e3c4905503a1ca44630f9724b2ccc518d0c06 |
| ESXi | a7eadcf81dd6fda0dd6affefaffcb33b1d8f64ddec6e5a1772d028ef2a7da0f2 |
| ESXi | 58e17dd61d4d55fa77c7f2dd28dd51875b0ce900c1e43b368b349e65f27d6fdd |