


요약
2026년 초부터 국제 여행사 직원을 대상으로 한 피싱 공격이 지속적으로 식별되었다.
공격자는 문의 메일 내에 이미지 파일로 위장한 LNK 악성코드를 첨부하여 TonRAT 악성코드를 유포하였다.
TonRAT 악성코드는 javascript 난독화 도구인 obfuscator.io로 난독화되어 악성 행위를 은닉하였으며, 텔레그램에서 개발한 TON 블록체인 내 컨트랙트로부터 C2 서버 주소를 조회하는 방식으로 탐지를 회피하였다.
1. 개요
2026년 초부터 VirusTotal에 같은 패턴의 이름을 가진 Windows 바로가기 파일(LNK)이 지속적으로 식별되었다. LNK 파일들은 이미지 및 영상 파일로 위장하였으며 실행 시 공격자 서버로부터 PowerShell 스크립트를 받아 실행하고, NodeJS를 다운로드해 최종적으로 JavaScript로 작성된TonRAT 악성코드를 실행한다.
외부 보고서[1] 확인 결과, 해당 LNK 파일들은 해외 여행사 직원 및 관련 숙박업체를 대상으로 유포된 것으로 확인되었다. TonRAT 악성코드는 텔레그램에서 개발한 TON(The Open Network) 블록체인의 특정 컨트랙트 0:c66119f0e5635c4380441d7a79baf0c02a0ab7ea6cd78de06507fc5dc2c1a5d9에 접근해 C&C 서버 도메인 주소를 획득한 뒤 WebSocket으로 명령을 수신한다. 또한 외부 URL에서 PE 파일을 받아와 메모리에 로드하여 실행하는 기능과 자가 업데이트 기능 등이 구현되어 있어 공격자가 추가 페이로드를 실행할 수 있게 하였다.
TonRAT 악성코드는 JavaScript 난독화 도구인 obfuscator.io의 상용 기능(VM 난독화)이 적용되어 있어 주요 악성 행위를 수행하는 함수들이 가상화되어 있고, 정적 분석과 동적 분석을 어렵게 하였다. 이후 공격자는 빠른 주기로 지속적으로 악성코드를 업데이트해 재배포하였으며, 난독화 패턴 또한 지속적으로 변경하였다.
2. 악성코드 분석
2.1. LNK 파일 분석
먼저 현재까지 식별된 LNK 파일들은 공통적으로 아래 표와 같은 메타데이터를 포함하고 있다.
이름 | 값 |
|---|---|
Machine ID | win-5r0dsv23ed0 |
MAC Address | 00:50:56:86:f7:6d |
Volume Droid | 967cc9dc-3fe1-429e-9dc4-38de7cc90834 |
Volume Serial | 865D06F4 |
[표 1] LNK 파일 메타데이터
MAC Address의 00:50:56은 VMware의 기본 MAC OUI 값으로, 공격자가 가상 환경에서 작업하였을 가능성이 있다. 다만 LNK 파일의 메타데이터는 수정이 가능하기에 실제 작업 환경을 단정 짓기에는 어려움이 있다.
VirusTotal에서 "metadata:win-5r0dsv23ed0" 질의를 통해 약 740여 개 LNK 파일을 식별할 수 있다.

caption - [그림 1] VirusTotal 쿼리 결과

caption - [그림 2] 이미지 파일로 위장한 LNK 파일
LNK 파일을 실행하면 powershell.exe를 "-ep bypass -c \"<명령>\"" 인자와 함께 실행한다. 전달되는 명령은 공격자 서버로부터 PowerShell 스크립트를 "%TEMP%\<무작위 이름>.ps1" 경로에 다운로드한 후 실행한다.
공격자는 LNK 파일이 실행하는 PowerShell 명령을 조금씩 수정하여 보안 제품의 탐지를 회피하였다. 초기에는 공격자 서버 주소가 평문으로 저장되어 있어 곧바로 다운로드하는 방식이었으나, 이후 URL을 두 큰 정수의 차로 정한 뒤 실행 시점에 256진법으로 한 글자씩 복원하는 BigInt Decoder 방식을 사용해 평문으로 노출되던 URL 주소를 감췄다.
caption - [그림 3] 2026-04-01 - IMG-215546827.png.lnk

caption - [그림 4] 2026-05-25 - IMG-845246947.png.lnk
최근에는 아래 그림처럼 Invoke-Expression 명령으로 PowerShell 스크립트를 저장하지 않고 바로 실행한다.
caption - [그림 5] 2026-07-24 - IMG_20260724_005632.jpg.lnk
2.2 PowerShell 스크립트 분석
공격자 C&C 서버에서 다운로드해 실행되는 PowerShell 스크립트는, 먼저 피해자가 감염된 사실을 인지하지 못하도록 창을 은닉한다. 이때 Add-Type 명령으로 user32.dll의 ShowWindow 함수를 불러와 자기 자신 프로세스부터 부모 프로세스까지 거슬러 올라가며 프로세스의 창을 숨긴다.

caption - [그림 6] 창 은닉 코드
이후 악성코드가 중복으로 실행되는 것을 방지하기 위해 "Global\<GUID>" 이름의 Mutex를 생성하고, 이미 존재하면 실행을 종료한다.

caption - [그림 7] Mutex 확인 코드
또한 NodeJS 공식 사이트에서 "v24.13.0/node-v24.13.0-win-x64.zip"를 "%USERPROFILE%\AppData\Local\Nodejs\" 경로에 설치 및 압축 해제하여 최종적으로 "node.exe"가 생성된다.

caption - [그림 8] NodeJS 공식 홈페이지에서 다운로드
이후 AES-256-CBC 암호화된 TonRAT 페이로드를 복호화해 "%USERPROFILE%\AppData\Local\Nodejs\<이름>.js" 경로에 저장한 뒤 인자로 URL을 전달해 node.exe로 실행한다.

caption - [그림 9] TonRAT 페이로드 저장 경로
스크립트 내에는 아래 표와 같이 5개의 주요 데이터를 base64 인코딩 및 AES 암호화하여 평문으로 노출하지 않게 한다. AES 복호화 과정은 사용되는 key와 iv를 디코딩한 뒤, 나머지 3개 값을 복호화한다.
항목 | 값 | 용도 |
|---|---|---|
key | e1b6c9b6a28861e65a8d0db11892d29f186383b61aa9b8f33bf5c5e52ab3516b | AES-256-CBC 키 |
iv | de8e71ceb31d513df2d8022a1bf5f07c | AES-256-CBC IV |
mutex | Global\0be276c4-7dfb-4d5f-a8e2-fd7c5735c489 | 단일 실행 Mutex 이름 |
URL | derbyonly[.]info | node 실행 인자로 전달되는 URL |
js | ... | UTIJVUDSUcl55k.js 파일 내용 |
[표 2] PowerShell 스크립트 내 암호화된 데이터
위 값들을 포함한 변수 및 함수 이름 등은 PowerShell 스크립트마다 다른 값으로 설정되었고, JavaScript 페이로드 실행 시 전달되는 인자 URL 값은 ".com", ".info", ".pro", ".cfd" 등 다양하게 사용되었다. Mutex 총 8가지 값이 사용되었고, 각 값들은 아래 표와 같다.
항목 |
|---|
Global\0be276c4-7dfb-4d5f-a8e2-fd7c5735c489 |
Global\6e5fcec5-3a02-4561-93ad-3f634f83a122 |
Global\24f2b9f2-7586-4623-9a9f-8f49ab2e2ba2 |
Global\59541b77-7fb8-4285-9d95-7bcce4e995ff |
Global\39d81f9b-6ab5-4f9e-8af9-61409f1ea339 |
Global\9045b3ab-cb45-495e-acca-eebe4c128a46 |
Global\c8f375c1-0241-4b84-a35a-aabfc4b3eef7 |
Global\61fc5f6f-4bdc-47e7-a533-c6f920059411 |
[표 3] 사용된 Mutex 목록
2.3 TonRAT 분석
NodeJS로 실행되는 JavaScript 페이로드는 obfuscator.io에 의해 난독화되어 있다.

caption - [그림 10] obfuscator.io로 난독화된 TonRAT 페이로드
먼저 obfuscator.io 공식 문서 및 깃허브에 공개된 소스코드를 통해 기본 난독화의 종류와 적용되는 방식을 확인할 수 있다.
문서: https://obfuscator.io/docs/
깃허브: https://github.com/javascript-obfuscator/javascript-obfuscator
무료로 제공되는 기본 난독화 기능은 문자열 난독화, 제어 흐름 평탄화(CFF), 안티 디버깅 등 분석을 방해하는 요소들로 이루어져 있다.

caption - [그림 11] obfuscator.io 공식 문서의 기본 난독화 기능
TonRAT 악성코드는 위와 같은 기본적인 난독화를 비롯해 상용 버전에서만 지원하는 VM 난독화가 적용되어 있다. TonRAT 악성코드는 배포된 것마다 각각 변수 및 함수 이름이 다르며, 여기서는 6VFpDMvd.ps1 파일에 포함된 P9A6nXkD.js를 기준으로 난독화 방식과 해제 방식을 설명한다.
파일명 | 해시(MD5) |
|---|---|
6VFpDMvd.ps1 | 02660615917556efd7606908765b6f7b |
P9A6nXkD.js | 2c20f7ffce30600c53a869d6d18a9cd3 |
[표 4] 분석 기준 샘플 파일 정보
2.3.1 산술 난독화
먼저 산술 난독화란 원본 코드 내 숫자 상수들을 각각 여러 산술 연산으로 길게 표현하여 값을 숨기는 기법이다. 아래 그림의 함수는 문자열 난독화에 사용되는 디코더 함수로, 곳곳에 길게 늘어진 산술 표현식이 존재한다.

caption - [그림 12] 산술 난독화가 적용된 문자열 디코더 함수
이처럼 산술 난독화가 적용되면 원래 값을 알기 위해 일일이 계산해야 하므로 분석이 어려워진다.
산술 난독화의 해제는 AST(Abstract Syntax Tree)에서 모든 산술 표현식을 찾아 계산된 값으로 치환하는 방식으로 가능하다.
AST의 각 노드를 순회하며 표현식 노드 중 이항, 단항 노드인 BinaryExpression / UnaryExpression 노드를 찾는다.
해당 표현식을 구성하는 노드를 재귀적으로 탐색하며 전부 number Literal인지 확인하여 계산 가능한 산술 표현식인지 확인한다.
vm 샌드박스 위에서 실행해 표현식을 계산한 뒤 그 값으로 표현식 전체를 치환한다.
산술 난독화를 해제한 뒤의 vme 함수는 아래 그림과 같다.

caption - [그림 13] 산술 난독화 해제 이후의 vme 함수
2.3.2 문자열 난독화
다음으로 문자열 난독화는 원본 코드 내 문자열 상수들을 하나의 배열에 순서를 섞어 저장한 뒤 사용될 때마다 꺼내 쓰는 방식이다. 추가로 배열에 문자열들을 저장할 때 base64 인코딩하거나 RC4 암호화하는 설정을 적용할 수 있으며 공격에 사용된 코드에는 RC4 암호화 설정이 적용되었다.
예시로 아래 그림은 전역에 require, exports와 같은 CommonJS 환경을 저장하는 난독화된 코드이고, 산술 난독화를 해제한 상태이다. 문자열 상수가 있던 자리가 모두 vme 함수 호출로 대체되어 있어 분석이 어렵다.

caption - [그림 14] 문자열 난독화 예시
vme 함수의 문자열 디코딩 과정은 다음과 같다.
전달받은 인덱스에서 특정 오프셋을 뺀다. 해당 샘플에서는 160이며, 이는 샘플마다 다른 값으로 확인된다.
vmz 함수가 반환하는 암호화된 문자열 배열에서 해당 인덱스의 값을 가져온다.
base64 디코딩한 뒤 인자로 받은 key로 RC4 복호화를 진행한다. 결과는 캐시에 저장해 다음에 같은 인덱스의 문자열을 디코딩할 때 사용한다.
난독화 해제는 산술 난독화와 동일하게 AST 기반으로 진행된다.
난독화 코드 내에서 가장 긴 배열을 갖는 함수를 찾는다. 현재 샘플에서는 vmz 함수이며, 함수 내에 RC4 암호화 및 base64 인코딩된 문자열 배열을 갖고 있다.
찾은 배열 함수를 호출하는 디코더 함수를 찾는다. vmz 함수를 호출하는 유일한 함수 vme가 디코더 함수이다.
샌드박스에서 찾은 두 함수의 코드를 실행해 vm context 내에 함수를 정의한다.
난독화 코드 내 모든 디코더 함수 호출부마다 vm context 위에서 실행해 값을 계산한 뒤 치환하여 난독화를 해제한다.

caption - [그림 15] 문자열 난독화를 해제한 코드
2.3.3 제어 흐름 평탄화
문자열 난독화를 해제한 코드를 보면 제어 흐름 평탄화, CFF(Control Flow Flattening) 난독화가 적용되어 있다. 기존 코드의 실행 흐름을 상태 머신 기반 코드로 변환하여 분석을 어렵게 하는 난독화 기법이다.
해당 기법에 대한 자세한 설명은 obfuscator.io의 공식 문서에서도 확인 가능하다. 보편적인 CFF와는 달리 실행 흐름 순서를 정하는 상태값이 문자열에 정적으로 저장되어 있어 쉽게 원본 흐름을 복구할 수 있다.

caption - [그림 16] obfuscator.io 공식 문서의 제어 흐름 평탄화 설명
난독화 시 controlFlowFlatteningThreshold 설정값으로 원본 코드 내 함수마다 일정 확률로 CFF를 적용할 수 있다.
난독화 해제 방식은 간단하다. CFF가 적용된 코드에는 기존 순서를 '|'로 구분해 저장한 문자열이 존재한다. 아래 그림의 코드에서는 "4|1|2|3|6|5|0"이며, 원본 코드의 흐름을 나타낸다.

caption - [그림 17] CFF가 적용된 코드의 원본 순서 문자열
조건에 따라 다음 상태가 결정되지 않고 문자열에 원본 흐름이 정적으로 저장되어 있어 복구가 가능하다. 복구 방법은 다음과 같다.
먼저 코드 내 모든 BlockStatement 노드에 대해 CFF 난독화가 적용되었는지 확인한다.
해당 BlockStatement 노드의 자식 중에 WhileStatement 노드를 찾고 안에 SwitchStatement 노드가 있는 경우, switch 구문의 평가식이 arr[counter++] 형식인지 확인한다. 평가식의 배열과 카운터 변수의 이름 또한 추출한다.

caption - switch 구문의 평가식 부분
현재 WhileStatement 노드 앞의 VariableDeclaration 노드 중 앞서 구한 switch 평가식 내 배열 정의 노드를 찾는다. const arr = "".split('|') 형식의 노드를 찾아 원본 순서를 저장한 문자열을 구한다.

caption - WhileStatement 노드 앞의 원본 순서 문자열
SwitchStatement 노드의 SwitchCase 노드마다 case 조건식의 값과 본문 내용을 대응시키고 앞서 구한 원본 순서대로 정렬시켜 순서를 복구한다.
난독화를 해제한 코드는 아래 그림과 같다.

caption - 제어 흐름 난독화를 해제한 코드
추가로 obfuscator.io의 CFF 난독화는 Control Flow Replacer 난독화를 포함한다. 이는 코드 내의 표현식을 프록시 함수로 포장하여 분석을 어렵게 한다.
https://github.com/javascript-obfuscator/javascript-obfuscator/tree/master/src/node-transformers/control-flow-transformers/control-flow-replacers

caption - obfuscator.io 깃허브 내 CFF 난독화 구현부
Control Flow Replacer 난독화는 크게 아래 4가지의 표현식을 프록시 함수로 감싸 분석을 방해한다.
이항 연산식 (BinaryExpression): a - b, a + b 등
논리 연산식 (LogicalExpression): a && b, a || b 등
함수 호출식 (CallExpression): f(a, b) 등
문자열 상수 (StringLiteral): "object", "function" 등
프록시 함수들은 각 함수 초반부에 정의된 객체에 무작위 이름의 키로 저장되며, 아래 그림처럼 표현식을 대체한다.

caption - Control Flow Replacer 난독화로 생성된 프록시 함수들
Control Flow Replacer 난독화 해제는 다음과 같이 이루어진다.
먼저 프록시 함수들을 저장한 객체들을 찾는다. ObjectExpression 노드 내 key가 영문 5자인 경우 해제 대상으로 취급한다.
객체 내 key에 대응되는 값이 Literal인 경우 값을 그대로 저장하고, FunctionExpression인 경우 그 함수의 ReturnStatement 노드가 어떤 식을 반환하는지 해석한다.
이항 연산식
아래 조건들을 동시에 만족하는 경우 이항 연산식으로 판단한다.
함수가 반환하는 식이 BinaryExpression 또는 LogicalExpression 노드
그 노드의 좌우 피연산자가 둘 다 Identifier 타입
함수에 전달된 두 개의 인자가 위 노드의 좌우 피연산자와 동일

caption - 이항 연산식 예시
단항 연산식
아래 조건들을 동시에 만족하는 경우 단항 연산식으로 판단한다.
함수가 반환하는 식이 UnaryExpression 노드
노드의 피연산자가 Identifier 타입
함수의 인자가 한 개이며 위 노드의 피연산자와 동일
함수 호출식
아래 조건들을 동시에 만족하는 경우 함수 호출식으로 판단한다.
함수가 반환하는 식이 CallExpression 노드
피호출자가 Identifier 타입
프록시 함수의 인자 수가 피호출자 인자 수보다 하나 더 많음
프록시 함수의 두 번째 인자부터 마지막 인자까지 피호출자 인자와 대응

caption - 함수 호출식 예시
AST 노드를 순회하며 객체의 속성에 . 이나 [] 연산자로 접근하는 MemberExpression 노드 중 key가 앞서 구한 key이면 대응되는 표현식으로 대체하여 난독화를 해제한다.

caption - Control Flow Replacer 난독화 해제 이전 코드

caption - Control Flow Replacer 난독화 해제 이후 코드
2.3.4 VM 난독화
최근 obfuscator.io의 난독화 기능에 VM 난독화가 추가되었으며 매번 그 방식이 변화하고 있다. 나머지 기본 난독화는 깃허브에 소스가 공개되어 있는 반면 VM 난독화는 비공개로 상용 버전을 구매해야 사용 가능하다. 홈페이지의 docs 페이지에서는 VM 난독화의 특징을 소개하고 있다.

caption - obfuscator.io 공식 문서의 VM 난독화 설명
VM 난독화는 원본 소스코드 내 함수들에 적용되며, 함수 본문을 가상 머신 바이트 코드로 변환하여 함수의 동작을 숨겨 분석을 어렵게 한다. 또한 지속적으로 업데이트 되고 있기에 구조가 바뀔 수 있음을 주의해야 한다.

caption - obfuscator.io 홈페이지에 소개된 VM 난독화 예시
현재 샘플에서도 VM 난독화가 적용된 것이 확인된다. 앞에서의 기본적인 난독화를 해제하면 거대한 VM 객체와 공격자가 구현한 원본 클래스와 함수의 껍데기가 드러난다.

caption - VM 객체

caption - vmn_2b352a 함수 호출로 함수 본문이 대체된 모습
난독화된 함수의 본문에는 정상적인 로직 대신 vmn_2b352a 함수를 호출하는 코드만 존재하여 각 함수가 어떤 행위를 수행하는지 확인이 어렵다. 이처럼 함수들은 난독화 시점에 가상화되어 vmn_2b352a 객체에 저장되고, 호출 시점에 가상 머신에 의해 실행된다.
함수를 가상화하는 방식과 구조는, 가상화된 함수 본문에서 vmn_2b352a 호출 시 첫 번째 인자로 각 함수의 인덱스를 전달한다. 각 함수의 가상화된 본체는 VM 내 배열에 base64 인코딩되어 저장되어 있어 전달된 인덱스에 해당하는 함수를 실행한다.

caption - VM 객체 내 가상화되어 저장된 여러 함수들의 본문
base64로 인코딩된 내용은 함수 디스크립터 정보이다. 디스크립터에는 원본 함수가 async / generator 함수인지, 인자가 몇 개인지 등의 정보와 바이트 코드가 저장되어 있다.

caption - 가상화된 함수의 디스크립터 정보를 파싱한 결과
디스크립터 구조
함수가 가상화 될 때 아래와 같은 디스크립터 구조로 정보를 저장한다.
필드 | 크기 | 의미 |
|---|---|---|
version | 1 byte | 버전 |
flagsBits | 4 byte (LE) | 함수 속성 + 추가 필드 존재 여부 비트마스크 |
locals | varint | 지역 변수 개수 |
args | varint | 인자 개수 |
optional fields | 가변 | flagsBits 비트에 따라 조건부로 존재하는 추가 필드 |
const_count | varint | 상수 배열 길이 |
constants | 가변 | 함수 내 사용되는 문자열, 정수, bool 등의 상수 배열 |
instr_count | varint | 바이트 코드 명령어 수 |
bytecode | 가변 | opcode + operand 쌍. 함수에 따라 저장 순서 변동 |
jump_table | 가변 | 분기 시 출발지와 도착지 정보를 담은 배열 |
exception_table | 가변 | try/catch/finally 핸들러 범위 정보를 담은 배열 |
[표 5] 가상화 함수 디스크립터 구조
bytecode 필드에는 함수의 가상화 코드가 opcode와 operand의 쌍으로 저장된다. 저장 순서는 디스크립터의 locals, args, instr_count, const_count 4개 값으로 연산한 결과에 따라 결정된다. 연산 식은 다음과 같다.
mode | 저장 순서 |
|---|---|
0 | [op0, arg0, op1, arg1, ...] - 순차 배치 |
1 | [arg0, op0, arg1, op1, ...] - 순서 뒤집힘 |
2 | [op0, op1, ..., opN, arg0, arg1, ..., argN] - opcode 먼저 모두, 이후 operand 모두 |
3 | [arg0, arg1, ..., argN, op0, op1, ..., opN] - operand 먼저 모두, 이후 opcode 모두 |
즉 이는 같은 프로그램 안에서도 함수마다 배치 모드가 다를 수 있음을 의미한다. 디스크립터를 해석할 때 배치 모드를 계산해 올바른 순서로 opcode/operand를 복원해야 한다.
jump_table 필드는 함수 내 존재하는 분기점들의 위치와 어느 위치로 분기하는지에 대한 정보가 저장 되어있다. flagBits 필드의 11번째 비트가 해당 필드의 존재 여부를 나타낸다.
exception_table 필드는 함수 내 try/catch/finally 구문이 시작하는 곳과 끝나는 곳의 위치 정보가 저장 되어있다. flagBits 필드의 12 번째 비트가 해당 필드의 존재 여부를 나타낸다.
분기 처리
VM은 JavaScript의 if/else, while, for, switch, 삼항 연산자, 논리 연산자(||, &&) 등 모든 제어 흐름을 점프와 조건부 점프 opcode로 표현한다.
점프 명령 자체에는 점프 대상 주소가 들어 있지 않다. 대신 디스크립터 내 jump_table에 분기마다 출발 위치 - 도착 위치 쌍이 저장되어 있고, 인터프리터가 점프 명령을 만나면 현재 PC(Program Counter)로 점프 테이블을 조회해 도착 PC를 얻는다.
예시로 아래 그림은 PowerShell 명령을 실행하는 함수 u의 바이트 코드이다. 원본 코드에서 Windows 환경인지 확인해 powershell.exe를 셸로 지정하는 if 분기가 있다. 해당 함수 디스크립터에는 19 → 24 점프 테이블이 포함되어 있다.

caption - 분기 처리
PC 17에서 "process.platform.indexOf('win') !== -1" 조건을 계산한 뒤 PC 19의 JMP_IF_FALSE가 점프 테이블(19→24)을 조회해 조건이 거짓이면 PC 24로 점프하고, 참이면 다음 PC(20)로 진행해 "opts.shell = 'powershell.exe'"를 실행한다. PC 24가 if/else 양쪽의 합류 지점이다.
예외 처리
try/catch/finally 구문은 TRY_PUSH opcode로 시작한다. TRY_PUSH가 실행되면 인터프리터의 내부 예외 핸들러 스택에 핸들러 정보를 push한다. 핸들러 정보는 디스크립터 내 exception_table에 저장되어 있다.

caption - VM 내 예외 처리 구조
exception_table의 각 항목은 TRY_PUSH가 위치한 PC를 키로 하고, catch 블록 시작 PC, finally 블록 시작 PC, try 구문 종료 PC 세 값을 가진다. -1은 해당 블록이 없음을 의미한다.
VM 인터프리터는 전체가 try - catch 문으로 감싸져 있어 명령 실행 도중 예외 발생 시 catch 문으로 이동한다. catch 문에서는 TRY_PUSH로 등록된 핸들러가 있다면 핸들러 스택에서 등록된 catch PC로 점프한다. catch 블록 진입 시 핸들러 코드를 실행한다.

caption - VM 인터프리터의 예외 처리
정상 실행이 완료되면 LEAVE/LEAVE2 opcode로 try 블록을 빠져나가 end PC로 점프한다. finally가 있는 경우 FINALLY opcode가 finally 블록의 시작을 표시하고, END_FINALLY가 끝을 표시한다.
JavaScript 복원 과정
가상화되어 있는 함수들을 원본 형태의 JavaScript 코드로 복구하는 과정은 크게 다음 순서로 이루어진다.
디스크립터 추출
OPCODE 정규화
CFG(Control Flow Graph) 파싱
JavaScript 코드 조립
먼저 디스크립터는 VM 객체 내 배열로 저장되어 있으며 하나의 배열당 최대 50개의 함수가 저장된다. 가상화된 함수가 50개가 넘어갈 경우 여러 배열에 나뉘어 저장되는 방식이다.

caption - VM 객체 내 배열에 나뉘어 저장된 디스크립터
아래 그림은 VM 명령 디스패처를 참고해 디스어셈블러를 제작한 뒤, 한 함수의 디스크립터를 파싱해 디스어셈블한 결과이다.

caption - 가상화된 함수를 파싱한 모습
이후 함수의 CFG를 구하여 함수를 구성하는 코드 블록을 세분화하고 제어 흐름을 확보한다. CFG는 코드를 기본 블록 단위로 구분하고 간선으로 이어 제어 흐름을 나타낸 그래프로, 기본 블록은 분기 없이 실행되는 명령의 묶음이다. 각 기본 블록에는 들어오는 간선과 다음 블록으로 나가는 간선이 있으며, 이 관계를 분석해 지배자 트리를 계산하여 제어 흐름 그래프를 완성할 수 있다.
아래 그림은 파싱된 CFG이며, 조건에 따른 분기와 반복 구간이 복원된 모습이다.

caption - 복원된 Control Flow Graph
이후 복원된 CFG의 각 블록들을 순회하며 JavaScript의 if/else/while/try-catch 등의 구조를 복원한다. VM은 스택 기반으로 작동하기에 복원 시 스택 시뮬레이션을 통해 기존 표현식을 재조립해야 한다.
복원된 connect 함수의 본문은 아래 그림과 같다.

caption - 복원된 connect 함수 모습
2.3.5 행위 분석
가상화되어 있던 각 함수들의 본문을 복구하면 악성 행위가 확인된다. 행위 분석은 버전 1.2.1인 VrgLgfEeFLPIoNIV.js (a7c37ba4cf51458fb0a430beed560f88) 샘플을 기준으로 한다.

caption - 복원된 메인 함수
TonRAT은 먼저 현재 실행 중인 TonRAT 프로세스가 있는지 검사한다. "(Get-Process | Where-Object { $_.Path -eq "<현재 node.exe 경로>" }).Count" PowerShell 명령을 실행해 앞서 다운로드한 경로 내 node.exe가 실행 중인지 확인하고, 자신을 포함해 2개 이상인 경우 종료한다.

caption - 프로세스 검사 함수
다음으로 현재 실행 중인 node.exe의 버전이 "26.4.0"이 아니면 공식 사이트에서 최신 버전을 다운로드해 node.exe로 저장한다. 이때 기존 node.exe를 "node.exe.1"로 저장한 뒤 Start-Process 명령으로 새로 설치한 node.exe를 실행한다. 다음 실행 시점에 "node.exe.1" 파일을 제거한다.

caption - 업데이트 함수
이후 지속성 유지를 위해 레지스트리 Run 키에 현재 JavaScript 페이로드를 백그라운드로 실행하는 명령을 등록한다. 값 이름은 이전 PowerShell 스크립트에서 중복 실행 방지에 사용한 Mutex와 동일한 GUID가 사용된다. 이때 이미 등록되어 있다면 건너뛴다.
등록되는 명령은 PowerShell 스크립트로, child_process.spawn으로 현재 JavaScript 페이로드를 실행하는 코드를 node.exe에 -e 옵션으로 전달해 실행한다.

caption - 지속성 유지 함수
그 다음, C&C 주소 연결을 위해 TON 블록체인 컨트랙트를 조회해 C&C 서버 도메인 주소를 받아온다. "https://tonapi.io/"를 사용하며, 하드코딩된 컨트랙트 계정 주소의 get_domain 메서드를 호출한다.
컨트랙트 주소 | 0:c66119f0e5635c4380441d7a79baf0c02a0ab7ea6cd78de06507fc5dc2c1a5d9 |
|---|
[표 6] TON 스마트 컨트랙트 주소
"https://tonapi.io/v2/blockchain/accounts/0:c66119f0e5635c4380441d7a79baf0c02a0ab7ea6cd78de06507fc5dc2c1a5d9/methods/get_domain" URL을 조회하면, 아래 그림처럼 C&C 도메인을 확인할 수 있다.

caption - C&C 도메인 조회 결과
이후 "wss://<도메인>/w"와 같이 조합해 WebSocket 접속 URL을 만든다. 이때 프로세스 환경 변수 DEV가 설정되어 있으면 wss 대신 ws를 사용한다. 조회에 실패하거나 도메인이 비어 있으면 2000~6000ms 사이 무작위 시간 대기 후 접속을 재시도한다.

caption - C&C 도메인 조회 함수
접속 URL을 준비한 이후 WebSocket으로 연결해 핸드셰이크 과정을 진행한다. 먼저 secp256k1 키 쌍을 생성하고 공개 키를 담은 메시지를 C&C 서버로 전송한다. 이에 대한 응답으로 서버는 자신의 공개 키와 salt 값을 담은 메시지를 전송하고, TonRAT은 두 공개 키로 공유 비밀을 계산한 뒤 HKDF-SHA256 해시 48바이트를 구한다. 해시의 앞 32바이트는 AES-256-CBC 키로 사용하고 뒤 16바이트는 IV로 사용한다.

caption - 핸드셰이크 함수
핸드셰이크를 수행한 이후의 모든 통신 과정은 앞서 계산한 AES 키와 IV로 암호화한 뒤 base64 인코딩을 거친다. 이때 사용되는 base64 테이블은 표준과 다르다.
base64 테이블: "gXldcExbCIjweVsOF0PK1N2iQkpBmfuH/oYWS9atJ6nZqh38MRGy+T5zD74LArUv"

caption - base64 인코딩 / 디코딩 클래스

caption - 메시지 전송 함수
핸드셰이크가 완료되면 감염된 PC의 시스템 정보를 수집해 C&C로 전송한다. 메시지의 type 필드 값은 4이며, 수집되는 정보는 아래 표와 같다.
필드 | 의미 |
|---|---|
id | 레지스트리 HKLM\SOFTWARE\Microsoft\Cryptography 경로의 MachineGuid 값 |
deviceId | 레지스트리 HKLM\SOFTWARE\Microsoft\SQMClient 경로의 MachineId |
platform | 설치된 운영체제 종류 |
arch | CPU 아키텍처 |
osVersion | 운영체제 커널 버전 |
cpuCount | 논리 CPU 코어 수 |
hostname | PC 호스트 이름 |
username | 현재 로그인한 사용자 이름 |
totalmem | 전체 메모리 크기 |
macAddress | MAC 주소 |
user_id | PowerShell 스크립트의 Mutex GUID와 동일한 값 |
domain | process.argv[2]로 전달된 도메인 문자열 |
version | 현재 실행 중인 TonRAT 버전 |
[표 7] C&C로 전송하는 시스템 정보 필드

caption - 시스템 정보 수집 함수
TonRAT은 C&C 서버로부터 메시지를 수신할 경우, 전달된 JSON 객체 내 type 필드 값에 따라 다른 명령을 수행한다. 명령의 종류는 아래 표와 같다.
type | 행위 | 상세 내용 |
0 | healthCheck | type 1 메시지를 회신 |
3 | 핸드셰이크 | 키 공유 이후 시스템 정보 수집 |
5 | 임의 JavaScript 실행 | C&C가 보낸 JS 코드를 실행한 뒤 결과 응답 |
7 | PE 파일 다운로드 및 실행 | C&C가 전송한 URL에서 파일을 받아 %TEMP%\<무작위 8~15자>.exe로 저장 후 실행 |
8 | PowerShell 명령 실행 | powershell.exe 실행. stdout/stderr을 응답으로 회신 |
9 | 업데이트 | 현재 js 파일을 C&C가 보낸 코드로 업데이트 후 재시작 |
10 | 재시작 | 자기 자신을 창 숨김 상태로 재실행 후 종료 |
11 | 메모리 PE 실행 | --experimental-ffi 모드로 재실행. |
[표 8] C&C 명령 종류

caption - C&C 명령 처리 함수
7번 명령의 경우 다운로드한 파일이 정상적인 PE 파일인지 "PE\x00\x00" 시그니처로 검사하며, 시그니처가 확인되지 않으면 오류를 반환한다. 이때 명령 처리 함수인 handleMessage는 호출부가 try-catch 문으로 감싸져 예외가 발생하면 오류 메시지가 C&C 서버로 전송된다.
11번 명령은 커맨드 라인 인자로 전달된 URL로부터 PE 파일을 받아와 파일로 저장하지 않고 메모리에 로드하여 실행한다. NodeJS에서 네이티브 DLL 함수를 호출하기 위해 "--experimental-ffi" 플래그가 설정되어 있어야 하기 때문에 해당 플래그와 같이 재실행한다. 즉, 11번 명령을 수신하면 URL과 플래그를 설정해 자신을 재실행하고, 재실행된 프로세스에서 PE 로드 함수가 호출된다.

caption - PE 로드 함수
다운로드한 PE 파일이 올바른지 검사한 뒤, 통상적인 Reflective Loader 과정을 따라 로드한다. 이때 .NET 실행 파일인 경우 CLR Hosting 기법을 이용하여 .NET 어셈블리를 메모리에 로드하는 함수가 별도로 구현되어 있다.

caption - CLR Hosting 기법을 이용한 .NET 파일 로드
또한 PE 로드 이후 백신 탐지를 피하기 위한 기법들이 구현되어 있다.
먼저 PE 이미지의 DOS 시그니처 부근과 앞뒤 바이트를 0으로 덮어써 흔적을 지운다.
다음으로 .NET 파일 로드 시 PEB의 InLoadOrderModuleList를 순회하며 이름이 "mscor", "oleaut32", "clr", "ucrtbase_clr", "VCRUNTIME140"로 시작하는 모듈을 리스트에서 분리한 뒤 이름을 널 문자로 덮어써 CLR 관련 DLL을 은닉한다.
2.4 TON 블록체인을 이용한 C2 주소 조회
TonRAT 악성코드는 앞서 확인한 것처럼 C&C 도메인을 TON 블록체인 컨트랙트에서 받아온다. 컨트랙트 주소는 [표 6]과 같으며 해당 컨트랙트의 get_domain getter 함수를 호출해 현재 도메인 주소를 조회한다.

caption - TON 컨트랙트 get_domain 조회 결과

caption - Tonviewer로 확인한 컨트랙트
해당 컨트랙트의 바이트 코드를 tdisasm 도구를 이용하여 분석하면 저장된 C&C 도메인 주소를 반환하는 get_domain 함수와 내부 메시지 핸들러가 구현된 것이 확인된다. 내부 핸들러는 발신자가 저장된 공격자 지갑과 같고 메시지 opcode가 0x17be49cf일 때 C&C 도메인 주소를 새로운 값으로 갱신한다.
즉, 공격자는 컨트랙트에 0x17be49cf 메시지를 보내 C&C 도메인을 변경할 수 있다.
역할 | 주소 |
|---|---|
스마트 컨트랙트 | 0:c66119f0e5635c4380441d7a79baf0c02a0ab7ea6cd78de06507fc5dc2c1a5d9 |
공격자 지갑 | 0:6d5b44d0678e6bd6703f2cabd2531ccf5a0b11cc081f8f7175bd679db4c12d41 |
[표 9] 컨트랙트와 공격자 지갑 주소

caption - 컨트랙트 Disassemble 결과
공격자는 지금까지 총 5번 C&C 도메인을 변경했으며, 가장 최근인 2026-09-05 21:07경에 변경된 C&C 도메인은 "nuypoiaklber[.]lol"이다.
일시(UTC) | opcode | C&C 도메인 |
|---|---|---|
2026-02-07 14:03 | (배포) | - |
2026-02-09 19:17 | 0x17be49cf | hsaertyuoang34[.]sbs |
2026-02-20 15:19 | 0x17be49cf | zloapobikahy23[.]bond |
2026-06-02 02:53 | 0x17be49cf | tonajukbhuakpo2[.]shop |
2026-06-21 18:30 | 0x17be49cf | njzlopghznkamkl[.]cfd |
2026-09-05 21:07 | 0x17be49cf | nuypoiaklber[.]lol |
[표 10] C&C 도메인 변경 이력
3. 추가 분석
외부 보고서에 따르면 해당 캠페인은 호텔 예약 사이트인 Booking.com 혹은 그 파트너사의 직원 및 운영진 대상으로 진행되었으며, 호텔 숙박 관련 문의를 주제로 한 피싱 메일이 공격에 사용된 것으로 확인되었다. 메일에 첨부된, 이미지 파일로 위장한 악성코드가 본 보고서에서 분석한 LNK 파일이며, 주로 IMG / PHOTO 관련 파일 이름이 사용되었다.
공격에 사용된 TonRAT 페이로드는 버전별로 기능이 추가되고 있는 것이 확인되었다. 초기에는 C&C 서버로부터 파일을 다운로드하는 기능과 임의 JavaScript 코드를 실행하는 기능 정도만 구현되어 있었지만, 1.2.1 버전부터 PE 로드 기능과 자가 업데이트 기능이 추가되었다.
최근에 배포된 1.3.0 버전의 TonRAT 페이로드는 checkDetection 함수가 추가되어 자기 자신 파일이 제거되었는지 탐지하고 서버로 정보를 전송한다. 이후 TonRAT 페이로드 앞뒤에 무작위 문자열을 추가한 뒤 새로 저장한다.

caption - 새로 추가된 checkDetection 함수
4. 마치며
본 보고서는 2026년 초부터 지속적으로 식별된 TonRAT의 유포 방식과 obfuscator.io를 이용한 난독화 방식을 분석하였다. 공격자는 호텔 예약 및 숙박 업체 직원에게 문의 메일을 보내, 악성 LNK 파일이 담긴 ZIP 파일을 다운로드해 실행하도록 유도하였다. 이미지 및 영상 파일로 위장된 LNK 파일 실행 시 공격자 서버로부터 PowerShell 스크립트를 받아 실행하며, 이때 NodeJS를 다운로드하여 TonRAT 악성코드를 실행한다.
TonRAT 악성코드는 텔레그램에서 개발한 TON 블록체인의 특정 컨트랙트에 접근하여 C&C 주소를 동적으로 받아와 명령을 수행한다. 이는 공격자가 C&C 주소를 주기적으로 교체하여 보안 탐지를 우회하는 방식이자 최근 EtherRAT 악성코드에서도 사용된 기법이다.
C&C가 전달한 URL에 접속해 실행 파일을 다운로드해 메모리에 로드하는 방식으로 추가 페이로드를 실행했으며, PE 헤더를 0으로 덮어쓰는 등 백신 탐지를 회피하는 방식이 구현되어 있었다. 또한 TonRAT은 JavaScript 난독화 도구인 obfuscator.io를 사용해 이러한 악성 행위를 은닉하고 분석을 어렵게 하였다.
5.부록
부록 A. IoCs
URL
hsaertyuoang34[.]sbs
zloapobikahy23[.]bond
tonajukbhuakpo2[.]shop
njzlopghznkamkl[.]cfd
nuypoiaklber[.]lol
Mutex
Global\0be276c4-7dfb-4d5f-a8e2-fd7c5735c489
Global\6e5fcec5-3a02-4561-93ad-3f634f83a122
Global\24f2b9f2-7586-4623-9a9f-8f49ab2e2ba2
Global\59541b77-7fb8-4285-9d95-7bcce4e995ff
Global\39d81f9b-6ab5-4f9e-8af9-61409f1ea339
Global\9045b3ab-cb45-495e-acca-eebe4c128a46
Global\c8f375c1-0241-4b84-a35a-aabfc4b3eef7
Global\61fc5f6f-4bdc-47e7-a533-c6f920059411

Popular Articles
More Articles






