VM 방어 텔레메트리 및 대응
vmDefenseHook으로 VM 방어 탐지 결과를 백엔드에 보고하고, vmDefenseReaction으로 탐지 범주별 대응 방식을 조정하세요. 전혀 중단되지 않는 텔레메트리 전용 빌드부터 탈취된 번들을 확실히 중단시키는 빌드까지 구성할 수 있습니다.
문제
VM 방어 기능, 즉 vmSelfDefending, vmDebugProtection, vmDomainLock은 로컬에서 작동합니다. 디버거, 자동화 도구, 변조된 환경, 승인되지 않은 도메인이 탐지되면 보호된 코드가 중단되거나 조용히 자기 결과를 오염시킵니다. 공격자는 막을 수 있지만, 기본 설정에서는 그 사실을 전혀 알 수 없습니다. 번들이 얼마나 자주 탐색당하는지, 어떤 탐지기가 작동했는지, 정상 사용자에게 방어 기능이 오작동하고 있지는 않은지 알 방법이 없습니다.
v7.1.0부터 두 가지 옵션이 이 공백을 메웁니다. 어느 쪽도 방어 기능을 활성화하지는 않으며, 이미 켜 둔 방어 기능을 관찰하고 방향을 잡아줄 뿐입니다.
vmDefenseHook- 방어 기능이 무언가를 탐지할 때마다 시그널 객체를 전달받는 전역 콜백입니다. 백엔드로 텔레메트리를 보내는 데 사용하세요.vmDefenseReaction- 활성화된 방어 기능이 어떻게 대응할지를 범주별로 선택하는 맵입니다. 중단하거나, 결과를 오염시키거나, 로컬에서는 아무것도 하지 않도록 할 수 있습니다.
레시피 1 - 탐지 결과를 백엔드로 보고하기
1단계 - 난독화된 번들이 로드되기 전에 전역 훅 함수를 등록하세요
VM 런타임과 그 방어 기능은 보호된 프로그램보다 먼저 실행되므로 상당수의 탐지가 시작 시점에 발생합니다. 훅은 난독화된 script 태그보다 앞선 위치에서 호스트 페이지의 평범한 전역으로 정의하세요.
<script>
// In your page, BEFORE the obfuscated script:
window.__vmDetection = function (signal) {
navigator.sendBeacon('/api/vm-defense', JSON.stringify(signal));
};
</script>
<script src="/app.obfuscated.js"></script>
2단계 - vmDefenseHook이 이를 가리키게 하세요
이 옵션은 name이 호출할 전역 함수 이름인 객체입니다(aliases는 선택 사항이며 아래에서 설명합니다).
JavaScriptObfuscator.obfuscate(source, {
vmObfuscation: true,
// the hook alone enables nothing - a defense must be on for detectors to run:
vmSelfDefending: true,
vmDebugProtection: true,
vmDefenseHook: { name: '__vmDetection' }
});
문자열만 넘기는 형태(vmDefenseHook: '__vmDetection')도 { name: '__vmDetection' }의 축약형으로 여전히 허용되지만 사용이 중단될 예정이므로 객체 형태를 사용하세요.
대시보드에서는 방어 기능(vmSelfDefending, vmDebugProtection, vmDomainLock) 중 하나 이상을 켜면 VM 옵션 패널에 VM Defense Hook 필드가 나타납니다.
3단계 - 백엔드에서 시그널을 받으세요
탐지가 발생할 때마다 훅은 하나의 signal 객체와 함께 호출됩니다.
source- 구체적인 탐지기입니다.headless,node,agent,domain,debugger,sandbox,nativeHook,timing,integrity중 하나입니다. v7.4.0부터 기존의env및inspector탐지기는source: 'debugger'로 보고됩니다.category-automation,debugger,sandbox,domain,tamper,integrity중 하나입니다.node소스는category: 'debugger'로 보고됩니다(v7.4.0+).score,threshold- 탐지 점수와 그 점수가 넘어선 임곗값입니다
최소한의 수신 엔드포인트 예시입니다(Express를 예로 들었지만 POST를 받을 수 있는 백엔드라면 무엇이든 됩니다). 본문을 배열로 정규화하므로 아래 버퍼 패턴이 보내는 일괄 형식도 함께 처리합니다.
app.post('/api/vm-defense', express.text({ type: '*/*' }), (req, res) => {
// a signal: { source: 'headless', category: 'automation', score: 7, threshold: 4 }
const signals = [].concat(JSON.parse(req.body));
for (const signal of signals) {
console.warn('vm-defense', { ...signal, ip: req.ip, ua: req.get('user-agent') });
}
res.sendStatus(204);
});
시그널 필드 이름 바꾸기 (aliases) v7.4.0+
기본 source / category 값은 뜻이 그대로 드러나는 이름이라, 콜백을 들여다보는 사람(또는 결과물을 읽는 사람)이 어떤 보호 기능인지, 어떤 탐지기가 작동했는지 알아볼 수 있습니다. aliases는 시그널 필드를 원하는 알아볼 수 없는 토큰으로 바꿔줍니다. 이 변환은 시그널이 발생하기 전에 VM 내부에서 적용되므로, 원래 이름은 결과물에 나타나지도 않고 콜백에 전달되지도 않습니다. 매핑은 여러분의 앱만 알고 있으며, 앱이 토큰을 백엔드로 전달합니다.
별칭은 필드 단위로 지정합니다. 각 항목은 key(콜백이 받게 될 프로퍼티 이름)를 받고, 문자열 이름 필드인 source와 category는 values 맵도 받습니다. 반면 score / threshold는 숫자이므로 key만 받습니다. 지정하지 않은 항목은 기본 이름을 유지합니다.
vmDefenseHook: {
name: '__vmDetection',
aliases: {
source: { key: 'a8Qm', values: { headless: 'xP4m9Q' } },
category: { key: 'p3Tx', values: { automation: 'bQ7s1M' } },
score: { key: 's1' },
threshold: { key: 't1' }
}
// the callback now receives e.g. { a8Qm: 'xP4m9Q', p3Tx: 'bQ7s1M', s1: <score>, t1: <threshold> }
}
대시보드에서는 VM Defense Hook 필드 아래에 Signal aliases 섹션이 있습니다.
레시피 2 - 기본 대응 방식 조정하기
vmDefenseReaction은 각 탐지 범주가 어떻게 대응할지를 설정합니다. 무언가를 활성화하지는 않습니다. 방어 기능 자체는 vmSelfDefending, vmDebugProtection, vmDomainLock으로 켜며, 이 옵션은 활성화된 방어 기능의 대응 방식만 선택합니다. 제어의 단위는 범주입니다. 한 범주에 속한 모든 탐지기는 그 범주의 대응 방식을 따르며, 해당 옵션이 꺼져 있는 범주에 대응을 설정해 봐야 아무 효과가 없습니다.
| 범주 | 활성화 옵션 | 대응 시점 |
|---|---|---|
automation | vmSelfDefending 또는 vmDebugProtection | 사람이 아니라 소프트웨어가 코드를 구동하고 있을 때입니다. 헤드리스 브라우저나 자동화된 브라우저, 스크래핑 / 테스트 프레임워크, 페이지를 단계별로 실행하는 AI 코딩 에이전트 등이 해당합니다. |
debugger | vmDebugProtection 또는 vmSelfDefending | 누군가 디버거나 브라우저 개발자 도구 인스펙터를 열어 두고, 실행 중인 코드를 단계별로 따라가며 분석하고 있을 때입니다. |
sandbox | vmDebugProtection | 코드가 실제 브라우저에서 실행되고 있지 않을 때입니다. 오프라인에서 실행하고 분석하기 위해 에뮬레이션되거나 스크립트로 구성된 JavaScript 환경으로 옮겨진 경우입니다. |
domain | vmDomainLock | 승인하지 않은 사이트에서 코드가 실행되고 있을 때입니다. vmDomainLock 허용 목록에 없는 호스트가 해당하며, 예를 들어 번들이 다른 사람의 도메인으로 복사된 경우입니다. |
tamper | vmSelfDefending | VM을 감시하거나 가로채기 위해 그 주변의 JavaScript 환경이 변경되었을 때입니다. 네이티브 브라우저 내장 객체가 계측된 버전으로 바꿔치기된 경우 등이 해당합니다. |
integrity | vmSelfDefending | 보호된 번들의 코드 자체가 생성 이후에 편집되거나 패치되었을 때입니다. |
키로는 위 여섯 개 범주 이름이나 default(지정하지 않은 범주에 적용되는 대체값)를 사용합니다. 값은 다음과 같습니다.
break- 즉시 중단합니다decoy- 오염된 상태로 계속 실행하며 조용히 잘못된 결과를 만들어 냅니다none- 로컬에서는 아무것도 하지 않습니다 (텔레메트리 전용)
설정하지 않은 범주는 기본값을 따릅니다.
// built-in defaults
vmDefenseReaction: {
automation: 'break',
debugger: 'decoy',
sandbox: 'decoy',
domain: 'break',
tamper: 'break',
integrity: 'break'
}
default는 구조적으로 오탐이 없는 범주(integrity, tamper)를 포함해 모든 범주에 적용됩니다. 따라서 { default: 'none' }은 실제로 아무것도 중단시키지 않는 텔레메트리 전용 빌드가 됩니다.
vmDefenseReaction: { default: 'none' } // never break - pair with vmDefenseHook
vmDefenseReaction: { automation: 'none' } // tolerate automation FPs; the rest keep their defaults (a bad domain still breaks)
대시보드에서는 방어 기능을 하나라도 켜면 VM 옵션 패널에 VM Defense Reactions 선택 항목이 나타납니다. 각 범주는 해당 탐지기를 내보내는 방어 기능이 켜져 있을 때만 편집할 수 있습니다.
텔레메트리에서 강제 적용으로
가시성과 강제 적용 중 하나를 처음부터 고를 필요는 없습니다. 방어 기능을 두 번의 빌드로 나눠 적용하세요. 먼저 보고만 하는 빌드를 내보내고, 텔레메트리가 깨끗해 보이면 실제로 대응하는 빌드를 내보내면 됩니다.
1단계 - 관찰 전용 빌드를 배포하세요
사용할 방어 기능을 모두 켜고, vmDefenseHook이 여러분의 엔드포인트를 가리키게 한 뒤, 모든 대응을 끄세요. 모든 탐지기가 그대로 동작하며 탐지 결과를 백엔드로 보고하지만, 아무것도 중단시키지는 않습니다.
JavaScriptObfuscator.obfuscate(source, {
vmObfuscation: true,
vmSelfDefending: true,
vmDebugProtection: true,
vmDomainLock: ['example.com'],
vmDefenseHook: '__vmDetection',
vmDefenseReaction: { default: 'none' } // observe only
});
2단계 - 수집된 시그널을 검토하세요
이 빌드가 실제 트래픽을 겪고 나면, 정상적인 사용이 유발한 탐지가 있는지 살펴보세요. 가장 흔한 두 가지는 다음과 같습니다.
- 직접 운영하는 E2E 테스트나 가동 상태 모니터링에서 발생한
automation탐지. 프로덕션에서 이 범주를 눈감아 주기보다는, 해당 산출물을 방어 기능 없이 빌드하세요. vmDomainLock허용 목록에 넣는 것을 잊은 스테이징이나 프리뷰 호스트에서 발생한domain탐지. 해당 호스트를 추가하세요.
대응을 약화하기보다 원인을 고치는 편이 낫습니다. none으로 남겨 둔 범주 하나하나가 공격자가 더 이상 신경 쓰지 않아도 되는 탐지기이기 때문입니다.
3단계 - 대응을 켜세요
default: 'none' 재정의를 지우면 범주별 기본 대응이 적용됩니다. 전환에 필요한 것은 그 한 줄뿐입니다. 없앨 수 없는 오탐이 계속 발생하는 범주가 있다면 그 범주만 none으로 두고(예: vmDefenseReaction: { automation: 'none' }) 나머지는 강제 적용하세요.
