เรากำลังเขียนการป้องกันการโจมตี DDoS บน XDP ส่วนนิวเคลียร์

เทคโนโลยี eXpress Data Path (XDP) ช่วยให้สามารถประมวลผลการรับส่งข้อมูลแบบสุ่มบนอินเทอร์เฟซ Linux ก่อนที่แพ็กเก็ตจะเข้าสู่สแต็กเครือข่ายเคอร์เนล แอปพลิเคชัน XDP - ป้องกันการโจมตี DDoS (CloudFlare), ตัวกรองที่ซับซ้อน, การรวบรวมสถิติ (Netflix) โปรแกรม XDP ดำเนินการโดยเครื่องเสมือน eBPF ดังนั้นจึงมีข้อจำกัดทั้งโค้ดและฟังก์ชันเคอร์เนลที่มีอยู่ ขึ้นอยู่กับประเภทของตัวกรอง

บทความนี้มีวัตถุประสงค์เพื่อเติมเต็มข้อบกพร่องของเนื้อหาจำนวนมากใน XDP ประการแรก ให้โค้ดสำเร็จรูปที่จะเลี่ยงคุณสมบัติของ XDP ทันที: มันถูกเตรียมไว้สำหรับการตรวจสอบหรือง่ายเกินไปที่จะทำให้เกิดปัญหา เมื่อคุณพยายามเขียนโค้ดตั้งแต่เริ่มต้น คุณจะไม่รู้ว่าจะทำอย่างไรกับข้อผิดพลาดทั่วไป ประการที่สอง วิธีทดสอบ XDP ภายในเครื่องโดยไม่มี VM และฮาร์ดแวร์จะไม่ครอบคลุมถึงแม้ว่าจะมีข้อผิดพลาดของตัวเองก็ตาม ข้อความนี้มีไว้สำหรับโปรแกรมเมอร์ที่คุ้นเคยกับระบบเครือข่ายและ Linux ที่สนใจ XDP และ eBPF

ในส่วนนี้ เราจะเข้าใจรายละเอียดวิธีการประกอบตัวกรอง XDP และวิธีการทดสอบ จากนั้นเราจะเขียนเวอร์ชันง่ายๆ ของกลไกคุกกี้ SYN ที่รู้จักกันดีในระดับการประมวลผลแพ็คเก็ต เราจะไม่สร้าง "บัญชีขาว" เลย
ลูกค้าที่ได้รับการยืนยัน เคาน์เตอร์และจัดการตัวกรอง - บันทึกที่เพียงพอ

เราจะเขียนด้วยภาษา C - มันไม่ทันสมัย ​​แต่ใช้งานได้จริง โค้ดทั้งหมดมีอยู่ใน GitHub ตามลิงก์ท้ายบทความ และแบ่งออกเป็นคอมมิตตามขั้นตอนที่อธิบายไว้ในบทความ

คำปฏิเสธ ตลอดบทความนี้ ฉันจะพัฒนาโซลูชันขนาดเล็กเพื่อป้องกันการโจมตี DDoS เนื่องจากนี่เป็นงานที่สมจริงสำหรับ XDP และความเชี่ยวชาญของฉัน อย่างไรก็ตาม เป้าหมายหลักคือการทำความเข้าใจเทคโนโลยีซึ่งไม่ใช่แนวทางในการสร้างการป้องกันแบบสำเร็จรูป รหัสบทช่วยสอนไม่ได้รับการปรับให้เหมาะสมและละเว้นความแตกต่างบางประการ

ภาพรวมโดยย่อของ XDP

ฉันจะร่างเฉพาะประเด็นสำคัญเพื่อไม่ให้ทำซ้ำเอกสารและบทความที่มีอยู่

ดังนั้นโค้ดตัวกรองจึงถูกโหลดลงในเคอร์เนล แพ็กเก็ตขาเข้าจะถูกส่งไปยังตัวกรอง เป็นผลให้ตัวกรองต้องตัดสินใจ: ส่งแพ็กเก็ตไปยังเคอร์เนล (XDP_PASS) วางแพ็กเก็ต (XDP_DROP) หรือส่งกลับ (XDP_TX). ตัวกรองสามารถเปลี่ยนแพ็คเกจได้โดยเฉพาะอย่างยิ่งสำหรับ XDP_TX. คุณสามารถยกเลิกโปรแกรมได้ (XDP_ABORTED) และรีเซ็ตแพ็คเกจ แต่สิ่งนี้คล้ายคลึงกัน assert(0) - สำหรับการดีบัก

เครื่องเสมือน eBPF (extensed Berkley Packet Filter) ได้รับการจงใจทำให้เรียบง่าย เพื่อให้เคอร์เนลสามารถตรวจสอบได้ว่าโค้ดไม่วนซ้ำและไม่ทำลายหน่วยความจำของผู้อื่น ข้อจำกัดและการตรวจสอบสะสม:

  • ห้ามวนซ้ำ (ย้อนกลับ)
  • มีสแต็กสำหรับข้อมูล แต่ไม่มีฟังก์ชัน (ฟังก์ชัน C ทั้งหมดต้องอยู่ในบรรทัด)
  • ห้ามเข้าถึงหน่วยความจำภายนอกสแต็กและบัฟเฟอร์แพ็กเก็ต
  • ขนาดโค้ดมีจำกัด แต่ในทางปฏิบัติก็ไม่สำคัญมากนัก
  • อนุญาตเฉพาะการเรียกฟังก์ชันเคอร์เนลพิเศษ (ตัวช่วย eBPF) เท่านั้น

การออกแบบและติดตั้งตัวกรองมีลักษณะดังนี้:

  1. ซอร์สโค้ด (เช่น kernel.c) ถูกคอมไพล์เป็นวัตถุ (kernel.o) สำหรับสถาปัตยกรรมเครื่องเสมือน eBPF ณ เดือนตุลาคม 2019 การคอมไพล์เป็น eBPF ได้รับการสนับสนุนโดย Clang และสัญญาไว้ใน GCC 10.1
  2. หากโค้ดออบเจ็กต์นี้มีการเรียกไปยังโครงสร้างเคอร์เนล (เช่น ตารางและตัวนับ) ID จะถูกแทนที่ด้วยศูนย์ ซึ่งหมายความว่าโค้ดดังกล่าวไม่สามารถดำเนินการได้ ก่อนที่จะโหลดลงในเคอร์เนล คุณจะต้องแทนที่ศูนย์เหล่านี้ด้วย ID ของอ็อบเจ็กต์เฉพาะที่สร้างขึ้นผ่านการเรียกเคอร์เนล (ลิงก์โค้ด) คุณสามารถทำได้โดยใช้ยูทิลิตี้ภายนอก หรือคุณสามารถเขียนโปรแกรมที่จะเชื่อมโยงและโหลดตัวกรองเฉพาะ
  3. เคอร์เนลตรวจสอบโปรแกรมที่โหลด มีการตรวจสอบการไม่มีรอบและความล้มเหลวในการเกินขอบเขตแพ็กเก็ตและสแต็ก หากผู้ตรวจสอบไม่สามารถพิสูจน์ได้ว่ารหัสนั้นถูกต้อง โปรแกรมจะถูกปฏิเสธ - คุณต้องทำให้เขาพอใจได้
  4. หลังจากการตรวจสอบยืนยันสำเร็จ เคอร์เนลจะคอมไพล์โค้ดออบเจ็กต์สถาปัตยกรรม eBPF ให้เป็นโค้ดเครื่องสำหรับสถาปัตยกรรมระบบ (ทันเวลาพอดี)
  5. โปรแกรมเชื่อมต่อกับอินเทอร์เฟซและเริ่มประมวลผลแพ็กเก็ต

เนื่องจาก XDP ทำงานในเคอร์เนล การดีบักจึงดำเนินการโดยใช้บันทึกการติดตาม และในความเป็นจริงแล้ว แพ็กเก็ตที่โปรแกรมกรองหรือสร้างขึ้น อย่างไรก็ตาม eBPF ช่วยให้แน่ใจว่าโค้ดที่ดาวน์โหลดนั้นปลอดภัยสำหรับระบบ ดังนั้นคุณจึงสามารถทดลองใช้ XDP บน Linux ในพื้นที่ของคุณได้โดยตรง

การเตรียมสิ่งแวดล้อม

การชุมนุม

Clang ไม่สามารถสร้าง object code สำหรับสถาปัตยกรรม eBPF ได้โดยตรง ดังนั้นกระบวนการจึงประกอบด้วยสองขั้นตอน:

  1. คอมไพล์โค้ด C เป็น LLVM bytecode (clang -emit-llvm).
  2. แปลง bytecode เป็นรหัสวัตถุ eBPF (llc -march=bpf -filetype=obj).

เมื่อเขียนตัวกรองไฟล์สองสามไฟล์ที่มีฟังก์ชันเสริมและมาโครจะมีประโยชน์ จากการทดสอบเคอร์เนล. สิ่งสำคัญคือต้องตรงกับเวอร์ชันเคอร์เนล (KVER). ดาวน์โหลดได้ที่ 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 (เคอร์เนล 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 มีเส้นทางไปยังส่วนหัวของเคอร์เนล ARCH - ระบบสถาปัตยกรรม. เส้นทางและเครื่องมืออาจแตกต่างกันเล็กน้อยระหว่างการแจกแจง

ตัวอย่างความแตกต่างสำหรับ Debian 10 (เคอร์เนล 4.19.67)

# другая команда
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 เชื่อมต่อไดเร็กทอรีกับส่วนหัวเสริมและหลายไดเร็กทอรีที่มีส่วนหัวเคอร์เนล เครื่องหมาย __KERNEL__ หมายความว่าส่วนหัว UAPI (userspace API) ถูกกำหนดไว้สำหรับโค้ดเคอร์เนล เนื่องจากตัวกรองถูกดำเนินการในเคอร์เนล

การป้องกันสแต็กสามารถปิดใช้งานได้ (-fno-stack-protector) เนื่องจากตัวตรวจสอบรหัส eBPF ยังคงตรวจสอบการละเมิดสแต็กนอกขอบเขต การเปิดการปรับให้เหมาะสมทันทีนั้นคุ้มค่า เนื่องจากขนาดของรหัสไบต์ eBPF นั้นมีจำกัด

เริ่มต้นด้วยตัวกรองที่ส่งแพ็กเก็ตทั้งหมดและไม่ทำอะไรเลย:

#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";

ทีม make สะสม xdp_filter.o. ตอนนี้จะลองได้ที่ไหน?

แท่นทดสอบ

ขาตั้งจะต้องมีสองอินเทอร์เฟซ: ซึ่งจะมีตัวกรองและแพ็กเก็ตใดจะถูกส่ง สิ่งเหล่านี้ต้องเป็นอุปกรณ์ Linux ที่มีคุณสมบัติครบถ้วนและมี IP ของตัวเองเพื่อตรวจสอบว่าแอปพลิเคชันทั่วไปทำงานร่วมกับตัวกรองของเราอย่างไร

อุปกรณ์ประเภท veth (อีเทอร์เน็ตเสมือน) เหมาะสำหรับเรา: นี่คืออินเทอร์เฟซเครือข่ายเสมือนคู่ที่ "เชื่อมต่อ" ถึงกันโดยตรง คุณสามารถสร้างได้เช่นนี้ (ในส่วนนี้คำสั่งทั้งหมด ip จะดำเนินการตั้งแต่ root):

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

ที่นี่ xdp-remote и xdp-local — ชื่ออุปกรณ์ บน xdp-local (192.0.2.1/24) จะมีการติดฟิลเตอร์ด้วย xdp-remote (192.0.2.2/24) ทราฟฟิกขาเข้าจะถูกส่งไป อย่างไรก็ตาม มีปัญหาเกิดขึ้น: อินเทอร์เฟซอยู่บนเครื่องเดียวกัน และ Linux จะไม่ส่งการรับส่งข้อมูลไปยังอินเทอร์เฟซใดเครื่องหนึ่งผ่านอีกเครื่องหนึ่ง คุณสามารถแก้ไขได้ด้วยกฎที่ยุ่งยาก iptablesแต่จะต้องเปลี่ยนแพ็คเกจซึ่งไม่สะดวกต่อการดีบัก ควรใช้เนมสเปซเครือข่าย (ต่อไปนี้จะเรียกว่า netns)

เนมสเปซเครือข่ายประกอบด้วยชุดของอินเทอร์เฟซ ตารางเส้นทาง และกฎ NetFilter ที่แยกได้จากออบเจ็กต์ที่คล้ายกันใน netns อื่นๆ แต่ละกระบวนการทำงานในเนมสเปซและมีสิทธิ์เข้าถึงเฉพาะอ็อบเจ็กต์ของ netns นั้นเท่านั้น ตามค่าเริ่มต้น ระบบจะมีเนมสเปซเครือข่ายเดียวสำหรับออบเจ็กต์ทั้งหมด ดังนั้นคุณจึงสามารถทำงานใน Linux ได้โดยไม่ต้องมีความรู้เกี่ยวกับ netns

มาสร้างเนมสเปซใหม่กันเถอะ xdp-test และย้ายมันไปที่นั่น xdp-remote.

ip netns add xdp-test
ip link set dev xdp-remote netns xdp-test

จากนั้นกระบวนการก็ดำเนินไป xdp-testจะไม่ "เห็น" xdp-local (จะยังคงอยู่ใน netns ตามค่าเริ่มต้น) และเมื่อส่งแพ็กเก็ตไปที่ 192.0.2.1 มันจะผ่านมันไป xdp-remoteเพราะเป็นอินเทอร์เฟซเดียวบน 192.0.2.0/24 ที่เข้าถึงกระบวนการนี้ได้ สิ่งนี้ยังทำงานในทิศทางตรงกันข้ามด้วย

เมื่อย้ายระหว่าง netns อินเทอร์เฟซจะหยุดทำงานและสูญเสียที่อยู่ หากต้องการกำหนดค่าอินเทอร์เฟซใน netns คุณต้องเรียกใช้ ip ... ในเนมสเปซคำสั่งนี้ ip netns exec:

ip netns exec xdp-test 
    ip address add 192.0.2.2/24 dev xdp-remote
ip netns exec xdp-test 
    ip link set xdp-remote up

อย่างที่คุณเห็น สิ่งนี้ไม่แตกต่างจากการตั้งค่าเลย xdp-local ในเนมสเปซเริ่มต้น:

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

ถ้าคุณวิ่ง tcpdump -tnevi xdp-localคุณจะเห็นว่าแพ็กเก็ตที่ส่งมาจาก xdp-testถูกส่งไปยังอินเทอร์เฟซนี้:

ip netns exec xdp-test   ping 192.0.2.1

สะดวกในการเปิดเปลือกหอย xdp-test. พื้นที่เก็บข้อมูลมีสคริปต์ที่ทำงานอัตโนมัติกับขาตั้ง ตัวอย่างเช่น คุณสามารถกำหนดค่าขาตั้งด้วยคำสั่ง sudo ./stand up และลบมันทิ้ง sudo ./stand down.

การติดตาม

ตัวกรองเชื่อมโยงกับอุปกรณ์ดังนี้:

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

คีย์ -force จำเป็นต้องลิงก์โปรแกรมใหม่หากมีการเชื่อมโยงโปรแกรมอื่นอยู่แล้ว “ไม่มีข่าวเป็นข่าวดี” ไม่เกี่ยวกับคำสั่งนี้ บทสรุปมีมากมาย ไม่ว่าในกรณีใด ระบุ verbose ไม่จำเป็น แต่มีรายงานปรากฏขึ้นเกี่ยวกับการทำงานของตัวตรวจสอบรหัสพร้อมรายการชุดประกอบ:

Verifier analysis:

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

ยกเลิกการเชื่อมโยงโปรแกรมจากอินเทอร์เฟซ:

ip link set dev xdp-local xdp off

ในสคริปต์นี่คือคำสั่ง sudo ./stand attach и sudo ./stand detach.

ด้วยการแนบตัวกรอง คุณสามารถมั่นใจได้ว่า ping ยังคงทำงานต่อไป แต่โปรแกรมทำงานหรือไม่? มาเพิ่มบันทึกกันเถอะ การทำงาน bpf_trace_printk() คล้ายกับ printf()แต่รองรับเพียงสามอาร์กิวเมนต์นอกเหนือจากรูปแบบ และรายการตัวระบุที่จำกัด มาโคร bpf_printk() ทำให้การโทรง่ายขึ้น

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

เอาต์พุตไปที่ช่องการติดตามเคอร์เนล ซึ่งจำเป็นต้องเปิดใช้งาน:

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

ดูกระทู้ข้อความ:

cat /sys/kernel/debug/tracing/trace_pipe

คำสั่งทั้งสองนี้ทำการโทร sudo ./stand log.

Ping ควรทริกเกอร์ข้อความดังนี้:

<...>-110930 [004] ..s1 78803.244967: 0: got packet: 00000000ac510377

หากคุณดูผลลัพธ์ของผู้ตรวจสอบอย่างใกล้ชิด คุณจะสังเกตเห็นการคำนวณแปลกๆ:

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
<...>

ความจริงก็คือโปรแกรม eBPF ไม่มีส่วนข้อมูล ดังนั้นวิธีเดียวที่จะเข้ารหัสสตริงรูปแบบคืออาร์กิวเมนต์ของคำสั่ง VM ในทันที:

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

ด้วยเหตุนี้ เอาต์พุตการดีบักจึงขยายโค้ดผลลัพธ์อย่างมาก

การส่งแพ็กเก็ต XDP

มาเปลี่ยนตัวกรองกัน: ปล่อยให้มันส่งแพ็กเก็ตขาเข้าทั้งหมดกลับ สิ่งนี้ไม่ถูกต้องจากมุมมองของเครือข่าย เนื่องจากจำเป็นต้องเปลี่ยนที่อยู่ในส่วนหัว แต่ตอนนี้งานในหลักการมีความสำคัญ

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

ปล่อย tcpdump บน xdp-remote. ควรแสดงคำขอ ICMP Echo ขาออกและขาเข้าที่เหมือนกัน และหยุดแสดง ICMP Echo Reply แต่มันไม่แสดง ปรากฎว่าสำหรับการทำงาน XDP_TX ในโปรแกรมบน xdp-local จำเป็นไปยังอินเทอร์เฟซคู่ xdp-remote มีการกำหนดโปรแกรมด้วย แม้ว่าจะว่างเปล่า และเขาก็ถูกเลี้ยงดูมา

ฉันรู้เรื่องนี้ได้อย่างไร?

ติดตามเส้นทางของแพ็คเกจในเคอร์เนล กลไกเหตุการณ์ perf อนุญาตให้ใช้เครื่องเสมือนเดียวกันนั่นคือ eBPF ใช้สำหรับการถอดแยกชิ้นส่วนด้วย eBPF

คุณต้องสร้างความดีจากความชั่ว เพราะไม่มีอะไรอื่นให้ทำอีกแล้ว

$ 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])
                                     <...>

รหัส 6 คืออะไร?

$ errno 6
ENXIO 6 No such device or address

ฟังก์ชัน veth_xdp_flush_bq() ได้รับรหัสข้อผิดพลาดจาก veth_xdp_xmit()โดยค้นหาด้วย ENXIO และค้นหาความคิดเห็น

มาคืนค่าตัวกรองขั้นต่ำ (XDP_PASS) ในไฟล์ xdp_dummy.cเพิ่มลงใน Makefile ผูกเข้ากับ xdp-remote:

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

ขณะนี้ tcpdump แสดงสิ่งที่คาดหวัง:

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

หากแสดงเฉพาะ ARP แทน คุณจะต้องลบตัวกรองออก (ซึ่งจะเป็นเช่นนั้น sudo ./stand detach), ไปกันเถอะ pingจากนั้นตั้งค่าตัวกรองแล้วลองอีกครั้ง ปัญหาก็คือว่าไส้กรอง XDP_TX ใช้ได้ทั้งใน ARP และหากเป็นสแต็ก
เนมสเปซ xdp-test จัดการเพื่อ "ลืม" ที่อยู่ MAC 192.0.2.1 ก็จะไม่สามารถแก้ไข IP นี้ได้

คำแถลงปัญหา

มาดูงานที่ระบุไว้กันดีกว่า: เขียนกลไกคุกกี้ SYN บน XDP

SYN Flood ยังคงเป็นการโจมตี DDoS ที่ได้รับความนิยม โดยมีสาระสำคัญดังนี้ เมื่อมีการสร้างการเชื่อมต่อ (TCP handshake) เซิร์ฟเวอร์จะได้รับ SYN จัดสรรทรัพยากรสำหรับการเชื่อมต่อในอนาคต ตอบสนองด้วยแพ็กเก็ต SYNACK และรอ ACK ผู้โจมตีเพียงแค่ส่งแพ็กเก็ต SYN นับพันต่อวินาทีจากที่อยู่ที่ปลอมแปลงจากแต่ละโฮสต์ในบอตเน็ตที่แข็งแกร่งหลายพันตัว เซิร์ฟเวอร์ถูกบังคับให้จัดสรรทรัพยากรทันทีที่แพ็คเก็ตมาถึง แต่จะปล่อยทรัพยากรเหล่านั้นหลังจากการหมดเวลาอย่างมาก ผลก็คือ หน่วยความจำหรือขีดจำกัดหมด การเชื่อมต่อใหม่ไม่ได้รับการยอมรับ และบริการไม่พร้อมใช้งาน

หากคุณไม่จัดสรรทรัพยากรตามแพ็กเก็ต SYN แต่ตอบสนองด้วยแพ็กเก็ต SYNACK เท่านั้น เซิร์ฟเวอร์จะเข้าใจได้อย่างไรว่าแพ็กเก็ต ACK ที่มาถึงในภายหลังอ้างอิงถึงแพ็กเก็ต SYN ที่ไม่ได้บันทึกไว้ ท้ายที่สุดแล้ว ผู้โจมตีสามารถสร้าง ACK ปลอมได้เช่นกัน จุดประสงค์ของคุกกี้ SYN คือการเข้ารหัสคุกกี้ seqnum พารามิเตอร์การเชื่อมต่อเป็นแฮชของที่อยู่ พอร์ต และการเปลี่ยนเกลือ หาก ACK มาถึงก่อนที่จะเปลี่ยนเกลือ คุณสามารถคำนวณแฮชอีกครั้งและเปรียบเทียบได้ acknum. ปลอม acknum ผู้โจมตีทำไม่ได้ เนื่องจากเกลือรวมความลับไว้ด้วย และจะไม่มีเวลาจัดการผ่านมันเนื่องจากมีช่องทางที่จำกัด

คุกกี้ SYN มีการใช้งานในเคอร์เนล Linux มานานแล้ว และยังสามารถเปิดใช้งานได้โดยอัตโนมัติหาก SYN มาถึงเร็วเกินไปและมีจำนวนมาก

โปรแกรมการศึกษาเรื่อง TCP handshake

TCP จัดให้มีการส่งข้อมูลในรูปแบบสตรีมของไบต์ ตัวอย่างเช่น คำขอ HTTP จะถูกส่งผ่าน TCP สตรีมถูกส่งเป็นชิ้น ๆ ในแพ็กเก็ต แพ็กเก็ต TCP ทั้งหมดมีแฟล็กลอจิคัลและหมายเลขลำดับ 32 บิต:

  • การรวมกันของแฟล็กจะกำหนดบทบาทของแพ็คเกจเฉพาะ ธง SYN ระบุว่านี่คือแพ็กเก็ตแรกของผู้ส่งในการเชื่อมต่อ ค่าสถานะ ACK หมายความว่าผู้ส่งได้รับข้อมูลการเชื่อมต่อทั้งหมดจนถึงไบต์ acknum. แพ็กเก็ตสามารถมีได้หลายแฟล็ก และถูกเรียกโดยการรวมกัน เช่น แพ็กเก็ต SYNACK

  • หมายเลขลำดับ (seqnum) ระบุออฟเซ็ตในสตรีมข้อมูลสำหรับไบต์แรกที่ถูกส่งในแพ็กเก็ตนี้ ตัวอย่างเช่น หากในแพ็กเก็ตแรกที่มีข้อมูล X ไบต์ หมายเลขนี้คือ N ในแพ็กเก็ตถัดไปที่มีข้อมูลใหม่จะเป็น N+X เมื่อเริ่มเชื่อมต่อแต่ละฝ่ายจะสุ่มเลือกหมายเลขนี้

  • หมายเลขตอบรับ (acknum) - ออฟเซ็ตเดียวกับ seqnum แต่ไม่ได้กำหนดจำนวนไบต์ที่กำลังส่ง แต่เป็นจำนวนไบต์แรกจากผู้รับซึ่งผู้ส่งไม่เห็น

เมื่อเริ่มต้นการเชื่อมต่อคู่สัญญาทั้งสองฝ่ายจะต้องตกลงกัน seqnum и acknum. ไคลเอ็นต์ส่งแพ็กเก็ต SYN พร้อมด้วย seqnum = X. เซิร์ฟเวอร์ตอบสนองด้วยแพ็กเก็ต SYNACK ซึ่งจะบันทึกข้อมูล seqnum = Y และเปิดเผย acknum = X + 1. ไคลเอนต์ตอบสนองต่อ SYNACK ด้วยแพ็กเก็ต ACK โดยที่ seqnum = X + 1, acknum = Y + 1. หลังจากนี้ การถ่ายโอนข้อมูลจริงจะเริ่มต้นขึ้น

หากเพียร์ไม่รับทราบการรับแพ็กเก็ต TCP จะส่งแพ็กเก็ตอีกครั้งหลังจากหมดเวลา

เหตุใดจึงไม่ได้ใช้คุกกี้ SYN เสมอไป?

ประการแรก หาก SYNACK หรือ ACK สูญหาย คุณจะต้องรอให้ส่งอีกครั้ง การตั้งค่าการเชื่อมต่อจะช้าลง ประการที่สองในแพ็คเกจ SYN - และในนั้นเท่านั้น! — มีตัวเลือกจำนวนหนึ่งถูกส่งซึ่งส่งผลต่อการดำเนินการเชื่อมต่อต่อไป เซิร์ฟเวอร์จะเพิกเฉยต่อตัวเลือกเหล่านี้โดยไม่จำแพ็กเก็ต SYN ขาเข้า ไคลเอ็นต์จะไม่ส่งแพ็กเก็ตเหล่านั้นในแพ็กเก็ตถัดไป TCP สามารถทำงานได้ในกรณีนี้ แต่อย่างน้อยในระยะเริ่มแรกคุณภาพของการเชื่อมต่อจะลดลง

ในแง่ของแพ็คเกจ โปรแกรม XDP ต้องทำดังต่อไปนี้:

  • ตอบสนองต่อ SYN ด้วย SYNACK ด้วยคุกกี้
  • ตอบสนองต่อ ACK ด้วย RST (ตัดการเชื่อมต่อ);
  • ทิ้งแพ็กเก็ตที่เหลือ

Pseudocode ของอัลกอริทึมพร้อมกับการแยกวิเคราะห์แพ็คเกจ:

Если это не Ethernet,
    пропустить пакет.
Если это не IPv4,
    пропустить пакет.
Если адрес в таблице проверенных,               (*)
        уменьшить счетчик оставшихся проверок,
        пропустить пакет.
Если это не TCP,
    сбросить пакет.     (**)
Если это SYN,
    ответить SYN-ACK с cookie.
Если это ACK,
    если в acknum лежит не cookie,
        сбросить пакет.
    Занести в таблицу адрес с N оставшихся проверок.    (*)
    Ответить RST.   (**)
В остальных случаях сбросить пакет.

หนึ่ง (*) จุดที่คุณต้องจัดการสถานะของระบบจะถูกทำเครื่องหมาย - ในขั้นตอนแรกคุณสามารถทำได้โดยไม่ต้องใช้สิ่งเหล่านั้นเพียงแค่ใช้การจับมือ TCP กับการสร้างคุกกี้ SYN เป็นลำดับ

ตรงจุด (**)ในขณะที่เราไม่มีโต๊ะ เราก็จะข้ามแพ็กเก็ตไป

การใช้การจับมือ TCP

แยกวิเคราะห์แพ็คเกจและตรวจสอบรหัส

เราจะต้องมีโครงสร้างส่วนหัวของเครือข่าย: อีเธอร์เน็ต (uapi/linux/if_ether.h), IPv4 (uapi/linux/ip.h) และ TCP (uapi/linux/tcp.h). ฉันไม่สามารถเชื่อมต่ออันหลังได้เนื่องจากข้อผิดพลาดที่เกี่ยวข้อง atomic64_tฉันต้องคัดลอกคำจำกัดความที่จำเป็นลงในโค้ด

ฟังก์ชันทั้งหมดที่ถูกไฮไลต์ด้วยภาษา C เพื่อให้อ่านง่ายจะต้องอยู่ในบรรทัดที่จุดเรียกใช้ เนื่องจากตัวตรวจสอบ eBPF ในเคอร์เนลห้ามไม่ให้ย้อนรอย ซึ่งในความเป็นจริงแล้วคือลูปและการเรียกใช้ฟังก์ชัน

#define INTERNAL static __attribute__((always_inline))

แมโคร LOG() ปิดใช้งานการพิมพ์ในรุ่นวางจำหน่าย

โปรแกรมเป็นตัวลำเลียงฟังก์ชัน แต่ละรายการจะได้รับแพ็กเก็ตที่มีการเน้นส่วนหัวของระดับที่เกี่ยวข้อง เช่น process_ether() คาดว่าจะเต็ม ether. จากผลการวิเคราะห์ภาคสนาม ฟังก์ชันสามารถส่งแพ็กเก็ตไปยังระดับที่สูงกว่าได้ ผลลัพธ์ของฟังก์ชันคือการดำเนินการ XDP ในตอนนี้ ตัวจัดการ SYN และ ACK จะผ่านแพ็กเก็ตทั้งหมด

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);
}

ฉันดึงความสนใจของคุณไปที่เช็คที่ทำเครื่องหมาย A และ B หากคุณใส่เครื่องหมาย A โปรแกรมจะสร้าง แต่จะมีข้อผิดพลาดในการตรวจสอบเมื่อโหลด:

Verifier analysis:

<...>
11: (7b) *(u64 *)(r10 -48) = r1
12: (71) r3 = *(u8 *)(r7 +13)
invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0)
R7 offset is outside of the packet
processed 11 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0

Error fetching program/map!

สตริงคีย์ invalid access to packet, off=13 size=1, R7(id=0,off=0,r=0): มีเส้นทางการดำเนินการเมื่อไบต์ที่สิบสามจากจุดเริ่มต้นของบัฟเฟอร์อยู่นอกแพ็กเก็ต เป็นการยากที่จะเข้าใจจากรายการว่าเรากำลังพูดถึงบรรทัดไหน แต่มีหมายเลขคำสั่ง (12) และตัวถอดประกอบที่แสดงบรรทัดของซอร์สโค้ด:

llvm-objdump -S xdp_filter.o | less

ในกรณีนี้จะชี้ไปที่เส้น

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

ซึ่งทำให้ชัดเจนว่าปัญหาคือ ether. มันก็จะเป็นเช่นนี้เสมอ

ตอบกลับ SYN

เป้าหมายในขั้นตอนนี้คือการสร้างแพ็กเก็ต SYNACK ที่ถูกต้องพร้อมค่าคงที่ seqnumซึ่งจะถูกแทนที่ด้วยคุกกี้ SYN ในอนาคต การเปลี่ยนแปลงทั้งหมดเกิดขึ้นใน process_tcp_syn() และพื้นที่โดยรอบ

การตรวจสอบพัสดุ

น่าแปลกที่นี่คือบรรทัดที่น่าทึ่งที่สุดหรือเป็นคำอธิบาย:

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

เมื่อเขียนโค้ดเวอร์ชันแรกจะใช้เคอร์เนล 5.1 ซึ่งตัวตรวจสอบมีความแตกต่างระหว่าง data_end и (const void*)ctx->data_end. ในขณะที่เขียนเคอร์เนล 5.3.1 ไม่มีปัญหานี้ อาจเป็นไปได้ว่าคอมไพเลอร์กำลังเข้าถึงตัวแปรในเครื่องแตกต่างจากฟิลด์ คติประจำใจของเรื่องราว: การลดความซับซ้อนของโค้ดสามารถช่วยได้เมื่อมีการซ้อนจำนวนมาก

ถัดไปคือการตรวจสอบความยาวตามปกติเพื่อดูความรุ่งโรจน์ของผู้ตรวจสอบ โอ MAX_CSUM_BYTES ด้านล่าง

const u32 ip_len = ip->ihl * 4;
if ((void*)ip + ip_len > data_end) {
    return XDP_DROP; /* malformed packet */
}
if (ip_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

const u32 tcp_len = tcp->doff * 4;
if ((void*)tcp + tcp_len > (void*)ctx->data_end) {
    return XDP_DROP; /* malformed packet */
}
if (tcp_len > MAX_CSUM_BYTES) {
    return XDP_ABORTED; /* implementation limitation */
}

การเปิดแพ็คเกจ

กรอก seqnum и acknumให้ตั้งค่า ACK (SYN ถูกตั้งค่าไว้แล้ว):

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

สลับพอร์ต TCP, ที่อยู่ IP และที่อยู่ MAC ไลบรารีมาตรฐานไม่สามารถเข้าถึงได้จากโปรแกรม XDP ดังนั้น memcpy() — มาโครที่ซ่อน Clang ภายใน

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);

การคำนวณเช็คซัมใหม่

การตรวจสอบ IPv4 และ TCP จำเป็นต้องเพิ่มคำ 16 บิตทั้งหมดในส่วนหัว และขนาดของส่วนหัวจะถูกเขียนลงในคำเหล่านั้น ซึ่งไม่ทราบในขณะรวบรวม นี่เป็นปัญหาเนื่องจากตัวตรวจสอบจะไม่ข้ามการวนซ้ำปกติไปยังตัวแปรขอบเขต แต่ขนาดของส่วนหัวมีจำกัด: สูงสุดแต่ละรายการได้ไม่เกิน 64 ไบต์ คุณสามารถสร้างการวนซ้ำโดยมีจำนวนการวนซ้ำคงที่ ซึ่งสามารถสิ้นสุดก่อนกำหนดได้

ฉันสังเกตว่ามี RFC 1624 เกี่ยวกับวิธีการคำนวณผลรวมใหม่บางส่วนหากมีการเปลี่ยนแปลงเฉพาะคำคงที่ของแพ็คเกจ อย่างไรก็ตาม วิธีการนี้ไม่เป็นสากล และการนำไปปฏิบัติคงรักษาได้ยากกว่า

ฟังก์ชันการคำนวณเช็คซัม:

#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;
}

แม้ว่า size ตรวจสอบโดยรหัสการโทร เงื่อนไขการออกที่สองเป็นสิ่งจำเป็นเพื่อให้ผู้ตรวจสอบสามารถพิสูจน์ความสมบูรณ์ของการวนซ้ำ

สำหรับคำแบบ 32 บิต จะใช้เวอร์ชันที่ง่ายกว่า:

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

คำนวณเช็คซัมใหม่และส่งแพ็กเก็ตกลับจริง:

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 << 8;
tcp->check = 0;
tcp_csum += sum16(tcp, tcp_len, data_end);
tcp->check = carry(tcp_csum);

return XDP_TX;

ฟังก์ชัน carry() สร้างการตรวจสอบจากผลรวม 32 บิตของคำ 16 บิตตาม RFC 791

การตรวจสอบการจับมือ TCP

ตัวกรองสร้างการเชื่อมต่ออย่างถูกต้องด้วย netcatข้าม ACK สุดท้ายซึ่ง Linux ตอบกลับด้วยแพ็กเก็ต RST เนื่องจากสแต็กเครือข่ายไม่ได้รับ SYN - มันถูกแปลงเป็น SYNACK และส่งกลับ - และจากมุมมองของระบบปฏิบัติการ แพ็กเก็ตมาถึงที่ไม่เกี่ยวข้องกับการเปิด การเชื่อมต่อ

$ sudo ip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

สิ่งสำคัญคือต้องตรวจสอบกับแอปพลิเคชันที่ครบถ้วนและสังเกต tcpdump บน xdp-remote เพราะตัวอย่างเช่น hping3 ไม่ตอบสนองต่อเช็คซัมที่ไม่ถูกต้อง

จากมุมมองของ XDP การตรวจสอบนั้นไม่สำคัญ อัลกอริธึมการคำนวณเป็นแบบดั้งเดิมและอาจเสี่ยงต่อผู้โจมตีที่มีความซับซ้อน ตัวอย่างเช่น เคอร์เนล Linux ใช้การเข้ารหัส SipHash แต่การใช้งาน XDP นั้นอยู่นอกเหนือขอบเขตของบทความนี้อย่างชัดเจน

เปิดตัวสำหรับ TODO ใหม่ที่เกี่ยวข้องกับการสื่อสารภายนอก:

  • โปรแกรม XDP ไม่สามารถจัดเก็บได้ cookie_seed (ส่วนลับของเกลือ) ในตัวแปรส่วนกลาง คุณต้องมีที่เก็บข้อมูลในเคอร์เนล ซึ่งค่าจะได้รับการอัปเดตเป็นระยะจากตัวสร้างที่เชื่อถือได้

  • หากคุกกี้ SYN ตรงกันในแพ็กเก็ต ACK คุณไม่จำเป็นต้องพิมพ์ข้อความ แต่จำ IP ของไคลเอ็นต์ที่ได้รับการตรวจสอบเพื่อส่งแพ็กเก็ตต่อไปได้

การตรวจสอบลูกค้าที่ถูกต้องตามกฎหมาย:

$ sudoip netns exec xdp-test   nc -nv 192.0.2.1 6666
192.0.2.1 6666: Connection reset by peer

บันทึกแสดงว่าการตรวจสอบผ่าน (flags=0x2 - นี่คือซิน flags=0x10 คือ 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 matches for client 20200c0

แม้ว่าจะไม่มีรายการ IP ที่ได้รับการยืนยัน แต่ก็จะไม่มีการป้องกันจาก SYN Flood เอง แต่นี่คือปฏิกิริยาต่อ ACK Flood ที่เรียกใช้โดยคำสั่งต่อไปนี้:

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

รายการบันทึก:

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

ข้อสรุป

บางครั้ง eBPF โดยทั่วไปและโดยเฉพาะอย่างยิ่ง XDP จะถูกนำเสนอเป็นเครื่องมือของผู้ดูแลระบบขั้นสูงมากกว่าเป็นแพลตฟอร์มการพัฒนา แท้จริงแล้ว XDP เป็นเครื่องมือสำหรับรบกวนการประมวลผลแพ็กเก็ตโดยเคอร์เนล และไม่ใช่ทางเลือกแทนเคอร์เนลสแต็ก เช่น DPDK และตัวเลือกการบายพาสเคอร์เนลอื่นๆ ในทางกลับกัน XDP ช่วยให้คุณสามารถใช้ตรรกะที่ค่อนข้างซับซ้อน ซึ่งยิ่งกว่านั้นยังง่ายต่อการอัปเดตโดยไม่หยุดชะงักในการประมวลผลการรับส่งข้อมูล ตัวตรวจสอบไม่ได้สร้างปัญหาใหญ่ โดยส่วนตัวแล้ว ฉันจะไม่ปฏิเสธสิ่งนี้สำหรับบางส่วนของรหัสพื้นที่ผู้ใช้

ในส่วนที่สอง หากหัวข้อน่าสนใจ เราจะจัดทำตารางไคลเอนต์ที่ได้รับการยืนยันและการยกเลิกการเชื่อมต่อ ติดตั้งตัวนับ และเขียนยูทิลิตีพื้นที่ผู้ใช้เพื่อจัดการตัวกรอง

อ้างอิง:

ที่มา: will.com

เพิ่มความคิดเห็น