Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
datadome-vm — 새로운 Datadome VM 리버스 엔지니어링 🔥 | Kitploit
도구/GitHubGitHub/xkiian/datadome-vm
Dynamic Analysis (Sandboxing)Reverse EngineeringBinary AnalysisLearning & EducationAnti-BotCAPTCHA Bypass
GitHubxkiian/datadome-vm

datadome-vm

새로운 Datadome VM 리버스 엔지니어링 🔥

저장소 보기
125196개월 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

DataDome VM 분석

요약

이 저장소는 DataDome이 CAPTCHA/인터스티셜 흐름에서 사용하는 브라우저 내 JavaScript VM(가상 머신)의 첫 번째 공개 버전을 문서화합니다. 이 분석은 다음을 다룹니다:

  • 바이트코드 로딩 및 디코딩 메커니즘
  • VM 메모리 레이아웃 및 아키텍처
  • 개념 증명 디스어셈블러
  • 제어 흐름 분석 노트

참고: 이 저장소는 (정적) VM 한 버전만 다루며 보안 연구 및 분석 목적으로 작성되었습니다. 동적 솔버나 프로덕션 솔버 구현은 포함되지 않습니다.

배경

2026년 1월 14일, DataDome은 클라이언트 태그에 새로운 VM 기반 구성 요소를 배포하기 시작했습니다.

난독화 해제

VM 코드는 캡차 챌린지에서 추출되어 vm.js (이 저장소에 있음)에 저장되었습니다.

첫 번째 단계는 스크립트의 난독화를 해제하는 것이었습니다:

난독화는 간단합니다: 각 변수를 평가하고 실제 값으로 대체합니다. 난독화 해제 스크립트는 deobf.js에서 확인할 수 있습니다.

초기 분석

난독화가 해제된 코드 (out.js)를 DevTools에서 실행하면 VM의 예상 출력을 볼 수 있습니다:

출력은 두 개의 숫자와 하나의 문자열을 포함하는 JSON 객체입니다. 이제 실제 VM 구현을 살펴보겠습니다.

바이트코드 디코딩

Q.exports 함수 시작 부분에서 바이트코드가 어떻게 디코딩되는지 확인할 수 있습니다:

  1. 입력 문자열을 base64 디코딩합니다.
  2. 길이가 129,263인 배열을 생성합니다.
  3. 각 인덱스를 특정 범위와 비교합니다:
    • 인덱스가 범위 안에 있으면 값을 디코딩합니다.
    • 그렇지 않으면 난수(B() 의사 난수 생성기 사용)를 반환합니다. -> D는 약간의 무작위 "노이즈"가 포함된 디코딩된 바이트코드를 보관합니다.

VM 아키텍처

아래로 스크롤하면 VM 진입점이 나타납니다: 두 개의 매개변수 A (바이트코드)와 Q (오류 처리를 위한 빈 딕셔너리)를 받는 함수입니다.

메모리 레이아웃

이 VM에서 가장 흥미로운 점은 아키텍처입니다: 모든 것이 단일 배열(A)에 저장됩니다. 이 배열에는 다음이 포함됩니다:

  • 스택
  • 레지스터
  • 오퍼코드
  • 바이트코드 자체
  • 명령어 포인터

이 설계는 서로 다른 메모리 영역을 가진 실제 컴퓨터 아키텍처를 반영합니다. 다음 단계는 각 오프셋을 매핑하여 어떤 데이터가 어디에 저장되는지 이해하는 것입니다:

root@kitploit:~
var stack_pointer = 4593
var instruction_pointer = 4635
var frame_base_pointer = 4674
var last_result = 4633
var exit_flag = 4656
var current_opcode_handler = 4685
var current_opcode_id = 4675
var stack_offset = 124482
var vm_start = 5258

이러한 오프셋이 매핑되면 VM 구조가 명확해집니다.

헬퍼 함수

VM은 다음을 처리하는 헬퍼 함수 모음으로 시작합니다:

  • 스택에서 타입이 있는 값 읽기
  • 스택과 "레지스터" 사이의 데이터 이동

VM 초기화

헬퍼 함수 다음으로 VM은 핵심 값을 초기화합니다:

  • 모든 포인터 (스택, 명령어, 프레임 베이스)
  • 종료 플래그
  • 마지막 결과 레지스터

초기화 아래에는 모든 명령어 핸들러가 있습니다.

디스패처 루프

디스패처는 `exit_flag`가 설정될 때까지 실행되는 메인 VM 루프입니다:
  • I는 현재 명령어를 나타냄
  • P는 난독화를 고려한 실제 배열 오프셋
  • 루프는 현재 명령어를 current_opcode_handler로 설정하고 current_opcode_id를 업데이트

오퍼코드 구현

오퍼코드 작동 방식

다음은 오퍼코드 핸들러의 기본 예제입니다:

  1. 바이트코드에서 즉시 값을 가져옴
  2. 스택 최상위 값을 검색
  3. 연산 수행 (예: %= 또는 ^=)
  4. 마지막에 fetch() 함수 호출

흥미로운 오퍼코드

오퍼코드 4919: 함수/클로저 생성

가장 복잡한 오퍼코드 중 하나로 클로저/함수를 생성합니다:

root@kitploit:~
A[4919] = function () {
    var Q = readUint8();  // 예상 인자 수
    var B = [];
    for (var E = readUint8(), D = 0; D < E; D++) {
        var g = readUint8();
        var a = A[A[frame_base_pointer] + g];
        B.push(a);  // 현재 스코프에서 변수 캡처
    }
    var h = A[instruction_pointer] + 3;  // 함수 본문의 주소 저장
    A[A[stack_pointer]++] = function (E) {
        // 호출 시 새 스택 프레임 설정
        var e = A[stack_pointer] - E;
        while (E < Q) {
            A[e + E++] = undefined;  // 누락된 인자를 undefined로 채움
        }
        A[stack_pointer] = e + Q;
        for (var D = 0; D < B.length; D++) {
            var g = B[D];
            A[A[stack_pointer]++] = g;  // 캡처한 변수 푸시
        }
        A[e - 2] = A[frame_base_pointer];  // 이전 프레임 포인터 저장
        A[e - 1] = A[instruction_pointer];  // 반환 주소 저장
        A[frame_base_pointer] = e;
        A[instruction_pointer] = h;  // 함수 본문으로 점프
    };
    fetch();
};

이 오퍼코드:

  1. 예상 인자 수를 읽음
  2. 현재 스코프의 변수 캡처 (클로저)
  3. 적절한 호출 규칙으로 새 스택 프레임을 설정하는 함수를 생성
  4. 누락된 인자를 undefined로 처리
  5. 올바른 반환을 위해 반환 주소와 프레임 포인터 저장

오퍼코드 5003: 동적 함수 호출

이 오퍼코드는 일반 호출과 생성자 호출을 모두 처리하는 함수 호출 래퍼를 생성합니다:

root@kitploit:~
A[5003] = function () {
    var Q = A[--A[stack_pointer]];  // POP 함수
    var B = A[--A[stack_pointer]];  // POP 'this' 컨텍스트

    function E(e) {  // e = 인자 수
        var D = A[stack_pointer];
        var g = A.slice(D - e, D);  // 스택에서 인자 가져오기
        if (this instanceof E) {
            // 생성자 호출 (new E(...))
            g.unshift(null);
            var h = Function.prototype.bind.apply(Q, g);
            A[stack_pointer] -= e;
            try {
                a = new h();
            } catch (A) {
                a = A.message;
            }
            A[A[stack_pointer]++] = a;
        } else {
            // 일반 함수 호출
            var t;
            try {
                t = Q.apply(B, g);
            } catch (A) {
                t = A.message;
            }
            A[stack_pointer] -= e + 2;
            A[A[stack_pointer]++] = t;
        }
    }

    A[A[stack_pointer]++] = E;
    fetch();
};

이 오퍼코드:

  1. 스택에서 함수와 컨텍스트를 꺼냄
  2. 인자와 함께 호출할 수 있는 래퍼를 생성
  3. 생성자 호출(new)인지 일반 호출인지 감지
  4. 적절한 컨텍스트와 오류 처리로 함수를 적용
  5. 결과를 다시 스택에 푸시

오퍼코드 4961: 객체 리터럴 생성

root@kitploit:~
A[4961] = function () {
    var Q = {};
    for (var E = readUint16(), e = 0; e < E; e++) {
        var D = A[--A[stack_pointer]];  // 첫 번째 POP
        var g = A[--A[stack_pointer]];  // 두 번째 POP
        Q[D] = g;
    }
    A[A[stack_pointer]++] = Q;
    fetch();
};

이 오퍼코드는 객체 리터럴을 다음과 같이 생성합니다:

  1. 바이트코드에서 프로퍼티 쌍의 개수를 읽음
  2. 스택에서 쌍을 꺼냄 (첫 번째 팝이 키가 됨)
  3. 객체 구성: object[firstPop] = secondPop
  4. 결과 객체를 스택에 푸시

Fetch 함수

각 명령어는 fetch()를 호출하여 종료되며, 이 함수는 다음 명령어를 준비합니다:

root@kitploit:~
function fetch() {
    var Q = A[instruction_pointer];
    var B = A[vm_start + Q];
    A[instruction_pointer] = Q + 1;
    var E = A[4783 + B];
    A[current_opcode_handler] = E;
    A[current_opcode_id] = B;
}

이 함수:

  1. 명령어 포인터를 읽음
  2. 바이트코드에서 다음 오퍼코드를 가져옴
  3. 명령어 포인터를 증가
  4. 오퍼코드 핸들러를 조회
  5. current_opcode_handler와 current_opcode_id를 업데이트

이는 디스패처 로직을 반영하며, VM 아키텍처의 전형적인 fetch-decode-execute 사이클을 만듭니다.

디스어셈블러 구현

분석을 돕기 위해 VM의 바이트코드를 사람이 읽을 수 있는 어셈블리로 변환하는 개념 증명 디스어셈블러(disasm.js)가 개발되었습니다.

접근 방식

디스어셈블러는 두 패스로 작동합니다:

패스 1: 레이블 발견

첫 번째 패스는 바이트코드를 스캔하여 모든 점프 대상을 식별합니다. 여기에는 다음이 포함됩니다:

  • 순방향 및 역방향 점프 (JMP_FWD, JMP_BACK)
  • 조건부 점프 (JZ, JNZ_KEEP, JZ_KEEP)
  • 클로저 경계 (함수 본문 및 종료 지점)

각 대상 주소는 레이블(예: L_0042)로 표시되어 제어 흐름을 더 쉽게 따라갈 수 있습니다.

패스 2: 디스어셈블리

두 번째 패스는 각 명령어를 어셈블리 형식의 출력으로 변환합니다:

root@kitploit:~
000042:  fa 00 0a           PUSH_IMM 10
000045:  19 00 19           PUSH_REG 25
000048:  eb                 ADD

각 줄에는 다음이 포함됩니다:

  • 주소: 바이트코드의 16진수 오프셋
  • 원시 바이트: 명령어를 구성하는 실제 바이트 (검증에 유용하고 멋져 보임)
  • 오퍼코드: 명령어의 니모닉 이름
  • 인자: 디코딩된 피연산자 (레지스터 번호, 즉시 값, 점프 대상)

값 디코딩

가장 복잡한 부분 중 하나는 바이트코드에 포함된 즉시 값을 디코딩하는 것입니다. VM은 타입 마커를 사용하여 다음 바이트를 해석하는 방법을 나타냅니다:

단순 타입 (추가 데이터 없음):

  • 0x28 → true
  • 0x7D → false
  • 0x4C → null
  • 0x3D → undefined

작은 정수 (0-127): 최상위 비트가 설정되어 인코딩됨

  • 0x85 → 5 (0x80 | 5)

문자열: XOR 인코딩되고 null로 종료됨

  • ASCII 문자열: 마커 0x67, XOR 키는 183부터 시작
  • UTF-8 문자열: 마커 0x27, XOR 키는 46부터 시작

숫자 타입:

  • 8비트 부호: 0x6F + 1바이트
  • 16비트 부호: 0x61 + 2바이트 (빅 엔디안)
  • 24비트 부호: 0x65 + 3바이트 (빅 엔디안)
  • 32비트 부호: 0x54 + 4바이트 (빅 엔디안)
  • IEEE 754 배정밀도: 0x05 + 8바이트

XOR 인코딩은 단순하지만 우연한 검사를 방지합니다:

root@kitploit:~
let str = '';
let xorKey = 183;  // ASCII 문자열의 초기 키
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
    str += String.fromCharCode(ch);
}

특수 오퍼코드

일부 오퍼코드는 사용자 정의 처리가 필요합니다:

CLOSURE (오퍼코드 136): 캡처된 변수로 함수/클로저 생성

root@kitploit:~
형식: CLOSURE 지역변수수, 캡처수, [캡처인덱스...], 건너뛸오프셋

건너뛸 오프셋은 함수 본문을 지나쳐 VM이 선형 실행 중 함수 정의를 건너뛸 수 있도록 합니다.

PUSH_MULTI_IMM (오퍼코드 96): 여러 값을 한 번에 푸시

root@kitploit:~
형식: PUSH_MULTI_IMM 개수, 값1, 값2, ...

사용법

root@kitploit:~
# 파일에서 디스어셈블리
node disasm.js bytecode.txt

예제 출력

root@kitploit:~
; DataDome VM 디스어셈블리
; 바이트코드 크기: 5428 바이트
; VM 상수: VM_START=5258, OPCODE_BASE=4783

000000:  88 01 00 04        CLOSURE locals=1, captures=[0, 4], body=L_0006, end=L_0a3f
L_0006:
000006:  fa 67 ...          PUSH_IMM "window"
00001f:  2b                 PUSH_WINDOW
000020:  02                 SET
000021:  fa 67 ...          PUSH_IMM "navigator"
00003a:  19 00 00           PUSH_REG 0
00003d:  fa 67 ...          PUSH_IMM "navigator"
000056:  ee                 GET
000057:  02                 SET
...

이 출력 형식을 사용하면 다음을 수행할 수 있습니다:

  • 점프 레이블을 따라 실행 흐름을 추적
  • CLOSURE 오퍼코드를 통해 함수 경계 식별
  • 어떤 값이 푸시되고 조작되는지 정확히 확인
  • 실제 VM 구현과 상호 참조

참고

저는 VM에 대해 아직 초보이므로 모든 내용을 가볍게 받아들이시기 바랍니다. 코드 문서화와 이 readme의 일부를 작성하는 데 AI를 사용했습니다 (문서 작성은 고통스럽기 때문입니다).

면책 조항

이것은 순전히 교육/보안 연구 목적입니다. 솔버나 우회 방법은 포함되어 있지 않습니다. VM이 어떻게 작동하는지 문서화하는 것뿐입니다. 정말 흥미롭기 때문입니다.

DataDome: 이 글을 읽고 계신다면 안녕하세요!!! 저는 그냥 장학금을 받으려고 하는 것뿐입니다 🙏. 고소하지 말아 주세요. 저는 완전 무일푼입니다. 이 저장소에 문제가 있으시면 알려주세요. 이야기할 수 있습니다 :)

VM 내부에 대해 이야기할 거라고 생각한 분들께 죄송합니다... 그런 일은 없습니다.

DM 보내서 DataDome API를 달라고 하지 마세요. 도와드리지 않겠습니다.

도구 다운로드