
مستخرج البرامج الثابتة لمعالجات CH55x
يُستخدم مستخرج البرامج الثابتة لـ CH55x لقراءة البرامج الثابتة من الدوائر المتكاملة من سلسلة CH55x. لا يوجد دعم مدمج في الأجهزة لقراءة البرامج الثابتة مباشرة عبر أداة الإقلاع. ومع ذلك، توجد وظيفة للتحقق من محتويات البرامج الثابتة 8 بايتات في كل مرة مقابل بيانات مقدمة. هذه الوظيفة معرضة لهجوم توقيت قياسي (timing attack) وهو ما يتم استغلاله هنا للسماح باستخراج البرامج الثابتة من هذه الأجهزة. يمكن الوصول إلى أداة الإقلاع إما عبر USB أو UART. يعمل مستخرج البرامج الثابتة هذا فقط عبر UART، لأن زمن الانتقال الأعلى لـ USB سيجعل هذا الهجوم صعبًا. الرقائق التي تم اختبارها هي CH552 و CH554، وإصدار أداة الإقلاع 2.4 و 2.5. يعتمد الحل المادي لمستخرج البرامج الثابتة على STM32 Blue Pill، لأنها متوفرة بسهولة، رخيصة الثمن، ولديها الأداء اللازم.
تمت قراءة أداة الإقلاع مسبقًا من الجهاز وتم هندسة بروتوكول الاتصال الخاص بها عكسيًا. أدناه أمر التحقق التقريبي الصحيح ووظيفة التحقق المستخدمة في أداة الإقلاع. هناك بعض الضوابط الموضوعة، والتي تتطلب أن يكون الطول مضاعفًا للرقم 8، وأن تكون العنوان محاذيًا لحد 8 بايت، وأن يكون العنوان أقل من 0x3800 وألا توجد أخطاء تحقق سابقة. الضابط الأخير يعني أن إعادة تشغيل CH55x مطلوبة بعد كل عملية تحقق غير ناجحة.
يمكننا أن نرى أن وظيفة التحقق تعود فورًا عند فشل بايت واحد من التحقق. هذا يعني أنه كلما زادت البايتات الصحيحة، زاد الوقت الذي تستغرقه وظيفة التحقق. هذا هو المثال التقليدي لهجوم التوقيت الذي يمكن استغلاله.
unsigned char verifycmd[] = {
// 0x57, 0xab, // UART magic not included to verify function
0xa6, // Verify command
5 + len, // Constant 5 plus length of data to verify
0, // Unused
addr_low, // Low byte of address
addr_high, // High byte of address
0, 0, 0, // Unused
0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, // Data to verify against
checksum
}
unsigned char verify(unsignec char *cmdbuffer)
{
static char prev_verify_error;
unsigned char len = cmdbuffer[1]-5
unsigned short addr = cmdbuffer[3] + cmdbuffer[4] << 8;
if (len & 0x07 || addr & 0x07 || addr > 0x3800 || prev_verify_error) {
return 0xfe;
}
for (int i=0; i < len; i++) {
// Key can be set through bootloader, and CBYTE[] means code memory
if(key[i & 0x07] ^ cmdbuffer[8+i] ^ CBYTE[addr+i]) {
prev_verify_error = 1;
return 0xf5;
}
}
return 0;
}
من خلال التجربة والخطأ وجدنا أن كل بايت صحيح يزيد وقت تنفيذ وظيفة التحقق بحوالي 4.2 µs. معدل الباود المستخدم من قبل أداة الإقلاع لاتصال UART هو 57600 (بغض النظر عما تقرأه في مكان آخر)، مما يعني أن نقل بت واحد يستغرق حوالي 17.4 µs. العلاقة بين هذين الوقتين مهمة، لأنه يبدو أن الرد يُرسل مع تذبذب (jitter) يبلغ أيضًا حوالي 17 µs. نحن نحاول بعد ذلك التمييز بين الردود التي تختلف بمقدار 4.2 µs في وقت الاستجابة من وظيفة التحقق، ولكن قد تختلف بما يصل إلى 17 µs بسبب تذبذب UART (توقيت الساعة). يبدو أن هذه مهمة صعبة، لكن من الممكن القيام بها من خلال الوسائل الإحصائية.
يمكننا تحديد ما إذا كان البايت صحيحًا أم لا من خلال محاولة التحقق منه عدة مرات وتسجيل النتائج. لنفترض على سبيل المثال أن أقصر وقت ممكن لتلقي رد إذا كان البايت الأول خاطئًا هو 30 µs. ثم مع تذبذب UART يمكننا توقع وقت رد أقصى قدره 30 µs + 17.4 µs = 47.4 µs لبايت أول غير صالح. في نفس الوقت، بايت أول صالح وبايت ثانٍ غير صالح سيدفعان هذا "النطاق" إلى 34.2 µs إلى 51.6 µs. بإضافة بعض الهوامش، يمكننا استنتاج أن الحرف الأول كان غير صالح في حال كان وقت الرد أقل من ~33 µs. وبالمثل، يمكننا القول أن الحرف الأول كان صالحًا في حال كان وقت الرد أعلى من 48 µs. هذا هو أساس هجوم التوقيت المستخدم لاستخراج البرامج الثابتة.
هذه ليست أداة تلقائية بالكامل لاستخراج البرامج الثابتة - سيكون من الضروري تعديل الكود المصدري وإعادة الترجمة (باستخدام VS Code مع PlatformIO). السبب الرئيسي لذلك هو أن خصائص التوقيت الدقيقة لكل بايت تختلف بين الإعدادات وستحتاج إلى ضبط. كان من الممكن أتمتة عملية الضبط، لكن لم يكن هذا هو هدف هذا المشروع. يتم الضبط الرئيسي من خلال المتغير prober_limits. على سبيل المثال، يحتوي prober_limits[0] على الحدود للبايت 0 من بين 8 بايتات سيتم التحقق منها. في حال كان وقت الاستجابة أقل من .invalid_under_time، نعلم أن البايت كان غير صالح. في حال كان أعلى من .valid_over_time، نعلم أن البايت كان صحيحًا. هناك أيضًا .min_delta يسمح بالتقدم إلى البايت التالي قبل معرفة ما إذا كان البايت صحيحًا أم لا بشكل مؤكد.
struct ProberByteLimits prober_limits[8] = {
{
// Byte 0
.invalid_under_time = 33,
.valid_over_time = 50,
.min_delta = 30
}, // ...
للعثور على القيم المناسبة لاستخدامها، يُوصى بتعيين .invalid_under_time إلى 0، و .valid_over_time إلى مثلاً 100، ويمكن الاحتفاظ بـ .min_delta عند 30. هذا يعني أن الماسح الضوئي (prober) سيفشل في العثور على البايت الصحيح الأول، ولكن سيتم طباعة التقدم إلى UART للحاسوب المضيف. سترى شيئًا يشبه:
[0x0000]=0x01? min=31 max=47 tries=63
min=31 max=48 tries=127
min=31 max=48 tries=191
min=30 max=48 tries=255
min=30 max=48 tries=319
بشرط أن يكون الحرف الأول الذي تم اختباره غير صالح، يمكننا استخدام قيم min/max هذه مع إزاحة مقدارها 1 أو 2 لتحديد الحدود المناسبة لاستخدامها. على سبيل المثال أعلاه، يمكننا تعيين .invalid_under_time إلى 33 و .valid_over_time إلى 50. يجب أن يحاول الماسح الضوئي بعد ذلك قيمًا مختلفة حتى يجد قيمة صالحة، والتي ستبدو كما يلي:
...
[0x0000]=0x7d? min=31 max=39 tries= 7
[0x0000]=0x02? min=40 max=56 tries= 7
[0x0001]=0x01? min=35 max=52 tries=34
نرى أن المحاولة الأخيرة عند العنوان 0x0000 كان لها أقصى وقت استجابة 56 µs، مما يعني أن هذا هو البايت الصحيح. يتقدم الماسح الضوئي بعد ذلك إلى البايت التالي، ويواصل العملية. لاحظ أنه يجب ضبط جميع حدود البايتات الثمانية بشكل منفصل، ولكن بمجرد الانتهاء، ستعمل للذاكرة بأكملها. عند العثور على 8 بايتات صحيحة، ستطبعها بتنسيق ihex:
:0800000002002932ffffffff9f
يتيح تسجيل خرج UART إلى ملف نصي استخدام grep للبحث عن ^: لاستخراج محتوى الذاكرة الكامل بتنسيق ihex.
يظهر أدناه مثال لدائرة استخراج البرامج الثابتة. يجعل الترانزستوران من الممكن إيقاف تشغيل الطاقة عن CH55x من Blue Pill. يُوصى بذلك بدلاً من استخدام إعادة التعيين البرمجي فقط، لأن إعادة التعيين البرمجي تعمل فقط عندما يكون CH55x في وضع أداة الإقلاع (ولأداة الإقلاع مهلة زمنية بعدها تبدأ تشغيل كود التطبيق). تم تضمين المقاومات على UART إلى CH55x لأن لدي شكًا في أن مقاومات السحب الداخلية في CH55x قد تغذيها بالطاقة من UART حتى عند إيقاف تشغيل الطاقة. المقاومة 10k بين V33 و P3.6 مطلوبة لوضع CH55x في وضع أداة الإقلاع. على Blue Pill، يتم ربط PA11 بدبوس UART RX لتتمكن من توقيت الاستجابة باستخدام المؤقت 1.

من خلال ضبط هذه الأداة واستخدامها، يمكن استخراج البرامج الثابتة لأجهزة CH55X. عملية الاستخراج ليست سريعة، لكنها ستستخرج الـ 14 كيلوبايت في يوم أو يومين. يعتمد الأمر قليلاً على الضبط ومدى تطابق جدول التردد المستخدم مع المجمع الفعلي للبرامج الثابتة. لسوء الحظ، الكود المصدري فوضوي بعض الشيء. لقد صنعت هذه الأداة لأنني كنت بحاجة إلى البرامج الثابتة من جهاز CH554، والآن بعد أن حصلت عليها، لا يوجد سبب حقيقي للعمل على الأداة نفسها. ومع ذلك، يجب أن تثبت فائدتها في حال احتاج شخص ما إلى استخراج البرامج الثابتة من أجهزة CH55x.