문서
/
레시피
/

VM 방어 텔레메트리 및 대응

VM 방어 텔레메트리 및 대응

Pro
v7.4.0+

vmDefenseHook으로 VM 방어 탐지 결과를 백엔드에 보고하고, vmDefenseReaction으로 탐지 카테고리별 대응 방식을 조정하세요. 전혀 중단되지 않는 텔레메트리 전용 빌드부터 탈취된 번들을 즉시 중단시키는 빌드까지 구성할 수 있습니다.

시청

Obfuscator.io Defense Reactions: Break, Decoy, and the VM Defense Hook

YouTube에서 보기

문제

VM 방어 기능, 즉 vmSelfDefending, vmDebugProtection, vmDomainLock은 로컬에서 작동합니다. 디버거, 자동화 도구, 변조된 환경, 승인되지 않은 도메인이 탐지되면 보호된 코드가 중단되거나 조용히 자기 결과를 오염시킵니다. 공격자는 막을 수 있지만, 기본 설정에서는 그 사실을 전혀 알 수 없습니다. 번들이 얼마나 자주 탐색당하는지, 어떤 탐지기가 작동했는지, 정상 사용자에게 방어 기능이 오작동하고 있지는 않은지 알 방법이 없습니다.

두 가지 옵션이 이 공백을 메웁니다. 어느 쪽도 방어 기능을 활성화하지는 않으며, 이미 켜 둔 방어 기능을 관찰하고 방향을 잡아줄 뿐입니다.

  • vmDefenseHook - 방어 기능이 무언가를 탐지할 때마다 시그널 객체를 전달받는 전역 콜백입니다. 백엔드로 텔레메트리를 보내는 데 사용하세요.
  • vmDefenseReaction - 활성화된 방어 기능이 어떻게 대응할지를 카테고리별로 선택하는 맵입니다. 중단하거나, decoy로 동작하거나, 로컬에서는 아무것도 하지 않도록 할 수 있습니다.

두 옵션 모두 v7.1.0에서 도입되었지만, 여기 나오는 모든 예시는 v7.4.0이 필요한 객체 형식 vmDefenseHook: { name }을 사용합니다. 이전 버전은 문자열만 받았습니다(vmDefenseHook: '__vmDetection'). 이 형식은 v8.0.0부터 거부되므로 항상 객체 형식을 사용하세요.

레시피 1 - 탐지 결과를 백엔드로 보고하기

1단계 - 난독화된 번들이 로드되기 전에 전역 훅 함수를 등록하세요

VM 런타임과 그 방어 기능은 보호된 프로그램보다 먼저 실행되므로 상당수의 탐지가 시작 시점에 발생합니다. 훅은 난독화된 script 태그보다 앞선 위치에서 호스트 페이지의 평범한 전역으로 정의하세요.

HTML

2단계 - vmDefenseHook이 이를 가리키게 하세요

이 옵션은 name이 호출할 전역 함수 이름인 객체입니다(aliases는 선택 사항이며 아래에서 설명합니다).

JavaScript

대시보드에서는 방어 기능(vmSelfDefending, vmDebugProtection, vmDomainLock) 중 하나 이상을 켜면 고급 보호 섹션에 VM Defense Hook 필드가 나타납니다.

3단계 - 백엔드에서 시그널을 받으세요

탐지가 발생할 때마다 훅은 하나의 signal 객체와 함께 호출됩니다.

  • source - 구체적인 탐지기입니다. headless, node, agent, agentBrowser, domain, debugger, sandbox, nativeHook, timing, integrity 중 하나입니다. v7.4.0부터 기존의 env 및 inspector 탐지기는 source: 'debugger'로 보고됩니다. agentBrowser는 category: 'automation'으로 보고되며, vmDebugProtection이 켜진 브라우저 대상에서 실행됩니다(v7.9.0+).
  • category - automation, debugger, sandbox, domain, tamper, integrity 중 하나입니다. node 소스는 category: 'debugger'로 보고됩니다(v7.4.0+).
  • score, threshold - 탐지 점수와 그 점수가 넘어선 임곗값입니다

최소한의 수신 엔드포인트 예시입니다(Express를 예로 들었지만 POST를 받을 수 있는 백엔드라면 무엇이든 됩니다). 본문을 배열로 정규화하므로 아래 버퍼 패턴이 보내는 일괄 형식도 함께 처리합니다.

JavaScript

이 훅은 보고 전용입니다. 반환값은 무시되며, 훅이 없거나 예외를 던져도 조용히 아무 일도 일어나지 않습니다. 훅이 방어 기능을 비활성화하는 일은 결코 없으므로, 공격자가 훅을 지우거나 망가뜨려도 얻는 것이 없습니다. 방어 기능이 무엇을 하는지를 바꾸려면 vmDefenseReaction(레시피 2)을 사용하세요.

훅은 난독화 대상 소스 안이 아니라 호스트 페이지에서 정의하세요

텔레메트리는 모든 탐지를 빠짐없이 수집하고 싶은데 상당수가 시작 시점에 발생합니다. 난독화된 번들 안에서 정의한 훅은 등록 시점이 너무 늦어 이런 탐지를 잡지 못하며, VM 컴파일까지 되면 프로그램이 실행되기 전에는 접근할 수 없습니다. 어느 쪽이든 안전하게 처리되지만(훅이 없으면 아무 일도 하지 않고, 탐지를 스스로 일으키는 훅은 재귀적으로 다시 호출되지 않습니다), 빠짐없이 수집하려면 호스트 페이지에서 미리 등록하세요.

한 가지 예외는 오직 런타임 탐지에만 대응하는 훅입니다. 예를 들어 사용 중에 디버거가 열렸을 때 정리 작업을 수행하는 경우입니다. 그런 훅은 난독화된 번들 안에 둘 수 있습니다. 레시피 3을 참고하세요.

그러면서도 보고 로직을 보호하고 싶다면, 등록하는 훅은 한 줄짜리 버퍼로 두고 난독화된 코드에서 이를 비워내세요.

JavaScript

JavaScript

시그널 필드 이름 바꾸기 (aliases)

기본 source / category 값은 뜻이 그대로 드러나는 이름이라, 콜백을 들여다보는 사람(또는 결과물을 읽는 사람)이 어떤 보호 기능인지, 어떤 탐지기가 작동했는지 알아볼 수 있습니다. aliases는 시그널 필드를 원하는 알아볼 수 없는 토큰으로 바꿔줍니다. 이 변환은 시그널이 발생하기 전에 VM 내부에서 적용되므로, 원래 이름은 결과물에 나타나지도 않고 콜백에 전달되지도 않습니다. 매핑은 여러분의 앱만 알고 있으며, 앱이 토큰을 백엔드로 전달합니다.

별칭은 필드 단위로 지정합니다. 각 항목은 key(콜백이 받게 될 프로퍼티 이름)를 받고, 문자열 이름 필드인 source와 category는 values 맵도 받습니다. 반면 score / threshold는 숫자이므로 key만 받습니다. 지정하지 않은 항목은 기본 이름을 유지합니다.

JavaScript

대시보드에서는 VM Defense Hook 필드 아래에 시그널 별칭 섹션이 있습니다.

이는 비밀 유지가 아니라 핑거프린트 회피 수단입니다. 반복 테스트로 매핑을 추론할 수는 있으므로, 고정적이고 뜻이 그대로 드러나는 이름을 노출하지 않는다는 점만이 이 기능의 이점입니다.

레시피 2 - 기본 대응 방식 조정하기

vmDefenseReaction은 각 탐지 카테고리가 어떻게 대응할지를 설정합니다. 무언가를 활성화하지는 않습니다. 방어 기능 자체는 vmSelfDefending, vmDebugProtection, vmDomainLock으로 켜며, 이 옵션은 활성화된 방어 기능의 대응 방식만 선택합니다. 제어의 단위는 카테고리입니다. 한 카테고리에 속한 모든 탐지기는 그 카테고리의 대응 방식을 따르며, 해당 옵션이 꺼져 있는 카테고리에 대응을 설정해 봐야 아무 효과가 없습니다.

카테고리활성화 옵션대응 시점
automationvmSelfDefending 또는 vmDebugProtection사람이 아니라 소프트웨어가 코드를 구동하고 있을 때입니다. 헤드리스 브라우저나 자동화된 브라우저, 스크래핑 / 테스트 프레임워크, 페이지를 단계별로 실행하는 AI 코딩 에이전트 등이 해당합니다.
debuggervmDebugProtection 또는 vmSelfDefending누군가 디버거나 브라우저 개발자 도구 인스펙터를 열어 두고, 실행 중인 코드를 단계별로 따라가며 분석하고 있을 때입니다.
sandboxvmDebugProtection코드가 실제 브라우저에서 실행되고 있지 않을 때입니다. 오프라인에서 실행하고 분석하기 위해 에뮬레이션되거나 스크립트로 구성된 JavaScript 환경으로 옮겨진 경우입니다.
domainvmDomainLock승인하지 않은 사이트에서 코드가 실행되고 있을 때입니다. vmDomainLock 허용 목록에 없는 호스트가 해당하며, 예를 들어 번들이 다른 사람의 도메인으로 복사된 경우입니다.
tampervmSelfDefendingVM을 감시하거나 가로채기 위해 그 주변의 JavaScript 환경이 변경되었을 때입니다. 네이티브 브라우저 내장 객체가 계측된 버전으로 바꿔치기된 경우 등이 해당합니다.
integrityvmSelfDefending보호된 번들의 코드 자체가 생성 이후에 편집되거나 패치되었을 때입니다.

키로는 위 여섯 개 카테고리 이름이나 default(지정하지 않은 카테고리에 적용되는 대체값)를 사용합니다. 값은 다음과 같습니다.

  • break - 즉시 중단합니다
  • decoy - 오염된 상태로 계속 실행하며 조용히 잘못된 결과를 만들어 냅니다. decoy는 브라우저 대상에서 vmDebugProtection 또는 vmDomainLock이 필요하며, 그렇지 않으면 break처럼 동작합니다.
  • none - 로컬에서는 아무것도 하지 않습니다 (텔레메트리 전용)

설정하지 않은 카테고리는 기본값을 따릅니다.

JavaScript

default는 integrity와 tamper를 포함해 모든 카테고리에 적용됩니다. 따라서 { default: 'none' }은 실제로 아무것도 중단시키지 않는 텔레메트리 전용 빌드가 됩니다.

JavaScript

JavaScript

대시보드에서는 방어 기능을 하나라도 켜면 고급 보호 섹션에 VM Defense Reactions 선택 항목이 나타납니다. 각 카테고리는 해당 탐지기를 내보내는 방어 기능이 켜져 있을 때만 편집할 수 있습니다.

레시피 3 - 방어가 중단시키기 전에 직접 만든 로직 실행하기

훅은 보고를 위한 것만이 아닙니다. 방어가 대응하기 전에 직접 만든 응답을 실행할 수 있는, 유일하게 믿을 만한 지점이기도 합니다. 실행 중인 페이지에서 디버거가 열렸을 때, 화면에 표시된 내용을 지우거나 화면을 404 페이지로 바꾸는 작업을 코드가 중단되기 전에 하고 싶을 수 있습니다.

왜 앱의 다른 코드가 아니라 훅이어야 할까요? break는 이후의 모든 바이트코드를 멈추므로, 방어가 작동한 뒤에 실행되는 정리 코드는(특히 그 코드 자체가 VM 난독화되어 있을 때) 바로 그 break가 실행을 막는 대상입니다. 훅은 탐지가 일어난 지점에서 대응이 실행되기 전에 동기적으로 호출됩니다. 따라서 훅이 호출하는 동기 함수가 먼저 끝난 다음 break가 VM을 멈춥니다.

응답은 vmDefenseHook으로 정의하세요. debugger 탐지는 런타임에, 즉 프로그램이 로드되어 훅을 정의한 뒤에 발생하므로, 훅은 난독화 대상 소스의 일부가 되어 번들의 나머지와 함께 바이트코딩될 수 있습니다. signal.category로 분기해 각 조건마다 알맞은 응답을 하도록 하고, 작업을 동기적으로 유지한 다음, 대응이 실행되도록 두세요.

JavaScript

JavaScript

이 방식은 앱이 실행되는 동안 발생하는 탐지에 적용됩니다. 아래의 훅을 바이트코딩하는 것이 통하는 경우를 참고하세요.

다음 사항을 염두에 두세요.

  • 동기 작업만이 먼저 끝난다고 보장됩니다. 대응은 훅이 반환된 직후의 문장에서 실행됩니다. 곧바로 넘겨주고 잊어버리는 호출은 괜찮지만(navigator.sendBeacon, 동기적인 DOM 및 canvas 편집), 나중으로 예약하는 작업, 즉 setTimeout, 프로미스 후속 처리, await 등은 그렇지 않으며, 추가 VM 바이트코드가 필요한 작업은 실행되지 않습니다. 바로 그것이 break가 멈추는 대상이기 때문입니다.
  • 훅은 대응보다 먼저 실행될 뿐, 대응을 대체하지는 않습니다. 반환값은 무시되며, 대응을 취소하거나 지연시키거나 그 동작을 바꿀 수 없습니다. 훅은 중단을 거부하기 위해서가 아니라 중단 전에 무언가를 하기 위해 사용하세요. 대응 자체를 바꾸려면 vmDefenseReaction(레시피 2)을 사용하세요.

훅을 바이트코딩하는 것이 통하는 경우

이처럼 훅을 난독화된 번들 안에 두는 방식이 통하는 것은 오직 debugger 탐지가 런타임에 발생하기 때문입니다. VM은 아직 살아 있는 상태에서, 즉 프로그램이 로드되어 훅을 정의한 뒤에 vmDefenseHook을 호출하므로, 바이트코딩된 훅이 먼저 디코딩되어 실행된 다음 break가 일어납니다. 바로 이 점이 훅 자체의 소스를 보호해 줍니다.

반면 시작 시점에 발생하는 탐지, 즉 automation, sandbox, domain, 또는 페이지가 로드될 때 이미 열려 있던 디버거에 대해서는 통하지 않습니다. 그 시점에는 바이트코딩된 훅이 아직 정의되지 않아 방어가 호출할 함수를 찾지 못하기 때문입니다. 그런 경우에는 레시피 1처럼 훅을 호스트 페이지의 평범한 전역으로 등록하세요. 확신이 서지 않는다면 호스트 페이지의 평범한 전역이 훅에 도달하는 모든 탐지를 처리해 줍니다. 바이트코딩은 훅 자체의 소스에 대한 보호만을 더해 줄 뿐이며, 그것도 런타임 탐지에 한합니다.

텔레메트리에서 강제 적용으로

가시성과 강제 적용을 반드시 함께 출시할 필요는 없습니다. 방어 기능을 두 단계로 나눠 배포하세요. 먼저 보고만 하는 빌드를 내보내고, 텔레메트리가 깨끗해 보이면 실제로 대응하는 빌드를 내보내면 됩니다.

1단계 - 관찰 전용 빌드를 배포하세요

사용할 방어 기능을 모두 켜고, vmDefenseHook이 여러분의 엔드포인트를 가리키게 한 뒤, 모든 대응을 끄세요. 모든 탐지기가 그대로 동작하며 탐지 결과를 백엔드로 보고하지만, 아무것도 중단되지 않습니다.

JavaScript

2단계 - 수집된 시그널을 검토하세요

이 빌드가 실제 트래픽을 겪고 나면, 정상적인 사용이 유발한 탐지가 있는지 살펴보세요. 가장 흔한 두 가지는 다음과 같습니다.

  • 직접 운영하는 E2E 테스트나 가동 상태 모니터링에서 발생한 automation 탐지. 프로덕션에서 이 카테고리를 눈감아 주기보다는, 해당 산출물을 방어 기능 없이 빌드하세요.
  • vmDomainLock 허용 목록에 넣는 것을 잊은 스테이징이나 프리뷰 호스트에서 발생한 domain 탐지. 해당 호스트를 추가하세요.

대응을 약화하기보다 원인을 고치는 편이 낫습니다. none으로 남겨 둔 카테고리 하나하나가 공격자가 마음 놓고 무시할 수 있는 탐지기이기 때문입니다.

3단계 - 대응을 켜세요

default: 'none' 재정의를 지우면 카테고리별 기본 대응이 적용됩니다. 그 한 줄이 변경의 전부입니다. 없앨 수 없는 오탐이 계속 발생하는 카테고리가 있다면 그 한 카테고리만 none으로 두고(예: vmDefenseReaction: { automation: 'none' }) 나머지는 강제 적용하세요.

강제 적용을 켠 뒤에도 vmDefenseHook은 그대로 두세요. 훅은 대응 방식과 관계없이 호출되므로, 방어 기능이 대응하는 동안에도 누가 번들을 탐색하고 있는지 계속 파악할 수 있습니다.