भविष्य को आकार देने वाली तकनीक पर गहन लेख।

Switch 2 जेलब्रेक: Secure Boot तोड़ने वाली Exploit Chain

Switch 2 जेलब्रेक ने BASIC interpreter overflow, fs path bug और loader race को जोड़कर Secure Boot को पार किया। exploit chains और secure boot से क्या सीखें।

एक हैंडहेल्ड कंसोल, जिसे रिमोट-कंट्रोल हार्डवेयर से जोड़ा गया है, और वर्कबेंच पर एक रोबोटिक लीवर उसका पावर बटन दबा रहा है।
आधुनिक कंसोल hacking रिग KVM-over-IP को एक Bluetooth लीवर के साथ जोड़ते हैं, जो हर crash के बाद फिज़िकली पावर बटन दबा देता है।

एक नए कंसोल में hardened microkernel, हर return address पर pointer authentication, execute-only memory, control-flow integrity और signed boot chain होती है। दो हफ्ते बाद एक hobbyist filesystem service के रूप में arbitrary native code चला रहा है, यानी पूरी मशीन का सबसे भरोसेमंद process। पहले public Switch 2 jailbreak की यही कहानी है, और secure systems बनाने वाले हर इंजीनियर को इससे थोड़ी बेचैनी होनी चाहिए। वजह यह नहीं कि mitigations फेल हो गए, उनमें से ज़्यादातर बिल्कुल वैसे ही काम किए जैसे डिज़ाइन हुए थे। असल बात यह है कि exploit को उन्हें सीधे हराने की ज़रूरत ही नहीं पड़ी। उसने एक-एक logic bug के सहारे उनके आसपास से रास्ता निकाल लिया।

मैं कई सालों से infrastructure security के defensive पक्ष पर काम कर रहा हूँ, और यहाँ का पैटर्न वही है जो मैंने production incident reviews में उससे कहीं ज़्यादा बार देखा है जितना मुझे अच्छा लगता है। हल्के-फुल्के दिखने वाले कई bugs की एक चेन, जिनमें से हर एक बस उतना privilege दिलाता है कि अगली कड़ी तक पहुँचा जा सके। शुरुआती foothold Switch 2 में भी नहीं था। वह Switch 1 के एक गेम में मेमोरी corruption bug था, यानी SmileBASIC interpreter, जो backward-compatibility mode में चल रहा था। वहाँ से चेन filesystem service तक पहुँची, loader में एक race का फायदा उठाया और आखिर में किसी भी sysmodule की identity में native code चलाने की क्षमता तक पहुँच गई। हर कड़ी देखने लायक है, क्योंकि हर कड़ी उन गलतियों से मेल खाती है जो मैंने server-side सिस्टम में टीमों को करते देखा है।

ब्राउज़र का दरवाज़ा क्यों बंद था, और गेम वाला क्यों नहीं

WebKit console jailbreaks का क्लासिक पहला निशाना होता है, और researcher को captive portal और एक n-day के ज़रिए वहाँ foothold मिल भी गया था। लेकिन वह रास्ता जल्दी ही बंद हो गया। Switch 2 का browser stack तीन ऐसे mitigations में लिपटा है जो एक साथ मिलकर बेहद असरदार हो जाते हैं। execute-only memory (XOM) का मतलब है कि arbitrary read/write होने पर भी आप उपयोगी code gadgets नहीं ढूँढ सकते। pointer authentication (PAC) का मतलब है कि signing oracle के बिना code pointer forge नहीं किया जा सकता। और CFI का मतलब है कि अगर pointer valid signed भी हो, तो ज़्यादातर call sites पर type checks उसे पास नहीं होने देते। इनमें से कोई एक तोड़ना research project है। तीनों को एक साथ चेन करके, JavaScript sandbox से तोड़ना करियर बनाने जैसा काम है।

इसलिए हमला मशीन के सबसे कमज़ोर tenant की तरफ मुड़ गया: third-party Switch 1 गेम्स। तर्क ठंडा था और सही भी था। गेम binaries स्टूडियो बनाते हैं, Nintendo की security टीम नहीं, इसलिए उनके पूरे mitigation stack के साथ compile होने की संभावना कम है। Switch 1 में PAC था ही नहीं, इसलिए उसके compatibility environment में चलने वाली हर चीज़ वही कमज़ोरी विरासत में लेती है। इससे भी बड़ी बात यह कि Switch 1 का root of trust सालों पहले compromise हो चुका था, यानी उसके गेम कोड को offline decrypt और audit किया जा सकता था। SmileBASIC, एक विशाल C++ BASIC interpreter जो 3DS पर पहले ही exploit हो चुका था, स्वाभाविक निशाना था।

यहाँ एक सबक है जो कंसोल से आगे भी लागू होता है: आपकी security posture उस सबसे कम hardened कोड जितनी ही होती है जिसे आप चलाने के लिए राज़ी होते हैं। Backward compatibility एक attack surface है जो आप विरासत में पाते हैं, चुनते नहीं। Xbox के security model ने 2000 के दशक में यह सबक कठिन तरीके से सीखा था। unhardened titles के saved-game exploits ही वह पाचर थे जिसने कहीं ज़्यादा कसे हुए kernel को खोला। इस पूरी कहानी पर हमने what the original Xbox hack teaches about hardware security models में लिखा है। बीस साल बाद भी वही चाल चल जाती है।

पहला दरवाज़ा खोलने वाला Integer Overflow

SmileBASIC की बग bounds checking में एक किताबी integer overflow है। यह वही चीज़ है जिसे हर secure-code review checklist पकड़ने को कहती है, और फिर भी यह ship होती रहती है। इंटरप्रेटर का array-removal command अपने arguments को कुछ ऐसे validate करता था:

// intent: ensure the removed range is inside the array
if (position + count <= array.length) {
memmove(backing + position * 8,          // start of the gap
backing + (position + count) * 8, // elements after the gap
(length - position) * 8);          // bytes to copy
}

चार elements वाले array पर REMOVE R, &H60000000, &H3FFFFFFF चलाइए, तो position + count wrap होकर 0x9FFFFFFF बन जाता है। Signed 32-bit मान के रूप में यह बहुत नेगेटिव है, यानी length से आराम से "कम" माना जाता है। 8 से किए गए multiplications भी wrap होते हैं, और अंत में memmove(backing, backing - 8, 32) चल जाता है। इसके बाद command out-of-memory error देता है, लेकिन copy तब तक हो चुकी होती है। और मज़ेदार हिस्सा यह है कि किसी भी SmileBASIC array से ठीक पहले वाले 8 bytes में array की खुद की ओर इशारा करने वाला pointer होता है। removal के बाद R[0] एक string की तरह पढ़ा जाता है, जिसका length field असल में वही pointer है। यानी एक बेहद लंबी string, जिसे आप पढ़ और लिख सकते हैं। Arbitrary read/write, और वह भी इंटरप्रेटर के अपने memory layout ने गिफ्ट-रैप करके दे दिया।

अगर आप रोज़ी-रोटी के लिए C या C++ लिखते हैं, तो उस check को एक मिनट ध्यान से देखिए। position + count <= length systems code में bounds check का सबसे आम idiom है, और जब भी योग overflow कर सकता है, यह गलत होता है। सही रूप position <= length && count <= length - position है, या checked arithmetic। मैंने इस bug के variants एक पुरानी नौकरी के packet parser में और एक open-source storage engine में देखे हैं। यह code review से इसलिए निकल जाता है क्योंकि यह देखने में बिल्कुल सही लगता है। यह specification और implementation के बीच के gap का छोटा रूप है। Spec कहता है "range validate करो", implementation range के आसपास की किसी चीज़ को validate करता है, और यही फर्क exploitable बन जाता है। specification-implementation gap ही वह जगह है जहाँ bugs रहते हैं, इस पर हम पहले लिख चुके हैं। कंसोल interpreters बस वह जगह हैं जहाँ ये bugs fuzzers की जगह लगनशील किशोर खोज लेते हैं।

Path-Handling Overflow से filesystem service तक पहुँच

गेम process के अंदर userspace read/write अपने आप में कुछ खास नहीं देता। Switch का microkernel डिज़ाइन हर service को अलग process बनाता है, जिसकी अपनी IPC surface होती है। असली कीमती निशाना fs था, जो built-in filesystem sysmodule है, और इसकी वजह मज़ेदार तरीके से recursive है: fs वही component है जो loader को decrypted code देता है। अगर fs आपके कब्ज़े में है, तो आप किसी भी दूसरे sysmodule का code offline analysis के लिए dump कर सकते हैं। किले का अपना quartermaster आपको blueprints थमा देता है।

यह bug path normalization में heap overflow था। GetEntryType IPC command अपने path argument को normalize करता है, और backslash वाले parent-directory component वाले paths, जैसे /..\, एक helper में चले जाते हैं। वह helper एक निश्चित 0x301-byte buffer allocate करता है और उसमें path को पर्याप्त length check के बिना copy कर देता है। लंबा path buffer से बाहर निकलकर पास के heap objects को overflow कर देता है। इस bug को खोजने पर researcher की अपनी टिप्पणी सिर्फ एक शब्द थी: "lol."

यह "lol" चुभना चाहिए, क्योंकि path normalization ठीक वैसा ही सामान्य plumbing code है जिस पर वह security scrutiny कभी नहीं पहुँचती जो crypto को मिलती है। Exploit ने overflow को पड़ोस के एक cache object को बिगाड़कर पूरे arbitrary read/write में बदल दिया। उस object का dirty bit file I/O को attacker-controlled pointer से redirect करता है, यह एक साफ heap-grooming primitive था। फिर memory scanning के जरिए exploit को fs की tables में अपने process का connection object मिला, और उसने उसमें stored process ID को 1 लिख दिया। इससे game process को filesystem पर "FullPermission" मिल गया। ध्यान दीजिए कि यहाँ एक भी cryptographic check नहीं टूटा। Permission model memory के डेटा से enforce होता था, और attacker memory लिख सकता था।

एक बहुमंज़िला किले का आइसोमेट्रिक कट-अवे, जिसमें एक छोटी आकृति संकरे सर्विस गलियारे से बंद गेट्स को बायपास कर रही है।
Each stage of the chain bypassed hardened doors by walking through a corridor nobody thought to guard.

Filesystem को बीच उड़ान में बदलकर Signature Checks को पार करना

पूरी चेन की सबसे चतुर कड़ी वह है जो बिना किसी key को छुए code signing को हरा देती है। Program loader (ldr) कुछ भी चलाने से पहले दो signatures verify करता है। एक Nintendo का signature है जो process-permission metadata और developer key पर होता है, और दूसरा developer का signature है जो content archive के header hash पर होता है। सिद्धांत में वह hash executable के हर byte को transitively fix कर देता है। व्यवहार में एक गैप है: यह सुनिश्चित करना fs का काम है कि जो filesystem वह दे रहा है, वह उस hash से मेल खाता हो जिसे उसने पहले ही report कर दिया है। और attacker के पास fs के अंदर arbitrary read/write था।

तो exploit तब तक इंतज़ार करता है जब तक loader executable filesystem माँग चुका हो और fs valid hash और signature दे चुका हो। फिर वह loader के IPC handle के पीछे के filesystem object को एक तैयार किए गए object से बदल देता है। नया object असली signed metadata file देता है, लेकिन executable की जगह कोई मनचाहा replacement देता है। Signature checks पास हो जाते हैं, क्योंकि वे उन bytes पर थे जिन्हें attacker को कभी बदलना ही नहीं पड़ा। Timing उस handle को फिर से लिखकर तय होती है जिसका fs loader के connection के लिए इंतज़ार करता है। उसे game process से नियंत्रित event में बदल दिया जाता है, और इस तरह race condition एक निश्चित interposition बन जाती है।

Header hash से लेकर executable की सामग्री तक cryptographic chain कागज़ पर मज़बूत थी। जिस एक कड़ी का अस्तित्व सिर्फ runtime assumption के रूप में था, यानी "फाइलें देने वाला process ईमानदार है", वही टूट गई।

यह एक time-of-check/time-of-use bug है जो ट्रेंच कोट पहनकर आया है, और पूरी write-up का यह सबसे ज़्यादा transferable सबक है। जब भी component A data को validate करता है और component B बाद में उस पर mutable reference के ज़रिए काम करता है, तो वहाँ swap window बन जाती है। मैंने एक बार एक internal deployment system की समीक्षा की थी जो container image digest validate करता था, और फिर rollout के समय registry से tag के आधार पर pull करता था। वही bug, बस दूसरी टोपी में। दोनों का हल एक ही है: use के बिंदु पर validate करें, या consumer को immutable, verified object दें, न कि ऐसा handle जिसे किसी तीसरे पक्ष के ईमानदार रहने पर भरोसा करना पड़े। जो defensive design यह मानकर चलता है कि हर intermediary compromised हो सकता है, वह paranoia नहीं है। ऐसी मशीन पर, जिसमें attacker पहले से आंशिक रूप से लिख सकता है, यह बस सटीक modeling है। यही defensive engineering for systems that resist catastrophe का मूल तर्क है, और यह exploit उसका एक साफ case study है।

Kernel Handle उधार लेकर fs को अंदर से पैच करना

आखिरी चाल छोटी और खूबसूरत है। fs process के अपने code को पैच करने के लिए exploit को उसका kernel handle चाहिए था। उसने memory में 4-byte वाले handle field को जादुई मान 0xffff8001 से overwrite करके यह पाया, जिसे kernel "current process का handle" मानता है। Game process ने एक benign API कॉल किया, अपना ही process handle वापस पाया, और उसे code-loading sysmodule की permissions वाले एक rogue process को थमा दिया। उस process ने legitimate kernel calls से fs के code को map और patch किया। हर अलग ऑपरेशन authorized था। पर उनका composition डिवाइस के सबसे भरोसेमंद process का पूरा compromise था।

अगर यह जाना-पहचाना लगता है, तो वजह यह है कि confused-deputy समस्याएँ हर जगह मिलती हैं, बस देखना शुरू कीजिए। sudo rules जो writable config वाले benign binary को चलने देते हैं, cloud IAM roles जो खुद को किसी ज़्यादा शक्तिशाली service को पास कर सकते हैं, और build systems जो CI job से मिली किसी भी चीज़ पर signature कर देते हैं। Privilege escalation के इस पहलू पर हमने the history of Unix privilege escalation में लिखा है। तरीके बदलते हैं, bug का आकार नहीं बदलता। Handles, capabilities और tokens उतने ही मज़बूत होते हैं जितनी उस memory की integrity मज़बूत होती है जो उन्हें रखती है।

Secure-Boot डिज़ाइनर क्या बार-बार गलत करते हैं

थोड़ा पीछे हटकर देखें तो पूरी चेन में एक ही पैटर्न दिखता है, और यही पैटर्न उन ज़्यादातर गंभीर enterprise breaches के पीछे है जिनकी मैंने समीक्षा की है: verified core टिका रहा, और उसके आसपास सब कुछ नहीं टिका। Nintendo का boot ROM, signature scheme और kernel mitigations ठीक थे। जो टूटा वह था एक interpreter का bounds check, एक path-normalization buffer, writable memory में रखा गया permission field, दो trusted services के बीच एक TOCTOU window, और एक handle table जिसने userspace-writable structures से kernel pointers लिए। इनमें से कुछ भी विदेशी नहीं है। ये सब वही चीज़ें हैं जो review में इसलिए बच जाती हैं क्योंकि हर एक स्थानीय रूप से उचित दिखता है।

  • बोरिंग कोड का ऑडिट करें। Path normalizers, length checks और handle tables ही वे जगहें हैं जहाँ exploit chains असल में शुरू होती हैं, crypto में नहीं। Review का समय algorithmic जटिलता के बजाय attacker की पहुँच के अनुपात में रखें।
  • हर mitigation gap को roadmap मानें। अगर सिस्टम का कोई process PAC/CFI के बिना है, जैसे compatibility layer, third-party binary या legacy interpreter, तो उसे entry point मानकर चलें और मॉडल करें कि वह क्या-क्या पहुँच सकता है।
  • Trust-by-position खत्म करें। जो component data validate करके आगे mutable handle दे देता है, उसने swap window बना दी है। Immutable, self-verifying objects दें, या use के समय फिर से verify करें।
  • मान लें कि कहीं भी writable memory हर जगह writable memory है। Permission flags, PID fields और handle values, जो attacker की पहुँच वाली memory में रखे हैं, आपकी enforcement व्यवस्था नहीं बल्कि आपकी attack surface का हिस्सा हैं।

एक और असहज निष्कर्श tooling के बारे में है। यह exploit एक AI agent की मदद से विकसित हुआ, जिसे KVM-over-IP डिवाइसों के ज़रिए हार्डवेयर तक फिज़िकल पहुँच दी गई थी। वीडियो अंदर आता था, controller input USB से जाता था, और हर crash के बाद पावर बटन दबाने के लिए एक Bluetooth लीवर गैजेट था। कई agent instances दो हफ्ते तक, चौबीसों घंटे, अलग-अलग attack surfaces पर समानांतर काम करते रहे। AI-assisted security research के बारे में आप जो भी सोचें, बंद हार्डवेयर पर vulnerability discovery का अर्थशास्त्र बदल चुका है। जिन bugs के लिए कभी धैर्यवान विशेषज्ञ चाहिए था, अब उन्हें ऐसा धैर्यवान विशेषज्ञ चाहिए जो तीन न थकने वाले सहायकों की निगरानी कर सके। अगर आपका threat model मानता था कि "कोई इस sysmodule को reverse-engineer करने की परवाह नहीं करेगा", तो वह मान्यता इसी महीने expire हो चुकी है।

चेन ही असली product है

Jailbreak write-up को Nintendo की गलतियों की सूची की तरह पढ़ना आसान है। लेकिन इसे एक आईने की तरह पढ़िए। जिस भी system की रक्षा में मैंने कभी मदद की है, उसमें SmileBASIC जैसा एक component था (ऐसा legacy हिस्सा जिसे किसी ने hardened नहीं किया), path buffer जैसा एक plumbing code था (जो अपनी मान्यताओं से बड़ा हो गया था), और loader race जैसी एक validation थी (जिसने एक mutable intermediary पर भरोसा किया)। हमलावरों की बढ़त किसी एक कदम की प्रतिभा में नहीं होती। उनकी बढ़त यह है कि वे चेन में सोचते हैं, जबकि defenders components में सोचते हैं।

तो आपकी टीम के लिए इस हफ्ते का अभ्यास यह नहीं है कि "क्या कोई हमारे signatures forge कर सकता है।" अभ्यास यह है कि अपनी सबसे भरोसेमंद internal service चुनिए, मान लीजिए कि हमलावर के पास उससे बात करने वाले सबसे कम hardened process में arbitrary write पहले से है, और चेन को आगे तक चलाकर देखिए। पहली escalation कहाँ उतरती है? वह identity हमलावर को कहाँ तक पहुँचाती है? आपको अपना fs sysmodule जल्द ही मिल जाएगा। बेहतर है कि वह आप खुद खोजें, बजाय इसके कि कोई KVM, Bluetooth लीवर और दो हफ्ते के धैर्य के साथ खोजे।