كشف ثغرة في محفظة Coldcard: سرقة 1,719 بيتكوين في هجوم عام 2026

ملخص سوق AI
أدّى عيب مُبلّغ عنه في إنتروبيا البرنامج الثابت لـ Coldcard إلى تمكين استعادة العبارات البذرية عبر هجوم تخمين بالقوة الغاشمة دون اتصال بالإنترنت لأجهزة ذات إعدادات افتراضية، ما أسفر عن سرقة مؤكدة لما لا يقل عن 1,719 BTC عبر آلاف العناوين. وتُعدّ الحادثة مهمة لأنها تعيد تأطير مخاطر "التخزين البارد" حول جودة توليد المفاتيح بدلًا من اختراق الشبكة، ما قد يفرض ضغوطًا على الثقة في المحافظ المادية، ويدفع إلى تحديثات عاجلة للبرنامج الثابت، ويزيد من تحركات الأموال المدفوعة بالأمن على المدى القريب ومن التدقيق عبر ممارسات حفظ BTC.
مستوى التأثير
● عالي
الأصول المتأثرة
BTC/USDT-1.62%
رؤية AI · BTC/USDTرؤية AI
▼ هابط
تداول الآن
⚠️ الرؤى التي يُنشئها AI مبنية على محتوى الأخبار، وتُقدَّم لأغراض معلوماتية فقط. لا تُشكّل نصيحة استثمارية، ولا تعبّر عن آراء BingX. ينطوي الاستثمار على مخاطر. يُرجى التداول بمسؤولية.
المؤلفان: Johan وLisa | تحرير: 77 في 30 يوليو 2026، بدأت عناوين متعددة على البلوكتشين بتحويل أرصدتها على نحو متتابع ومنتظم. خلال 41 دقيقة فقط، جرى تفريغ 1,196 عنواناً من نوع توقيع أحادي، ما أدى لاختفاء نحو 1,082 بيتكوين. لم تكن هذه سوى الموجة الأولى. وبحلول أوائل أغسطس، ارتفعت الخسائر المؤكدة إلى ما لا يقل عن 1,719 بيتكوين، أي ما يعادل قرابة 111 مليون دولار، عبر أكثر من 5,200 عنوان، فيما توزّع الهجوم على ثلاث إلى أربع موجات. اللافت أن غالبية الأرصدة المستهدفة كانت محفوظة في محافظ باردة ولم تُحرَّك منذ أشهر أو سنوات، ما يشير إلى أن المشكلة لم تكن اختراقاً تشغيلياً بل خللاً على مستوى المفاتيح الخاصة. التحقيقات خلصت إلى أن جميع المفاتيح الخاصة وُلِّدت باستخدام محافظ Coldcard العتادية، من دون إضافة "dice entropy" ومن دون تفعيل عبارة مرور BIP39. أي أن الضحايا اعتمدوا الإعدادات الافتراضية الأبسط. تم تقييم نطاق التأثير بحسب خطّ البرامج الثابتة (Firmware). في طرازَي Mk2 وMk3 مع الإصدارات 4.0.1 إلى 4.1.9، انخفضت العشوائية الفعلية إلى نحو 40 بت، وهي الحالة الأكثر خطورة. وفي طرازات Mk4 وMk5 وQ ضمن إصدارات محددة، قُدِّرت العشوائية بنحو 72 بت، وهو مستوى لا يزال ضمن نطاق هجمات التخمين القسري دون اتصال (offline bruteforce). شركة Coinkite تحركت سريعاً: في 30 يوليو أصدرت تحذيراً، وفي اليوم التالي طرحت تحديثاً مصححاً. التوصيات كانت: تحديث Mk2 وMk3 إلى 4.2.0 أو أعلى، وMk4 وMk5 إلى 5.6.0 أو أعلى، وQ إلى 1.5.0Q أو أعلى. كما أعلنت إتلاف كامل المخزون من الإصدارات المعرضة للخطر وتعليق الشحن. بحسب التحليل، لا يرتبط الهجوم باختراق عن بُعد أو بتسميم سلسلة التوريد. المهاجم لم يحتج إلى لمس الأجهزة. ما حدث هو استخراج المفاتيح الخاصة عبر تخمين قسري دون اتصال، من خلال اختبار كل "seed" محتمل ضمن مساحة البحث. الثغرة ظلت كامنة لسنوات إلى أن طوّر باحثون خارجيون أداة تخمين كشفت قابلية الاستغلال. لاحقاً أعاد Muan إنتاج سلسلة الهجوم كاملة. يعيد هذا التقرير بناء السبب التقني انطلاقاً من كود البرامج الثابتة، باستخدام Mk3 بالإصدار 4.1.9 مثالاً، لتحديد سطر واحد فتح الباب أمام تفريغ جماعي لآلاف الأجهزة. جذر المشكلة صُممت Coldcard لتوليد "seed" عبر مولّد أرقام عشوائية حقيقي مدمج (TRNG) في شريحة STM32L475. لكن خطأين في البرنامج الثابت تسببا في تحويل التوليد إلى مولّد عشوائي برمجي (PRNG) بحالة داخلية شبه متطابقة عبر الأجهزة عالمياً. هذا المولّد يُدعى Yasmarang، وتسبب في خفض العشوائية الفعلية على Mk3 إلى قرابة 40 بت. الخطأ الأول: تعطيل RNG العتادي عمداً في إعدادات البناء، قامت Coldcard بتعطيل دعم RNG العتادي في MicroPython داخل mpconfigboard.h: // stm32/COLDCARD/mpconfigboard.h // The team has implemented their own version of ckcc.rng_bytes, so disable MicroPython's builtin version #define MICROPY_HW_ENABLE_RNG (0) قد يبدو ذلك بلا ضرر للوهلة الأولى لأن Coinkite تملك ckcc.rng_bytes الذي يستدعي TRNG مباشرة. لكن المشكلة تظهر عند توسعة الماكرو: دالة my_random_bytes() في libngu تقرأ العشوائية عبر CHIP_TRNG_32() الذي ينتهي باستدعاء rng_get() في طبقة MicroPython الخاصة بـ STM32 على أنه TRNG. عند تعطيل MICROPY_HW_ENABLE_RNG، تتدهور rng_get() بصمت إلى مسار بديل برمجي، ما يقود للخطأ الثاني. الخطأ الثاني: التعامل مع المسار البديل لـ rng_get() وكأنه TRNG بتتبع توسعات الماكرو في libngu (random.c) وصولاً إلى ports/stm32/rng.c، يظهر أن rng_get() تُترجم وفق قيمة MICROPY_HW_ENABLE_RNG. إن كانت غير صفرية تقرأ من RNG>DR (TRNG العتادي). وإن كانت صفراً تُعيد ناتج PRNG برمجي: // external/libngu/ngu/random.c #ifdef MICROPY_PY_STM // ports/stm32/rng.c extern uint32_t rng_get(void); # define CHIP_TRNG_SETUP() # define CHIP_TRNG_32() rng_get() #endif ... void my_random_bytes(uint8_t *dest, uint32_t count) { uint32_t chip = CHIP_TRNG_32(); // Assumes reading from hardware TRNG, but may actually receive Yasmarang output if (chip == last) ... chip ^= my_yasmarang(); ... } ومع إعداد Coldcard الذي يحدد MICROPY_HW_ENABLE_RNG بقيمة 0، تسير كل أجهزة Mk3 الخارجة من المصنع في المسار البرمجي: // external/micropython/ports/stm32/rng.c #if MICROPY_HW_ENABLE_RNG uint32_t rng_get(void) { ... read RNG>DR hardware TRNG ... } #else static uint32_t pyb_rng_yasmarang(void) { static bool seeded = false; static uint32_t pad = 0, n = 0, d = 0; static uint8_t dat = 0; if (!seeded) { seeded = true; rtc_init_finalise(); pad = *(uint32_t *)MP_HAL_UNIQUE_ID_ADDRESS ^ SysTick>VAL; n = RTC>TR; d = RTC>SSR; } ... } #endif عملياً، "MP_HAL_UNIQUE_ID_ADDRESS" يزود جزءاً محدوداً من العشوائية (نحو 14 بت وفق تحليل مساحة المعرّفات). "SysTick>VAL" يتراوح بين 0 و79,999 نتيجة تردد 80 MHz وقيمة إعادة تحميل 80,000، أي نحو 17 بت. سجلا RTC>TR وRTC>SSR يُقرآن فور التشغيل قبل تهيئتهما، ومساحة قيمهما صغيرة جداً؛ وفي جميع عينات السلسلة التي جرى التحقق منها كانت القيمتان صفراً. كما تضيف ضغطات الأزرار قرابة 5 بت. المحصلة: مصدر عشوائية حقيقي بحدود 36 إلى 37 بت لعملية توليد "seed"، وهو ما يتسق مع تقدير Coinkite "نحو 40 بت"، ويجعل المساحة قابلة للتخمين بواسطة عنقود GPU خلال أيام. كيف نُفّذ الهجوم في الإقلاع الأول لـ Mk3، تُستهلك "العشوائية" وفق ثلاثة أنماط شائعة للاستهلاك. المهمة الأساسية للمهاجم هي نمذجة كل نمط بدقة لإعادة بناء كل خطوة من PRNG. المرحلة الأولى: الحالة الابتدائية فور تشغيل الجهاز تحدث عمليتان: Power on └── libngu static Yasmarang (ngu/random.c) pad=0x0a8ce26f n=69 d=233 dat=0 └── rng_get() initial call seeding (ports/stm32/rng.c) pad=UID^SysTick n=RTC_TR=0 d=RTC_SSR=0 dat=0 بهذا، تُضغط "العشوائية" داخل pad بحجم 32 بت مع أبعاد صغيرة قابلة للتعداد، ما يحصر حالة Mk3 ضمن صندوق بحث قابل للاستنفاد. المرحلة الثانية: تفاعلات الأزرار أثناء الإعداد الأول، يُجبر المستخدم على تعيين PIN، وتأكيد الشروط عبر OK، والتنقل بالقائمة. كل ضغطة مفتاح تستدعي _start_scan() في shared/mempad.py، والتي تستدعي _rand_below() ثلاث مرات عبر shuffle(self.scan_order). طول scan_order يساوي NUM_ROWS (=4). الدالة shuffle موجودة في shared/random.py، وrandbelow مربوطة مباشرة بدالة ngu.random.uniform (وهي _rand_below في C داخل libngu): # shared/random.py import ngu randbelow = ngu.random.uniform bytes = ngu.random.bytes def shuffle(lst): for i in reversed(range(1, len(lst))): j = randbelow(i + 1) lst[i], lst[j] = lst[j], lst[i] # shared/mempad.py # Each key press > _start_scan() > shuffle(self.scan_order) # scan_order length = NUM_ROWS = 4 بعد مراجعة كود v4.1.9، تبيّن وجود ثلاثة ملفات استهلاك: الملف A (إعداد المستخدم العادي لأول مرة): ضغطتان قبل قبول الشروط (kpad_a = 2). عند settings.save() يتم البحث عن خانة فارغة بين 32 خانة مع استثناء my_pos، ما يترك 31 احتمالاً وينتج 30 استدعاءً لـ _rand_below. ثم إدخال PIN والتنقل (kpad_b ضمن [4, 34])، وأخيراً random_bytes(32). kpad_a × shuffle(4) shuffle(31) kpad_b × shuffle(4) random_bytes(32) الملف B (أول إقلاع بعد التفليش أو المسح مع NVRAM فارغة): يبدأ nvstore بـ shuffle(32) (31 استدعاءً لـ _rand_below)، ثم يستهلك 3072 خطوة متزامنة عبر 3 خانات × 16 كتلة × 256 بايت من دون تغذية عكسية (يتقدم الوضع فقط). في سيناريو NVRAM الفارغة، تُنفذ shuffle(32) إضافية، ثم استهلاك ضغطات الأزرار (kpad_b ضمن [4, 34])، ثم my_random_bytes(32). nvstore shuffle(32) nvstore blanking 3×16×256B emptynvramonboarding profile: shuffle(32) Key press consumption (kpad_b enum [4, 34]) my_random_bytes(32) الملف C (Paper Wallet): بعد إنشاء محفظة قائمة، الدخول مجدداً إلى القائمة يتطلب 8 إلى 25 ضغطة. ثم تُستخدم my_random_bytes(32) مباشرة كمفتاح خاص، متجاوزة قائمة كلمات BIP39 بالكامل. المرحلة الثالثة: بناء "seed" واستخراج العنوان من random_bytes(32) إلى عنوان بيتكوين النهائي، يتبع Mk3 السلسلة التالية: raw_bytes = random_bytes(32) entropy = ngu.hash.sha256s(raw_bytes) mnemonic = BIP39(entropy, wordlist=english) seed = PBKDF2HMACSHA512(mnemonic, "mnemonic", 2048) master = HMACSHA512("Bitcoin seed", seed) child = m/{44,49,84}'/0'/0'/0/0 address = bech32(hash160(compressed_pubkey)) كل خطوة حتمية وقابلة لإعادة الإنتاج. إذا خمن المهاجم pad بشكل صحيح، يمكنه إعادة حساب السلسلة كاملة دون اتصال. المرحلة الرابعة: التخمين القسري على GPU والوصول للإصابات منهج المهاجم يتلخص في أربع خطوات: 1) بناء مجموعة pad المرشحة: أخذ إحداثيات X وY للرقاقة من 0 إلى 72، ودمجها مع مساحة UID التي تقارب 5,300 قيمة، ثم إجراء حاصل ديكارتي مع SysTick من 0 إلى 79,999. الناتج 424 مليون pad مرشح، ويمكن إنهاؤه خلال أيام على GPU. 2) تعداد عدد ضغطات الأزرار لكل pad، مع kpad_b بين 4 و34. وبما أن rtc_tr وrtc_ssr سجلا إصابات صفرية، وُضعا في الحلقة الأبعد (الأقل تأثيراً) ضمن عملية التعداد. 3) تشغيل سلسلة الاشتقاق بالكامل داخل نواة GPU: SHA256 وBIP39 وPBKDF2 لـ 2048 دورة وBIP32 وHash160 وBech32. تيار الثوابت من libngu يمكن حسابه مسبقاً دون اتصال، وتسترجع النواة الكلمات بالمؤشر لتجنب كلفة تحريك PRNG داخل كل خيط. 4) المطابقة باستخدام Bloom filter أو مصفوفة مرتبة لقيم hash160 بتعقيد O(log n)، مع استهداف مجموعة عناوين singlesignature P2WPKH على الشبكة. من حيث التكلفة، تشغيل مساحة الملف A على GPU من نوع Apple M1 — مع شبكة 72×72 و80,000 قيمة SysTick و31 عداداً لضغطات الأزرار — أنتج نحو 14.8 مليار مرشح واستغرق قرابة 8.6 أيام. عنقود A100 بمواصفات مراكز البيانات يمكنه ضغط الزمن إلى ساعات. أما مساحة ~72 بت على Mk4 وMk5 وQ فهي أكبر بمقدار 2^32 من Mk3، لكن أصل العلة واحد، وطريقة نمذجة أنماط الاستهلاك تبقى نفسها. الفارق أن أداة التخمين تنتقل من جهاز واحد إلى عنقود، ويرتفع الزمن من أيام إلى أسابيع، وهي كلفة ما زال مهاجمون مستعدون لتحملها، ما يفسر ظهور هذه الطرازات أيضاً ضمن قوائم الضحايا.