DDoS támadások elleni védelmet írunk az XDP-re. A magja

Az eXpress Data Path (XDP) technológia lehetővé teszi az adatok szabad kezelését Linux interfészeken, mielőtt a csomagok a kernel hálózati veremébe kerülnek. Az XDP alkalmazásai közé tartozik a DDoS-támadások védelme (CloudFlare), a bonyolult szűrők és a statisztikák gyűjtése (Netflix). Az XDP programokat az eBPF virtuális gépe futtatja, ezért korlátozások vonatkoznak mind a kódjára, mind a kernel elérhető funkcióira a szűrő típusa szerint.

Cikkünk célja, hogy pótolja a XDP-val kapcsolatos anyagok hiányosságait. Először is, ezekben kész kódok találhatók, amelyek az XDP sajátosságait könnyedén megkerülik: verifikálásra készültek, vagy túl egyszerűek ahhoz, hogy problémát okozzanak. Amikor később saját kódot próbálunk írni a nulláról, nem lesz tisztában azzal, mit kell kezdeni a sajátos hibákkal. Másodszor, nem foglalkozik azokkal a módokkal, hogyan lehet helyben tesztelni az XDP-t VM és „hardi” nélkül, miközben ezekkel is vannak „csapdák”. A szöveg a hálózatokkal és Linux-szal ismerkedő programozók számára készült, akiket érdekel az XDP és az eBPF.

Ebben a részben részletesen hogyan épül fel egy XDP-szűrő és hogyan lehet azt tesztelni, majd írnunk kell egy egyszerű változatát a híres SYN cookie mechanizmusnak a csomagkezelés szintjén. Egyelőre nem fogjuk képezni a „fehér listát”
ellenőrzött ügyfelek, számlálókat vezetni és kezelni a szűrőt – elegendőek a naplók.

C-ben fogunk írni – ez nem divatos, de praktikus. Az összes kód elérhető a GitHub-on, a cikk végén található linken, és lépésről lépésre commitokba van bontva, ahogyan a cikkben le van írva.

Felelősségkizárás. A cikk során egy mini-megoldás kerül kidolgozásra a DDoS-támadások elleni védekezésre, mert ez egy reális feladat az XDP számára és az én területem. Azonban a fő cél az, hogy megértsük a technológiát, ez nem egy útmutató a kész védelem létrehozására. A tanulási kód nem optimalizált, és bizonyos árnyalatokat figyelmen kívül hagy.

Rövid áttekintés az XDP-ről

Csak a lényeges pontokat fogom ismertetni, hogy ne ismételjem meg a dokumentációt és a meglévő cikkeket.

Tehát a szűrő kódja betöltésre kerül a kernelbe. A bejövő csomagok átadódnak a szűrőnek. Végül a szűrőnek döntést kell hoznia: továbbengedi a csomagot a kernelbe (XDP_PASS), eldobja a csomagot (XDP_DROP), vagy visszajuttatja azt (XDP_TX). A szűrő módosíthatja a csomagot, ez különösen aktuális a XDP_TX. A programot vészhelyzetben is lehet szakítani (XDP_ABORTED), és eldobni a csomagot, de ez az assert(0) – a hibakereséshez.

Az eBPF (kiterjesztett Berkley Packet Filter) virtuális gép kifejezetten egyszerűre lett tervezve, hogy a kernel ellenőrizni tudja, hogy a kód nem ciklizál és nem rongálja mások memóriáját. Összegzett korlátok és ellenőrzések:

  • Ciklusok (visszalépések) tilosak.
  • Van verem az adatok számára, de nincsenek funkciók (minden C-funkciónak be kell épülnie).
  • A verem és a csomagbuffer határain túlmutató memóriahozzáférés tilos.
  • A kód mérete korlátozott, de a gyakorlatban ez nem jelentős.
  • Csak a kernel speciális funkciói (eBPF segítők) hívhatóak.

A szűrő fejlesztése és telepítése a következőképpen zajlik:

  1. A forráskód (például, kernel.c) objektumkódra (kernel.o) van lefordítva az eBPF virtuális gép architektúrájára. 2019 októberétől az eBPF fordítását a Clang támogatja, és a GCC 10.1-ben ígérik.
  2. Ha a kód objektumban vannak hivatkozások a kernel struktúrákra (például táblákra és számlálókkal), a megfelelő ID-k helyett nullákat használnak, tehát az ilyen kód nem futtatható. A kernelbe történő betöltés előtt ezeket a nullákat ki kell cserélni a konkrét objektumok ID-jaira, amelyeket kernel hívásokkal hozunk létre (linkelni kell a kódot). Ezt külső segédprogramokkal vagy egy program írásával végezhetjük el, amely bizonyos szűrőt linkel és tölt be.
  3. A kernel érvényesíti a betölthető programot. Ellenőrzi a ciklusok és a csomag és a verem határain kívüli mozgások hiányát. Ha az érvényesítő nem tudja bizonyítani, hogy a kód helyes, a programot elutasítják – ügyelni kell arra, hogy kedvébe járjunk.
  4. A sikeres érvényesítés után a kernel az eBPF architektúra objektumkódját a rendszer architektúrájának gépi kódjává fordítja (just-in-time).
  5. A program csatlakozik az interfészhez, és elkezdi a csomagok feldolgozását.

Mivel az XDP a kernelben működik, a hibakeresés a nyomkövetési naplók és a program által szűrt vagy generált csomagok alapján történik. Ennek ellenére az eBPF biztosítja a tölthető kód biztonságát a rendszer számára, így az XDP-vel közvetlenül a helyi Linuxon is kísérletezhetünk.

Környezet előkészítése

Összeállítás

A Clang nem tud közvetlenül eBPF architektúrára objektumkódot generálni, így a folyamat két lépésből áll:

  1. A C kódot LLVM byte-kódra kell fordítani (clang -emit-llvm).
  2. A byte-kódot eBPF objektumkódra alakítani (llc -march=bpf -filetype=obj).

A szűrő írásakor hasznos lehet néhány fájl a segédfunkciók és makrók számára a kernel tesztjeiből. Fontos, hogy ezek megfeleljenek a kernel verziójának (KVER). Letöltjük őket a helpers/:

export KVER=v5.3.7
export BASE=https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/tools/testing/selftests/bpf
wget -P helpers --content-disposition "${BASE}/bpf_helpers.h?h=${KVER}" "${BASE}/bpf_endian.h?h=${KVER}"
unset KVER BASE

Makefile Arch Linux számára (kernel 5.3.7):

CLANG ?= clang
LLC ?= llc

KDIR ?= /lib/modules/$(shell uname -r)/build
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

CFLAGS = 
    -Ihelpers 
    
    -I$(KDIR)/include 
    -I$(KDIR)/include/uapi 
    -I$(KDIR)/include/generated/uapi 
    -I$(KDIR)/arch/$(ARCH)/include 
    -I$(KDIR)/arch/$(ARCH)/include/generated 
    -I$(KDIR)/arch/$(ARCH)/include/uapi 
    -I$(KDIR)/arch/$(ARCH)/include/generated/uapi 
    -D__KERNEL__ 
    
    -fno-stack-protector -O2 -g

xdp_%.o: xdp_%.c Makefile
    $(CLANG) -c -emit-llvm $(CFLAGS) $< -o - | 
    $(LLC) -march=bpf -filetype=obj -o $@

.PHONY: all clean

all: xdp_filter.o

clean:
    rm -f ./*.o

KDIR tartalmazza a kernel fejlécek útvonalát, ARCH — a rendszer architektúráját. Az útvonalak és eszközök kissé eltérhetnek a disztribútorok között.

Példa különbségekre Debian 10 (kernel 4.19.67) esetén

# другая команда
CLANG ?= clang
LLC ?= llc-7

# другой каталог
KDIR ?= /usr/src/linux-headers-$(shell uname -r)
ARCH ?= $(subst x86_64,x86,$(shell uname -m))

# два дополнительных каталога -I
CFLAGS = 
    -Ihelpers 
    
    -I/usr/src/linux-headers-4.19.0-6-common/include 
    -I/usr/src/linux-headers-4.19.0-6-common/arch/$(ARCH)/include 
    # далее без изменений

CFLAGS tartalmazza a segédfájlok fejléceit és néhány kernel fejléctartományt. A szimbólum __KERNEL__ jelzi, hogy az UAPI (userspace API) fejlécek a kernel kódjára vonatkoznak, mivel a szűrő a kernelben fut.

A verem védelmét ki lehet kapcsolni (-fno-stack-protector), mivel az eBPF kódellenőr ugyanakkor ellenőrzi a verem határait. Az optimalizálásokat azonnal érdemes bekapcsolni, mivel az eBPF bytecode mérete korlátozott.

Kezdjük egy olyan szűrővel, amely minden csomagot átenged és semmit sem tesz:

#include <uapi/linux/bpf.h>

#include <bpf_helpers.h>

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

A parancs make adatokat az ablakkezelő rendszerre vonatkozóan („Wayland”, „Wayland/drm”, „x11”) xdp_filter.o. Hol teszteljük most?

Tesztkörnyezet

A standnak két interfészt kell tartalmaznia: az egyiken lesz a szűrő, a másikon pedig a csomagokat fognak küldeni. Ezeknek teljes értékű Linux eszközöknek kell lenniük saját IP-kkel, hogy ellenőrizhessük, hogyan működnek a megszokott alkalmazások a szűrőnkkel.

A veth (virtual Ethernet) típusú eszközök megfelelőek: ez egy pár virtuális hálózati interfész, amely közvetlenül „összekapcsolódik” egymással. Ilyen módon létrehozhatók (ebben a szakaszban minden parancs ip futtatásához , felhasználó nevében fut, mivel alapvető rendszerbeállításokat módosítanak, de a WordPress közvetlenül egy normál felhasználóként működik.):

ip link add xdp-remote type veth peer name xdp-local

Itt xdp-remote és xdp-local — az eszközök nevei. Az xdp-local (192.0.2.1/24) fogja csatlakoztatni a szűrőt, míg xdp-remote (192.0.2.2/24) küldi a bejövő forgalmat. Van azonban egy probléma: az interfészek egy gépen találhatók, és a Linux nem fogja átkonfigurálni a forgalmat az egyik interfészen a másikra. Ezt ügyes szabályokkal lehet megoldani iptables, de ezek módosítani kénytelenek a csomagokat, ami nem kényelmes a hibakeresés során. Jobb, ha hálózati névtér használatát alkalmazzuk (network namespaces, a továbbiakban netns).

A hálózati névtér egy interfészek, forgalmi táblák és NetFilter szabályok halmazát tartalmazza, amelyek elkülönülnek a többi netns objektumtól. Minden folyamat egy névtérben fut, és csak ennek a netns-nek az objektumaihoz fér hozzá. Alapértelmezés szerint a rendszer egyetlen hálózati névtérrel rendelkezik minden objektumhoz, így a Linuxban való munka során nem kell tudni a netns-ről.

Hozzunk létre egy új névteret xdp-teszt és helyezzük át oda xdp-remote.

ip netns add xdp-teszt
ip link set dev xdp-távoli netns xdp-teszt

Így a folyamat, amelyik xdp-teszt, nem fog 'látni' xdp-local (marad a alapértelmezett netns-ben) és a 192.0.2.1 címre küldött csomagot az xdp-remote, mert ez az egyetlen elérhető interfész a 192.0.2.0/24 hálózaton ennek a folyamatnak. Ez a forgalom irányában is érvényes.

A névtér közötti áthelyezés során az interfészt le kell venni, és elveszíti a címét. Az interfész beállításához a netns-ben a következő parancsot kell futtatni ip ... ebben a névtérben a parancssorból ip netns exec:

ip netns exec xdp-teszt 
    ip address add 192.0.2.2/24 dev xdp-távoli
ip netns exec xdp-teszt 
    ip link set xdp-távoli up

Amint látható, ez nem különbözik a xdp-local alapértelmezett névtérben történő beállítástól:

    ip address add 192.0.2.1/24 dev xdp-helyi
    ip link set xdp-helyi up

Ha futtatjuk a tcpdump -tnevi xdp-helyi, láthatjuk, hogy a xdp-teszt-ből küldött csomagok, ezekre az interfészekre érkeznek:

ip netns exec xdp-teszt   ping 192.0.2.1

Jó ötlet shellt indítani a xdp-teszt. A repozitóriumban van egy script, amely automatizálja a tesztkörnyezet kezelését, például a környezet beállításához a sudo . /stand up és eltávolíthatjuk azt sudo . /stand down.

Nyomkövetés

A szűrőt a következő módon lehet csatlakoztatni a berendezéshez:

ip -force link set dev xdp-helyi xdp object xdp_filter.o verbose

Kulcs -force szükséges ahhoz, hogy új programot kapcsoljunk, ha egy másik már csatlakoztatva van. A 'Nincs hír, jó hír' nem vonatkozik erre a parancsra, a kimenet minden esetben bőséges. Megadása verbose nem kötelező, de ezzel a verifikáló kód munkájáról szóló jelentést és az assembler szöveglistáját is kapjuk:

Verifier analysis:

0: (b7) r0 = 2
1: (95) exit

A program leválasztása a interfészről:

ip link set dev xdp-helyi xdp off

A scriptben ezek a parancsok sudo . /stand attach és sudo . /stand detach.

A szűrő csatlakoztatásával ellenőrizhetjük, hogy ping továbbra is működik, de működik-e a program? Adjunk hozzá naplózást. A bpf_trace_printk() hasonlít a printf()-re,de csak legfeljebb három argumentumot támogat a sablonnal együtt, és egy korlátozott lista formátumot. A makró, bpf_printk() egyszerűsíti a hívást.

   SEC("prog")
   int xdp_main(struct xdp_md* ctx) {
+      bpf_printk("csomagot kaptam: %pn", ctx);
       return XDP_PASS;
   }

A kimenet a mag kernel nyomkövetési csatornára irányul, amit be kell kapcsolni:

echo -n 1 | sudo tee /sys/kernel/debug/tracing/options/trace_printk

Üzenetfolyam megtekintése:

cat /sys/kernel/debug/tracing/trace_pipe

Ezen parancsok mindkettő hívást végez: sudo ./stand log.

A Ping most ilyen üzeneteket kellene generáljon:

-110930 [004] ..s1 78803.244967: 0: kapott csomag: 00000000ac510377

Ha alaposan megnézünk a hitelesítő kimenetét, furcsa számításokat lehet észlelni:

0: (bf) r3 = r1
1: (18) r1 = 0xa7025203a7465
3: (7b) *(u64 *)(r10 -8) = r1
4: (18) r1 = 0x6b63617020746f67
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
8: (07) r1 += -16
9: (b7) r2 = 16
10: (85) call bpf_trace_printk#6

A helyzet az, hogy az eBPF programok számára nincs adat szekció, ezért az egyetlen mód a formátum sztring kódolására a VM utasításainak az azonnali argumentumai:

$ python -c "import binascii; print(bytes(reversed(binascii.unhexlify('0a7025203a74656b63617020746f67'))))"
b'kapott csomag: %pn'

Ezért a hibakeresési kimenet nagymértékben megnöveli a végső kódot.

XDP csomagküldés

Módosítsuk a szűrőt: lehetővé teszi, hogy az összes bejövő csomág visszaküldésre kerüljön. Ez hálózati szempontból nem helyes, mert a fejlécben található címeket ki kellene cserélni, de most az alapvető működés számít.

       bpf_printk("kapott csomag: %pn", ctx);
-      return XDP_PASS;
+      return XDP_TX;
   }

Indítás tcpdump Gyorsan emlékezzünk vissza, hogyan működik a moduláris aritmetika. Az óra már egy ismert koncepció. Az órák használata xdp-remote. Azt kellene mutatnia, hogy az ICMP Echo Request és a válasz azonos, de nem mutatja. Kiderül, hogy a működéshez XDP_TX a programon a xdp-local szükséges, hogy a párhuzamos interfésznek xdp-remote is legyen programja, legalább üres, és rendelkezésre álljon.

Honnan tudtam ezt?

A csomag útjának nyomon követése a kernelben lehetővé teszi a perf events mechanizmus, amely egyébként ugyanezt a virtuális gépet használja, tehát az eBPF részletezésekhez eBPF-t alkalmazunk.

Jót kell tenned a gonosz ellen, mert másból nem lehet.

$ sudo perf trace --call-graph dwarf -e 'xdp:*'
   0.000 ping/123455 xdp:xdp_bulk_tx:ifindex=19 action=TX sent=0 drops=1 err=-6
                                     veth_xdp_flush_bq ([veth])
                                     veth_xdp_flush_bq ([veth])
                                     veth_poll ([veth])

Mi az a 6-os kód?

$ errno 6
ENXIO 6 Nincs ilyen eszköz vagy cím

Funkció veth_xdp_flush_bq() kódot kap a veth_xdp_xmit()-tól, ahol a ENXIO keresése és megtalálása megjegyzés.

Állítsuk vissza a minimális szűrőt (XDP_PASS) a fájlban xdp_dummy.c, adjuk hozzá a Makefile-hoz, rendeljük hozzá a xdp-remote:

ip netns exec remote 
    ip link set dev int xdp object dummy.o

Most tcpdump az várható dolgokat mutatja:

62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64
62:57:8e:70:44:64 > 26:0e:25:37:8f:96, ethertype IPv4 (0x0800), length 98: (tos 0x0, ttl 64, id 13762, offset 0, flags [DF], proto ICMP (1), length 84)
    192.0.2.2 > 192.0.2.1: ICMP echo request, id 46966, seq 1, length 64

Ha a helyette csak ARP-k érkeznek, akkor a szűrőket el kell távolítani (ezt teszi sudo . /stand detach), átkonfigurálni ping, majd újra beállítani a szűrőket és megpróbálni ismét. A probléma az, hogy a szűrő XDP_TX hatással van az ARP-ra is, és ha a névterek
el tudták „felejteni” a 192.0.2.1 MAC-címét, akkor nem lesz képes feloldani ezt az IP-t. xdp-teszt Feladat meghatározása

Lépjünk át a kitűzött feladatra: írjunk egy XDP mechanizmust SYN cookie-kkal.

A SYN flood továbbra is népszerű DDoS támadási forma, amely lényege a következő: A kapcsolat létrehozásakor (TCP handshake) a szerver SYN-t kap, erőforrásokat foglal a jövőbeli kapcsolatra, majd SYNACK csomaggal válaszol, és várja az ACK-t. A támadó egyszerűen hamis címekkel továbbít olyan SYN csomagokat, amelyek másodpercenként ezreket küldenek el egy több ezer hostból álló botnetből. A szerver kénytelen azonnal erőforrásokat allokálni, amint a csomag megérkezik, és azok a nagy időzítés miatt felszabadulnak, ennek következtében a memória vagy a határok kimerülnek, új kapcsolatok nem fogadhatók el, a szolgáltatás nem elérhető.

Ha nem allokálunk erőforrásokat egy SYN csomaghoz, hanem csak SYNACK csomaggal válaszolunk, hogyan tudja a szerver megérteni, hogy az később érkező ACK csomag a meg nem őrzött SYN csomaghoz tartozik? Végül is a támadó hamis ACK csomagokat generálhat. A SYN cookie lényege, hogy a kapcsolat paramétereit olyan h hash-ezéssel kódolja, amely a címek, portok és az állandó só változásaiból származik. Ha az ACK megérkezett a só változása előtt, akkor újra kiszámolhatjuk a hash-t, és összehasonlíthatjuk a

acknum t. A támadó nem tudja hamisítani, hiszen a só titkosat tartalmaz, és a korlátozott sávszélesség miatt időben nem tudja újra próbálni. A SYN cookie régóta megvalósították a Linux kernelben, és akár automatikusan is bekapcsolódik, ha a SYN-ek túl gyorsan és tömegesen érkeznek. TCP handshake alapokA TCP adatátvitelt byte streamként biztosít, például a HTTP kérések a TCP-n keresztül kerülnek továbbításra. Az adatfolyam csomagokban kerül továbbításra. Minden TCP csomagnak logikai jelzői és 32-bités sorrend számai vannak: TCP handshake alapok A TCP csomagok a lentiek szerint

Amennyiben a buffer korlátozott, akkor a régebbi csomagok eltávolításra kerülnek.

Kiegészítve a legújabb szűrőszabályok beállításával, ami szükséges, hogy a címek listázásához mindhárom fennálljon a server side-tól.

A TCP nagyobb megbízhatóságot és sebességet tud garantálni a fejlesztők oldaláról, és hangulati szempontból is kiterjed a hozamra.

  • A zászlók kombinációja meghatározza egy adott csomag szerepét. A SYN zászló azt jelzi, hogy ez a küldő első csomagja a kapcsolatban. Az ACK zászló pedig azt jelenti, hogy a küldő megkapta az összes összeköttetési adatot a byte-ig. TCP handshake alapok. A csomagnak lehet több zászlója is, és a kombinációjuk alapján nevezik el, például SYNACK csomagnak.

  • A szekvenciaszám (seqnum) meghatározza a pozíciót az adatfolyamban az első byte számára, amely ebben a csomagban kerül átküldésre. Például, ha az első csomag X byte adatot tartalmazott, és a száma N volt, a következő csomag új adatokkal N+X lesz. A kapcsolat elején mindkét fél véletlenszerűen választja ki ezt a számot.

  • Az elismerési szám (acknum) ugyanolyan pozíciót jelöl, mint a seqnum, de nem a küldött byte számát, hanem a vevő első byte számát határozza meg, amelyet a küldő még nem látott.

A kapcsolat elején a feleknek meg kell állapodniuk t. A támadó nem tudja hamisítani, hiszen a só titkosat tartalmaz, és a korlátozott sávszélesség miatt időben nem tudja újra próbálni. és TCP handshake alapok. Az ügyfél SYN csomagot küld a saját seqnum = X. A szerver SYNACK csomaggal válaszol, amelybe beírja a saját seqnum = Y és beállítja acknum = X + 1. Az ügyfél a SYNACK-ra ACK csomaggal válaszol, ahol seqnum = X + 1, acknum = Y + 1. Ezt követően megkezdődik az adatok tényleges átadása.

Ha a partner nem erősíti meg a csomag fogadását, a TCP újraküldi a megadott időtúllépés alapján.

Miért nem használják mindig a SYN cookie-kat?

Először is, ha a SYNACK vagy az ACK elveszik, várakozni kell az újraküldésre, ami lelassítja a kapcsolat létrehozását. Másodsorban a SYN csomagban – és csak abban! – számos opciót továbbítanak, amelyek befolyásolják a kapcsolat további működését. A beérkező SYN csomagok megjegyzésének hiányában a szerver figyelmen kívül hagyja ezeket az opciókat, így a következő csomagokban az ügyfél már nem küldi el őket. A TCP így működhet, de a kapcsolat kezdeti szakaszában a minőség csökkenni fog.

Csomagok szempontjából az XDP programnak a következőket kell tennie:

  • a SYN-re SYNACK válasz cookie-val;
  • az ACK-re RST választ adni (megszakítani a kapcsolatot);
  • a többi csomagot eldobni.

Az algoritmus pszeudokódja a csomag elemzésével együtt:

Ha ez nem Ethernet,
    hagyja figyelmen kívül a csomagot.
Ha ez nem IPv4,
    hagyja figyelmen kívül a csomagot.
Ha a cím a hitelesített táblázatban szerepel,               (*)
        csökkentse a maradék ellenőrzések számlálóját,
        hagyja figyelmen kívül a csomagot.
Ha ez nem TCP,
    dobja el a csomagot.     (**)
Ha ez SYN,
    válaszoljon SYN-ACK-kel cookie-val.
Ha ez ACK,
    ha az acknum nem cookie-t tartalmaz,
        dobja el a csomagot.
    Vegye fel a címet a N maradék ellenőrzéssel rendelkező táblázatba.    (*)
    Válaszoljon RST-vel.   (**)
Minden más esetben dobja el a csomagot.

Egy (*) megjegyzett pontok, ahol kezelni kell a rendszer állapotát – az első lépésben nélkülük is el lehet boldogulni, egyszerűen megvalósítva a TCP kézfogást a SYN cookie generálásával seqnum-ként.

A helyszínen (**), amíg nincs táblázatunk, hagyjuk figyelmen kívül a csomagot.

A TCP kézfogás megvalósítása

A csomag elemzése és a kód hitelesítése

Szükségünk lesz a hálózati fejléc struktúráira: Ethernet (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) és TCP (uapi/linux/tcp.h). Az utolsót nem sikerült csatlakoztatnom hibák miatt, amelyek a atomic64_t-val kapcsolatosak, ezért a szükséges definíciókat kellett a kódba másolnom.

Minden olyan C-ben definiált funkciót, amely a könnyebb olvashatóság érdekében szerepel, be kell ágyazni a hívás helyére, mivel az eBPF ellenőrzője a kernelben tiltotta a visszalépéseket, tehát gyakorlatilag ciklusokat és funkcióhívásokat.

#define INTERNAL static __attribute__((always_inline))

A LOG() letiltja a kimenetet a kiadási verzióban.

A program egy funkciók láncolataként működik. Minden egy csomagot fogad, amelyben a megfelelő szint fejlécét kiemelik, például, process_ether() arra számít, hogy a etherki van töltve. Az elemzés eredménye alapján a funkció átadhatja a csomagot a magasabb szintre. A funkció eredménye – XDP művelet. Jelenleg a SYN és ACK kezelők minden csomagot hagynak figyelmen kívül.

struct Packet {
    struct xdp_md* ctx;

    struct ethhdr* ether;
    struct iphdr* ip;
    struct tcphdr* tcp;
};

INTERNAL int process_tcp_syn(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp_ack(struct Packet* packet) { return XDP_PASS; }
INTERNAL int process_tcp(struct Packet* packet) { ... }
INTERNAL int process_ip(struct Packet* packet) { ... }

INTERNAL int
process_ether(struct Packet* packet) {
    struct ethhdr* ether = packet->ether;

    LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

    if (ether->h_proto != bpf_ntohs(ETH_P_IP)) {
        return XDP_PASS;
    }

    \/\/ B
    struct iphdr* ip = (struct iphdr*)(ether + 1);
    if ((void*)(ip + 1) > (void*)packet->ctx->data_end) {
        return XDP_DROP; \/\* malformed packet *\/ 
    }

    packet->ip = ip;
    return process_ip(packet);
}

SEC("prog")
int xdp_main(struct xdp_md* ctx) {
    struct Packet packet;
    packet.ctx = ctx;

    \/\/ A
    struct ethhdr* ether = (struct ethhdr*)(void*)ctx->data;
    if ((void*)(ether + 1) > (void*)ctx->data_end) {
        return XDP_PASS;
    }

    packet.ether = ether;
    return process_ether(&packet);
}

Figyelem, hogy az A és B jelölésű ellenőrzések vannak. Ha az A-t megjegyezzük, a program összegyűlik, de a betöltéskor hitelesítési hiba lép fel:

Verifier elemzés:


11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
érvénytelen hozzáférés a csomaghoz, off=13 size=1, R7(id=0,off=0,r=0)
R7 eltolása a csomagon kívül van
feldolgozott 11 utasítás (limit 1000000) max_states_per_insn 0 összes állapot 0 csúcsállapotok 0 mark_read 0

Hiba a program/map betöltésekor!

Kulcssor érvénytelen hozzáférés a csomaghoz, off=13 size=1, R7(id=0,off=0,r=0): vannak végrehajtási utak, amikor a tizenharmadik bájt a puffer elejétől a csomagon kívül található. A listing alapján nehéz megérteni, hogy melyik sorról van szó, de van utasítás száma (12) és egy disassembláló, amely bemutatja a forráskód sorait:

llvm-objdump -S xdp_filter.o | less

Ebben az esetben ez a sor jelzi

LOG("Ether(proto=0x%x)", bpf_ntohs(ether->h_proto));

amelyből világossá válik, hogy a probléma a ether. Mindig így lenne.

A SYN válasza

A cél ezen a ponton az, hogy helyes SYNACK csomagot képezzünk, fixált t. A támadó nem tudja hamisítani, hiszen a só titkosat tartalmaz, és a korlátozott sávszélesség miatt időben nem tudja újra próbálni., amely a jövőben SYN cookie-ra cserélődik. Minden változás a process_tcp_syn() és környezetében történik.

A csomag ellenőrzése

Hallatlan, de ez a legmegjegyezhetőbb sor, pontosabban a hozzá írt megjegyzés:

/* Required to verify checksum calculation */
const void* data_end = (const void*)ctx->data_end;

Az első kódverzió írásakor az 5.1-es kernel volt használatban, amelynek verifikátoránál különbség volt a data_end és (const void*)ctx->data_end. A cikk írásának idején az 5.3.1-es kernel nem tartalmazott ilyen problémát. Lehetséges, hogy a fordító másképp hivatkozott a lokális változóra, mint a mezőre. Tanulság - nagy belső rétegességnél a kód egyszerűsítése segíthet.

Továbbá rutinszerű hossz-ellenőrzések a verifikátor dicsőségére; a MAX_CSUM_BYTES alatt.

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* érvénytelen csomag */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* megvalósítási korlátozás */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* érvénytelen csomag */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* megvalósítási korlátozás */
}

A csomag kicsomagolása

Kitöltjük t. A támadó nem tudja hamisítani, hiszen a só titkosat tartalmaz, és a korlátozott sávszélesség miatt időben nem tudja újra próbálni. és TCP handshake alapok, beállítjuk az ACK-t (a SYN már be van állítva):

const u32 cookie = 42;
tcp->ack_seq = bpf_htonl(bpf_ntohl(tcp->seq) + 1);
tcp->seq = bpf_htonl(cookie);
tcp->ack = 1;

Megcseréljük a TCP portokat, az IP címet és a MAC címeket. A standard könyvtár nem érhető el az XDP programból, így memcpy() — egy makró, amely elrejti a Clang intrinziket.

const u16 temp_port = tcp->source;
tcp->source = tcp->dest;
tcp->dest = temp_port;

const u32 temp_ip = ip->saddr;
ip->saddr = ip->daddr;
ip->daddr = temp_ip;

struct ethhdr temp_ether = *ether;
memcpy(ether->h_dest, temp_ether.h_source, ETH_ALEN);
memcpy(ether->h_source, temp_ether.h_dest, ETH_ALEN);

A checksum újraszámítása

Az IPv4 és TCP ellenőrző összegek megkövetelik a 16 bites szavak összeadását a fejlécekben, a fejléc mérete viszont a fordítás pillanatában ismeretlen. Ez probléma, mert a verifikáló nem engedi, hogy egy normál ciklus eljusson a tartományváltozóhoz. Viszont a fejléc mérete korlátozott: legfeljebb 64 bájt lehet minden egyes fejléc esetében. Készíthető egy rögzített számú iterációval rendelkező ciklus, amely idő előtt befejeződhet.

Megjegyzem, hogy létezik RFC 1624 arról, hogyan lehet részben újraszámítani az ellenőrző összeget, ha csak a csomagok rögzített szavai változtak meg. Azonban ez a módszer nem univerzális, a megvalósítása pedig nehezebben fenntartható.

Az ellenőrző összeg számító funkció:

#define MAX_CSUM_WORDS 32
#define MAX_CSUM_BYTES (MAX_CSUM_WORDS * 2)

INTERNAL u32
sum16(const void* data, u32 size, const void* data_end) {
    u32 s = 0;
#pragma unroll
    for (u32 i = 0; i < MAX_CSUM_WORDS; i++) {
        if (2*i >= size) {
            return s; /* normal exit */
        }
        if (data + 2*i + 1 + 1 > data_end) {
            return 0; /* should be unreachable */
        }
        s += ((const u16*)data)[i];
    }
    return s;
}

Annak ellenére, hogy méret a hívó kód által ellenőrizve, szükséges a második kilépési feltétel, hogy a verifikáló bizonyítani tudja a ciklus befejezését.

A 32 bites szavakhoz egy egyszerűbb verzió került megvalósításra:

INTERNAL u32
sum16_32(u32 v) {
    return (v >> 16) + (v & 0xffff);
}

A tényleges ellenőrző összegek újraszámítása és a csomag visszaküldése:

ip->check = 0;
ip->check = carry(sum16(ip, ip_len, data_end));

u32 tcp_csum = 0;
tcp_csum += sum16_32(ip->saddr);
tcp_csum += sum16_32(ip->daddr);
tcp_csum += 0x0600;
tcp_csum += tcp_len <check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

Funkció carry() a 32 bites összeget 16 bites szavakká alakítja át ellenőrző összeggé, a RFC 791 szerint.

TCP kézfogás ellenőrzése

A szűrő helyesen állítja fel a kapcsolatot netcat, átengedve a végső ACK-t, amelyre a Linux RST csomaggal válaszolt, mivel a hálózati verem nem kapott SYN-t - azt átformálták SYNACK-ká és visszaküldték - és az OS szempontjából a csomag egy nyitott kapcsolathoz nem tartozó csomagként érkezett.

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Kapcsolat visszaállítva az ismeretlen felől

Fontos, hogy éppen teljes alkalmazásokkal ellenőrizzük és figyeljük tcpdump Gyorsan emlékezzünk vissza, hogyan működik a moduláris aritmetika. Az óra már egy ismert koncepció. Az órák használata xdp-remote mert például hping3 nem reagál a hibás ellenőrző összegekre.

Az XDP szempontjából a tényleges ellenőrzés triviális. A számítási algoritmus primitív, és valószínűleg sebezhető egy rafinált támadóval szemben. A Linux kernel például kriptográfiai SipHasht használ, de a XDP-hez való megvalósítása egyértelműen túlmutat a cikken.

Új TODO-k merültek fel a külső interakciókkal kapcsolatosan:

  • Az XDP program nem tárolhatja a cookie_seed (a só titkos részét) globális változóban, szükséges a kernelben egy tároló, amelynek értéke időről időre frissül egy megbízható generátorból.

  • Ha a SYN cookie egy ACK csomagban egyezik, akkor ne nyomj meg üzenetet, hanem jegyezd meg az ellenőrzött kliens IP-címét, hogy később át tudjuk engedni a tőle érkező csomagokat.

Ellenőrzés legális kliens által:

$ sudo ip netns exec xdp-test nc -nv 192.0.2.1 6666
192.0.2.1 6666: A kapcsolat megszakítva a távoli fél által

A naplóban rögzítettük az ellenőrzés átmenetét (flags=0x2 — ez SYN, flags=0x10 — ez ACK):

Ether(proto=0x800)
  IP(src=0x20e6e11a dst=0x20e6e11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x2)
Ether(proto=0x800)
  IP(src=0xfe2cb11a dst=0xfe2cb11e proto=6)
    TCP(sport=50836 dport=6666 flags=0x10)
      cookie egyezik az ügyféllel 20200c0

Amíg nincs ellenőrzött IP-k listája, a valódi SYN flood ellen nem lesz védelem, de itt van a reakció az ACK floodra, amelyet ezzel a paranccsal indítunk:

sudo ip netns exec xdp-test hping3 --flood -A -s 1111 -p 2222 192.0.2.1

Bejegyzések a naplóban:

Ether(proto=0x800)
  IP(src=0x15bd11a dst=0x15bd11e proto=6)
    TCP(sport=3236 dport=2222 flags=0x10)
      cookie eltérés

Zárás

Néha az eBPF, és különösképpen az XDP inkább egy fejlett rendszergazda eszközként jelenik meg, mintsem fejlesztési platformként. Valóban, az XDP a csomagkezelés beavatkozásának eszköze a kernelben, nem alternatíva a kernel verem számára, mint a DPDK és más kernel bypass megoldások. Másrészt az XDP lehetővé teszi egy elég bonyolult logika megvalósítását, amely könnyen frissíthető anélkül, hogy megszakítanánk a forgalom feldolgozását. A verifikátor nem okoz nagy problémákat, én személy szerint nem mondanék nemet egy ilyen megoldásra a userspace kód egyes részeihez.

A második részben, ha a téma érdekes, befejezzük az ellenőrzött kliensek és a kapcsolatszakadások táblázatát, bevezetjük a számlálókat, és írunk egy userspace segédprogramot a szűrő kezelésére.

Linkek:

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster