Tinklaraštis

Frag Gap (CVE-2026-53362): 15 baitų, verčiančių UDP lizdą į root

Frag_Gap_(CVE-2026-53362)
CVE / Docker / Linux / Saugumas

Frag Gap (CVE-2026-53362): 15 baitų, verčiančių UDP lizdą į root

Įprastas procesas jūsų serveryje atidaro UDPv6 lizdą, sujungia į jį porą splice() iškvietimų iš pipe — ir per kelias sekundes jau skaito ir rašo į bet kurią fizinę atmintį. Ne per jūsų aplikacijos klaidą. Ne per pažeidžiamą systemd tarnybą. O per branduolio kodą, kuris tuos pačius 15 baitų suskaičiuoja du kartus: vieną kartą kaip duomenų dalį, antrą kartą kaip kažką, kas jau guli kitoje vietoje. Ši painiava vadinasi Frag Gap — CVE-2026-53362, lokali teisių eskalacija iki root, kuriai jau yra veikiantis eksploitas ir išsamus tyrėjų physicube ir qwerty aprašymas, paskelbtas 2026 m. liepos 20 d. Pagal Red Hat duomenis, pažeidžiamumui priskirtas CVSS 7.0 (vektorius AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H), CWE-131 — netinkamas buferio dydžio skaičiavimas; Amazon Linux savo advisory nurodo šiek tiek kitokį įvertinimą — 7.8. Skirtumas kelių dešimtųjų ribose branduolio CVE atveju yra normalu — skirtingi CNA vektorių perskaičiuoja truputį kitaip, bet abiem atvejais tai High klasė su pilnu poveikiu confidentiality, integrity ir availability. Ginčas eina ne dėl to, ar klaida rimta, o dėl kelių dešimtųjų balo. Pataisa jau yra, eksploitas viešas, bet CISA kol kas nepažymėjo šito kaip aktyviai išnaudojamo „laukinėje gamtoje” — klaida rasta ir atskleista per kernelCTF, Google branduolio pažeidžiamumų paieškos konkursą.

Šio atvejo specifika ta, kad klaida gyvena UDP corking posistemyje, skirtame IPv6 — kode, kuris yra kiekviename Linux serveryje su įjungtu CONFIG_IPV6=y, o tai absoliuti dauguma šiuolaikinių distribucijų. Trigeriui nereikia jokių specialių teisių: paprastas neprivilegijuotas lokalus vartotojas gali pasiekti root vos per keletą sisteminių iškvietimų.

KAS YRA UDP CORKING IR FRAGGAP

UDP corking — mechanizmas, sujungiantis kelis send() iškvietimus į vieną datagramą prieš jai išeinant į tinklą. Įjungiamas UDP_CORK vėliavėle: kol lizdas „užkimštas”, duomenys kaupiasi branduolio buferyje, o ne siunčiami iš karto. Tai optimizacija — mažiau paketų, mažiau antraštės pridėtinių kaštų.

Problema prasideda tada, kai sukaupti duomenys peržengia fragmento ribą — maksimalų dydį, telpantį į vieną tinklo paketą (MTU minus antraštės). Funkcija __ip6_append_data() suskaičiuoja baitus, kurie „peršoko” per šią ribą iš ankstesnio skb (socket buffer — branduolio struktūra, saugojanti tinklo paketą), ir vadina juos fraggap. Šiuos baitus reikia perkelti į kito skb tiesinę sritį — tai yra būtent nukopijuoti kaip duomenis, o ne traktuoti kaip kažką išorinio.

Būtent čia ir įvyksta painiava. Yra du būdai išskirti atmintį naujam skb: įprastas (non-paged) ir paged — tas, kuris naudojamas su MSG_MORE, NETIF_F_SG arba dideliais fragmentais. Non-paged šakoje fraggap įskaičiuojamas teisingai — kintamasis alloclen apima šiuos baitus. Paged šakoje — ne:

datalen = length + fraggap;
fraglen = datalen + fragheaderlen;
pagedlen = 0;

/* ... */

else {
    alloclen = fragheaderlen + transhdrlen;
    pagedlen = datalen - transhdrlen;
}

datalen jau apima fraggap. Bet alloclen — dydis, kuris realiai išskiriamas tiesinei sričiai — jo neįskaičiuoja. O pagedlen, priešingai, paveldi papildomus baitus iš datalen. Dėl to kintamasis copy sugriūna į neigiamą reikšmę, lygią -fraggap. Anksčiau tai sukeldavo klaidą -EINVAL — branduolys tiesiog atsisakydavo vykdyti operaciją. Bet commit’as ce650a166335, pridėtas siekiant palaikyti MSG_SPLICE_PAGES (zero-copy duomenų perdavimą tiesiai iš pipe į lizdą), panaikino šią patikrą būtent neigiamo copy atvejui. Nuo tos akimirkos klaida tapo realiai išnaudojama.

KUR TIKSLIAI ĮVYKSTA RAŠYMAS UŽ RIBŲ

Toliau kodas ignoruoja tai, kad fraggap yra duomenys, kuriems nerasta vietos, ir vis tiek juos nukopijuoja į tiesinę sritį:

data = skb_put(skb, fraglen - pagedlen);
data += fragheaderlen;

if (fraggap) {
    skb->csum = skb_copy_and_csum_bits(
        skb_prev, maxfraglen,
        data + transhdrlen, fraggap);
}

skb_put() pastumia buferio „uodegą” lygiai iki skb->end — realiai duomenims išskirtos atminties pabaigos. Iškart už šios ribos atmintyje guli struktūra skb_shared_info — pakete tarnybiniai metaduomenys: fragmentų skaičius, vėliavėlės, destruktoriaus rodyklė. fraggap baitų kopijavimas vyksta būtent šioje srityje — už to, kas realiai buvo išskirta duomenims, ribų.

Physicube ir qwerty aprašyme skaičiavimas atrodo taip: 320 baitų IPv6 maršruto antraštė duoda fragheaderlen = 360. Pirmasis skb yra 1287 baitų ilgio. Fragmento riba — 1272 baitai. Skirtumas — lygiai 15 fraggap baitų. Šie 15 baitų patenka tiksliai į skb_shared_info pradžią: poslinkiai nuo +0x00 iki +0x0e.

KAIP TAI IŠNAUDOJAMA

Ataka pastatyta ant dviejų splice() iškvietimų į užkimštą UDPv6 lizdą. Pirmasis perkelia 919 baitų iš pipe — su UDP ir maršruto antraštėmis galutinis skb gaunasi lygiai 1287 baitų, 15 daugiau nei fragmento riba. UDP_CORK laiko jį eilėje vietoj tiesioginio siuntimo. Antrasis splice() prideda dar 20 baitų — ir būtent šiame žingsnyje copy perskaičiavimas nueina į minusą, sukurdamas naują skb su fraggap = 15.

Iš 15 prieinamų baitų puolėjui reikia tik vieno — baito poslinkyje +0x02, lauko nr_frags struktūroje skb_shared_info. Tai prisegtų puslapių-fragmentų skaičiaus skaitiklis. Normaliai jis lygus nuliui šviežiame skb, bet ši konkreti atminties sritis nėra visiškai išvaloma: funkcija __finalize_skb_around() struktūrą išvalo tik iki lauko dataref, o masyvas frags[], kuriame kaip tik saugomi puslapių deskriptoriai, eina po jo ir gali turėti duomenų iš ankstesnio tos pačios atminties naudojimo. Nustatęs nr_frags = 1 per OOB rašymą, eksploitas priverčia branduolį patikėti, kad frags[0] yra galiojantis puslapio deskriptorius, nors šis puslapis jau buvo atlaisvintas.

Toliau eina klasikinė grooming technika: eksploitas iš anksto pastato galiojantį puslapio deskriptorių tame pačiame objektų kaupe per tee() iškvietimų seriją, pakeliančią pradinio pipe puslapio refcount iki devynių, kad jis išgyventų atlaisvinimą ir tos pačios atminties pakartotinį panaudojimą. Kai lizdas užsidaro, skb destruktorius iškviečia put_page() visiems puslapiams, kurių skaičių nurodo tas pats nr_frags — įskaitant tą, kuris buvo aktyvuotas per OOB. Gaunasi kabantis (dangling) puslapis: fizinė atmintis grąžinta atgal į alokatorių, bet į ją vis dar rodo puolėjo pipe_buffer.

Toliau eksploitas iškviečia mmap() ir liečia jį taip, kad alokatorius pakartotinai panaudotų būtent šį fizinį puslapį kaip puslapių lentelės įrašą (page table entry, PTE). Kai tik tai įvyksta, rašymas per pipe į seną kabantį puslapį virsta rašymu į paties proceso PTE — vadinasi, puolėjas gali keisti fizinį adresą, į kurį rodo jo paties virtuali atmintis. Tai suteikia laisvą fizinės atminties skaitymą ir rašymą iš userspace.

Paskutinis žingsnis — rasti fizinėje atmintyje branduolio kintamąjį core_pattern (jis nustato, kokia programa paleidžiama, kai procesas avariškai baigia darbą su core dump) ir perrašyti jį į kelią, panašų į |/proc/%P/fd/666 %P. Kai vaikinis procesas avariškai baigiasi dėl SIGSEGV, branduolys paleidžia šią komandą root teisėmis.

KAS KEIČIASI PRIKLAUSOMAI NUO SISTEMOS KONFIGŪRACIJOS

Suveikimo sąlyga viena, bet griežta: CONFIG_IPV6=y. Jei IPv6 branduolio dalis išjungta surinkimo etape — atakos vektorius IPv6 keliui neegzistuoja. Bet klaida turėjo ir IPv4 versiją su ta pačia logine klaida, uždarytą atskira pataisa — commit’u eca856950f7c. Vien tik IPv6 išjungimo nepakanka, jei jūsų branduolys nepataisytas: IPv4 kelias patvirtintas kaip išnaudojamas — tai aprašyme aiškiai patvirtino Sultan Alsawaf iš CIQ komandos.

Konteinerizacija savaime neapsaugo. Pažeidžiamumas gyvena branduolio tinklo stack’e, o ne userspace kode, todėl neprivilegijuotas vartotojas Docker konteineryje be papildomų syscall apribojimų (seccomp profilio, blokuojančio splice() arba tinklo operacijas) lygiai taip pat pasiekia root serveryje — jei konteineris naudoja host’o branduolį, kas su Docker beveik visada teisinga.

ĮVYKIŲ EIGA

Klaida fraggap apskaitoje pateko į branduolį 2022 m. liepos 12 d. — commit 773ba4fe9104 „ipv6: avoid partial copy for zc”, zero-copy darbo dalis. Po metų, 2023 m. rugpjūčio 2 d., commit’as ce650a166335 panaikino neigiamo copy patikrą MSG_SPLICE_PAGES atvejui — būtent šis momentas padarė klaidą realiai suveikiančią, o ne vien latentine logine klaida. Trejus metus ši kombinacija tiesiog laukė, kol kas nors iš jos sudarys eksploitą.

2026 m. gegužės 15 d. physicube ir qwerty pranešė apie pažeidžiamumą tiesiogiai [email protected]. IPv6 pataisa (commit 736b380e28d0) pateko į tinklo šaką net birželio 21 d., o į pagrindinę branduolio šaką — birželio 25 d. CVE identifikatorių CVE-2026-53362 Linux kernel CNA paskelbė liepos 4 d. Liepos 14 d. tyrėjai informavo uždarą Openwall sąrašą linux-distros — standartinę koordinuoto atskleidimo procedūrą distribucijoms. Pilnas aprašymas ir veikiantis eksploitas viešai pasirodė 2026 m. liepos 20 d. — juos galima rasti GitHub repozitorijoje qwerty-po/security-research.

KODĖL TAI SVARBU

Hostingo tiekėjams su multi-tenant aplinka tai viena blogiausių klaidų kategorijų: neprivilegijuotas vartotojas bendrame hostinge, VPS su bendru branduoliu arba bet kuriame konteineryje be griežto seccomp profilio gauna root host’e — tai yra prieigą prie visų kitų klientų toje pačioje mašinoje. Nereikia nei specialios programinės įrangos, nei specifinės aplikacijos konfigūracijos — tik veikiantis branduolys su įjungtu IPv6 ir galimybe atidaryti UDP lizdą, o tai paprastai leidžiama pagal numatytuosius nustatymus.

Paprastam sistemos administratoriui, valdančiam dedikuotus serverius, rizika mažesnė, bet ne nulinė: jei serveryje yra bent viena neprivilegijuota shell prieiga — programuotojams, CI/CD raneriams, bet kuriems tarnybiniams paskyroms su ribotomis teisėmis — ši klaida paverčia ją pilnu root. Turint omenyje, kad eksploitas jau paskelbtas kartu su aprašymu, laikas nuo publikacijos iki adaptuotų variantų atsiradimo realiose atakose gali būti matuojamas dienomis, ne savaitėmis.

Atskira nerimo priežastis — pati klaidos prigimtis. Tai ne buferio perpildymas dvasia „pamiršo patikrinti ilgį”. Tai apskaitos desinchronizacija tarp dviejų kodo šakų, kurios tuos pačius baitus formaliai apdoroja skirtingai. Tokias klaidas sistemingai sunkiau pagauti statine analize ir kodo peržiūra, nes kiekviena atskira šaka atrodo teisinga — problema atsiranda tik palyginus du vykdymo kelius.

ATNAUJINIMAS

IPv6 pataisa prieinama commit’e 736b380e28d0, IPv4 — commit’e eca856950f7c. Pagal oficialų advisory, abi pataisos įtrauktos į stabilias branduolio versijas 6.1.177, 6.6.144, 6.12.95, 6.18.38, 7.1.3, taip pat į 7.2-rc1.

Debian ir Ubuntu sistemose įdiegtą branduolio versiją galima patikrinti taip — komanda išveda jūsų sistemoje surinkto paketo versiją:

uname -r

Jei versija žemesnė nei nurodytos aukščiau jūsų šakoje — branduolys pažeidžiamas. Toliau standartinis atnaujinimas per paketų tvarkyklę:

apt-get update && apt-get upgrade linux-image-$(uname -r | sed 's/-generic//')

Svarbus momentas: šio straipsnio publikavimo metu, pagal oficialų Ubuntu Security puslapį, pagrindiniams branduolio paketams linux, linux-aws, linux-gcp, linux-azure 24.04 LTS ir 26.04 LTS leidimuose statusas vis dar „Vulnerable” — pataisytas paketas dar neišleistas. Prieš laikydami klausimą uždarytu, patikrinkite aktualų statusą puslapyje ubuntu.com/security/CVE-2026-53362: kitas apt atnaujinimas automatiškai atsisius pataisą, kai tik Canonical išleis surinktą paketą, bet iki to momento jums reikia laikinos priemonės.

Kol pataisa dar nepasiekė jūsų distribucijos, laikina priemonė — užblokuoti vektorių per nftables, apribojant UDPv6 lizdų kūrimą neprivilegijuotiems vartotojams, arba, jei IPv6 jūsų infrastruktūroje visai nenaudojamas, išjungti jį per sysctl. Atkreipkite dėmesį: pilnam IPv6 išjungimui Debian/Ubuntu sistemose reikia iš karto abiejų parametrų, kitaip dalis stack’o liks aktyvi:

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1

Pirmoji komanda išjungia IPv6 visose jau esamose sąsajose, antroji — nustato tai kaip numatytąjį nustatymą sąsajoms, kurios atsiras vėliau (pavyzdžiui, po naujo Docker tilto pakėlimo). Be antros komandos naujas konteineris vėl atsidurs su aktyviu IPv6 stack’u. Tai būtent laikina priemonė, o ne pataisos pakaitalas: kai tik jūsų distribucijoje bus prieinamas atnaujintas branduolys — diekite jį, o jei IPv6 reikalingas tarnyboms — įjunkite atgal.

Tikslesniam variantui, jei visiškai išjungti IPv6 negalima, galima apriboti prieigą prie splice() per seccomp profilį konteineriams ir tarnybinėms paskyroms, kurioms šis syscall nereikalingas — bet šis kelias neaprašytas oficialiame advisory kaip rekomenduojama priemonė, todėl naudokite jį kaip papildomą defense-in-depth, o ne kaip pagrindinę apsaugą.

IŠVADOS

Jei esate hostingo tiekėjas su bendra infrastruktūra arba valdote multi-tenant konteinerių klasterį — tai prioritetas numeris vienas šią savaitę. Patikrinkite uname -r kiekviename mazge, palyginkite su pataisytomis versijomis (6.1.177 / 6.6.144 / 6.12.95 / 6.18.38 / 7.1.3 / 7.2-rc1) ir, jei atnaujinimas iš distribucijos dar neparuoštas — išjunkite IPv6 per abu sysctl parametrus dabar, o ne po to, kai atnaujinimas atsiras saugykloje.

Jei administruojate dedikuotus serverius be nepatikimų lokalių vartotojų — rizika mažesnė, bet nepraleiskite: CI/CD raneriai, tarnybinės paskyros ir bet kokios ribotos shell prieigos šios klaidos požiūriu irgi yra lokalūs vartotojai. Diekite pataisas pagal įprastą atnaujinimo ciklą, bet neatidėliokite.

Jei esate hostingo kliento pusėje, tiesioginio veiksmo nereikia — tai tiekėjo atsakomybė — bet verta paklausti jo tiesiogiai, kada konkrečiai CVE-2026-53362 bus uždarytas jūsų infrastruktūroje, ypač jei dalinatės mazgu su kitais klientais.

Leave your thought here

El. pašto adresas nebus skelbiamas. Būtini laukeliai pažymėti *