Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

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

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 리버스 엔지니어링 🔥

저장소 보기
12519637개월 전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)에 저장됩니다. 이 배열에는 다음이 포함됩니다:

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

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

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: 함수/클로저 생성

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

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: 동적 함수 호출

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

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: 객체 리터럴 생성

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()를 호출하여 종료되며, 이 함수는 다음 명령어를 준비합니다:

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: 디스어셈블리

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

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 인코딩은 단순하지만 우연한 검사를 방지합니다:

let str = '';
let xorKey = 183;  // ASCII 문자열의 초기 키
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
    str += String.fromCharCode(ch);
}
도구 다운로드