Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
datadome-vm — 新しいDatadome VMのリバースエンジニアリング 🔥 | Kitploit
ツール/GitHubGitHub/xkiian/datadome-vm
動的分析 (サンドボックス)リバースエンジニアリングバイナリ解析学習と教育アンチボットCAPTCHAバイパス
GitHubxkiian/datadome-vm

datadome-vm

新しいDatadome VMのリバースエンジニアリング 🔥

リポジトリを見る
12519106ヶ月前Kitploit レビュー済み

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

DataDome VM 解析

概要

このリポジトリは、DataDome の CAPTCHA/インタスティシャルフローで使用されるブラウザ内 JavaScript 仮想マシン (VM) の初の公開版を文書化したものです。この解析では以下を扱います:

  • バイトコードのロードおよびデコード機構
  • VM メモリレイアウトとアーキテクチャ
  • 概念実証の逆アセンブラ
  • 制御フロー解析のメモ

注記: このリポジトリは、静的 VM の1つのバージョンのみを対象としており、セキュリティ研究および解析目的で作成されています。動的ソルバーやプロダクション用ソルバーの実装は含まれていません。

背景

2026年1月14日、DataDome はクライアントタグに新しい VM ベースのコンポーネントを導入し始めました。

難読化解除

VM コードは CAPTCHA チャレンジから抽出され、vm.js(このリポジトリ内にあり)に保存されています。

最初のステップはスクリプトの難読化解除でした:

難読化は単純で、各変数を評価し、実際の値で置き換えるだけです。難読化解除スクリプトは deobf.js にあります。

初期解析

難読化解除されたコード(out.js)を DevTools で実行すると、VM の期待される出力が表示されます:

出力は2つの数値と1つの文字列を含む JSON オブジェクトです。それでは実際の VM 実装に深く潜り込んでみましょう。

バイトコードデコード

Q.exports 関数の先頭で、バイトコードがどのようにデコードされるかがわかります:

  1. 入力文字列は Base64 デコードされる
  2. 長さ 129,263 の配列が作成される
  3. 各インデックスは特定の範囲に対してチェックされる:
    • インデックスが範囲内にある場合、値はデコードされる
    • それ以外の場合は、乱数が返される(B() を使用。疑似乱数生成器) -> D はデコードされたバイトコードを保持し、ランダムな「ノイズ」が含まれている

VM アーキテクチャ

スクロールダウンすると、VM のエントリポイントが現れます:2つのパラメータ 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: 関数/クロージャの作成

最も複雑なオペコードの1つで、クロージャ/関数を作成します:

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]];  // 2番目の 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)が開発されました。

アプローチ

逆アセンブラは2パスで動作します:

パス 1: ラベル発見

最初のパスではバイトコードをスキャンし、すべてのジャンプ先を特定します。これには以下が含まれます:

  • 前方ジャンプと後方ジャンプ(JMP_FWD、JMP_BACK)
  • 条件付きジャンプ(JZ、JNZ_KEEP、JZ_KEEP)
  • クロージャの境界(関数本体とその終了点)

各ターゲットアドレスにはラベル(例:L_0042)が付けられ、制御フローを追跡しやすくします。

パス 2: 逆アセンブル

2番目のパスでは、各命令をアセンブリ風の出力に変換します:

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

各行には以下が含まれます:

  • アドレス: バイトコード内の16進数オフセット
  • 生バイト: 命令を構成する実際のバイト(検証に便利で、見た目もかっこいい)
  • オペコード: 命令のニーモニック名
  • 引数: デコードされたオペランド(レジスタ番号、即値、ジャンプ先)

値のデコード

より複雑な側面の1つは、バイトコードに埋め込まれた即値のデコードです。VM は型マーカーを使用して、後続のバイトをどのように解釈するかを示します:

単純型(追加データなし):

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

小整数(0-127):上位ビットがセットされてエンコード

  • 0x85 → 5 (0x80 | 5)

文字列: XOR エンコードされ、ヌル終端

  • 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 locals, capture_count, [capture_indices...], skip_offset

スキップオフセットは関数本体の後を指し、線形実行中に VM が関数定義をスキップできるようにします。

PUSH_MULTI_IMM(オペコード 96): 一度に複数の値をプッシュ

root@kitploit:~
形式: PUSH_MULTI_IMM count, val1, val2, ...

使用方法

root@kitploit:~
# ファイルから逆アセンブル
node disasm.js bytecode.txt

出力例

root@kitploit:~
; DataDome VM Disassembly
; Bytecode size: 5428 bytes
; VM Constants: 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 内部の話をすると思った方々…それはありません。

DataDome API を求めて DM しないでください。お手伝いできません。

ツールをダウンロード