악성코드 분석 2026-09-15
| 위협 유형 | 다단계 실행 구조의 랜섬웨어 · TXT와 HTML 랜섬 노트에는 CRPxO 명칭 사용 |
| 실행 단계 | 드로퍼 → 내장 로더(index.dll) → sys_service.py → 내부 Python 페이로드 |
| 암호화 방식 | Fernet(AES-128-CBC + HMAC-SHA256) · Fernet 키는 RSA-4096 공개키와 OAEP-SHA256으로 암호화 |
| 암호화 범위 | 파일 앞부분 최대 1 MiB만 암호화 · 나머지 데이터는 그대로 유지 |
| 감염 흔적 | .crpx0 확장자 · TXT와 HTML 랜섬 노트 · .d0078e02.png 배경 이미지 · scan_report.json |
| 네트워크 전파 | 기본 실행 경로에서 키 형식 처리 오류로 호스트 검색 전에 중단 |
| 01개요 | 06Python 페이로드 분석 |
| 02구성 요소 연결 및 실행 흐름 | 07결론 |
| 03드로퍼 분석 | 08IOCs |
| 04네이티브 로더 분석 | 09참고 자료 |
| 05Python 복원 스크립트 분석 |
CRPX0는 다단계 실행 구조의 랜섬웨어로, TXT와 HTML 랜섬 노트에는 CRPxO라는 명칭이 사용된다. 공격자는 자료 유출 사이트와 Tor 기반 협상 주소, 메신저 연락처를 제시하며 암호화폐 결제를 요구한다. 24시간 이내에 연락하면 요구 금액을 50% 할인한다고 안내하고, 48시간이 지나면 탈취한 자료를 공개하거나 판매하겠다고 위협한다.
분석 대상은 내장 로더와 sys_service.py를 거쳐 내부 Python 페이로드를 복원하는 다단계 구조다. 이 페이로드에는 장치 정보 수집, 선별 파일 전송, 파일 앞부분 최대 1 MiB 암호화, 랜섬 노트 생성 및 배경화면 변경 기능이 포함된다. 다만 네트워크 전파는 기본 실행 경로에서 키 형식 처리 오류로 호스트 검색 전에 중단된다.
최초 실행 파일은 내장 로더를 %LOCALAPPDATA%\index.dll로 복원해 저장한다. 로더 실행을 요청한 직후에는 종료를 기다리지 않고 해당 파일을 삭제하려 시도한다. 로더는 실행 환경을 검사한 뒤 필요한 Python ZIP 파일을 내려받고 sys_service.py를 복원한다. 이어 get-pip.py를 준비하고 Python ZIP의 압축을 해제해 실행 환경을 구성한 뒤 스크립트 실행을 요청한다. sys_service.py는 Base64 디코딩과 zlib 압축 해제를 거쳐 내부 Python 페이로드를 복원한다.
페이로드는 장치 정보를 수집한 뒤 권한 상승을 시도하고, 중복 실행 여부를 확인한다. 이어 지속성 설정과 보안 제품 종료를 시도한다. 이후 파일을 탐색하고 전송할 파일을 선별해 외부 전송을 시도한다. 전송 시도가 끝나면 10초간 대기한 뒤 TXT 랜섬 노트를 기록하고 파일을 암호화한다. 이후 배경화면 설정과 추가 랜섬 노트 기록을 거쳐 자기 삭제 단계로 넘어간다. 다만 기본 실행 경로에서는 키 형식 처리 오류로 호스트 검색 전에 전파 함수가 종료되므로 네트워크 전파는 진행되지 않는다.
< 그림 1 > 전체 실행 흐름
| 구성 요소 | 식별 내용 |
|---|---|
| 드로퍼 (sys_d1c5307f.exe) |
PE32 실행 파일(32비트) · 3,884,544 B SHA-256 : 28685dff00aa1752b62a8580955b2530d63092bdcc0528b872a668cddad78c11 |
| 내장 로더 (index.dll) |
PE32 실행 파일(32비트, GUI) · 3,868,672 B SHA-256 : 5856f684c90dba657f5cd77dd337d81fd7503b5225e1843341c1f483eebc9560 |
| Python 복원 스크립트 (sys_service.py) |
Python 소스 코드 · 3,804,024 B SHA-256 : d76c40ff5766b90f153da40f372ea9a98e4890e450c705eee495b042c2a098ec |
| 내부 Python 페이로드 | Python 소스 코드 · 3,930,267 B SHA-256 : 752b8fe4e67803e15be65ac5d88be1b12d7d375ecb399cc96efa6f428e04fed2 |
| 배경 이미지 (.d0078e02.png) |
PNG 이미지 · 2,813,145 B SHA-256 : db34de096cfb2aa5e4ea579c11c3ed6b2095d39c73e74bf4d444fc9d6fe4bb4a |
| 드로퍼 → 로더 | 내장 데이터 복호화 결과의 SHA-256 확인 |
| 로더 → 복원 스크립트 | 0xD6 단일 바이트 XOR로 복원 |
| 페이로드 → 이미지 | 내장 Base64 복원 결과의 PNG 서명 및 SHA-256 확인 |
※ index.dll은 저장 파일명이며, PE 헤더에는 DLL로 설정되어 있지 않다.
< 표 1 > 구성 요소의 형식과 연결 관계
최초 실행 파일은 64바이트 크기의 키 두 개를 복원한 뒤, 내장 데이터의 각 바이트를 두 번째 키와 XOR 연산한다. 이후 결과값을 오른쪽으로 1비트 회전하고 모든 비트를 반전한 다음, 첫 번째 키와 다시 XOR해 로더를 복원한다.
복원한 로더를 %LOCALAPPDATA%\index.dll로 저장한 뒤 3밀리초 동안 대기한다. 이후 창을 표시하지 않는 옵션으로 실행을 시도하고, 로더의 종료를 기다리지 않은 채 파일 삭제 API를 호출한다.
< 그림 2 > 내장 데이터 복호화 과정
< 그림 3 > 저장 경로 구성과 복호화 결과 기록
< 그림 4 > 프로세스 실행 및 복원 파일 삭제 요청
디버거 실행 여부와 관련 설정을 확인하고, CPU가 1개 이하이거나 물리 메모리가 약 1.5 GiB 이하인지, C 드라이브 용량이 50 GiB 미만이거나 시스템 가동 시간이 5분 이하인지 점검한다. 이 중 하나라도 해당하면 사실상 무기한 대기하며, 어느 조건에도 해당하지 않을 때만 약 4초간 추가로 대기한 뒤 다음 단계로 진행한다.
< 그림 5 > 실행 환경 검사 및 무기한 대기 분기
XOR 연산으로 난독화된 DLL 및 API 이름을 복원한 뒤 해당 DLL을 로드하고 필요한 API 주소를 동적으로 조회한다. 조회한 API는 프로세스 생성, 메모리 조작, 네트워크 통신 및 파일 처리에 활용한다.
< 그림 6 > XOR 문자열 복원 및 API 주소 동적 조회
HTTP 상태 코드를 확인해 200~299 범위의 응답만 받아들이지만, 상태 코드 조회에 실패하면 이 검사를 건너뛰고 데이터를 수신한다. 수신한 데이터는 8,192바이트 단위로 파일에 저장하며, 받은 데이터가 없으면 다운로드 실패로 처리한다. 첫 바이트가 ‘<’로 시작하는 응답은 HTML로 분류하고, 전체 크기가 8,191바이트 이하이면 저장한 파일을 삭제한다.
< 그림 7 > HTTP 상태 확인과 수신 조건
Python 3.10.11의 amd64 및 win32 임베디드 ZIP과 get-pip.py 다운로드 주소는 코드에 고정되어 있다. URL에 .onion이 포함된 경우에만 별도 처리 분기로 이동하지만, 실제 사용하는 세 주소에는 .onion이 없어 해당 분기는 실행되지 않는다.
< 그림 8 > Python 실행 환경 다운로드 URL 및 조건 분기
Windows 아키텍처에 따라 amd64 또는 win32 Python ZIP 파일을 선택하고, 다섯 개 임시 디렉터리 후보 중 하나를 작업 경로로 사용한다. 이후 환경 변수와 python310._pth 설정에 필요한 문자열을 복원해 Python 실행 환경을 구성한다.
< 그림 9 > Python 실행 환경 및 작업 경로 구성
tar 명령으로 Python ZIP 파일의 압축 해제를 시도하고 프로세스 종료를 최대 60초간 기다린다. 압축 해제에 실패하면 Windows 기본 압축 해제 기능으로 재시도하며, Python 실행 파일과 관련 설정 파일은 같은 작업 디렉터리에 배치한다.
< 그림 10 > tar 실행 및 종료 코드 확인
로더는 내장된 3,804,024바이트 데이터를 0xD6으로 XOR 연산해 sys_service.py로 복원하고 저장한다.
< 그림 11 > sys_service.py 복원 및 파일 저장
로더는 get-pip.py 실행, sys_service.py 실행, pip 업그레이드, 필수 패키지 설치 순으로 프로세스 생성을 요청한다. 이때 앞서 생성한 프로세스가 종료될 때까지 기다리지 않으므로, 필수 패키지 설치가 끝나기 전에 sys_service.py가 실행될 수 있다.
< 그림 12 > sys_service.py 실행 요청
※ 그림 11과 그림 12는 실제 로더 코드를 요약해 재구성한 의사코드다.
sys_service.py에는 내부 Python 페이로드를 zlib으로 압축한 뒤 Base64로 인코딩한 데이터가 내장되어 있다. 복원에 사용하는 함수명은 문자열 대신 문자 코드 값으로 표현되어 있으며, 실행 시 각 값을 chr()로 문자로 변환한 뒤 join()으로 이어 붙여 함수명을 구성한다. 이렇게 구성한 함수명은 getattr()에 전달해 해당 모듈의 함수를 참조한다.
이 방식으로 base64 모듈의 b64decode() 함수를 호출해 내장 데이터를 디코딩하고, 그 결과를 zlib 모듈의 decompress() 함수로 압축 해제한다. 이를 통해 3,930,267바이트의 Python 소스 코드가 복원된다.
복원한 소스 코드는 별도 파일로 저장하지 않고 compile()을 통해 Python이 실행할 수 있는 코드 객체로 변환한 뒤 exec()로 실행한다. 이 과정에서 별도의 프로세스를 생성하지 않으므로, 내부 페이로드는 sys_service.py와 동일한 Python 프로세스에서 동작한다.
< 그림 13 > 내장 데이터 디코딩과 내부 페이로드 실행
※ 그림 13은 실제 코드의 일부이며, 분량이 많은 Base64 문자열은 생략하였다.
복원된 Python 페이로드는 장치 정보를 수집하고 코드에 저장된 C2 주소와 인증값, 랜섬 노트 내용을 복원한다. 이후 디버거 연결 여부와 가상화 및 분석 환경의 흔적을 검사하고, NTDLL 복원과 AMSI 및 ETW 패치를 차례로 시도한다.
환경 변수 CRPX0_EMBEDDED가 1이면 암호화 라이브러리의 설치 여부를 확인하고, 설치되어 있지 않으면 설치를 시도한다. 그 외에는 필요에 따라 Python 가상 환경을 구성해 페이로드를 다시 실행한다. 상위 식별자가 전달되면 해당 값과 장치 정보를 갱신한다.
기본 실행 경로에서는 권한 상승을 시도한 뒤 중복 실행 여부를 확인한다. 이어 자동 실행을 설정하고 보안 제품을 종료하려 시도하며, 파일을 탐색해 선별한 파일의 외부 전송을 시도한다. 전송 시도가 끝나면 TXT 랜섬 노트를 기록하고 파일을 암호화한 뒤 배경화면을 변경하고 랜섬 노트를 추가로 기록한다. 암호화 처리 함수가 예외 없이 반환하면 자기 삭제 절차로 넘어가며, 함수 내부에서 처리하지 못한 예외가 발생한 경우에는 오류 상태를 C2로 전송한다.
< 그림 14 > 초기 설정 복원과 Python 실행 환경 준비
< 그림 15 > 실행 옵션 분기와 기본 실행 흐름
프로세스 디버그 포트를 확인하고, 대기와 반복 계산에 걸린 시간을 측정해 디버깅이나 실행 지연 여부를 검사한다. IsDebuggerPresent를 이용한 안티디버깅 코드도 있지만, API 이름이 잘못 복원돼 실제 호출로 이어지지 않는다. 50,000회 반복문에는 Windows API 5개 호출과 사인, 코사인 및 MD5 계산이, 1,000,000회 반복문에는 GetCurrentProcessId 호출과 제곱근, 사인 계산이 들어 있다.
< 그림 16 > 디버거 API 복원 오류와 시간 기반 검사
CPU 수와 MAC 주소 패턴, 분석 도구 관련 경로, 사용자명과 컴퓨터 이름, 가상화 관련 서비스를 확인해 가상 머신이나 분석 환경의 흔적을 찾는다. 또한 하드웨어 중단점 검사를 시도하지만, GetThreadContext로 현재 실행 중인 스레드의 상태를 조회하는 방식이어서 유효한 검사 결과를 얻지 못한다.
< 그림 17 > 디버깅 및 가상화 환경 검사 코드
디스크의 ntdll.dll에서 .text 섹션을 읽어 현재 프로세스에 로드된 NTDLL의 같은 영역에 덮어쓴다. 이 과정에서 해당 메모리 영역을 쓰기 가능한 상태로 변경하고, 복사가 끝나면 기존 보호 속성으로 되돌린다.
< 그림 18 > NTDLL .text 섹션 복원 과정
AMSI 패치 함수는 DLL과 함수명을 XOR로 복원하지만, 잘못된 이름이 생성돼 대상 함수의 주소를 얻지 못하고 패치 전에 종료된다. 이후에 위치한 32비트 패치 코드에도 메모리 보호 범위를 6바이트로 지정하면서 실제로는 8바이트를 기록하는 불일치가 있다. ETW 패치 함수는 EtwEventWrite의 주소를 찾아 패치 데이터를 기록하고, 기존 메모리 보호 속성으로 되돌리도록 한다.
< 그림 19 > AMSI 문자열 복원 오류와 ETW 패치 코드
MAC 주소를 기반으로 장치 식별자를 생성하고, 외부 IP 주소와 국가·위치 정보를 조회한다. 수집한 정보는 운영체제, 사용자명, 상위 식별자와 함께 C2 요청에 포함된다.
< 그림 20 > 장치 식별 정보 수집 및 구성
C2로 전송하는 JSON 데이터에는 작업 식별자와 상태, 장치 정보, 제휴자 번호, 시간이 포함된다. 요청 헤더에는 임의로 선택한 User-Agent와 Bearer 인증값을 설정하고, 작업 식별자와 상태도 함께 넣는다.
C2 주소 목록은 IP 주소와 .onion 주소로 구성된다. IP 주소에는 HTTP로 접속하므로 통신이 TLS로 암호화되지 않는다. 또한 SOCKS나 Tor 프록시를 설정하는 코드가 없어 .onion 주소에 접속하기 위한 경로는 별도로 마련되어 있지 않다.
< 그림 21 > C2 요청 데이터와 연결 방식
로그인 시 Python 페이로드가 자동으로 실행되도록 Windows에서는 OneDrive Sync Maintenance 예약 작업 등록을 시도하고, macOS에서는 LaunchAgents 폴더에 com.apple.sync.plist 파일을 기록한다.
< 그림 22 > 운영체제별 로그인 자동 실행 설정 시도
중복 실행을 방지하기 위해 Windows에서는 이름이 지정된 Mutex를 사용하고, macOS와 Unix 계열에서는 사용자 홈 디렉터리에 잠금 파일을 생성한다.
< 그림 23 > 운영체제별 중복 실행 방지
운영체제별 명령을 이용해 백업과 복구 기능을 방해하려 시도하고, 이어 Windows에서는 C:부터 Z:까지의 드라이브를, Unix 계열에서는 루트 경로(/)를 탐색한다. 탐색 결과를 scan_report.json에 저장하고, 해당 파일에는 작업 식별자와 장치 정보, 전체 파일 수와 총용량, 범주별 파일 경로를 기록한다. 이후 scan_report.json과 탐색 소요 시간, 디스크 사용 요약, 범주별 파일 개수를 C2로 전송하려 시도한다.
< 그림 24 > 운영체제별 파일 탐색 시작 경로
운영체제별로 지정된 경로와 점(.)으로 시작하는 이름은 탐색 대상에서 제외하지만, .ssh, .bash_history, .zsh_history에는 이 이름 제외 규칙을 적용하지 않는다. 다만 .bash_history와 .zsh_history도 확장자 분류 조건을 충족해야 탐색 보고서와 암호화 대상에 포함된다. 탐색한 파일은 유형별로 분류하고 크기를 합산해 scan_report.json에 기록한다.
< 그림 25 > 운영체제별 제외 조건과 파일 분류
Desktop, Documents, Downloads에서 일반 문서를 무작위로 최대 5개 선택한다. 이어 사용자 홈에서 파일명에 지정 키워드가 포함되거나 지정된 확장자를 가진 파일 중 크기가 50 MiB 미만인 파일을 최대 10개 추가로 선별하고, 파일명을 기준으로 중복을 제거한다. 브라우저나 메신저의 데이터 형식을 직접 분석하는 코드는 없다.
AppData는 대소문자 비교 오류로 제외되지 않아 하위 파일도 선별 조건을 충족하면 수집 대상에 포함될 수 있다. 이와 별개로 일반 파일 탐색과 암호화 대상에는 .db, .sqlite, .sqlite3, .json 확장자도 포함된다.
< 그림 26 > 문서 및 우선 수집 대상 파일 선별과 일반 탐색 범위
선별한 파일을 ZIP으로 압축해 512 KiB 단위로 나눠 전송을 시도하고, 전송이 끝나면 성공 여부와 관계없이 ZIP 파일 삭제를 시도한다. 실행 과정에서는 crpx0_debug.log에 디버그 로그를 기록할 수 있다.
< 그림 27 > ZIP 생성 및 512 KiB 단위 분할 전송 시도
백업과 복구를 방해하기 위해 Windows에서는 볼륨 섀도 복사본과 백업 카탈로그 삭제를 시도한다. macOS에서는 tmutil로 Time Machine 로컬 스냅샷을 정리하고, Linux에서는 timeshift로 스냅샷을 삭제하려 시도한다.
< 그림 28 > 운영체제별 백업 및 복구 데이터 삭제 명령
사전에 지정된 프로세스와 서비스를 대상으로 taskkill을 이용한 프로세스 종료와 net stop·sc stop을 이용한 서비스 중지, sc delete를 이용한 서비스 삭제를 시도한다. 별도로 정의된 EDR 우회 함수는 기본 실행 경로에서 호출되지 않는다.
< 그림 29 > 프로세스 종료와 서비스 중지 및 삭제 명령
ms-settings 관련 레지스트리 값을 설정한 뒤 fodhelper를 실행해 권한 상승을 시도한다. 이때 실제로 관리자 권한이 획득되었는지는 별도로 확인하지 않으며, 별다른 오류 없이 실행되면 성공한 것으로 간주한다. Python 페이로드는 이 결과를 기준으로 현재 프로세스를 종료하므로, 실제 권한 상승에 실패한 경우에도 기존 프로세스가 종료될 수 있다.
< 그림 30 > fodhelper를 이용한 권한 상승 시도
최대 32개 파일을 동시에 처리하며, 각 파일의 앞부분 최대 1 MiB만 Fernet으로 암호화한다. 암호화한 앞부분과 나머지 원본 데이터를 .crpx0 확장자를 붙인 새 파일에 저장한 뒤 원본 파일을 삭제하려 시도한다. 이때 임시 파일을 거치거나 저장 내용을 별도로 검증하는 절차는 없다.
새 파일의 접근·수정 시간을 원본 파일과 동일하게 설정하고, 암호화 처리량은 암호화한 앞부분 데이터의 크기만 합산한다. 암호화가 진행되는 동안에는 조건에 따라 사용자 홈에 1~4 KiB 임시 파일을 생성했다가 삭제하며, AMSI와 ETW 패치 함수도 별도의 조건에 따라 다시 호출한다.
< 그림 31 > 파일 앞부분 암호화와 결과 기록
Fernet 키를 생성한 뒤 등록된 C2 주소로 원본 키를 포함한 요청을 순서대로 보낸다. 어느 주소에서도 전송에 성공하지 못하면 전체 과정을 반복하되, 최초 시도를 포함해 최대 10회 수행한다. HTTP 200 응답을 정상적으로 처리하면 나머지 주소에 대한 요청과 이후 재시도를 중단한다. 모든 전송이 실패해도 원본 키를 로컬에 저장하지 않고 파일 암호화 단계로 진행한다.
파일 암호화에 앞서 RSA-4096 공개키와 OAEP-SHA256을 이용해 Fernet 키를 암호화한다. 파일 암호화가 끝난 뒤 처리 상태를 C2로 전송할 때도 원본 Fernet 키를 함께 보낸다.
< 그림 32 > Fernet 키 전송의 중단 조건 및 RSA-OAEP 암호화
Fernet 키를 직접 입력받는 복호화 경로에서는 먼저 지속성 설정을 재시도한다. 이후 파일 선두의 RSA 암호화 데이터 영역을 건너뛰고 입력받은 키로 Fernet 토큰을 복호화한 뒤, 복호화한 데이터와 암호화되지 않은 나머지 데이터를 결합해 별도 파일로 저장한다. 마지막으로 기존 .crpx0 파일의 삭제를 시도한다.
< 그림 33 > Fernet 키 기반 복호화와 별도 출력 파일 기록
RSA 개인키로 파일에 저장된 암호화 데이터를 복호화해 Fernet 키를 복원한다. 이 키로 파일의 앞부분을 복호화하고, 암호화되지 않은 나머지 부분과 결합해 별도 파일에 저장한 뒤 기존 .crpx0 파일을 삭제하려 시도한다.
< 그림 34 > RSA 개인키를 이용한 Fernet 키 복원
< 그림 35 > 암호문 읽기와 별도 출력 파일 기록
※ 두 복호화 경로는 scan_report.json에 기록된 경로를 우선 사용하며, 대상이 없으면 Windows의 C:~Z: 드라이브 또는 Unix 계열의 / 경로에서 .crpx0 파일을 재귀 검색한다. 지속성 설정 재시도는 Fernet 키를 직접 전달받는 경로에서만 수행된다.
.crpx0 파일에는 RSA-OAEP-SHA256으로 암호화한 Fernet 키, 파일 앞부분을 암호화한 Fernet 토큰, 암호화하지 않은 나머지 데이터가 순서대로 저장된다. 암호화된 키 앞에는 길이를 나타내는 4바이트 값이, Fernet 토큰 앞에는 길이를 나타내는 8바이트 값이 붙는다.
복호화할 때는 HMAC-SHA256으로 Fernet 토큰을 검증하고, RSA 개인키로 키를 복원하는 과정에서 OAEP 검증을 수행한다. 그러나 길이 값과 암호화되지 않은 나머지 데이터까지 보호하는 인증값은 없어 파일 전체의 변조 여부를 확인하지는 못한다.
파일 형식을 구분하는 고정값이나 버전 정보는 없으며, 원본 파일의 크기·경로·전체 해시도 저장하지 않는다. 복호화 시 암호화된 키와 Fernet 토큰의 길이가 허용 범위에 있는지 검사하지만, 이것만으로 파일 전체의 구조가 정상인지 보장할 수는 없다.
< 그림 36 > .crpx0 파일 구성과 영역별 검증 범위
전파 코드에는 도메인과 호스트 이름을 조회하고 DNS로 IP 주소를 확인하는 기능이 포함되어 있다. 이후 로컬 /24 대역과 ARP 캐시에서 사설 IP 주소를 수집해 TCP 445 포트 접속 여부와 SMB 공유 정보를 확인하도록 구성되어 있다. 다만 기본 실행 경로에서는 키 형식 처리 오류로 전파 함수가 호스트 검색 전에 종료돼, 이러한 네트워크 탐색은 수행되지 않는다.
< 그림 37 > 도메인과 호스트 정보 조회 및 IP 주소 확인
< 그림 38 > 로컬 /24 대역과 ARP 캐시의 대상 후보 수집
< 그림 39 > TCP 445 연결 및 SMB 공유 경로 검사
전파 함수는 바이트 형식의 Fernet 키에 문자열 인코딩을 적용하려다 오류가 발생한다. 이 오류를 내부에서 처리한 뒤 호스트 검색 전에 종료되므로, 기본 실행 경로에서는 네트워크 전파가 진행되지 않는다.
< 그림 40 > Fernet 키 형식 처리 오류
일반 공유와 관리자 공유에서 5 MiB 이하 파일을 찾아 앞부분 최대 1 MiB를 암호화하도록 작성된 코드다. 다만 파일 처리 건수가 다음 호스트에도 누적돼, 해당 호스트에서 파일을 하나도 처리하지 않아도 성공으로 집계될 수 있다. 기본 실행 경로에서는 앞선 키 형식 처리 오류로 함수가 종료되므로 이 암호화 코드는 실행되지 않는다.
< 그림 41 > 공유 폴더 부분 암호화와 처리 건수 누적
관리자 공유 경로에 파일을 복사한 뒤 WMIC로 원격 실행을 시도하고, 실패한 것으로 판단하면 OneDrive Maintenance 예약 작업을 등록해 SYSTEM 권한으로 실행을 시도한다.
< 그림 42 > 관리자 공유·WMIC·예약 작업을 이용한 원격 실행 코드
SSH 접속 대상은 known_hosts 파일과 TCP 22 포트 연결 여부를 확인해 찾는다. 이후 SCP로 대상 시스템에 파일을 복사하고, SSH로 원격 명령 실행을 시도한다.
별도로, 도메인 정보가 있고 앞선 전파 시도에서 성공으로 기록된 대상이 없으면 SYSVOL 경로를 확인한다. 해당 경로가 존재하면 실행 명령을 담은 startup.bat 파일을 생성하려 시도한다.
< 그림 43 > SCP·SSH 실행 및 SYSVOL 스크립트 기록 조건
※ 기본 실행 경로에서는 앞선 키 형식 처리 오류로 이 절에서 설명한 원격 실행 및 전파 코드에 도달하지 않는다.
Explorer 프로세스에 메모리를 할당해 코드와 명령 문자열을 기록한 뒤 원격 스레드 생성을 시도한다. 메모리 쓰기의 성공 여부는 확인하지 않으며, 32비트 주입 코드에는 잘못된 스택 값을 인자로 전달하는 오류가 있다. 또한 원격 스레드 생성에 실패해도 주입 함수가 0을 반환할 수 있다.
< 그림 44 > Explorer 프로세스 원격 메모리 쓰기 및 스레드 생성 시도
명령 실행 함수는 주입 함수가 반환한 0을 성공으로 판단한다. 따라서 실제로 주입에 실패했더라도 반환값이 0이면 다른 방식으로 명령 실행을 시도하지 않는다.
< 그림 45 > 반환값 처리 오류와 대체 실행 분기
내장된 정수 배열에서 TXT와 HTML 랜섬 노트 내용을 XOR로 복원하고, RECOVERY ID 항목에 작업 식별자 OP_1785692479를 넣는다. 암호화 초기에는 기본 위치에 TXT 노트를 생성하며, Fernet 키의 RSA-OAEP 암호화가 끝나면 같은 위치에 다시 기록하려 시도한다. 파일 암호화 후에는 암호화한 파일이 있는 폴더에도 TXT 노트를 남긴다. 이어 전파 함수를 호출하고, 함수가 반환하면 바탕 화면에 HTML 노트를 저장해 열기를 시도한다.
배경 이미지는 내장된 Base64 데이터를 디코딩해 사용자 홈 폴더에 .d0078e02.png로 저장한 뒤 배경화면 설정을 시도한다. 파일명은 점(.)으로 시작하지만, Windows의 숨김 속성은 별도로 설정하지 않는다.
< 그림 46 > Base64 PNG 복원 및 운영체제별 배경화면 설정
< 그림 47 > TXT 랜섬 노트 기록 위치 및 파일 생성
< 그림 48 > Python 페이로드 내부 Base64 데이터에서 복원한 배경 이미지
그림 49는 내장 템플릿에서 복원한 TXT 랜섬 노트의 첫 부분이다. 공격자는 파일 암호화와 데이터 탈취를 주장하며, 24시간 이내에 연락하면 요구 금액을 할인하고 48시간 이후에는 자료를 공개하겠다고 안내한다. 노트에는 작업 식별자와 자료 유출 사이트 주소도 포함되어 있다.
< 그림 49 > Python 페이로드에 내장된 템플릿에서 복원한 TXT 랜섬 노트 일부
랜섬 노트의 주장과 코드에서 확인한 구현에는 다음과 같은 차이가 있다.
| 구분 | 랜섬 노트 주장 | 코드 확인 결과 |
|---|---|---|
| 암호화 방식 | AES-256과 RSA-2048 사용 | Fernet은 AES-128-CBC와 HMAC-SHA256을 사용하며, Fernet 키는 RSA-4096 공개키와 OAEP-SHA256을 이용해 암호화한다. |
| 수집 및 전송 범위 | 모든 중요 데이터의 사본 확보 | 일반 문서는 최대 5개, 우선 수집 대상 파일은 최대 10개까지 선별해 전송을 시도한다. 탐색한 파일의 경로와 통계는 별도의 C2 요청에 포함한다. |
| 암호화 및 공개 범위 | 중요 파일 전체 암호화와 48시간 뒤 자동 공개를 주장 | 대상 파일의 앞부분 최대 1 MiB만 암호화하며, 나머지 데이터는 그대로 둔다. 감염된 시스템에서 데이터를 자동으로 공개하는 코드는 없다. |
※ 랜섬 노트는 공격자의 협박 문구이므로, 암호화 방식과 전송 범위, 자료 공개 여부에 대한 주장은 코드에서 확인한 동작과 구분해 해석해야 한다.
< 표 2 > 랜섬 노트 주장과 코드 확인 결과
암호화 처리 함수가 오류 없이 반환하면 파일 삭제용 스크립트를 생성한다. Windows에서는 VBS 스크립트가 약 3초간 대기한 뒤 대상 파일의 내용을 덮어쓰고 삭제하며, 마지막으로 스크립트 자체도 삭제한다. macOS와 Linux에서는 /tmp에 셸 스크립트를 만들어 대상 파일의 내용을 비운 뒤 삭제하도록 한다.
페이로드가 저장된 폴더의 경로에 Cache_Sys 또는 Temp가 포함되어 있으면 개별 파일뿐 아니라 해당 폴더 전체도 삭제 대상에 포함한다.
< 그림 50 > 자기 삭제 대상 경로와 VBS 대기 명령 구성
< 그림 51 > VBS 파일·폴더 삭제 및 스크립트 자체 삭제
< 그림 52 > macOS·Linux 삭제 스크립트와 기본 호출 순서
CRPX0는 내장 로더와 sys_service.py를 통해 Python 페이로드를 복원하는 다단계 랜섬웨어다. 최초 실행 파일이 로더를 복원해 실행을 요청하고, 로더는 Python 환경을 준비한 뒤 sys_service.py의 실행을 요청한다.
내부 페이로드는 장치 정보를 수집하고, 지속성 설정과 보안 제품 종료를 시도한다. 선별한 파일을 압축해 외부 전송을 시도하며, 암호화 대상 파일은 앞부분 최대 1 MiB만 암호화한다. 랜섬 노트 생성과 배경화면 변경, 자기 삭제 기능도 포함되어 있다. 다만 네트워크 전파는 키 형식 처리 오류로 호스트 검색 전에 중단된다.
TXT와 HTML 랜섬 노트에는 Tor 기반 협상 주소와 qTox·Session 연락처가 포함되어 있다. 공격자는 데이터를 탈취했다고 주장하며 48시간 후 공개를 위협하지만, 노트에 적힌 암호화 방식과 데이터 유출 범위는 코드에서 확인한 구현과 일치하지 않는다.
| 구성 요소 | SHA-256 |
|---|---|
| 드로퍼 | 28685dff00aa1752b62a8580955b2530d63092bdcc0528b872a668cddad78c11 |
| 내장 로더 | 5856f684c90dba657f5cd77dd337d81fd7503b5225e1843341c1f483eebc9560 |
| Python 복원 스크립트 | d76c40ff5766b90f153da40f372ea9a98e4890e450c705eee495b042c2a098ec |
| 내부 Python 페이로드 | 752b8fe4e67803e15be65ac5d88be1b12d7d375ecb399cc96efa6f428e04fed2 |
| 배경 이미지 | db34de096cfb2aa5e4ea579c11c3ed6b2095d39c73e74bf4d444fc9d6fe4bb4a |
< 표 3 > 분석 대상 구성 요소 SHA-256
0dc6a5ce14838813e297a83e3c3c0868c7a211ee2da9c9705e5a282e65f089fe
ebb58e0cc4d8e081707f843508b13cc45c6f8c3dfff42f27c4fc4bc70493f0d7
55a81d88fff3dc3de0e42724b0a992d4ef382b0e1a82187abf9d635ab89b2bae