इसे छोड़कर कंटेंट पर जाएं

QBridge एक्सप्लॉइट का तकनीकी विश्लेषण

यह एक्सप्लॉइट का पूर्ण तकनीकी विश्लेषण है जो ऑन-चेन साक्ष्य, कॉन्ट्रैक्ट सोर्स कोड, और हमले के समय कैप्चर किए गए स्क्रीनशॉट साक्ष्यों पर आधारित है। पीड़ित समुदाय द्वारा मूल चीनी भाषा में किया गया विश्लेषण; यहाँ सभी निकाले गए डेटा के साथ अनुवादित और विस्तारित किया गया है।


भूमिकापता
हमलावर0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7
QBridge कॉन्ट्रैक्ट (Ethereum)0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6
QBridge हैंडलर (BSC)0x4d8ae68fcae98bf93299548545933c0d273ba23a
ETH resourceID0x0000000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01

चरण 1 — BSC पहला अपराध स्थल नहीं था

Section titled “चरण 1 — BSC पहला अपराध स्थल नहीं था”

जब जांचकर्ताओं ने BSC पर हमलावर के पते को पहली बार देखा, तो कुछ तुरंत गलत लगा:

BSCScan पर QubitFin Exploiter के borrow ट्रांज़ैक्शन दिखा रहा है — कोई तैयारी चरण नहीं

हमलावर सीधे borrow() पर गया — कोई तैयारी नहीं, कोई फ्लैश लोन नहीं, कोई कॉन्ट्रैक्ट डिप्लॉयमेंट नहीं। इसका मतलब था कि BSC वह जगह नहीं थी जहाँ हमला शुरू हुआ। हमलावर के पास लेंडिंग प्रोटोकॉल को छूने से पहले ही qXETH टोकन थे।

उन qXETH टोकनों को पीछे ट्रेस करने पर पता चला कि वे ब्रिज रिलेयर द्वारा मिंट किए गए थे — जिसका मतलब था कि मूल स्रोत Ethereum था।


चरण 2 — BSC पर voteProposal ट्रांज़ैक्शन

Section titled “चरण 2 — BSC पर voteProposal ट्रांज़ैक्शन”

qXETH मिंटिंग ट्रांज़ैक्शनों में से एक:

ट्रांज़ैक्शन: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc

BSCScan voteProposal ट्रांज़ैक्शन — null एड्रेस से हमलावर को xETH मिंट होते दिखा रहा है

इस ट्रांज़ैक्शन से मुख्य अवलोकन:

  • 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 (कोलैटरल के लिए मिंट किया गया)
  • मूल्य: 0 BNB — कहीं भी कोई वास्तविक ETH जमा नहीं किया गया

voteProposal फ़ंक्शन केवल रिलेयर द्वारा कॉल किया जा सकता था। रिलेयर ने इसे इसलिए कॉल किया क्योंकि उसने Ethereum पर एक Deposit इवेंट देखा। वह इवेंट नकली था।


चरण 3 — Ethereum पर हमलावर की जमा राशियाँ

Section titled “चरण 3 — Ethereum पर हमलावर की जमा राशियाँ”

Etherscan पर हमलावर से QBridge तक शून्य-मूल्य वाली अनेक Deposit कॉल्स दिखा रहा है

हमलावर ने Ethereum QBridge कॉन्ट्रैक्ट (0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6) पर deposit() को अनेक बार कॉल किया:

ट्रांज़ैक्शन हैशब्लॉकमूल्य
0x3dfa33b5...140902340 ETH
0x58020654...140902300 ETH
0x501f8541...140902300 ETH
0xa6282e60...140902230 ETH
0x94031569...140902160 ETH
0xbdedb13d...140902100 ETH
0xeb9f622e...140902010 ETH

हर एक ट्रांज़ैक्शन में 0 ETH मूल्य था। मेथड: deposit। प्रत्येक ने एक Deposit इवेंट ट्रिगर किया जिसे रिलेयर ने ईमानदारी से प्रोसेस किया।

इनमें से एक ट्रांज़ैक्शन का विवरण:

Etherscan पर एक deposit() कॉल का विवरण — 0 Ether, ETH resourceID के साथ 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 फ़ंक्शन थे:

QBridge कॉन्ट्रैक्ट कोड जो दिखा रहा है कि deposit() और depositETH() दोनों एक ही Deposit इवेंट emit करते हैं

  • depositETH() — ETH के लिए सही पथ, msg.value > 0 आवश्यक था
  • deposit() — ERC-20 टोकन के लिए डिज़ाइन किया गया, ETH की आवश्यकता नहीं

दोनों बिल्कुल एक ही Deposit इवेंट प्रकार emit करते थे। रिलेयर Deposit इवेंट को सुनता था और यह भेद नहीं कर सकता था कि कौन से फ़ंक्शन ने उन्हें ट्रिगर किया।


चरण 5 — हैंडलर कोड और व्हाइटलिस्ट

Section titled “चरण 5 — हैंडलर कोड और व्हाइटलिस्ट”

QBridgeHandler कोड जो deposit() फ़ंक्शन दिखा रहा है, लाइन 128 पर व्हाइटलिस्ट जाँच और लाइन 135 पर safeTransferFrom

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 से मैप किया गया है

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 किया।

0x Protocol EOA ट्रिक 2019 में प्रलेखित

यह बिल्कुल यही ट्रिक 2019 में 0x Protocol सुरक्षा अपडेट में सार्वजनिक रूप से प्रलेखित की गई थी। Mound Inc. ने 2022 में इसे ध्यान में रखे बिना अपना ब्रिज डिप्लॉय किया।


चरण 8 — Borrow फ़ंक्शन सही था, लेकिन बहुत देर हो चुकी थी

Section titled “चरण 8 — Borrow फ़ंक्शन सही था, लेकिन बहुत देर हो चुकी थी”

Qore borrow() फ़ंक्शन — Compound-शैली, सही borrowAllowed जाँच

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 — निर्णायक सबूत: हमले से पहले पैरामीटर परिवर्तन”

यह वह विवरण है जिसे कभी समझाया नहीं गया।

Ethereum ट्रांज़ैक्शन जो दिखा रहा है कि 1 दिसंबर 2021 को deposit() फ़ंक्शन के माध्यम से WETH जमा किया जा रहा था — resourceID बदलने से पहले

हमले से पहले, किसी ने 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 के कॉन्ट्रैक्ट पते से शून्य पते पर पुनः असाइन किया।

कोड पुष्टि कर रहा है कि resourceID पैरामीटर को owner फ़ंक्शन द्वारा बदला गया था

केवल कॉन्ट्रैक्ट का मालिक ही यह परिवर्तन कर सकता था। इसके लिए किसी टाइमलॉक की आवश्यकता नहीं थी। कोई घोषणा नहीं हुई। इस एकल अदृश्य पैरामीटर परिवर्तन ने ही एक काम करने वाले ब्रिज को शोषण योग्य बना दिया।


सारांश: सात एक साथ विफलताएँ

Section titled “सारांश: सात एक साथ विफलताएँ”
विफलताप्रभाव
deposit() और depositETH() एक ही इवेंट emit करते हैंरिलेयर वास्तविक और नकली जमा के बीच अंतर नहीं कर सकता
EOA साइलेंट-सक्सेस का ध्यान नहीं रखा गयाशून्य पते पर safeTransferFrom बिना ट्रांसफर के पास हो जाता है
शून्य पता व्हाइटलिस्ट मेंEOA कॉल व्हाइटलिस्ट जाँच पार कर जाता है
बिना ऑडिट के प्रोडक्शन डिप्लॉयमेंटकिसी बाहरी समीक्षक ने उपरोक्त में से कुछ भी नहीं पकड़ा
Owner फ़ंक्शन पर कोई टाइमलॉक नहींresourceID चुपचाप, तुरंत, अदृश्य रूप से पुनः मैप किया गया
हमले से पहले resourceID पुनः मैप किया गयावह परिवर्तन जिसने शोषण को संभव बनाया
अंधा रिलेयर इवेंट पर भरोसा करता हैऑफ-चेन सेवा ऑन-चेन मूल्य सत्यापित किए बिना टोकन मिंट करती है


निर्णायक सबूत का ट्रांज़ैक्शन — 13 दिसंबर, 2021 को setResource() कॉल किया गया

Section titled “निर्णायक सबूत का ट्रांज़ैक्शन — 13 दिसंबर, 2021 को setResource() कॉल किया गया”

यह सबसे महत्वपूर्ण साक्ष्य है।

640_7.jpg — 13 दिसंबर 2021 को setResource() owner कॉल जिसने ETH resourceID को शून्य पते पर पुनः मैप किया

ट्रांज़ैक्शन: 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66

फ़ील्डमूल्य
स्थिति✅ सफल
ब्लॉक13797391
टाइमस्टैम्पDec-13-2021 02:31:21 PM UTC
प्रेषक0xbee3971293374d0b4db7bf1654936951e5bdfe5a6
प्राप्तकर्ता0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 (QBridge, Ethereum)
मूल्य0 ETH
फ़ंक्शनsetResource(address handlerAddress, bytes32 resourceID, address tokenAddress)

डिकोड किए गए इनपुट आर्गुमेंट:

#पैरामीटरमूल्य
0handlerAddress0x17B7163cf1Dbb6286262ddc68b553D899B893f526
1resourceID0x0000000000000000000000002f422fe9ea622049d6e73f81a906b9b8cff03b7f01
2tokenAddress0x0000000000000000000000000000000000000000

setResource() एक onlyOwner फ़ंक्शन है। केवल Mound Inc. टीम ही इसे कॉल कर सकती थी।

13 दिसंबर, 2021 को — हैक से 45 दिन पहले — कॉन्ट्रैक्ट के मालिक ने जानबूझकर ETH resourceID के लिए tokenAddress मैपिंग को शून्य पते में बदल दिया। इस कॉल से पहले, वही resourceID WETH टोकन कॉन्ट्रैक्ट की ओर इंगित करता था। इस कॉल के बाद, यह कहीं नहीं इंगित करता था — जिससे EOA साइलेंट-सक्सेस एक्सप्लॉइट संभव हो गया।

पूर्व voteProposal कोड जो दिखाता है कि केवल रिलेयर ही इसे कॉल कर सकते हैं:

640_1.jpg — voteProposal फ़ंक्शन: onlyRelayers मॉडिफायर हाइलाइट किया गया

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() कोड जिसने सब कुछ बदल दिया:

640_7.jpg — setResource() onlyOwner फ़ंक्शन

function setResource(address handlerAddress, bytes32 resourceID, address tokenAddress) external onlyOwner {
resourceIDToHandlerAddress[resourceID] = handlerAddress;
IQBridgeHandler(handlerAddress).setResource(resourceID, tokenAddress);
}

qXETH मिंटिंग अनुक्रम (BSC टोकन ट्रांसफर)

Section titled “qXETH मिंटिंग अनुक्रम (BSC टोकन ट्रांसफर)”

640_8.jpg — QubitFin Exploiter को अनेक राउंड में null एड्रेस से qXETH मिंट होकर प्राप्त हो रहा है

हमलावर को कई राउंड में null एड्रेस से सीधे qXETH मिंट होकर प्राप्त हुए:

ट्रांज़ैक्शन हैशराशिटोकनस्रोत
0xd8bba15555...999qXETHNull एड्रेस → Exploiter
0xf6008ab482...499qXETHNull एड्रेस → Exploiter
0xcfa4379af6...140ETH (BEP-20)0xb4b778… → Exploiter
0x61ca8bc28f...190qXETHNull एड्रेस → Exploiter
0x881a68c9c9...0.1qXETHNull एड्रेस → Exploiter
0x8c5877d1b6...0.1qXETHNull एड्रेस → Exploiter

हर “Null एड्रेस → Exploiter” qXETH मिंट Ethereum पर एक नकली deposit() कॉल से मेल खाता है।