
CH55x माइक्रोप्रोसेसरों के लिए फ़र्मवेयर एक्सट्रैक्टर
CH55x फर्मवेयर एक्सट्रैक्टर का उपयोग CH55x इंटीग्रेटेड सर्किट से फर्मवेयर पढ़ने के लिए किया जाता है। इन उपकरणों में बूटलोडर के माध्यम से सीधे फर्मवेयर पढ़ने की अंतर्निहित सुविधा नहीं होती है। हालाँकि, प्रदान किए गए डेटा के विरुद्ध फर्मवेयर सामग्री की एक बार में 8 बाइट्स सत्यापित करने की कार्यक्षमता मौजूद है। यह कार्यक्षमता एक पाठ्यपुस्तकीय टाइमिंग अटैक (timing attack) के लिए संवेदनशील है, और यही यहाँ इन उपकरणों से फर्मवेयर निकालने के लिए शोषित की गई है। बूटलोडर को USB या UART के माध्यम से एक्सेस किया जा सकता है। यह फर्मवेयर एक्सट्रैक्टर केवल UART पर काम करता है, क्योंकि USB की उच्च विलंबता (latency) इस हमले को कठिन बना देगी। परीक्षण किए गए चिप्स CH552 और CH554 हैं, तथा बूटलोडर संस्करण 2.4 और 2.5 हैं। फर्मवेयर एक्सट्रैक्टर का हार्डवेयर समाधान STM32 Blue Pill पर आधारित है, क्योंकि ये आसानी से उपलब्ध, सस्ते और आवश्यक प्रदर्शन वाले होते हैं।
बूटलोडर को पहले डिवाइस से पढ़कर निकाला जा चुका है और इसके संचार प्रोटोकॉल को रिवर्स इंजीनियर किया गया है। नीचे लगभग सही verify कमांड और बूटलोडर में उपयोग किया जाने वाला verify फ़ंक्शन दिया गया है। कुछ सुरक्षा जाँच (guards) मौजूद हैं, जिनके लिए आवश्यक है कि लंबाई 8 की गुणज हो, पता (address) 8 बाइट की सीमा पर संरेखित हो, पता 0x3800 से कम हो, और पहले कोई verify त्रुटि न हुई हो। अंतिम जाँच का अर्थ है कि प्रत्येक असफल सत्यापन के बाद CH55x को पुनः आरंभ (restart) करना आवश्यक है।
हम देख सकते हैं कि सत्यापन फ़ंक्शन तुरंत लौट आता है जब सत्यापन का एक बाइट विफल हो जाता है। इसका मतलब है कि जितने अधिक बाइट सही होंगे, सत्यापन फ़ंक्शन उतना ही अधिक समय लेगा। यह टाइमिंग अटैक का एक पाठ्यपुस्तकीय उदाहरण है जिसका शोषण किया जा सकता है।
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;
}
परीक्षण और त्रुटि के माध्यम से हमने पाया कि प्रत्येक सही बाइट verify फ़ंक्शन के निष्पादन समय को लगभग 4.2 µs बढ़ा देता है। बूटलोडर द्वारा UART संचार के लिए उपयोग की जाने वाली बॉड दर 57600 है (चाहे आप कहीं और कुछ भी पढ़ें), जिसका अर्थ है कि एक बिट के संचरण में लगभग 17.4 µs लगते हैं। इन दोनों समयों के बीच का संबंध महत्वपूर्ण है, क्योंकि ऐसा प्रतीत होता है कि उत्तर भी लगभग ~17 µs के जिटर (jitter) के साथ भेजा जाता है। हम तब ऐसे प्रतिक्रियाओं के बीच अंतर करने का प्रयास कर रहे हैं जिनके प्रतिक्रिया समय में verify फ़ंक्शन के कारण 4.2 µs का अंतर होता है, लेकिन UART जिटर (क्लॉक टाइमिंग) के कारण 17 µs तक का अंतर हो सकता है। यह एक कठिन कार्य प्रतीत होता है, लेकिन सांख्यिकीय माध्यमों से इसे करना संभव है।
हम यह निर्धारित कर सकते हैं कि कोई बाइट सही था या नहीं, उसे कई बार सत्यापित करने और परिणामों को दर्ज करने द्वारा। मान लीजिए, उदाहरण के लिए, यदि पहला बाइट गलत है तो उत्तर प्राप्त करने का सबसे कम संभव समय 30 µs है। फिर UART जिटर के साथ, एक अमान्य पहले बाइट के लिए हम अधिकतम उत्तर समय 30 µs + 17.4 µs = 47.4 µs की उम्मीद कर सकते हैं। साथ ही, एक मान्य पहला बाइट और अमान्य दूसरा बाइट इस "सीमा" को 34.2 µs से 51.6 µs तक बढ़ा देगा। कुछ मार्जिन जोड़ने पर, हम यह निष्कर्ष निकाल सकते हैं कि यदि उत्तर समय ~33 µs से कम है तो पहला अक्षर अमान्य था। इसी प्रकार, हम कह सकते हैं कि यदि उत्तर समय 48 µs से अधिक था तो पहला अक्षर मान्य था। यह उस टाइमिंग अटैक का आधार है जिसका उपयोग फर्मवेयर निकालने के लिए किया जाता है।
यह फर्मवेयर निकालने के लिए पूर्णतः स्वचालित उपकरण नहीं है - स्रोत कोड में संशोधन और पुनर्संकलन (recompilation) आवश्यक होगा (VS Code के साथ PlatformIO का उपयोग करके)। इसका मुख्य कारण यह है कि प्रत्येक बाइट की सटीक टाइमिंग विशेषताएँ विभिन्न सेटअपों के बीच भिन्न होती हैं और उन्हें समायोजित (tune) करने की आवश्यकता होगी। ट्यूनिंग प्रक्रिया को स्वचालित किया जा सकता था, लेकिन यह इस परियोजना का लक्ष्य नहीं था। मुख्य ट्यूनिंग prober_limits वेरिएबल के माध्यम से की जाती है। उदाहरण के लिए, prober_limits[0] में सत्यापित किए जाने वाले 8 बाइट्स में से बाइट 0 के लिए सीमाएँ होती हैं। यदि प्रतिक्रिया समय .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 पर रखा जा सकता है। इसका अर्थ है कि प्रोबर पहला सही बाइट खोजने में विफल रहेगा, लेकिन प्रगति होस्ट PC 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 बाइट सीमाओं को अलग-अलग ट्यून करना होगा, लेकिन एक बार ऐसा कर लेने पर, यह पूरी मेमोरी के लिए काम करेगा। जब 8 सही बाइट मिल जाते हैं, तो यह उन्हें ihex प्रारूप में मुद्रित करेगा:
:0800000002002932ffffffff9f
UART आउटपुट को एक टेक्स्ट फ़ाइल में लॉग करने से ^: के लिए grep करके संपूर्ण मेमोरी सामग्री को ihex प्रारूप में निकाला जा सकता है।
फर्मवेयर निकालने के लिए एक उदाहरण सर्किट नीचे दिखाया गया है। दो ट्रांज़िस्टर Blue Pill से CH55x की बिजली बंद करना संभव बनाते हैं। यह केवल सॉफ्टवेयर रीसेट का उपयोग करने की तुलना में अनुशंसित है, क्योंकि सॉफ्टवेयर रीसेट केवल तब काम करता है जब CH55x बूटलोडर मोड में हो (और बूटलोडर में एक टाइमआउट होता है जिसके बाद यह एप्लिकेशन कोड शुरू कर देता है)। CH55x की ओर जाने वाले UART पर रेसिस्टर्स इसलिए शामिल किए गए हैं क्योंकि मुझे संदेह है कि CH55x पर आंतरिक पुल-अप (pull-ups) बिजली बंद होने पर भी UART से उसमें बिजली वापस पहुँचा सकते हैं। V33 और P3.6 के बीच 10k रेसिस्टर CH55x को बूटलोडर मोड में डालने के लिए आवश्यक है। Blue Pill पर, PA11 को UART RX पिन से जोड़ा गया है ताकि टाइमर 1 का उपयोग करके प्रतिक्रिया का समय निर्धारित किया जा सके।

इस उपकरण को ट्यून करके और उपयोग करके, CH55X उपकरणों का फर्मवेयर निकाला जा सकता है। निष्कर्षण प्रक्रिया तेज़ नहीं है, लेकिन यह 14 kb को एक या दो दिनों में निकाल लेगी। यह कुछ हद तक ट्यूनिंग और इस बात पर निर्भर करता है कि उपयोग की गई आवृत्ति तालिका वास्तविक फर्मवेयर असेंबलर से कितनी मेल खाती है। दुर्भाग्य से, स्रोत कोड थोड़ा अव्यवस्थित है। मैंने यह उपकरण इसलिए बनाया क्योंकि मुझे CH554 डिवाइस से फर्मवेयर की आवश्यकता थी, और अब जब मैंने उसे प्राप्त कर लिया है, तो उपकरण पर ही काम करने का कोई वास्तविक कारण नहीं है। फिर भी, यह उपयोगी साबित होना चाहिए यदि किसी को CH55x उपकरणों से फर्मवेयर निकालने की आवश्यकता हो।