Why NextWhy Next
सूची पर लौटें
💻 डेवलपमेंट14 मिनट में पढ़ें

हमारे ऐप का TLS इंटरसेप्ट कर रहे 9 यूज़र्स को ट्रैक किया, तो वे गूगल निकले

सर्टिफ़िकेट पिनिंग को report-only मोड में चालू करते ही Sentry में SPKI मिसमैच के 74 इवेंट जमा हो गए। देखने में यह MITM हमला लग रहा था, और AI ने 'गूगल आपका HTTPS इंटरसेप्ट नहीं करता' कहकर स्टोर रिव्यू की संभावना दो टूक ख़ारिज कर दी। प्रोडक्शन API लॉग से निकाले गए असली IP ने उस अनुमान को उलट दिया।

मुख्य सारांश

सर्टिफ़िकेट पिनिंग को report-only मोड में चालू करने के पहले ही हफ़्ते Sentry में SPKI मिसमैच के 74 इवेंट (9 यूज़र) आ गए। ऐप को जो सर्टिफ़िकेट मिल रहा था वह हमारे सर्वर के सर्टिफ़िकेट से एक भी जगह मेल नहीं खाता था, यानी कोई बीच में TLS इंटरसेप्ट कर रहा था। डिवाइस फ़िंगरप्रिंट देखा तो 74 के 74 इवेंट 'OnePlus 8 Pro' के थे, मगर आर्किटेक्चर x86 और स्क्रीन 288x448। यह फ़िज़िकल डिवाइस का भेस धरे एमुलेटर फ़ार्म था। पहचान का दायरा तंग करते वक़्त AI एजेंट ने 'गूगल आपका HTTPS MITM नहीं करता' कहकर स्टोर के ऑटोमैटिक स्कैन की संभावना ख़ारिज कर दी, लेकिन मिसमैच के ठीक उसी वक़्त प्रोडक्शन API पर आए असली IP सब के सब गूगल की रेंज के थे। सिद्धांत से निकला अनुमान असल माप के सामने 3 मिनट में ढह गया।

इस पेज पर

हमारा ऐप एक बार सर्टिफ़िकेट पिनिंग की वजह से मर चुका है। 5 अप्रैल 2026 को हमने leaf सर्टिफ़िकेट का SPKI पिन के तौर पर गाड़ रखा था, और ACM ने सर्टिफ़िकेट अपने आप रिन्यू किया तो leaf की पब्लिक की बदल गई। उसी पल सारे यूज़र्स के लिए API बंद हो गया। रिमोट किल-स्विच भी नहीं था, इसलिए हॉटफ़िक्स ऐप दोबारा स्टोर पर चढ़ाना पड़ा। उसके बाद रेपो में एक नियम लिख दिया गया: "सर्टिफ़िकेट पिनिंग मत लाना।"

दो महीने बाद हमने पिनिंग फिर से बनाई। इस बार leaf नहीं, बल्कि Amazon Root CA के चार SPKI पिन के तौर पर गाड़े। रूट, ACM के रिन्यू करने पर भी नहीं बदलता, इसलिए वही हादसा दोबारा नहीं होगा। रिमोट कॉन्फ़िग से ऑन-ऑफ़ करने वाला किल-स्विच भी लगा दिया। 29 जून को हमने रिमोट से report-only मोड चालू किया - जो ब्लॉक नहीं करता, सिर्फ़ रिपोर्ट करता है।

दो दिन बाद Sentry में एक इशू पड़ा मिला।

[CertPinning] SPKI mismatch (report-only)
74 events · 9 users · 2026-06-29

दो हैश, जो हमने कभी भेजे ही नहीं

इवेंट खोलकर देखा तो ऐप को असल में मिले सर्टिफ़िकेट के दो SPKI छपे हुए थे।

mode: "reportOnly"
host: "web.example.com"
expected_pin_count: 4
served_spki: [
  "sha256/9hqPsoMiyQMwLCoRPk6FoCYmOsPiGqzQqUcpuZIfvgs=",
  "sha256/r9mjYco6rQO8YkTqr/XXGsQlDUuQqz2mGr67S0imt7M="
]

openssl s_client से सर्वर की असली चेन निकालकर मिलान किया। हमारा सर्वर जो leaf भेजता है, जो इंटरमीडिएट भेजता है, जो रूट भेजता है - इनमें से कोई भी इन दो हैश से मेल नहीं खाता। इन 9 लोगों तक हमारे सर्वर का असली सर्टिफ़िकेट पहुंचा ही नहीं। कोई बीच में TLS काटकर, अपने सर्टिफ़िकेट से दोबारा साइन करके ऐप को थमा रहा था।

served_spki में सिर्फ़ दो एंट्री होना भी एक सुराग़ था। सामान्य पाथ में ऐप जो चेन रिपोर्ट करता है उसमें रूट समेत तीन होते हैं। सिर्फ़ दो (leaf + इंटरमीडिएट) छपे हैं, यानी यह रिपोर्ट किसी दूसरी ब्रांच से निकली है।

यहीं मैंने पहला नतीजा निकाला। ठीक-ठीक कहूं तो नतीजा AI एजेंट ने निकाला और मुझे वह जंचा। "यह ठीक करने वाला बग नहीं है, बल्कि report-only पिनिंग अपने डिज़ाइन के मुताबिक़ काम करते हुए किसी और के नेटवर्क में हो रही इंटरसेप्शन पकड़ रही है। कंपनी या स्कूल की प्रॉक्सी, एंटीवायरस का वेब प्रोटेक्शन, कैप्टिव पोर्टल वग़ैरह। ये हमारे बस में नहीं हैं, तो Sentry में इसे mute करके सिर्फ़ मेट्रिक के तौर पर देखते हैं।"

साथ में यह सुझाव भी आया कि जब हाथ लगा ही रहे हैं तो एक बैकअप पिन जोड़ लेते हैं। मैंने वही चुना।

बैकअप पिन इस इशू का एक भी इवेंट कम नहीं करता

पिन जोड़ने का काम शुरू होते ही मुझे लगा कि यह बात आपस में मेल नहीं खा रही, तो मैंने पलटकर पूछा।

मिसमैच तो होना ही नहीं चाहिए था न?

इस एक लाइन ने अभी-अभी चुनी हुई दिशा गिरा दी। एजेंट ने एंड्रॉयड प्रोब का कोड दोबारा पढ़ा और अपनी बात सुधारी।

val verified = verifiedChainSpki(offered, host)
if (verified == null) {
    // ट्रस्ट स्टोर सत्यापन विफल = इंटरसेप्शन/जालसाजी -> पिन तुलना के बिना ही रिपोर्ट
    report(host, offered.mapNotNull(::spkiSha256))
    return
}
// पिन तुलना सिर्फ उसी chain पर होती है जो सत्यापन पास कर चुकी है
if (verified.none(pins::contains)) { report(host, verified) }

प्रोब दो चरणों में चलती है। पहले OS के ट्रस्ट स्टोर से चेन वेरीफ़ाई होती है, और सिर्फ़ पास हुई चेन की तुलना पिन लिस्ट से होती है। हमारे 74 इवेंट पहले चरण में ही फेल थे। verified == null है, इसलिए पिन की तुलना किए बिना सीधे रिपोर्ट हो जाती है। बैकअप पिन दूसरे चरण में, यानी "वेरिफ़िकेशन तो हुआ, पर पिन मेल नहीं खाया" वाली हालत में ही काम आता है।

निष्कर्ष: Starfield G2 को पिन में जोड़ने से भी इस इशू की गिनती एक भी कम नहीं होगी।

तभी यह भी साफ़ हुआ कि सामान्य यूज़र को यह रिपोर्ट आ ही नहीं सकती। लाइव चेन हमारे पिन से ठीक-ठीक मेल खाती है। यानी ये 9 लोग परिभाषा से ही सामान्य यूज़र नहीं हैं। यहां ठीक करने लायक़ कोई बग था ही नहीं। असल में तो यह पता करना था कि ये 9 कौन हैं।

नज़रअंदाज़ किया तो कभी चालू ही नहीं कर पाएंगे

"नज़रअंदाज़ करके मॉनिटर करते रहते हैं" वाली सलाह भी मैं मान नहीं सका।

ठीक से फ़िक्स करना होगा, आख़िर इसे बाद में असल में चालू भी तो करना है। ऐसे में तो चालू ही नहीं कर पाएंगे।

इस पिनिंग का आख़िरी लक्ष्य enforce है। enforce चालू किया जा सकता है या नहीं, यह तय करने वाला गेट डिज़ाइन डॉक्युमेंट में पहले से लिखा था: दो हफ़्ते तक असली यूज़र्स की मिसमैच दर 0.5% से कम रहनी चाहिए। मिसमैच की असलियत जाने बिना छोड़ दिया तो ये 74 इवेंट उस गेट को लगातार प्रदूषित करते रहेंगे। mute कर दें तो 74 इवेंट डैशबोर्ड से ग़ायब हो जाएंगे, मगर गेट वैसा का वैसा प्रदूषित पड़ा रहेगा।

तो मैंने डेटा और खोदा। टैग डिस्ट्रीब्यूशन निकालते ही एक अजीब चीज़ सामने आ गई।

74 के 74 इवेंट एक ही डिवाइस के, मगर वह डिवाइस दुनिया में है ही नहीं

74 इवेंट का device टैग 100% एक ही था: OnePlus8Pro। मगर उस डिवाइस का फ़िंगरप्रिंट अजीब था।

फ़ील्डइवेंट की वैल्यूअसली OnePlus 8 Pro
archsx86_64, x86 शामिलसिर्फ़ arm64 (Snapdragon 865)
स्क्रीन288 x 448 px, 106 dpi1440 x 3168, 513 dpi
CPU2 कोर, फ़्रीक्वेंसी 08 कोर
simulatorfalse-

कंज़्यूमर ARM फ़ोन x86 ABI का विज्ञापन नहीं करते। 288x448 और 106dpi वाला एंड्रॉयड फ़ोन भी दुनिया में नहीं है। यह x86 वर्चुअल डिवाइस है जिसने सिर्फ़ Build.MODEL को असली डिवाइस के नाम का भेस दे रखा है। simulator: false लिखकर वह फ़िज़िकल डिवाइस होने का नाटक तक कर रहा था।

9 यूज़र ID भी इंसानों की नहीं थीं। नौ के नौ सिर्फ़ 29 जून वाले दिन ही मौजूद थे, और उसी एक दिन में उन्होंने 1.7.0, 1.8.0, 1.8.1 - तीनों वर्ज़न चलाए। यह इंसान नहीं, पाइपलाइन है। 74 के 74 इवेंट का इलाक़ा अमेरिका था, मगर शहर हर जगह null। डेटा सेंटर की रेंज से आने पर ऐसा ही छपता है।

यहां मैंने पूछा कि आख़िर हमारे ऐप का विश्लेषण कर कौन रहा है। एजेंट ने पहले नंबर पर सिक्योरिटी वेंडर के APK ऑटो-एनालिसिस सैंडबॉक्स को रखा, उसके बाद ऐप इंटेलिजेंस कंपनियां और APK मिरर साइटें। स्टोर का ऑटोमैटिक स्कैन चौथे नंबर पर था।

चौथे नंबर की परिकल्पना तीन IP से पहले नंबर पर आ गई

मेरा मन चौथे नंबर पर अटका था। तो मैंने पूछा।

Google Play Console या Apple Store वाला मामला भी हो सकता है क्या?

जवाब दो टूक था।

Google Play pre-launch report / Test Lab - क़रीब-क़रीब नहीं। निर्णायक वजह: गूगल आपका HTTPS MITM नहीं करता। Play pre-launch स्टैंडर्ड ट्रस्ट स्टोर से आपका असली सर्टिफ़िकेट ज्यों का त्यों लेता है। स्टोर क्रॉलर ट्रैफ़िक इंटरसेप्ट करके दोबारा साइन नहीं करते। वह काम एनालिसिस सैंडबॉक्स का है।

सिद्धांत के लिहाज़ से इस दलील में कोई ख़ामी नहीं है। और यह ग़लत है।

जो चीज़ नहीं थी, वह IP का सबूत था। Sentry IP को PII मानकर स्क्रब कर देता है, इसलिए इवेंट का user.ip हर जगह null था, और पुराने इवेंट का मूल IP रिकवर नहीं किया जा सकता। पहचान उजागर करने के लिए IP चाहिए था, और IP रखने वाला कोई टूल पास में नहीं था।

बदले में प्रोडक्शन API के एक्सेस लॉग मौजूद थे। हर रिक्वेस्ट पर X-Forwarded-For और User-Agent दर्ज होता था, और हमारे ऐप का User-Agent Dart/3.12 (dart:io) के रूप में अलग पहचान वाला था। और 29 जून अब भी लॉग रिटेंशन (7 दिन) के भीतर था।

पहले Sentry से 74 इवेंट के समय निकालकर तीन गुच्छों में बांधे: 02:12-02:20, 11:30-11:41, 17:32-17:40 (UTC)। फिर उन तीनों अंतरालों में Dart/3.12 से आई रिक्वेस्ट के IP CloudWatch में खोजे।

02:12-02:20  ...  66.249.84.132, 66.249.84.141
11:30-11:41  ...  66.102.7.69
17:32-17:40  ...  66.249.84.141

66.249.x.x वह रेंज है जो Googlebot इस्तेमाल करता है, और 66.102.x.x भी गूगल की है। तीनों अंतरालों में गूगल का IP मौजूद था। उनमें से दो अंतरालों में तो उस समय आई ऐप रिक्वेस्ट का IP सिर्फ़ गूगल का ही था। बचे हुए एक अंतराल में असली यूज़र जैसा दिखने वाला IP भी साथ था, मगर गूगल का IP वहां भी था।

3 मिनट बाद एजेंट ने ख़ुद अपनी बात सुधारी।

सुधार: मैंने पहले कहा था कि "गूगल MITM नहीं करता, इसलिए यह वह नहीं होगा", मगर IP का सबूत उस अनुमान को उलट देता है। यह गूगल ही है। असल में मापा गया IP अनुमान से ज़्यादा मज़बूत सबूत है।

हमारे ऐप को खंगालने वाला कोई और नहीं, गूगल का वही रोज़मर्रा का ऑटोमैटिक स्कैन था जो Play पर चढ़े हर ऐप को झेलना पड़ता है। न हमलावर, न प्रतिस्पर्धी, न सिक्योरिटी वेंडर। वह स्कैन एनवायरनमेंट ट्रैफ़िक को इंस्ट्रूमेंट करता है। इसीलिए हमारी प्रोब की नज़र में चेन बिना वेरिफ़िकेशन वाली दिख रही थी।

तो आख़िर ठीक क्या किया

पिनिंग को वैसा ही रहने दिया, और यह बदला कि प्रोब कहां लगेगी। तय किया कि स्कैनर एनवायरनमेंट में पिनिंग चालू ही नहीं करेंगे।

बचा यह सवाल कि स्कैनर को पहचानेंगे किस चीज़ से। simulator: false पर भरोसा नहीं किया जा सकता, यह तो साफ़ हो ही चुका था, इसलिए ऐसा सिग्नल चाहिए था जिसे स्पूफ़ न किया जा सके। CPU आर्किटेक्चर छिपाया नहीं जा सकता।

// निर्णायक संकेत: उपभोक्ता ARM फोन कभी x86 ABI घोषित नहीं करता।
// Google का स्कैनर Build.MODEL को असली डिवाइस नाम से छिपा सकता है (देखा गया: x86 पर "OnePlus8Pro"),
// लेकिन x86_64 होने का तथ्य छिपा नहीं सकता।
if (supportedAbis.any((abi) => abi.contains('x86') || abi.contains('i686'))) {
  return true;
}

इसके साथ Cuttlefish/GCE श्रेणी के हार्डवेयर नाम और test-keys फ़िंगरप्रिंट की जांच भी जोड़ी। यह डिटेक्टर जानबूझकर "यह एमुलेटर है" की तरफ़ झुका हुआ है। ग़लत पहचान होने पर बस उस एक एनवायरनमेंट में पिनिंग स्किप होगी, और एमुलेटर पर ऐप चलाने वाला हमलावर वैसे भी ऐप पैच करके क्लाइंट पिनिंग बेकार कर सकता है, तो खोने को कुछ है नहीं। इसके उलट, स्कैनर को पहचानने से चूकना कहीं बुरा है, क्योंकि तब वह ब्लॉक स्क्रीन में फंस जाएगा।

यह क्यों अहम है, यह ऐसे समझिए: अगर हम enforce यूं ही चालू कर देते तो यह होता। गूगल का pre-launch क्रॉलर हमारे ऐप की "असुरक्षित कनेक्शन" वाली ब्लॉक स्क्रीन में फंसता। वही Play Console की pre-launch रिपोर्ट में फ़ेल्योर के तौर पर दर्ज हो जाता। कोड में कोई ख़राबी नहीं, फिर भी स्टोर रिव्यू की स्क्रीन पर लाल लकीर खिंच जाती।

जो बचा

Sentry IP को PII मानकर स्क्रब करता है। यह सही डिफ़ॉल्ट है। मगर इसी वजह से वह इकलौता फ़ील्ड, जो घटना की असलियत बता सकता था, इवेंट से ग़ायब था। आख़िरकार ऐप के User-Agent और घटना के समय - इन दो चीज़ों को क्रॉस करके, Sentry ने जो जानकारी फेंक दी थी उसे एक्सेस लॉग से वापस निकाला। एक टूल में नहीं है, इसका मतलब यह नहीं कि कहीं भी नहीं है।

"गूगल HTTPS इंटरसेप्ट नहीं करता" वाला वाक्य दलील के तौर पर बिल्कुल चिकना था। स्टोर क्रॉलर का मक़सद, स्टैंडर्ड ट्रस्ट स्टोर का बर्ताव, सैंडबॉक्स से उसका फ़र्क़ - सब गिनाते हुए उस परिकल्पना को चौथे नंबर पर धकेल दिया गया। फिर तीन IP सामने रखते ही 3 मिनट में निष्कर्ष पलट गया। AI से सिद्धांत पूछोगे तो सिद्धांत ही वापस मिलेगा। वह सिद्धांत हक़ीक़त से मेल खाता है या नहीं, यह लॉग देखे बिना पता नहीं चलता।

अगर इन 74 इवेंट को Sentry में mute कर दिया होता तो डैशबोर्ड साफ़ हो जाता, और जिस दिन enforce चालू करते उस दिन बिना वजह जाने स्टोर रिव्यू में लाल लकीर मिलती।

हमने enforce अभी चालू नहीं किया है। गेट की मांग है कि दो हफ़्ते तक असली यूज़र्स की मिसमैच दर 0.5% से कम रहे। स्कैनर को छान दिया है, तो वे दो हफ़्ते शुरू से दोबारा गिनने होंगे।

अक्सर पूछे जाने वाले सवाल

सर्टिफ़िकेट पिनिंग report-only में चालू की तो मिसमैच रिपोर्ट आने लगीं। बैकअप पिन जोड़ने से ये कम हो जाएंगी?

पहले यह देखना होगा कि रिपोर्ट किस कोड पाथ से निकली है। पिनिंग प्रोब आम तौर पर दो चरणों में काम करती है। पहले OS के ट्रस्ट स्टोर से सर्टिफ़िकेट चेन वेरीफ़ाई होती है, और वेरिफ़िकेशन पास होने के बाद ही उस चेन की तुलना पिन लिस्ट से की जाती है। अगर चेन वेरिफ़िकेशन ही फेल हो जाए (इंटरसेप्शन या जालसाज़ी), तो पिन की तुलना किए बिना ही सीधे रिपोर्ट चली जाती है। हमारी सारी रिपोर्ट इसी दूसरी क़िस्म की थीं, इसलिए बैकअप पिन चाहे जितने जोड़ लो, एक भी रिपोर्ट कम नहीं होगी। बैकअप पिन सिर्फ़ उस हालत में काम आता है जहां 'वेरिफ़िकेशन तो हुआ, पर पिन मेल नहीं खाया'।

Sentry इवेंट में IP नहीं है। फिर भी कनेक्शन का स्रोत पता किया जा सकता है?

Sentry IP को PII मानकर स्क्रब कर देता है, इसलिए वह अक्सर इवेंट में बचता ही नहीं, और पुराने इवेंट का मूल IP रिकवर नहीं किया जा सकता। इसके बजाय दूसरे लॉग के साथ समय क्रॉस-चेक करें। हमारे सर्वर के एक्सेस लॉग हर रिक्वेस्ट पर X-Forwarded-For और User-Agent रखते थे, और हमारे ऐप का User-Agent 'Dart/3.12 (dart:io)' के रूप में अलग पहचान वाला था। Sentry से मिसमैच के ठीक-ठीक समय अंतराल निकाले, और उन अंतरालों में उसी User-Agent से आई रिक्वेस्ट के IP देखे, तो स्रोत सामने आ गया।

सर्टिफ़िकेट पिनिंग को enforce में चालू करने से Google Play रिव्यू में दिक़्क़त आ सकती है?

आ सकती है। Play का ऑटोमैटिक स्कैन एनवायरनमेंट ट्रैफ़िक को इंस्ट्रूमेंट करता हुआ दिखता है, और अगर उस माहौल में ऐप पिनिंग वाला ब्लॉक स्क्रीन दिखा दे तो pre-launch रिपोर्ट में वह फेल दर्ज हो सकता है। स्कैनर या एमुलेटर एनवायरनमेंट में पिनिंग प्रोब को ही स्किप कर देना सुरक्षित रहता है। वैसे भी एमुलेटर पर ऐप चलाने वाला हमलावर ऐप को पैच करके क्लाइंट पिनिंग बेकार कर सकता है, इसलिए उस एनवायरनमेंट को अपवाद रखने से बचाव का दायरा घटता नहीं है।

संबंधित लेख

0 टिप्पणियाँ