QBridge एक्सप्लॉइट का तकनीकी विश्लेषण
यह एक्सप्लॉइट का पूर्ण तकनीकी विश्लेषण है जो ऑन-चेन साक्ष्य, कॉन्ट्रैक्ट सोर्स कोड, और हमले के समय कैप्चर किए गए स्क्रीनशॉट साक्ष्यों पर आधारित है। पीड़ित समुदाय द्वारा मूल चीनी भाषा में किया गया विश्लेषण; यहाँ सभी निकाले गए डेटा के साथ अनुवादित और विस्तारित किया गया है।
प्रमुख पते
Section titled “प्रमुख पते”| भूमिका | पता |
|---|---|
| हमलावर | 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 |
| QBridge कॉन्ट्रैक्ट (Ethereum) | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 |
| QBridge हैंडलर (BSC) | 0x4d8ae68fcae98bf93299548545933c0d273ba23a |
| ETH resourceID | 0x0000000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01 |
चरण 1 — BSC पहला अपराध स्थल नहीं था
Section titled “चरण 1 — BSC पहला अपराध स्थल नहीं था”जब जांचकर्ताओं ने BSC पर हमलावर के पते को पहली बार देखा, तो कुछ तुरंत गलत लगा:

हमलावर सीधे borrow() पर गया — कोई तैयारी नहीं, कोई फ्लैश लोन नहीं, कोई कॉन्ट्रैक्ट डिप्लॉयमेंट नहीं। इसका मतलब था कि BSC वह जगह नहीं थी जहाँ हमला शुरू हुआ। हमलावर के पास लेंडिंग प्रोटोकॉल को छूने से पहले ही qXETH टोकन थे।
उन qXETH टोकनों को पीछे ट्रेस करने पर पता चला कि वे ब्रिज रिलेयर द्वारा मिंट किए गए थे — जिसका मतलब था कि मूल स्रोत Ethereum था।
चरण 2 — BSC पर voteProposal ट्रांज़ैक्शन
Section titled “चरण 2 — BSC पर voteProposal ट्रांज़ैक्शन”qXETH मिंटिंग ट्रांज़ैक्शनों में से एक:
ट्रांज़ैक्शन: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc

इस ट्रांज़ैक्शन से मुख्य अवलोकन:
- QBridge BSC कॉन्ट्रैक्ट पर
voteProposal(uint8 chainID, uint64 depositNonce, bytes32 resourceID, bytes data)कॉल किया गया - 3 टोकन ट्रांसफर हुए:
- Null एड्रेस →
0xa6b1bb...— 59,900 Cross-Chain xETH (मिंट किया गया) 0xa6b1bb...→0xfd7a55...— 59,900 xETH (ब्रिज किया गया)- Null एड्रेस → QubitFin Exploiter — 59,900 qXETH (कोलैटरल के लिए मिंट किया गया)
- Null एड्रेस →
- मूल्य: 0 BNB — कहीं भी कोई वास्तविक ETH जमा नहीं किया गया
voteProposal फ़ंक्शन केवल रिलेयर द्वारा कॉल किया जा सकता था। रिलेयर ने इसे इसलिए कॉल किया क्योंकि उसने Ethereum पर एक Deposit इवेंट देखा। वह इवेंट नकली था।
चरण 3 — Ethereum पर हमलावर की जमा राशियाँ
Section titled “चरण 3 — Ethereum पर हमलावर की जमा राशियाँ”
हमलावर ने Ethereum QBridge कॉन्ट्रैक्ट (0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6) पर deposit() को अनेक बार कॉल किया:
| ट्रांज़ैक्शन हैश | ब्लॉक | मूल्य |
|---|---|---|
0x3dfa33b5... | 14090234 | 0 ETH |
0x58020654... | 14090230 | 0 ETH |
0x501f8541... | 14090230 | 0 ETH |
0xa6282e60... | 14090223 | 0 ETH |
0x94031569... | 14090216 | 0 ETH |
0xbdedb13d... | 14090210 | 0 ETH |
0xeb9f622e... | 14090201 | 0 ETH |
हर एक ट्रांज़ैक्शन में 0 ETH मूल्य था। मेथड: deposit। प्रत्येक ने एक Deposit इवेंट ट्रिगर किया जिसे रिलेयर ने ईमानदारी से प्रोसेस किया।
इनमें से एक ट्रांज़ैक्शन का विवरण:

- ब्लॉक: 14090216
- टाइमस्टैम्प: Jan-27-2022 09:45:32 PM UTC
- प्रेषक:
0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7(हमलावर) - प्राप्तकर्ता:
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge Ethereum) - मूल्य: 0 Ether ($0.00)
- कॉल किया गया फ़ंक्शन:
deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) - हमले के समय ETH की कीमत: $2,425.83
चरण 4 — दो फ़ंक्शन, एक इवेंट
Section titled “चरण 4 — दो फ़ंक्शन, एक इवेंट”QBridge कॉन्ट्रैक्ट में दो deposit फ़ंक्शन थे:

depositETH()— ETH के लिए सही पथ,msg.value > 0आवश्यक थाdeposit()— ERC-20 टोकन के लिए डिज़ाइन किया गया, ETH की आवश्यकता नहीं
दोनों बिल्कुल एक ही Deposit इवेंट प्रकार emit करते थे। रिलेयर Deposit इवेंट को सुनता था और यह भेद नहीं कर सकता था कि कौन से फ़ंक्शन ने उन्हें ट्रिगर किया।
चरण 5 — हैंडलर कोड और व्हाइटलिस्ट
Section titled “चरण 5 — हैंडलर कोड और व्हाइटलिस्ट”
QBridgeHandler.deposit() फ़ंक्शन:
function deposit( bytes32 resourceID, address depositer, bytes calldata data) external override { address tokenAddress = _resourceIDToTokenContractAddress[resourceID]; // get token address require(_contractWhitelist[tokenAddress], "not whitelisted"); // line 128: whitelist check // ... ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount); // line 135}ETH के resourceID के लिए, कॉन्ट्रैक्ट ने tokenAddress = 0x0000000000000000000000000000000000000000 (शून्य पता) लौटाया।
चरण 6 — शून्य पता व्हाइटलिस्ट में था
Section titled “चरण 6 — शून्य पता व्हाइटलिस्ट में था”
जांचकर्ताओं ने कॉन्ट्रैक्ट से पूछताछ की: क्या शून्य पता व्हाइटलिस्ट में था?
हाँ। यह था।
यह आवश्यक था क्योंकि depositETH() भी ETH के प्लेसहोल्डर के रूप में शून्य पते के साथ उसी व्हाइटलिस्ट तंत्र का उपयोग करता था। लेकिन इसने एक घातक साइड इफेक्ट पैदा किया: शून्य पता deposit() में भी व्हाइटलिस्ट जाँच पास कर गया।

ETH resourceID को 0x0000000000000000000000000000000000000000 — शून्य पता — से मैप किया गया था, जैसा कि resourceIDToTokenContractAddress की क्वेरी से पुष्टि हुई।
चरण 7 — EOA को कॉल करना चुपचाप सफल हो जाता है
Section titled “चरण 7 — EOA को कॉल करना चुपचाप सफल हो जाता है”tokenAddress = 0x0000...0000 के साथ, हैंडलर ने यह निष्पादित किया:
ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount);शून्य पता एक EOA है — एक Externally Owned Account जिसमें कोई कॉन्ट्रैक्ट कोड नहीं है।
EVM में: बिना कॉन्ट्रैक्ट कोड वाले किसी भी पते पर कोई भी फ़ंक्शन कॉल करना चुपचाप सफल हो जाता है — कोई revert नहीं, कोई त्रुटि नहीं, कोई वास्तविक निष्पादन नहीं।
safeTransferFrom कॉल “सफल” हो गई। कुछ भी नहीं हिला। न ETH, न टोकन। लेकिन कोड ऐसे चलता रहा जैसे सब कुछ ठीक था — और एक वैध Deposit इवेंट emit किया।

यह बिल्कुल यही ट्रिक 2019 में 0x Protocol सुरक्षा अपडेट में सार्वजनिक रूप से प्रलेखित की गई थी। Mound Inc. ने 2022 में इसे ध्यान में रखे बिना अपना ब्रिज डिप्लॉय किया।
चरण 8 — Borrow फ़ंक्शन सही था, लेकिन बहुत देर हो चुकी थी
Section titled “चरण 8 — Borrow फ़ंक्शन सही था, लेकिन बहुत देर हो चुकी थी”
function borrow(address qToken, uint amount) external override onlyListedMarket(qToken) nonReentrant { _enterMarket(qToken, msg.sender); require(IQValidator(qValidator).borrowAllowed(qToken, msg.sender, amount), "Qore: cannot borrow");
IQToken(payable(qToken)).borrow(msg.sender, amount); qDistributor.notifyBorrowUpdated(qToken, msg.sender);}लेंडिंग कॉन्ट्रैक्ट सही था — borrowAllowed() ने कोलैटरल मूल्य की ठीक से जाँच की। लेकिन qXETH टोकन ऑन-चेन वैध दिखते थे। धोखाधड़ी पहले ही ब्रिज पर हो चुकी थी। जब borrow() चला, तब तक हमलावर के पास वास्तविक दिखने वाला कोलैटरल था जिसके पीछे कुछ भी नहीं था।
चरण 9 — निर्णायक सबूत: हमले से पहले पैरामीटर परिवर्तन
Section titled “चरण 9 — निर्णायक सबूत: हमले से पहले पैरामीटर परिवर्तन”यह वह विवरण है जिसे कभी समझाया नहीं गया।

हमले से पहले, किसी ने deposit() फ़ंक्शन के इतिहास की जाँच की। उन्होंने पाया:
1 दिसंबर, 2021 — एक वैध ट्रांज़ैक्शन (0xc85df3fbfee7a8541e9fd354e98df78dca21ac6be40b929ff5da52ea8eab80de):
- ब्लॉक: 13719888
- प्रेषक:
0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 - प्राप्तकर्ता: QBridge कॉन्ट्रैक्ट
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 - ट्रांसफर किए गए टोकन: 0.1 WETH (उस समय $241.81)
- फ़ंक्शन: उसी
resourceIDके साथdeposit()
दिसंबर 2021 में, उस resourceID के साथ deposit() ने वास्तविक WETH ट्रांसफर किया — क्योंकि resourceID वास्तविक WETH कॉन्ट्रैक्ट पते से मैप किया गया था।
दिसंबर 2021 और 28 जनवरी, 2022 के बीच कभी — एक owner-only फ़ंक्शन कॉल किया गया जिसने resourceID मैपिंग को WETH के कॉन्ट्रैक्ट पते से शून्य पते पर पुनः असाइन किया।

केवल कॉन्ट्रैक्ट का मालिक ही यह परिवर्तन कर सकता था। इसके लिए किसी टाइमलॉक की आवश्यकता नहीं थी। कोई घोषणा नहीं हुई। इस एकल अदृश्य पैरामीटर परिवर्तन ने ही एक काम करने वाले ब्रिज को शोषण योग्य बना दिया।
सारांश: सात एक साथ विफलताएँ
Section titled “सारांश: सात एक साथ विफलताएँ”| विफलता | प्रभाव |
|---|---|
deposit() और depositETH() एक ही इवेंट emit करते हैं | रिलेयर वास्तविक और नकली जमा के बीच अंतर नहीं कर सकता |
| EOA साइलेंट-सक्सेस का ध्यान नहीं रखा गया | शून्य पते पर safeTransferFrom बिना ट्रांसफर के पास हो जाता है |
| शून्य पता व्हाइटलिस्ट में | EOA कॉल व्हाइटलिस्ट जाँच पार कर जाता है |
| बिना ऑडिट के प्रोडक्शन डिप्लॉयमेंट | किसी बाहरी समीक्षक ने उपरोक्त में से कुछ भी नहीं पकड़ा |
| Owner फ़ंक्शन पर कोई टाइमलॉक नहीं | resourceID चुपचाप, तुरंत, अदृश्य रूप से पुनः मैप किया गया |
हमले से पहले resourceID पुनः मैप किया गया | वह परिवर्तन जिसने शोषण को संभव बनाया |
| अंधा रिलेयर इवेंट पर भरोसा करता है | ऑफ-चेन सेवा ऑन-चेन मूल्य सत्यापित किए बिना टोकन मिंट करती है |
बाहरी संदर्भ
Section titled “बाहरी संदर्भ”- हमले का ट्रांज़ैक्शन (BSC):
0x8c5877d1... - Etherscan पर हमलावर:
0xd01ae1... - QBridge (Ethereum):
0x20e5e3... - EOA शून्य-पता ट्रिक (0x Protocol, 2019): blog.0xproject.com
- मूल चीनी विश्लेषण: qbt.wiki
निर्णायक सबूत का ट्रांज़ैक्शन — 13 दिसंबर, 2021 को setResource() कॉल किया गया
Section titled “निर्णायक सबूत का ट्रांज़ैक्शन — 13 दिसंबर, 2021 को setResource() कॉल किया गया”यह सबसे महत्वपूर्ण साक्ष्य है।

ट्रांज़ैक्शन: 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66
| फ़ील्ड | मूल्य |
|---|---|
| स्थिति | ✅ सफल |
| ब्लॉक | 13797391 |
| टाइमस्टैम्प | Dec-13-2021 02:31:21 PM UTC |
| प्रेषक | 0xbee3971293374d0b4db7bf1654936951e5bdfe5a6 |
| प्राप्तकर्ता | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 (QBridge, Ethereum) |
| मूल्य | 0 ETH |
| फ़ंक्शन | setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) |
डिकोड किए गए इनपुट आर्गुमेंट:
| # | पैरामीटर | मूल्य |
|---|---|---|
| 0 | handlerAddress | 0x17B7163cf1Dbb6286262ddc68b553D899B893f526 |
| 1 | resourceID | 0x0000000000000000000000002f422fe9ea622049d6e73f81a906b9b8cff03b7f01 |
| 2 | tokenAddress | 0x0000000000000000000000000000000000000000 |
setResource() एक onlyOwner फ़ंक्शन है। केवल Mound Inc. टीम ही इसे कॉल कर सकती थी।
13 दिसंबर, 2021 को — हैक से 45 दिन पहले — कॉन्ट्रैक्ट के मालिक ने जानबूझकर ETH resourceID के लिए tokenAddress मैपिंग को शून्य पते में बदल दिया। इस कॉल से पहले, वही resourceID WETH टोकन कॉन्ट्रैक्ट की ओर इंगित करता था। इस कॉल के बाद, यह कहीं नहीं इंगित करता था — जिससे EOA साइलेंट-सक्सेस एक्सप्लॉइट संभव हो गया।
पूर्व voteProposal कोड जो दिखाता है कि केवल रिलेयर ही इसे कॉल कर सकते हैं:

function voteProposal(uint8 originDomainID, uint64 depositNonce, bytes32 resourceID, bytes calldata data) external onlyRelayers notPaused { address handlerAddress = resourceIDToHandlerAddress[resourceID]; require(handlerAddress != address(0), "QBridge: invalid handler"); // ... if (proposal._status == ProposalStatus.Passed) { executeProposal(originDomainID, depositNonce, resourceID, data, true); return; } // ... if (proposal._status == ProposalStatus.Inactive) { proposal = Proposal({ status: ProposalStatus.Active, _yesVotes: 0, _yesVotesTotal: 0, _proposedBlock: uint40(block.number) });और setResource() कोड जिसने सब कुछ बदल दिया:

function setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) external onlyOwner { resourceIDToHandlerAddress[resourceID] = handlerAddress; IQBridgeHandler(handlerAddress).setResource(resourceID, tokenAddress);}qXETH मिंटिंग अनुक्रम (BSC टोकन ट्रांसफर)
Section titled “qXETH मिंटिंग अनुक्रम (BSC टोकन ट्रांसफर)”
हमलावर को कई राउंड में null एड्रेस से सीधे qXETH मिंट होकर प्राप्त हुए:
| ट्रांज़ैक्शन हैश | राशि | टोकन | स्रोत |
|---|---|---|---|
0xd8bba15555... | 999 | qXETH | Null एड्रेस → Exploiter |
0xf6008ab482... | 499 | qXETH | Null एड्रेस → Exploiter |
0xcfa4379af6... | 140 | ETH (BEP-20) | 0xb4b778… → Exploiter |
0x61ca8bc28f... | 190 | qXETH | Null एड्रेस → Exploiter |
0x881a68c9c9... | 0.1 | qXETH | Null एड्रेस → Exploiter |
0x8c5877d1b6... | 0.1 | qXETH | Null एड्रेस → Exploiter |
हर “Null एड्रेस → Exploiter” qXETH मिंट Ethereum पर एक नकली deposit() कॉल से मेल खाता है।