1 पॉइंट द्वारा GN⁺ 2024-03-31 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • xz/liblzma backdoor सिर्फ अंतिम binary payload की समस्या नहीं है; build के दौरान चलने वाला Bash चरण extraction और decryption की कई परतों के पीछे छिपा है, जिससे analysis काफी कठिन हो जाता है
  • प्रभावित versions xz/liblzma 5.6.0 और 5.6.1 हैं, और obfuscated script तथा binary payload दो ऐसी files में मौजूद हैं जो test files जैसी दिखती हैं
  • Stage 0 m4/build-to-host.m4 से शुरू होता है, corrupted दिखने वाली xz stream को restore करता है, और xz -d से Stage 1 script निकालकर चलाता है
  • Stage 1 good-large_compressed.lzma से data काटता और replace करता है, फिर Stage 2 Bash script चलाता है; 5.6.1 में Linux पर चलने की 5 बार जांचने वाला code जोड़ा गया है
  • Stage 2 .o file को build और link process में घुसाने के लिए file carving, substitution cipher, और AWK-आधारित RC4-variant decryption का उपयोग करता है; 5.6.1 में आगे अतिरिक्त scripts चलाने के लिए एक extensible structure भी शामिल है

xz/liblzma backdoor के Bash चरण का दायरा

  • Andres Freund ने oss-security mailing list पर xz/liblzma backdoor की खोज की सूचना दी, और यह backdoor OpenSSH server को प्रभावित करता है
  • analysis का विषय binary backdoor खुद नहीं, बल्कि उससे पहले चलने वाला Bash-आधारित शुरुआती चरण और उसका obfuscation तरीका है
  • Bash flow Stage 0 से Stage 2 तक जाता है, और एक संभावित Stage 3 के कुछ संकेत भी दिखते हैं जो पूरी तरह implemented नहीं लगता
  • obfuscated और encrypted चरणों के साथ बाद का binary backdoor दो test files के भीतर छिपे हैं
    • tests/files/bad-3-corrupt_lzma2.xz
    • tests/files/good-large_compressed.lzma
  • प्रभावित versions 5.6.0 और 5.6.1 हैं, और दोनों versions के बीच कुछ छोटे अंतर हैं

Stage 0: corrupted test file जैसी दिखने वाली xz stream को restore करना

  • शुरुआती बिंदु m4/build-to-host.m4 का code है, जो build process के किसी चरण में execute होता दिखता है
  • मुख्य pipeline sed, tr, और xz -d को जोड़कर Stage 1 script निकालती है
  • tests/files/bad-3-corrupt_lzma2.xz के bytes पढ़ने के बाद tr "\t \-_" " \t_\-" से कुछ bytes replace किए जाते हैं
    • 0x09 tab को 0x20 space में बदला जाता है
    • 0x20 space को 0x09 tab में बदला जाता है
    • 0x2d hyphen को 0x5f underscore में बदला जाता है
    • 0x5f underscore को 0x2d hyphen में बदला जाता है
  • यह replacement bad-3-corrupt_lzma2.xz को फिर से एक सही xz stream में बदल देता है
  • अंत में restored stream को xz से decompress किया जाता है, errors को ignore करते हुए Stage 1 script हासिल की जाती है और तुरंत execute कर दी जाती है
    • stream कटी हुई लगती है, लेकिन meaningful output पहले ही लिखी जा चुकी होती है, इसलिए समस्या नहीं होती

Stage 1: good-large_compressed.lzma से Stage 2 निकालना

  • Stage 1 Andres Freund के mail में ####Hello#### से शुरू होने वाली एक छोटी Bash file है
  • दोनों versions का पहला अंतर दूसरी लाइन के comment के अंदर मौजूद random bytes हैं
    • 5.6.0: 86 F9 5A F7 2E 68 6A BC
    • 5.6.1: E5 55 89 B7 24 04 D8 17
  • 5.6.1 में [ ! $(uname) = "Linux" ] && exit 0 के रूप में Linux check जोड़ा गया है, और यही check 5 बार दोहराया गया है
  • Stage 2 extraction एक लंबी export i=... लाइन से शुरू होती है, और यह value बाद में eval $i के जरिए किसी function जैसी इस्तेमाल होती है
  • extraction flow एक ऐसी pipeline structure है जो standard output और standard input को लगातार जोड़ती रहती है
    • पहले xz -dc से good-large_compressed.lzma को decompress किया जाता है
    • eval $i बार-बार head call करके 1024 bytes छोड़ता है और 2048 bytes output करता है
    • file के अंत में 5.6.0 में 724 bytes, और 5.6.1 में 939 bytes output होते हैं
  • यह head chain खाली low-entropy क्षेत्रों को हटाकर सिर्फ वास्तविक data वाले हिस्सों को जोड़ती है
  • इसके बाद tail -c +31233 से शुरुआती हिस्सा हटा दिया जाता है
    • इसी शुरुआती हिस्से में आगे निकलने वाला binary backdoor छिपा है
    • 5.6.0 में प्रभावी रूप से शुरुआती 31264 bytes, और 5.6.1 में 31232 bytes हटते हैं
  • इसके बाद tr को एक साधारण substitution cipher की तरह इस्तेमाल किया जाता है, और 5.6.0 तथा 5.6.1 की mapping keys अलग हैं
    • 5.6.0: tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
    • 5.6.1: tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
  • replaced data को फिर xz -F raw --lzma1 -dc से decompress किया जाता है, और परिणामस्वरूप मिला Stage 2 तुरंत execute हो जाता है

Stage 2: build process में बदलाव और extension mechanism

  • Stage 2 वही infected.txt file है जिसे Andres Freund ने मूल mail में संलग्न किया था, और यही वह Bash script है जहां वास्तविक compilation process modification होता है
  • obfuscation के नज़रिए से Stage 2 में तीन दिलचस्प fragments हैं, जिनमें से दो सिर्फ 5.6.1 में दिखाई देते हैं
  • 5.6.1 के ये दो fragments लगभग एक जैसे हैं और tests/files/ में खास signatures वाली files खोजते हैं
    • Fragment 1 ~!:_ W और |_!{ - खोजता है
    • Fragment 3 jV!.^% और %.R.1Z खोजता है
  • signature मिलने पर filename और offset निकाले जाते हैं, और पहले signature के बाद से दूसरे signature से पहले तक की byte range काट ली जाती है
  • काटे गए data को 5.6.0 में उपयोग की गई tr key से substitution decryption किया जाता है, फिर xz -F raw --lzma2 -dc से decompress कर eval के जरिए execute किया जाता है
  • जांचे गए 5.6.0 और 5.6.1 TAR archives में उन signatures वाली कोई file नहीं मिली
  • यह structure एक extension और patch system जैसा दिखता है, जो मौजूदा payload-carrying test files को दोबारा बदले बिना, सिर्फ नई test files जोड़कर भविष्य में scripts चलाने की सुविधा देता है

Stage 2: binary backdoor .o file निकालना

  • Stage 2 का एक और मुख्य हिस्सा .o file निकालकर उसे compile और link process में शामिल करने वाला code है
  • extraction का target output liblzma_la-crc64-fast.o है, और यही अंतिम binary backdoor है
  • extraction process पहले के चरणों की तरह कई standard tools को pipeline में जोड़ती है
    • good-large_compressed.lzma को xz -dc से decompress किया जाता है
    • Stage 1 में इस्तेमाल हुई eval $i की head chain फिर से उपयोग की जाती है
    • sed "s/\(.\)/\1\n/g" से हर byte के बाद newline जोड़कर उसे प्रति byte एक line के रूप में बदल दिया जाता है
  • अगली AWK script input stream पर RC4-जैसी decryption करती है
    • key array ((i * 7) + 5) % 256 से बनाई जाती है
    • 5.6.0 output PRNG stream के शुरुआती 4096 bytes छोड़ता है
    • 5.6.1 शुरुआती 8192 bytes छोड़ता है
    • मूल RC4 XOR का उपयोग करता है, लेकिन यह code (v + k) % 256 रूप की addition का उपयोग करता है
  • decrypted data को xz -dc --single-stream से decompress किया जाता है
  • इसके बाद head से आवश्यक byte range काटकर liblzma_la-crc64-fast.o के रूप में save किया जाता है
    • compressed लेकिन आंशिक रूप से modified payload का आकार 5.6.0 में 88792 bytes और 5.6.1 में 88664 bytes है
    • अंतिम extracted range लगभग 86KB की है

केवल standard tools से बने obfuscation की प्रकृति

  • पूरा Bash चरण केवल standard command-line tools से बना है
    • sed
    • tr
    • xz
    • head
    • tail
    • awk
    • grep
    • cut
    • expr
  • payload उन binary files में छिपा है जो सामान्य test files जैसी दिखती हैं, और आवश्यक हिस्से ही file carving के जरिए निकाले जाते हैं
  • obfuscation में simple substitution cipher और AWK-आधारित RC4-variant decryption दोनों का इस्तेमाल होता है
  • execution कुल 3 चरणों में आगे बढ़ती है, और 5.6.1 में बाद में अलग test files जोड़कर scripts चलाने की संरचना शामिल है
  • पूरी संरचना को harmless दिखाने और अच्छी तरह छिपाने में काफी मेहनत की गई है, और अगर यह मामला संयोग से पकड़ा गया, तो यह सवाल छोड़ता है कि अब तक ऐसे कितने मिलते-जुलते मामले अनदेखे रह गए होंगे

1 टिप्पणियां

 
GN⁺ 2024-03-31
Hacker News की राय
  • सरल किए गए explanation और noise image comparison की वजह से समझ आता है कि लोग जिस sophistication की बात कर रहे हैं, उसका मतलब क्या है
    reddit पर यह भी देखा कि “sandboxing” वाला तरीका एक dot से टूट गया था, और #include के ठीक अगली line में सबसे बाईं तरफ dot दिखता है
    https://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
    https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...

    • सच में बेहद खतरनाक। diff में यह बस +, +., + जैसा दिखता है, और वह एक dot नजर से छूट जाता है
    • ऐसे compile/build-time conditionals का इस्तेमाल मुझे बिल्कुल पसंद नहीं। यह test करना मुश्किल है कि उन्हें कब on होना चाहिए और कब off, और खासकर तब और मुश्किल जब वे ऐसे build system के अंदर हों जिसमें unit test framework नहीं है
      सिर्फ साधारण गलती से test हमेशा fail या हमेशा pass हो जाए, वही काफी सिरदर्द है; ऐसे में समझ आता है कि यह malicious behavior के लिए अच्छा target क्यों बनता है
    • यह जानबूझकर किया गया नहीं, बल्कि साधारण गलती होने की संभावना बहुत ज्यादा है
      a) इस package को cmake से build करने वाला असल में लगभग कोई नहीं है
      b) cmake और -DENABLE_SANDBOX=landlock से build करके देखें तो build बस fail हो जाता है: https://i.imgur.com/7xbeWFx.png
      वह dot sandboxing को disable नहीं करता, सिर्फ cmake से build करना असंभव बना देता है। अगर किसी ने सच में cmake से build किया होता, तो error देखकर समझ जाता कि कुछ गलत है; इसलिए इसे security कम करने की malicious कोशिश मानना तार्किक नहीं लगता
  • “अगर यह संयोग से मिला, तो अभी कितनी चीजें अनदेखी बची होंगी” सबसे अहम सवाल है
    Andres Freund ने backdoor लगे लोकप्रिय open source projects में से सिर्फ एक को ही संयोग से खोज लिया होगा, ऐसा संभव नहीं लगता। अगर ऐसी एक दर्जन चीजें पहले से बाहर मौजूद हों तो भी हैरानी नहीं होगी

    • यह संयोग से खोजने से ज्यादा, महसूस करने जैसा था। दोनों अलग बातें हैं
      अगला attacker run time बढ़ने जैसे निशान छोड़ने में कहीं ज्यादा सावधान होगा
    • हो सकता है, लेकिन असल में critical cases बहुत ज्यादा न भी हों। अगर बहुत होते, तो शायद हमें ऐसी स्थितियां ज्यादा बार दिखतीं
  • यहां असहज करने वाली बात यह है कि unit tests ने attack path खोल दिया। tests न होते तो इसे इस तरह छिपाना काफी मुश्किल होता

    • attacker ने शुरुआती payload के निशान तक README के harmless paragraph से ढक दिए। कुछ “यहां देखने लायक कुछ नहीं!” जैसा
      bad-3-corrupt_lzma2.xz में तीन streams हैं, और पहली व तीसरी valid xz streams हैं। लिखा है कि बीच वाली stream में stream header, block header, index, stream footer सही हैं और सिर्फ LZMA2 data corrupt है, और --single-stream इस्तेमाल करने पर decompress होना चाहिए
      ####Hello#### और ####World#### strings README के निर्देश सच में follow करने पर plausible normal result दिखाने में मदद करती हैं
      $ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream
      ####Hello####
      ये strings shell comments हैं, इसलिए payload execution में बाधा नहीं डालतीं
      आखिर में, बाद में ये एक marker की तरह काम करती हैं, जिससे regex filename को सीधे reference किए बिना या असली Hello/World strings का इस्तेमाल किए बिना file ढूंढ सकता है
      $ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``
      $ echo $gl_am_configmake
      ./tests/files/bad-3-corrupt_lzma2.xz
    • security-critical project हो तो build infrastructure को इस तरह configure करना सही लगता है कि binary files build में शामिल होने पर error या कम से कम warning आए
      यह check transitively apply होना चाहिए, ताकि Linux distributions जब नए liblzma version में update करने की कोशिश करें, तो नए binary dependency की वजह से build fail हो या warning आए
      Linux distribution builds में यह practice कितनी common है, मुझे नहीं पता। अगर common है तो cleanup में जबरदस्त काम लगेगा, और शुरू से ही यह संभव है या नहीं, यह भी unclear है। bazel से संभव लगता है, लेकिन दूसरे build systems के बारे में पक्का नहीं कह सकता
  • जानना चाहूंगा कि क्या किसी ने GitHub पर ऐसे ही head | tail trick पहले से खोजे हैं। यह मानना मुश्किल है कि इसे खास इसी के लिए नया invent किया गया होगा

    • commercial software vendors के Unix installers में यह तरीका काफी देखा है
      एक बहुत बड़ी .sh file license दिखाती है और consent लेने के बाद, खुद को cat करके head/tail pipe से गुजारती है और cpio के जरिए असली assets extract करती है
    • smart तो है, लेकिन बिल्कुल नया नहीं; यह इन tools के intended use cases के करीब है
    • paper लिखने का मौका है
  • बेहतर जवाब तो नहीं है, लेकिन यह उलझा हुआ bash का ढेर अपने-आप में संदिग्ध नहीं लगता?
    जैसे डेवलपमेंट दुनिया के दूसरे हिस्सों में होता है, क्या इसे कम अपारदर्शी तरीके से नहीं लिखा जा सकता था ताकि साफ दिखे कि क्या हो रहा है?
    मैं समझता हूँ कि maintainer किसी बाहरी contributor जितनी कड़ी review के बिना malicious code डाल सकता है, लेकिन “संक्षिप्त” code के ढेर, जो असल में अनजाने obfuscation जैसा दिखता है, उससे बेहतर रास्ता होना चाहिए

    • यह बहुत पुरानी बदबू है
      मूल समस्या यह है कि 80–90 के दशक में कई Unix-जैसे systems थे, जिनमें हर एक में अपनी खामियाँ और गायब features थे, और software authors build dependencies को न्यूनतम रखना चाहते थे
      इसलिए कई communities ने हर जगह चलने वाली shell scripts से builds automate करने को standard बना लिया. लेकिन shell scripts लिखना दर्दनाक था, और लोग M4 macro preprocessor जैसे tools से shell scripts generate करने लगे
      नतीजा यह हुआ कि अगर कोई AIX या किसी टूटे-फूटे प्राचीन Unix पर code चलाना चाहे, तो उसके लिए कई projects के पास विशाल और अपारदर्शी shell scripts के ढेर हो गए
      इस भेदना-कठिन shell झाड़ी को हटाने के लिए supported platforms की संख्या बहुत घटानी होगी, ज्यादा साफ build tools पर standardize करना होगा, और portable builds के लिए shell की जरूरत न रखने वाली languages में core infrastructure का अधिक हिस्सा बनाना होगा
      लेकिन यह बहुत बड़ा काम है, और core C libraries का बड़ा हिस्सा एक-दो unpaid volunteers maintain करते हैं. “Obscurnix-1997” support हटाना भी अक्सर काफी विवादास्पद फैसला बन जाता है
      इसलिए core infrastructure का बड़ा हिस्सा अब भी अज्ञात-सी machine-generated shell script दलदल से घिरा हुआ है
    • वह shell इंसान ने नहीं लिखा, वह generated code है. autoconf व्यापक रूप से इस्तेमाल होता है, इसलिए generated shell configuration code के पहाड़ जमा हैं, और autoconf M4 macro preprocessor scripts से हजारों lines की मुश्किल-से-पढ़ी जाने वाली portable shell scripts बनाता है
      काफी संख्या में tools इसी तरीके से build होते हैं
    • पहली नजर में bash का जटिल दिखना अपने-आप में मुझे red flag नहीं लगता. बहुत कसकर लिखी bash script कभी-कभी ऐसी दिखती है. क्या उसे इतना कसकर लिखना चाहिए, यह अलग debate है
      जो संदिग्ध लगता है वह बार-बार होने वाली tr calls हैं. ऐसी चीज दिखे तो लगता है कोई बहुत चालाक बनने की कोशिश कर रहा है, और यहाँ “चालाकी” नकारात्मक अर्थ में है. अगर मैं maintainer होता तो ऐसा code आने पर पूछता कि यह क्या करता है. क्योंकि ऐसी chaining से बचने के लिए लगभग हमेशा बेहतर समाधान होता है
      असली समस्या यह थी कि यह code आने पर देखने के लिए कोई दूसरा maintainer नहीं था. stack का एक महत्वपूर्ण component एक व्यक्ति पर निर्भर था, और इस मामले में वह व्यक्ति malicious था
    • “अनजाने में” नहीं, मुख्य बात यह है कि इसे जानबूझकर obfuscate किया गया था
    • शायद मैंने XV backdoor के खतरे को गलत समझा हो, लेकिन क्या bash को कुछ भी exec करने से रोकते हुए चलाने का कोई तरीका है? सोचता हूँ कि bash में “secure mode” जैसा कुछ हो तो कैसा हो
      हालांकि xv के configure script में ऐसे काल्पनिक “secure mode” के साथ bash कैसे चलाया जा सकता है, इसकी कल्पना नहीं कर पा रहा, इसलिए बात वापस लेता हूँ
  • https://github.com/tukaani-project/.github/issues/2

    • काफी मजेदार है. यह सिर्फ एक भयानक जानबूझकर डाला गया backdoor ही नहीं, बल्कि backdoor derivative work है, source शामिल नहीं है और “modification के लिए preferred form” में distribute नहीं किया गया, इसलिए यह GPL violation भी है
  • build tools और पुराने Unix utilities सहित पूरा C ecosystem एक security mess है जो exploit होने का इंतजार कर रहा है, और अंततः exploit होगा
    बस यह देख लीजिए कि एक dot से सब कुछ बिगाड़ना कितना आसान है. अब लोगों को समझना चाहिए कि दुनिया की security C पर दाँव पर नहीं लगाई जा सकती
    आधुनिक toolchain वाले Ada या Rust का इस्तेमाल करें

    • Rust में dot जोड़ने से वह नहीं बिगड़ता क्या?
  • समझ नहीं आता कि यह code review पार करके merge कैसे हो गया. अगर मैं कुछ miss नहीं कर रहा हूँ, तो यह हास्यास्पद रूप से लापरवाही भरा लगता है

    • malicious actor repository का co-maintainer था, कुछ समय से original maintainer से ज्यादा active था, और उसके पास full commit rights थे. कोई PR या review नहीं था, सीधे master में commit किया गया था
      ऊपर से यह test file के रूप में marked binary file, यानी “good”/“bad” xz compressed test files के अंदर बुरी तरह obfuscate किया गया था. जब तक पता न हो कि क्या ढूँढना है, इसे पहचानने का कोई तरीका नहीं था
    • test file commit message में दावा था कि इसे random number generator से बनाया गया है. release tarball बनाने वाले व्यक्ति ने आखिरी line सही जगह डाल दी, लेकिन उसे repository में check in नहीं किया
    • जिस package में सिर्फ एक active maintainer हो, उसमें code review नहीं होता
  • LTS distros इस्तेमाल करने से कुछ हद तक protection मिल सकती है. Slackware packages के लिए lzma, यानी tar.xz, इस्तेमाल करता लगता है, लेकिन -current को छोड़कर आखिरी stable release में यह समस्या नहीं थी
    अगर free software की दिशा में एक कदम और जाना चाहें तो Hyperbola GNU में भी यह समस्या नहीं थी
    इसके अलावा Slackware -current भी sshd को xz से link नहीं करता, और systemd भी इस्तेमाल नहीं करता