Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Hej alla! Jag heter Dmitry Samsonov, jag arbetar som ledande systemadministratör på Odnoklassniki. Vi har mer än 7 tusen fysiska servrar, 11 tusen containrar i vårt moln och 200 applikationer, som i olika konfigurationer bildar 700 olika kluster. De allra flesta servrar kör CentOS 7.
Den 14 augusti 2018 publicerades information om sårbarheten FragmentSmack
(CVE-2018-5391) och SegmentSmack (CVE-2018-5390). Det är sårbarheter med en nätverksattackvektor och en ganska hög poäng (7.5), vilket hotar denial of service (DoS) på grund av resursutmattning (CPU). En kärnfix för FragmentSmack föreslogs inte vid den tiden, den kom dessutom ut mycket senare än publiceringen av information om sårbarheten. För att eliminera SegmentSmack föreslogs det att uppdatera kärnan. Själva uppdateringspaketet släpptes samma dag, allt som återstod var att installera det.
Nej, vi är inte alls emot att uppdatera kärnan! Men det finns nyanser...

Hur vi uppdaterar kärnan vid produktion

I allmänhet, inget komplicerat:

  1. Ladda ner paket;
  2. Installera dem på ett antal servrar (inklusive servrar som är värd för vårt moln);
  3. Se till att inget är trasigt;
  4. Se till att alla standardkärninställningar tillämpas utan fel;
  5. Vänta några dagar;
  6. Kontrollera serverns prestanda;
  7. Byt utplacering av nya servrar till den nya kärnan;
  8. Uppdatera alla servrar per datacenter (ett datacenter åt gången för att minimera effekten på användarna vid problem);
  9. Starta om alla servrar.

Upprepa för alla grenar av kärnorna vi har. För tillfället är det:

  • Stock CentOS 7 3.10 - för de flesta vanliga servrar;
  • Vanilla 4.19 - för vår moln med ett moln, eftersom vi behöver BFQ, BBR, etc.;
  • Elrepo kernel-ml 5.2 - för högt belastade distributörer, eftersom 4.19 brukade bete sig instabilt, men samma funktioner behövs.

Som du kanske har gissat tar det längsta tiden att starta om tusentals servrar. Eftersom inte alla sårbarheter är kritiska för alla servrar, startar vi bara om de som är direkt tillgängliga från Internet. I molnet, för att inte begränsa flexibiliteten, binder vi inte externt tillgängliga behållare till enskilda servrar med en ny kärna, utan startar om alla värdar utan undantag. Lyckligtvis är proceduren där enklare än med vanliga servrar. Till exempel kan tillståndslösa behållare helt enkelt flyttas till en annan server under en omstart.

Det återstår dock mycket arbete, och det kan ta flera veckor, och om det finns några problem med den nya versionen, upp till flera månader. Angripare förstår detta mycket väl, så de behöver en plan B.

FragmentSmack/SegmentSmack. Jobba runt

Lyckligtvis finns det för vissa sårbarheter en sådan plan B, och den kallas Workaround. Oftast är detta en förändring i kärnan/applikationsinställningar som kan minimera den möjliga effekten eller helt eliminera exploateringen av sårbarheter.

När det gäller FragmentSmack/SegmentSmack föreslogs denna lösning:

«Du kan ändra standardvärdena på 4MB och 3MB i net.ipv4.ipfrag_high_thresh och net.ipv4.ipfrag_low_thresh (och deras motsvarigheter för ipv6 net.ipv6.ipfrag_high_thresh och net.ipv6.ipfrag_low_thresh) till 256 kB respektive 192 kB och respektive lägre. Tester visar små till betydande minskningar i CPU-användning under en attack beroende på hårdvara, inställningar och förhållanden. Det kan dock bli en viss prestandapåverkan på grund av ipfrag_high_thresh=262144 byte, eftersom endast två 64K-fragment kan passa in i återmonteringskön åt gången. Det finns till exempel en risk att applikationer som fungerar med stora UDP-paket går sönder".

Själva parametrarna i kärndokumentationen beskrivs enligt följande:

ipfrag_high_thresh - LONG INTEGER
    Maximum memory used to reassemble IP fragments.

ipfrag_low_thresh - LONG INTEGER
    Maximum memory used to reassemble IP fragments before the kernel
    begins to remove incomplete fragment queues to free up resources.
    The kernel still accepts new fragments for defragmentation.

Vi har inga stora UDP:er på produktionstjänster. Det finns ingen fragmenterad trafik på LAN, det finns fragmenterad trafik på WAN, men inte signifikant. Det finns inga tecken - du kan rulla ut Workaround!

FragmentSmack/SegmentSmack. Första blod

Det första problemet vi stötte på var att molncontainrar ibland bara tillämpade de nya inställningarna delvis (endast ipfrag_low_thresh), och ibland inte tillämpade dem alls - de kraschade helt enkelt i början. Det var inte möjligt att reproducera problemet stabilt (alla inställningar tillämpades manuellt utan några svårigheter). Att förstå varför containern kraschar i början är inte heller så lätt: inga fel hittades. En sak var säker: att återställa inställningarna löser problemet med containerkrascher.

Varför räcker det inte att applicera Sysctl på värden? Behållaren bor i sitt eget dedikerade nätverk Namespace, så åtminstone en del av nätverkets Sysctl-parametrar i behållaren kan skilja sig från värden.

Hur exakt tillämpas Sysctl-inställningarna i behållaren? Eftersom våra behållare är oprivilegierade kommer du inte att kunna ändra någon Sysctl-inställning genom att gå in i själva behållaren - du har helt enkelt inte tillräckligt med rättigheter. För att köra containrar använde vårt moln på den tiden Docker (nu poddman). Parametrarna för den nya behållaren skickades till Docker via API, inklusive de nödvändiga Sysctl-inställningarna.
När man letade igenom versionerna visade det sig att Docker API inte returnerade alla fel (åtminstone i version 1.10). När vi försökte starta behållaren via "docker run" såg vi äntligen åtminstone något:

write /proc/sys/net/ipv4/ipfrag_high_thresh: invalid argument docker: Error response from daemon: Cannot start container <...>: [9] System error: could not synchronise with container process.

Parametervärdet är inte giltigt. Men varför? Och varför är det inte giltigt bara ibland? Det visade sig att Docker inte garanterar i vilken ordning Sysctl-parametrarna tillämpas (den senaste testade versionen är 1.13.1), så ibland försökte ipfrag_high_thresh ställas in på 256K när ipfrag_low_thresh fortfarande var 3M, det vill säga den övre gränsen var lägre än den nedre gränsen, vilket ledde till felet.

Vid den tiden använde vi redan vår egen mekanism för att omkonfigurera behållaren efter start (frysning av behållaren efter gruppfrys och exekvera kommandon i containerns namnutrymme via ip netns), och vi lade också till att skriva Sysctl-parametrar till den här delen. Problemet löstes.

FragmentSmack/SegmentSmack. Första blodet 2

Innan vi hann förstå användningen av Workaround i molnet började de första sällsynta klagomålen från användare komma. Då hade det gått flera veckor sedan man började använda Workaround på de första servrarna. Den första undersökningen visade att klagomål mottogs mot enskilda tjänster och inte alla servrar för dessa tjänster. Problemet har återigen blivit extremt osäkert.

Först och främst försökte vi naturligtvis återställa Sysctl-inställningarna, men detta hade ingen effekt. Olika manipulationer med servern och applikationsinställningarna hjälpte inte heller. Omstart hjälpte. Att starta om Linux är lika onaturligt som det var normalt för Windows förr i tiden. Det hjälpte dock, och vi kritade upp det till ett "kärnfel" när vi tillämpade de nya inställningarna i Sysctl. Vad oseriöst det var...

Tre veckor senare återkom problemet. Konfigurationen av dessa servrar var ganska enkel: Nginx i proxy/balanseringsläge. Inte mycket trafik. Ny introduktion: antalet 504 fel på klienter ökar varje dag (Gateway Timeout). Grafen visar antalet 504 fel per dag för denna tjänst:

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Alla fel är ungefär samma backend - om den som finns i molnet. Minnesförbrukningsdiagrammet för paketfragment på denna backend såg ut så här:

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Detta är en av de mest uppenbara manifestationerna av problemet i operativsystemgrafer. I molnet, precis samtidigt, fixades ett annat nätverksproblem med QoS (Traffic Control)-inställningar. På grafen över minnesförbrukning för paketfragment såg det exakt likadant ut:

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Antagandet var enkelt: om de ser likadana ut på graferna har de samma anledning. Dessutom är alla problem med denna typ av minne extremt sällsynta.

Kärnan i det fixade problemet var att vi använde fq-paketschemaläggaren med standardinställningar i QoS. Som standard, för en anslutning, låter den dig lägga till 100 paket till kön, och vissa anslutningar, i situationer med kanalbrist, började täppa till kön till kapacitet. I det här fallet släpps paket. I tc-statistik (tc -s qdisc) kan det ses så här:

qdisc fq 2c6c: parent 1:2c6c limit 10000p flow_limit 100p buckets 1024 orphan_mask 1023 quantum 3028 initial_quantum 15140 refill_delay 40.0ms
 Sent 454701676345 bytes 491683359 pkt (dropped 464545, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
  1024 flows (1021 inactive, 0 throttled)
  0 gc, 0 highprio, 0 throttled, 464545 flows_plimit

"464545 flows_plimit" är de paket som tappats på grund av att kögränsen för en anslutning överskridits, och "dropped 464545" är summan av alla tappade paket i denna schemaläggare. Efter att ha ökat kölängden till tusen och startat om behållarna slutade problemet att uppstå. Du kan luta dig tillbaka och dricka en smoothie.

FragmentSmack/SegmentSmack. Sista blodet

För det första, flera månader efter tillkännagivandet av sårbarheter i kärnan, dök äntligen en fix för FragmentSmack upp (låt mig påminna dig om att tillsammans med tillkännagivandet i augusti släpptes en fix endast för SegmentSmack), vilket gav oss en chans att överge Workaround, vilket orsakade oss ganska mycket problem. Under denna tid hade vi redan lyckats överföra några av servrarna till den nya kärnan, och nu var vi tvungna att börja från början. Varför uppdaterade vi kärnan utan att vänta på FragmentSmack-fixen? Faktum är att processen att skydda mot dessa sårbarheter sammanföll (och slogs samman) med processen att uppdatera själva CentOS (vilket tar ännu mer tid än att bara uppdatera kärnan). Dessutom är SegmentSmack en farligare sårbarhet, och en fix för den dök upp omedelbart, så det var vettigt ändå. Vi kunde dock inte bara uppdatera kärnan på CentOS eftersom FragmentSmack-sårbarheten, som dök upp under CentOS 7.5, bara fixades i version 7.6, så vi var tvungna att stoppa uppdateringen till 7.5 och börja om från början med uppdateringen till 7.6. Och detta händer också.

För det andra har sällsynta användarklagomål om problem kommit tillbaka till oss. Nu vet vi redan med säkerhet att de alla är relaterade till uppladdningen av filer från klienter till några av våra servrar. Dessutom gick ett mycket litet antal uppladdningar från den totala massan genom dessa servrar.

Som vi minns från historien ovan hjälpte det inte att rulla tillbaka Sysctl. Omstart hjälpte, men tillfälligt.
Misstankar om Sysctl togs inte bort, men denna gång var det nödvändigt att samla in så mycket information som möjligt. Det fanns också en enorm brist på förmåga att återskapa uppladdningsproblemet på klienten för att kunna studera mer exakt vad som hände.

Analys av all tillgänglig statistik och loggar förde oss inte närmare att förstå vad som hände. Det fanns en akut brist på förmåga att reproducera problemet för att ”känna” ett specifikt samband. Slutligen lyckades utvecklarna, med hjälp av en speciell version av applikationen, uppnå stabil reproduktion av problem på en testenhet när de var anslutna via Wi-Fi. Detta var ett genombrott i utredningen. Klienten kopplade till Nginx, som proxy till backend, som var vår Java-applikation.

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Dialogen för problem var så här (fixad på Nginx proxy-sidan):

  1. Klient: begäran om att få information om nedladdning av en fil.
  2. Java-server: svar.
  3. Klient: POST med fil.
  4. Java-server: fel.

Samtidigt skriver Java-servern till loggen att 0 byte data togs emot från klienten, och Nginx-proxyn skriver att begäran tog mer än 30 sekunder (30 sekunder är timeouten för klientapplikationen). Varför timeout och varför 0 byte? Ur ett HTTP-perspektiv fungerar allt som det ska, men POST med filen verkar försvinna från nätverket. Dessutom försvinner det mellan klienten och Nginx. Det är dags att beväpna dig med Tcpdump! Men först måste du förstå nätverkskonfigurationen. Nginx proxy ligger bakom L3-balanseraren NFware. Tunneling används för att leverera paket från L3-balanseraren till servern, som lägger till sina rubriker till paketen:

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

I det här fallet kommer nätverket till denna server i form av Vlan-taggad trafik, som också lägger till sina egna fält till paketen:

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Och denna trafik kan också vara fragmenterad (den mycket lilla andelen av inkommande fragmenterad trafik som vi pratade om när vi bedömde riskerna från Workaround), vilket också ändrar innehållet i rubrikerna:

Se upp för sårbarheter som leder till arbetsrundor. Del 1: FragmentSmack/SegmentSmack

Än en gång: paket är inkapslade med en Vlan-tagg, inkapslade med en tunnel, fragmenterade. För att bättre förstå hur detta händer, låt oss spåra paketvägen från klienten till Nginx-proxyn.

  1. Paketet når L3-balanseraren. För korrekt routing inom datacentret kapslas paketet in i en tunnel och skickas till nätverkskortet.
  2. Eftersom paket + tunnelhuvuden inte passar in i MTU:n skärs paketet i fragment och skickas till nätverket.
  3. Omkopplaren efter L3-balanseraren, när den tar emot ett paket, lägger till en Vlan-tagg till den och skickar den vidare.
  4. Switchen framför Nginx-proxyn ser (baserat på portinställningarna) att servern förväntar sig ett Vlan-inkapslat paket, så den skickar det som det är, utan att ta bort Vlan-taggen.
  5. Linux tar fragment av enskilda paket och slår samman dem till ett stort paket.
  6. Därefter når paketet Vlan-gränssnittet, där det första lagret tas bort från det - Vlan-inkapsling.
  7. Linux skickar det sedan till tunnelgränssnittet, där ett annat lager tas bort från det - tunnelinkapsling.

Svårigheten är att skicka allt detta som parametrar till tcpdump.
Låt oss börja från slutet: finns det rena (utan onödiga rubriker) IP-paket från klienter, med vlan och tunnelinkapsling borttagen?

tcpdump host <ip клиента>

Nej, det fanns inga sådana paket på servern. Så problemet måste vara där tidigare. Finns det några paket med endast Vlan-inkapsling borttagen?

tcpdump ip[32:4]=0xx390x2xx

0xx390x2xx är klientens IP-adress i hex-format.
32:4 — adress och längd på fältet där SCR IP skrivs i tunnelpaketet.

Fältadressen måste väljas med brute force, eftersom de på Internet skriver om 40, 44, 50, 54, men det fanns ingen IP-adress där. Du kan också titta på ett av paketen i hex (parametern -xx eller -XX i tcpdump) och beräkna den IP-adress du känner till.

Finns det paketfragment utan Vlan- och Tunnelinkapsling borttagen?

tcpdump ((ip[6:2] > 0) and (not ip[6] = 64))

Denna magi kommer att visa oss alla fragment, inklusive det sista. Förmodligen kan samma sak filtreras efter IP, men jag försökte inte, eftersom det inte finns så många sådana paket, och de jag behövde var lätt att hitta i det allmänna flödet. Här är de:

14:02:58.471063 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 1516: (tos 0x0, ttl 63, id 53652, offset 0, flags [+], proto IPIP (4), length 1500)
    11.11.11.11 > 22.22.22.22: truncated-ip - 20 bytes missing! (tos 0x0, ttl 50, id 57750, offset 0, flags [DF], proto TCP (6), length 1500)
    33.33.33.33.33333 > 44.44.44.44.80: Flags [.], seq 0:1448, ack 1, win 343, options [nop,nop,TS val 11660691 ecr 2998165860], length 1448
        0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
        0x0010: 4500 05dc d194 2000 3f09 d5fb 0a66 387d E.......?....f8}
        0x0020: 1x67 7899 4500 06xx e198 4000 3206 6xx4 [email protected].
        0x0030: b291 x9xx x345 2541 83b9 0050 9740 0x04 .......A...P.@..
        0x0040: 6444 4939 8010 0257 8c3c 0000 0101 080x dDI9...W.......
        0x0050: 00b1 ed93 b2b4 6964 xxd8 ffe1 006a 4578 ......ad.....jEx
        0x0060: 6966 0000 4x4d 002a 0500 0008 0004 0100 if..MM.*........

14:02:58.471103 In 00:de:ff:1a:94:11 ethertype IPv4 (0x0800), length 62: (tos 0x0, ttl 63, id 53652, offset 1480, flags [none], proto IPIP (4), length 40)
    11.11.11.11 > 22.22.22.22: ip-proto-4
        0x0000: 0000 0001 0006 00de fb1a 9441 0000 0800 ...........A....
        0x0010: 4500 0028 d194 00b9 3f04 faf6 2x76 385x E..(....?....f8}
        0x0020: 1x76 6545 xxxx 1x11 2d2c 0c21 8016 8e43 .faE...D-,.!...C
        0x0030: x978 e91d x9b0 d608 0000 0000 0000 7c31 .x............|Q
        0x0040: 881d c4b6 0000 0000 0000 0000 0000 ..............

Dessa är två fragment av ett paket (samma ID 53652) med ett fotografi (ordet Exif är synligt i det första paketet). På grund av att det finns paket på den här nivån, men inte i den sammanslagna formen på soptippen, ligger problemet helt klart med monteringen. Äntligen finns det dokumentära bevis på detta!

Paketavkodaren avslöjade inga problem som skulle förhindra konstruktionen. Testade här: hpd.gasmi.net. Till en början, när du försöker stoppa något där, gillar avkodaren inte paketformatet. Det visade sig att det fanns några extra två oktetter mellan Srcmac och Ethertype (ej relaterat till fragmentinformation). Efter att ha tagit bort dem började dekodern fungera. Det visade dock inga problem.
Vad man än kan säga, så hittades inget annat än de Sysctl. Allt som återstod var att hitta ett sätt att identifiera problemservrar för att förstå omfattningen och besluta om ytterligare åtgärder. Den krävda räknaren hittades tillräckligt snabbt:

netstat -s | grep "packet reassembles failed”

Det finns också i snmpd under OID=1.3.6.1.2.1.4.31.1.1.16.1 (ipSystemStatsReasmFails).

"Antalet misslyckanden som upptäckts av IP-återmonteringsalgoritmen (av någon anledning: timeout, fel, etc.)."

Bland gruppen av servrar där problemet studerades ökade denna räknare snabbare på två, på två långsammare och på två till ökade den inte alls. Att jämföra dynamiken hos denna räknare med dynamiken för HTTP-fel på Java-servern visade en korrelation. Det vill säga att mätaren skulle kunna övervakas.

Att ha en tillförlitlig indikator på problem är mycket viktigt så att du exakt kan avgöra om återställning av Sysctl hjälper, eftersom vi från föregående berättelse vet att detta inte omedelbart kan förstås från applikationen. Denna indikator skulle tillåta oss att identifiera alla problemområden i produktionen innan användarna upptäcker det.
Efter att ha rullat tillbaka Sysctl upphörde övervakningsfelen, därmed var orsaken till problemen bevisad, liksom det faktum att återställningen hjälper.

Vi rullade tillbaka fragmenteringsinställningarna på andra servrar, där ny övervakning kom till spel, och någonstans tilldelade vi ännu mer minne för fragment än vad som tidigare var standard (detta var UDP-statistik, vars partiella förlust inte märktes mot den allmänna bakgrunden) .

De viktigaste frågorna

Varför är paket fragmenterade på vår L3-balanserare? De flesta paket som kommer från användare till balanserare är SYN och ACK. Storlekarna på dessa paket är små. Men eftersom andelen sådana paket är mycket stor, märkte vi mot deras bakgrund inte närvaron av stora paket som började fragmenteras.

Anledningen var ett trasigt konfigurationsskript advmss på servrar med Vlan-gränssnitt (det fanns väldigt få servrar med taggad trafik i produktion vid den tiden). Advmss låter oss förmedla informationen till kunden om att paket i vår riktning bör vara mindre i storlek så att de inte behöver fragmenteras efter att ha fäst tunnelhuvuden till dem.

Varför hjälpte inte Sysctl-återställning, men omstart gjorde det? Återställning av Sysctl ändrade mängden tillgängligt minne för att slå samman paket. Samtidigt ledde tydligen själva faktumet med minnesspill för fragment till att anslutningarna bromsades ned, vilket ledde till att fragment försenades länge i kön. Det vill säga att processen gick i cykler.
Omstarten rensade minnet och allt återgick till sin ordning.

Var det möjligt att klara sig utan Workaround? Ja, men det finns en stor risk att lämna användare utan service i händelse av en attack. Naturligtvis resulterade användningen av Workaround i olika problem, inklusive nedgången av en av tjänsterna för användarna, men vi anser ändå att åtgärderna var berättigade.

Stort tack till Andrey Timofeev (atimofejev) för hjälp med att genomföra utredningen, samt Alexey Krenev (enhetx) - för det enorma arbetet med att uppdatera Centos och kärnor på servrar. En process som i det här fallet fick startas från början flera gånger, varför den drog ut på tiden i många månader.

Källa: will.com

Lägg en kommentar