تحليل تقني لاستغلال QBridge
هذا تحليل تقني شامل للاستغلال بناءً على الأدلة المسجلة على السلسلة، والكود المصدري للعقود، ولقطات الشاشة التي تم التقاطها وقت الهجوم. التحليل الأصلي باللغة الصينية من مجتمع الضحايا؛ تُرجم ووُسّع هنا مع جميع البيانات المستخرجة.
العناوين الرئيسية
Section titled “العناوين الرئيسية”| الدور | العنوان |
|---|---|
| المهاجم | 0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7 |
| عقد QBridge (Ethereum) | 0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6 |
| معالج QBridge (BSC) | 0x4d8ae68fcae98bf93299548545933c0d273ba23a |
| معرّف مورد ETH | 0x0000000000000000000000002f422fe9ea622049d6f73f81a906b9b8cff03b7f01 |
الخطوة 1 — لم تكن BSC مسرح الجريمة الأول
Section titled “الخطوة 1 — لم تكن BSC مسرح الجريمة الأول”عندما فحص المحققون عنوان المهاجم على BSC لأول مرة، كان هناك شيء خاطئ على الفور:

انتقل المهاجم مباشرة إلى borrow() — بدون تحضير، بدون قرض فوري، بدون نشر عقد. هذا يعني أن BSC لم تكن نقطة بداية الهجوم. كان المهاجم يمتلك بالفعل رموز qXETH قبل أن يلمس بروتوكول الإقراض.
تتبع تلك الرموز qXETH كشف أنها سُكّت بواسطة مُرحّل الجسر — مما يعني أن المصدر كان Ethereum.
الخطوة 2 — معاملة voteProposal على BSC
Section titled “الخطوة 2 — معاملة voteProposal على BSC”إحدى معاملات سك qXETH:
المعاملة: 0x8c5877d1b618f29f6a3622cb610ace08ca96e04d8218f587072a3f91e8545bdc

الملاحظات الرئيسية من هذه المعاملة:
- استدعت
voteProposal(uint8 chainID, uint64 depositNonce, bytes32 resourceID, bytes data)على عقد QBridge على BSC - حدثت 3 عمليات نقل رموز:
- العنوان الصفري →
0xa6b1bb...— 59,900 Cross-Chain xETH (سُكّت) 0xa6b1bb...→0xfd7a55...— 59,900 xETH (نُقلت عبر الجسر)- العنوان الصفري → QubitFin Exploiter — 59,900 qXETH (سُكّت كضمان)
- العنوان الصفري →
- القيمة: 0 BNB — لم يتم إيداع أي ETH حقيقي في أي مكان
دالة voteProposal كانت قابلة للاستدعاء فقط من قبل المُرحّل. استدعاها المُرحّل لأنه رأى حدث Deposit على Ethereum. ذلك الحدث كان مزيفاً.
الخطوة 3 — إيداعات المهاجم على Ethereum
Section titled “الخطوة 3 — إيداعات المهاجم على Ethereum”
أجرى المهاجم استدعاءات متعددة لـ deposit() على عقد QBridge على Ethereum (0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6):
| هاش المعاملة | الكتلة | القيمة |
|---|---|---|
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
- الطابع الزمني: 27 يناير 2022، الساعة 09:45:32 مساءً بالتوقيت العالمي
- من:
0xd01ae1a708614948b2b5e0b7ab5be6afa01325c7(المهاجم) - إلى:
0x20e5e35ba29dc3b540a1aee781d0814d5c77bce6(QBridge على Ethereum) - القيمة: 0 Ether ($0.00)
- الدالة المستدعاة:
deposit(uint8 destinationDomainID, bytes32 resourceID, bytes data) - سعر ETH وقت الهجوم: $2,425.83
الخطوة 4 — دالتان، حدث واحد
Section titled “الخطوة 4 — دالتان، حدث واحد”كان عقد QBridge يحتوي على دالتين للإيداع:

depositETH()— المسار الصحيح لـ ETH، يتطلبmsg.value > 0deposit()— مصمم لرموز ERC-20، لا يتطلب ETH
كلاهما أطلق نفس نوع حدث Deposit بالضبط. كان المُرحّل يستمع لأحداث 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}بالنسبة لـ resourceID الخاص بـ ETH، أعاد العقد tokenAddress = 0x0000000000000000000000000000000000000000 (العنوان الصفري).
الخطوة 6 — العنوان الصفري كان على القائمة البيضاء
Section titled “الخطوة 6 — العنوان الصفري كان على القائمة البيضاء”
استعلم المحققون من العقد: هل كان العنوان الصفري مدرجاً في القائمة البيضاء؟
نعم. كان كذلك.
كان هذا ضرورياً لأن depositETH() استخدمت أيضاً نفس آلية القائمة البيضاء مع العنوان الصفري كعنصر نائب لـ ETH. لكن هذا خلق أثراً جانبياً قاتلاً: العنوان الصفري اجتاز فحص القائمة البيضاء في deposit() أيضاً.

كان معرّف مورد ETH مربوطاً بـ 0x0000000000000000000000000000000000000000 — العنوان الصفري — كما تأكد من خلال استعلام resourceIDToTokenContractAddress.
الخطوة 7 — استدعاء EOA ينجح بصمت
Section titled “الخطوة 7 — استدعاء EOA ينجح بصمت”مع tokenAddress = 0x0000...0000، نفّذ المعالج:
ERC20Interface(tokenAddress).safeTransferFrom(depositer, address(this), amount);العنوان الصفري هو EOA — حساب مملوك خارجياً بدون كود عقد.
في EVM: استدعاء أي دالة على عنوان بدون كود عقد ينجح بصمت — بدون تراجع، بدون خطأ، بدون تنفيذ فعلي.
استدعاء safeTransferFrom “نجح.” لم يتحرك شيء. لا ETH، لا رموز. لكن الكود استمر كما لو أن كل شيء على ما يرام — وأطلق حدث Deposit شرعياً.

هذه الخدعة بالضبط تم توثيقها علنياً في تحديث أمني لبروتوكول 0x في 2019. نشرت Mound Inc. جسرها في 2022 دون مراعاة ذلك.
الخطوة 8 — دالة الاقتراض كانت صحيحة، لكن بعد فوات الأوان
Section titled “الخطوة 8 — دالة الاقتراض كانت صحيحة، لكن بعد فوات الأوان”
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 في ذلك الوقت)
- الدالة:
deposit()مع نفسresourceID
في ديسمبر 2021، نقلت deposit() مع ذلك resourceID WETH حقيقي — لأن resourceID كان مربوطاً بعنوان عقد WETH الحقيقي.
في وقت ما بين ديسمبر 2021 و28 يناير 2022 — تم استدعاء دالة مقصورة على المالك لإعادة تعيين ربط resourceID من عنوان عقد WETH إلى العنوان الصفري.

فقط مالك العقد يمكنه إجراء هذا التغيير. لم يتطلب قفلاً زمنياً. لم يكن هناك إعلان. هذا التغيير الواحد غير المرئي في المعاملات هو ما حوّل جسراً عاملاً إلى جسر قابل للاستغلال.
الملخص: سبعة إخفاقات متزامنة
Section titled “الملخص: سبعة إخفاقات متزامنة”| الإخفاق | الأثر |
|---|---|
deposit() و depositETH() يطلقان نفس الحدث | المُرحّل لا يستطيع التمييز بين الإيداعات الحقيقية والمزيفة |
| عدم مراعاة النجاح الصامت لـ EOA | safeTransferFrom على العنوان الصفري تمر بدون نقل |
| العنوان الصفري مدرج في القائمة البيضاء | استدعاء EOA يتجاوز فحص القائمة البيضاء |
| نشر إنتاجي بدون تدقيق | لم يكتشف أي مراجع خارجي أياً مما سبق |
| لا قفل زمني على دوال المالك | resourceID أُعيد تعيينه بصمت وفوراً وبشكل غير مرئي |
resourceID أُعيد تعيينه قبل الهجوم | التغيير الذي جعل الاستغلال ممكناً |
| مُرحّل أعمى يثق بالأحداث | خدمة خارج السلسلة تسك رموزاً دون التحقق من القيمة على السلسلة |
المراجع الخارجية
Section titled “المراجع الخارجية”- معاملة الهجوم (BSC):
0x8c5877d1... - المهاجم على Etherscan:
0xd01ae1... - QBridge (Ethereum):
0x20e5e3... - خدعة العنوان الصفري لـ EOA (بروتوكول 0x، 2019): blog.0xproject.com
- التحليل الأصلي بالصينية: qbt.wiki
معاملة الدليل الدامغ — استدعاء setResource() في 13 ديسمبر 2021
Section titled “معاملة الدليل الدامغ — استدعاء setResource() في 13 ديسمبر 2021”هذا هو الدليل الأكثر أهمية.

المعاملة: 0xe3da555d506638bd7b697c0bdf7920be8defc9a175cd35bf72fb10bc77167b66
| الحقل | القيمة |
|---|---|
| الحالة | ✅ ناجحة |
| الكتلة | 13797391 |
| الطابع الزمني | 13 ديسمبر 2021، الساعة 02:31:21 مساءً بالتوقيت العالمي |
| من | 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 يوماً من الاختراق — غيّر مالك العقد عمداً ربط tokenAddress لمعرّف مورد ETH إلى العنوان الصفري. قبل هذا الاستدعاء، كان نفس معرّف المورد يشير إلى عقد رمز 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)”
استلم المهاجم جولات متعددة من qXETH مسكوكة مباشرة من العنوان الصفري:
| هاش المعاملة | الكمية | الرمز | المصدر |
|---|---|---|---|
0xd8bba15555... | 999 | qXETH | العنوان الصفري → المستغل |
0xf6008ab482... | 499 | qXETH | العنوان الصفري → المستغل |
0xcfa4379af6... | 140 | ETH (BEP-20) | 0xb4b778… → المستغل |
0x61ca8bc28f... | 190 | qXETH | العنوان الصفري → المستغل |
0x881a68c9c9... | 0.1 | qXETH | العنوان الصفري → المستغل |
0x8c5877d1b6... | 0.1 | qXETH | العنوان الصفري → المستغل |
كل عملية سك qXETH من “العنوان الصفري → المستغل” تقابل استدعاء deposit() مزيف على Ethereum.