
8BitDo फर्मवेयर एन्क्रिप्शन के लिए रिवर्स-इंजीनियर किए गए दस्तावेज़ और टूल्स
8BitDo द्वारा कई GD32-आधारित उत्पादों के लिए उपयोग किए जाने वाले फर्मवेयर एन्क्रिप्शन के लिए रिवर्स-इंजीनियर की गई डॉक्यूमेंटेशन और टूलिंग।
इस दस्तावेज़ में वर्णित एल्गोरिदम को लागू करने वाले डिक्रिप्शन और री-एन्क्रिप्शन टूल के लिए 8cryptdo.py देखें।
यह टूल संभावित रूप से अनुकूलित फर्मवेयर बनाने की संभावना को सक्षम बनाता है। हालाँकि, इस रिपॉज़िटरी में कोई भी आधिकारिक फर्मवेयर डेटा शामिल नहीं है। नवीनतम फर्मवेयर फ़ाइलों को डाउनलोड करने के लिए एक स्क्रिप्ट fwupd/8bitdo-firmware रिपॉज़िटरी में पाई जा सकती है, जिसमें कुछ पुराने फर्मवेयर बाइनरी संग्रहीत हैं।
सतह पर एक फर्मवेयर .dat फ़ाइल को देखने पर, एक 28-बाइट प्लेनटेक्स्ट हेडर मौजूद होता है।
import struct
raw = open("sn30-v2_07.dat", "rb").read()
version, addr, payload_len = struct.unpack("<III", raw[:12])
# version = 207 -> firmware == v2.07
# addr = 0x08003400 -> destination (probably)
# payload_len = 99328 -> section payload length in bytes
हाल के फर्मवेयर फ़ाइलों में इनके बाद एक अज्ञात 32-बिट मान रखा गया है। हेडर का शेष भाग शून्य है।
कुछ फर्मवेयर फ़ाइलों पर एक पैटर्न दिखाई देता है: एक लेयर जो प्रत्येक शब्द को अगले में मिला देती है।
def rotr(x, r):
return ((x >> r) | (x << (32 - r))) & 0xFFFFFFFF
dechained = [words[0]]
for i in range(1, len(words)):
dechained.append(words[i] ^ rotr(words[i - 1], 3))
यह चेनिंग प्रत्येक 128-शब्द ब्लॉक सीमा पर रीसेट होती है। एक ब्लॉक के पहले शब्द (pos = 0) का कोई पूर्ववर्ती पद नहीं होता, और pos = 1..127 पिछले शब्द से चेन करते हैं।
एक कीलेस लेयर कोई सुरक्षा नहीं जोड़ती। इसे हटाने से एक साफ़ इंटरमीडिएट (dechained) प्रकट होता है, जहाँ प्लेनटेक्स्ट को छिपाने वाली एकमात्र चीज़ कीस्ट्रीम है।
P[i] = dechained[i] ^ keystream[i]
fwupd/8bitdo-firmware रिपॉज़िटरी में पुराने फर्मवेयर की एक सूची है। इन सभी फर्मवेयर में चेनिंग लेयर प्रतीत होती थी।
चेनिंग लेयर को हटाने के बाद उनमें से कुछ को एक साथ XOR करने पर शून्य की लंबी श्रृंखलाएँ और पठनीय संरचना प्रकट होती है।
plaintext_diff = [a ^ b for a, b in zip(dechained_a, dechained_b)]
यदि दोनों में समान कीस्ट्रीम का उपयोग किया जाता है...
a = dechained_a[i] ^ dechained_b[i]
b = (P_a[i] ^ keystream[i]) ^ (P_b[i] ^ keystream[i])
c = P_a[i] ^ P_b[i]
assert a == b == c
तो कीस्ट्रीम रद्द हो जाती है।
यह एक क्लासिक मेनी-टाइम पैड है। ऐसी कीस्ट्रीम केवल तभी सुरक्षित होती है जब एक बार उपयोग की जाए। किसी कारण से, 8BitDo ने न केवल प्रति-उत्पाद कीस्ट्रीम, बल्कि कई उत्पादों में एक ही कीस्ट्रीम का उपयोग करने का निर्णय लिया। इसने उनके सिफर की सुरक्षा को काफी कमजोर कर दिया और यहाँ बाकी विश्लेषण को संभव बनाया।
एक बार जब आप जान लेते हैं कि दो प्लेनटेक्स्ट एक साथ XOR किए गए हैं, तो आप एक का हिस्सा अनुमान लगा सकते हैं और दूसरे को पढ़ने के लिए अपने अनुमान को घटा सकते हैं। यह उस स्थान पर keystream को पुनर्प्राप्त करता है।
keystream[i] = dechained[i] ^ P[i]
for fw in all_fw:
P[fw][i] = dechained[fw][i] ^ keystream[i]
यह स्ट्रिंग्स के साथ सबसे आसान होता, लेकिन यहाँ सबसे उत्पादक तरकीब ARM निर्देशों का क्रॉस-वर्ज़न रीलोकेशन था। एक फ़ंक्शन दो फर्मवेयर वर्ज़न में स्थानांतरित स्थितियों पर प्रकट हो सकता है। जहाँ एक वर्ज़न में कीस्ट्रीम ज्ञात है, वहाँ आप साझा कोड को दूसरे में संरेखित कर सकते हैं और नए कीस्ट्रीम मान प्राप्त कर सकते हैं।
इसका फायदा उठाने से ज्ञात कीस्ट्रीम शब्द ~1,500 से कई हज़ार तक पहुँच गए, जो फर्मवेयर का लगभग 18% पठनीय है।
keystream के बड़े नमूने के साथ, पैटर्न दिखाई देने लगे। इसमें 16 शब्दों के अंतराल पर स्थितियाँ थीं जो लगभग स्थिर मात्रा में बढ़ती थीं, और प्रत्येक बाइट एक अनुमानित आकार से आगे बढ़ती थी।
परीक्षण और त्रुटि के माध्यम से, यह पता चला कि एक ब्लॉक में प्रत्येक कीस्ट्रीम शब्द एक सूत्र का पालन करता है:
STEP = 0x92A753FA
A_MUL = 0x80000301
ROT = 18
UINT32_MAX = 0xFFFFFFFF
def rotr(x, r):
r &= 31
return ((x >> r) | (x << (32 - r))) & UINT32_MAX if r else x
def block_base(block):
return (block * A_MUL + block // 2) & UINT32_MAX
def keystream(block, pos, block_key):
counter = (block_base(block) + pos * STEP) & UINT32_MAX
mask = rotr(block_key, ROT * pos)
return counter ^ mask
एक ब्लॉक के भीतर, एक साधारण काउंटर (block_base(block) + pos*STEP) चलाएँ, और इसे एक मास्क के साथ XOR करें जो block_key से शुरू होता है और प्रत्येक चरण में 18 बिट घूमता है। पूरा 128-शब्द ब्लॉक एक 32-बिट संख्या, block_key, द्वारा निर्धारित होता है।
एक ज्ञात शब्द पूरे ब्लॉक को अनलॉक कर देता है। सूत्र को पुनर्व्यवस्थित करने से किसी भी ज्ञात कीस्ट्रीम शब्द से block_key प्राप्त होता है:
def block_key_from_known(block, pos, known_keystream):
counter = (block_base(block) + pos * STEP) & UINT32_MAX
return rotr(known_keystream ^ counter, -ROT * pos & 31)
प्रत्येक ब्लॉक की कुंजी कहाँ से आई? यह 256-प्रविष्टि सीड टेबल से निकाले गए दो 16-बिट हिस्सों में विभाजित होती है:
def block_key_of(block, seed_table):
hi = seed_table[block]
lo = seed_table[block ^ 0xF9]
return (hi << 16) | lo
सीड टेबल स्वयं का कोई विशिष्ट सूत्र प्रतीत नहीं होता, यह संभवतः प्रत्येक डिवाइस के बूटलोडर में कहीं संग्रहीत 512 स्थिर बाइट्स है। हालाँकि, 0xF9 के साथ XOR एक सुंदर साइड इफेक्ट प्रदान करता है: एक मिरर लॉ। एक ब्लॉक और उसका साथी block ^ 0xF9 समान दो टेबल प्रविष्टियों से बने होते हैं जो आपस में बदली हुई हैं।
mirror = block_key_of(block ^ 0xF9, seed_table)
assert mirror == rotr(block_key_of(block, seed_table), 16)
इसने पहले अज्ञात क्षेत्रों को क्रैक कर दिया। पूर्ण 32-बिट block_key का अंधाधुंध अनुमान लगाने के बजाय, आप दो 16-बिट हिस्सों का अनुमान लगा सकते हैं और यह स्कोर कर सकते हैं कि कौन सा विकल्प दोनों ब्लॉकों को विश्वसनीय स्ट्रिंग्स या ARM निर्देशों में बदल देता है।
इसके साथ, इस विशिष्ट सिफर का उपयोग करने वाले सभी फर्मवेयर डेटा का 100% कवरेज प्राप्त किया गया।
यह सिफर लगभग 2020 के अधिकांश 8BitDo उत्पादों को कवर करता प्रतीत होता है, और वर्तमान उत्पाद जो अभी भी GD32 SoC का उपयोग करते हैं (SN30 Pro सहित)।
M30 और Zero 2 जैसे कुछ डिवाइस मल्टी-सेक्शन प्रतीत होते हैं, जिनमें कुछ फर्मवेयर डेटा इस सिफर का उपयोग करता है और अन्य फर्मवेयर एक अलग एन्क्रिप्शन योजना के साथ।
नए डिवाइस, कम से कम वे जिनमें एक अलग SoC है, एक मजबूत फर्मवेयर एन्क्रिप्शन योजना रखते प्रतीत होते हैं। कुछ सरल विश्लेषण से पता चलता है कि यह संभवतः साझा हो सकता है, लेकिन निश्चित नहीं है। इसकी और जाँच करने की आवश्यकता है।