
नए Datadome VM का रिवर्स इंजीनियरिंग 🔥
यह रिपॉजिटरी DataDome के CAPTCHA/इंटरस्टीशियल फ़्लो में उपयोग किए जाने वाले ब्राउज़र-आंतरिक JavaScript वर्चुअल मशीन (VM) के पहले सार्वजनिक संस्करण का दस्तावेज़ीकरण करती है। इस विश्लेषण में शामिल हैं:
नोट: यह रिपॉजिटरी केवल एक (स्थिर) VM संस्करण को कवर करती है और सुरक्षा अनुसंधान एवं विश्लेषण उद्देश्यों के लिए है। इसमें डायनामिक सॉल्वर या उत्पादन सॉल्वर कार्यान्वयन शामिल नहीं हैं।
14 जनवरी, 2026 को DataDome ने अपने क्लाइंट टैग में एक नया VM-आधारित घटक भेजना शुरू किया।
VM कोड को कैप्चा चैलेंज से निकालकर vm.js (इस रिपॉजिटरी में उपलब्ध) में रखा गया है।
पहला कदम स्क्रिप्ट को डिओबफ़स्केट करना था:
ओबफ़स्केशन सीधा है: प्रत्येक वेरिएबल का मूल्यांकन करें और उसे उसके वास्तविक मान से बदलें। deobf.js में एक डिओबफ़स्केशन स्क्रिप्ट उपलब्ध है।
डिओबफ़स्केट किए गए कोड (out.js) को DevTools में चलाने पर VM का अपेक्षित आउटपुट दिखाई देता है:
आउटपुट एक JSON ऑब्जेक्ट है जिसमें दो संख्याएँ और एक स्ट्रिंग है। अब वास्तविक VM कार्यान्वयन में गोता लगाते हैं।
Q.exports फ़ंक्शन की शुरुआत में, हम देख सकते हैं कि बाइटकोड कैसे डिकोड किया जाता है:
B() का उपयोग करके, एक स्यूडो-रैंडम नंबर जनरेटर)
-> D कुछ यादृच्छिक "शोर" के साथ डिकोड किए गए बाइटकोड को धारण करता है
नीचे स्क्रॉल करने पर 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` सेट होने तक चलता है:
I वर्तमान इंस्ट्रक्शन को दर्शाता हैP सरणी में वास्तविक ऑफ़सेट है (ओबफ़स्केशन को ध्यान में रखते हुए)current_opcode_handler पर सेट करता है और current_opcode_id को अपडेट करता है
यहाँ एक ऑपकोड हैंडलर का एक मूल उदाहरण है:
%= या ^=)fetch() फ़ंक्शन को कॉल करता हैसबसे जटिल ऑपकोडों में से एक क्लोज़र/फ़ंक्शन बनाता है:
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();
};
यह ऑपकोड:
undefined से भरकर संभालता हैयह ऑपकोड फ़ंक्शन कॉल के लिए एक रैपर बनाता है जो नियमित और कंस्ट्रक्टर दोनों कॉल को संभालता है:
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();
};
यह ऑपकोड:
new) है या नियमित कॉल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();
};
यह ऑपकोड ऑब्जेक्ट लिटरल बनाता है:
object[firstPop] = secondPopप्रत्येक इंस्ट्रक्शन 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;
}
यह फ़ंक्शन:
current_opcode_handler और current_opcode_id को अपडेट करता हैयह डिस्पैचर तर्क को दर्शाता है, जो VM आर्किटेक्चर के विशिष्ट फ़ेच-डिकोड-एक्ज़ीक्यूट चक्र बनाता है।
विश्लेषण में सहायता के लिए, एक प्रूफ-ऑफ-कॉन्सेप्ट डिसअसेम्बलर (disasm.js) विकसित किया गया था जो VM के बाइटकोड को मानव-पठनीय असेंबली में परिवर्तित करता है।
डिसअसेम्बलर दो पास में काम करता है:
पहला पास सभी जंप लक्ष्यों की पहचान करने के लिए बाइटकोड को स्कैन करता है। इसमें शामिल है:
JMP_FWD, JMP_BACK)JZ, JNZ_KEEP, JZ_KEEP)प्रत्येक लक्ष्य पते को एक लेबल (जैसे L_0042) से चिह्नित किया जाता है ताकि नियंत्रण प्रवाह को आसानी से समझा जा सके।
दूसरा पास प्रत्येक इंस्ट्रक्शन को असेंबली-जैसे आउटपुट में परिवर्तित करता है:
000042: fa 00 0a PUSH_IMM 10
000045: 19 00 19 PUSH_REG 25
000048: eb ADD
प्रत्येक पंक्ति में शामिल है:
अधिक जटिल पहलुओं में से एक बाइटकोड में एम्बेडेड तत्काल मानों को डिकोड करना है। VM यह इंगित करने के लिए टाइप मार्कर का उपयोग करता है कि निम्नलिखित बाइट्स की व्याख्या कैसे करें:
सरल प्रकार (कोई अतिरिक्त डेटा नहीं):
0x28 → true0x7D → false0x4C → null0x3D → undefinedछोटे पूर्णांक (0-127): उच्च बिट सेट के साथ एन्कोडेड
0x85 → 5 (0x80 | 5)स्ट्रिंग्स: XOR-एन्कोडेड और नल-टर्मिनेटेड
0x67, XOR कुंजी 183 से शुरू होती है0x27, XOR कुंजी 46 से शुरू होती हैसंख्यात्मक प्रकार:
0x6F + 1 बाइट0x61 + 2 बाइट्स (बिग-एंडियन)0x65 + 3 बाइट्स (बिग-एंडियन)0x54 + 4 बाइट्स (बिग-एंडियन)0x05 + 8 बाइट्सस्ट्रिंग्स के लिए XOR एन्कोडिंग सीधी है लेकिन आकस्मिक निरीक्षण को रोकती है:
let str = '';
let xorKey = 183; // ASCII स्ट्रिंग्स के लिए प्रारंभिक कुंजी
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
str += String.fromCharCode(ch);
}
कुछ ऑपकोडों को कस्टम हैंडलिंग की आवश्यकता होती है:
CLOSURE (ऑपकोड 136): कैप्चर किए गए वेरिएबल के साथ फ़ंक्शन/क्लोज़र बनाता है
Format: CLOSURE locals, capture_count, [capture_indices...], skip_offset
स्किप ऑफ़सेट फ़ंक्शन बॉडी से आगे इंगित करता है, जिससे VM रैखिक निष्पादन के दौरान फ़ंक्शन परिभाषा को छोड़ सकता है।
PUSH_MULTI_IMM (ऑपकोड 96): एक साथ कई मान पुश करता है
Format: PUSH_MULTI_IMM count, val1, val2, ...
# फ़ाइल से डिसअसेम्बल करें
node disasm.js bytecode.txt
; 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
...
यह आउटपुट प्रारूप निम्न को संभव बनाता है:
मैं VM के मामले में अभी भी काफी नया हूँ, इसलिए हर चीज़ को संदेह की दृष्टि से लें। कोड को दस्तावेज़ित करने और इस रीडमी के कुछ हिस्सों को लिखने में AI का उपयोग किया गया है (क्योंकि डॉक्स लिखना दर्दनाक है)।
यह पूरी तरह से शैक्षिक/सुरक्षा अनुसंधान उद्देश्यों के लिए है। इसमें कोई सॉल्वर या बाइपास शामिल नहीं है – बस यह दस्तावेज़ित करना कि VM कैसे काम करता है क्योंकि यह वास्तव में दिलचस्प है।
DataDome: यदि आप इसे पढ़ रहे हैं, नमस्ते!!! यह सिर्फ मैं एक छात्रवृत्ति पाने की कोशिश कर रहा हूँ 🙏। कृपया मुझ पर मुकदमा न करें, मैं बहुत गरीब हूँ। यदि आपको इस रेपो से कोई समस्या है, तो बस मुझे बताएं और हम इस पर बात कर सकते हैं :)
उन लोगों के लिए खेद है जिन्होंने सोचा कि मैं VM के अंदर के बारे में बात करूंगा… ऐसा नहीं होने वाला है
मुझे DM करके DataDome API मत मांगिए, मैं आपकी मदद नहीं करूंगा।