BPF สำหรับเด็ก ตอนที่ XNUMX: BPF แบบคลาสสิก

Berkeley Packet Filters (BPF) เป็นเทคโนโลยีเคอร์เนล Linux ที่อยู่ในหน้าแรกของสื่อสิ่งพิมพ์ด้านเทคโนโลยีภาษาอังกฤษมาหลายปีแล้ว การประชุมเต็มไปด้วยรายงานเกี่ยวกับการใช้และการพัฒนา BPF David Miller ผู้ดูแลระบบย่อยเครือข่าย Linux กล่าวถึงคำพูดของเขาที่ Linux Plumbers 2018 “การพูดคุยนี้ไม่เกี่ยวกับ XDP” (XDP เป็นกรณีการใช้งานหนึ่งสำหรับ BPF) เบรนแดน เกร็กก์ บรรยายเรื่องหัวข้อ พลังพิเศษของ Linux BPF. โทเก เฮยลันด์-ยอร์เกนเซ่น หัวเราะว่าตอนนี้เคอร์เนลกลายเป็นไมโครเคอร์เนลแล้ว Thomas Graf ส่งเสริมแนวคิดที่ว่า BPF เป็นจาวาสคริปต์สำหรับเคอร์เนล.

ยังไม่มีคำอธิบายที่เป็นระบบของ BPF ใน Habré ดังนั้นในชุดบทความ ฉันจะพยายามพูดถึงประวัติความเป็นมาของเทคโนโลยี อธิบายเครื่องมือสถาปัตยกรรมและการพัฒนา และร่างขอบเขตการใช้งานและแนวปฏิบัติของการใช้ BPF บทความนี้เป็นศูนย์ในซีรีส์นี้ จะบอกเล่าประวัติศาสตร์และสถาปัตยกรรมของ BPF แบบคลาสสิก และยังเปิดเผยความลับของหลักการปฏิบัติงานอีกด้วย tcpdump, seccomp, straceและอีกมากมาย

การพัฒนา BPF ถูกควบคุมโดยชุมชนเครือข่าย Linux แอปพลิเคชันหลักที่มีอยู่ของ BPF นั้นเกี่ยวข้องกับเครือข่าย ดังนั้น เมื่อได้รับอนุญาต @eucariotฉันเรียกซีรีส์นี้ว่า “BPF เพื่อเจ้าตัวน้อย” เพื่อเป็นเกียรติแก่ซีรีส์ที่ยิ่งใหญ่ “เครือข่ายเพื่อลูกน้อย”.

หลักสูตรระยะสั้นในประวัติศาสตร์ของ BPF(c)

เทคโนโลยี BPF สมัยใหม่เป็นเวอร์ชันปรับปรุงและขยายของเทคโนโลยีเก่าที่มีชื่อเดียวกัน ซึ่งปัจจุบันเรียกว่า BPF แบบคลาสสิกเพื่อหลีกเลี่ยงความสับสน ยูทิลิตี้ที่รู้จักกันดีถูกสร้างขึ้นตาม BPF แบบคลาสสิก tcpdump,กลไก seccompรวมถึงโมดูลที่ไม่ค่อยมีใครรู้จัก xt_bpf สำหรับ iptables และลักษณนาม cls_bpf. ใน Linux สมัยใหม่ โปรแกรม BPF แบบคลาสสิกจะถูกแปลเป็นรูปแบบใหม่โดยอัตโนมัติ อย่างไรก็ตาม จากมุมมองของผู้ใช้ API ยังคงอยู่และยังคงพบการใช้งานใหม่สำหรับ BPF แบบคลาสสิก ดังที่เราจะเห็นในบทความนี้ ด้วยเหตุนี้ และเนื่องจากตามประวัติศาสตร์ของการพัฒนา BPF แบบคลาสสิกใน Linux จะชัดเจนมากขึ้นว่าทำไมและทำไมจึงพัฒนาเป็นรูปแบบสมัยใหม่ ฉันจึงตัดสินใจเริ่มด้วยบทความเกี่ยวกับ BPF แบบคลาสสิก

ในช่วงปลายทศวรรษที่แปดสิบของศตวรรษที่ผ่านมา วิศวกรจากห้องปฏิบัติการ Lawrence Berkeley ที่มีชื่อเสียงเริ่มสนใจคำถามเกี่ยวกับวิธีการกรองแพ็กเก็ตเครือข่ายบนฮาร์ดแวร์ที่ทันสมัยในช่วงปลายทศวรรษที่แปดสิบของศตวรรษที่ผ่านมาอย่างเหมาะสม แนวคิดพื้นฐานของการกรองซึ่งเดิมนำมาใช้ในเทคโนโลยี CSPF (CMU/Stanford Packet Filter) คือการกรองแพ็กเก็ตที่ไม่จำเป็นโดยเร็วที่สุดเช่น ในพื้นที่เคอร์เนล เนื่องจากจะช่วยหลีกเลี่ยงการคัดลอกข้อมูลที่ไม่จำเป็นลงในพื้นที่ผู้ใช้ เพื่อให้การรักษาความปลอดภัยรันไทม์สำหรับการรันโค้ดผู้ใช้ในพื้นที่เคอร์เนล จึงมีการใช้เครื่องเสมือนแบบแซนด์บ็อกซ์

อย่างไรก็ตาม เครื่องเสมือนสำหรับตัวกรองที่มีอยู่ได้รับการออกแบบให้ทำงานบนเครื่องแบบสแต็ก และไม่ได้ทำงานอย่างมีประสิทธิภาพบนเครื่อง RISC รุ่นใหม่ เป็นผลให้ด้วยความพยายามของวิศวกรจาก Berkeley Labs เทคโนโลยี BPF (Berkeley Packet Filters) ใหม่ได้รับการพัฒนาสถาปัตยกรรมเครื่องเสมือนซึ่งได้รับการออกแบบโดยใช้โปรเซสเซอร์ Motorola 6502 ซึ่งเป็นผลงานของผลิตภัณฑ์ที่มีชื่อเสียงเช่น Apple II หรือ NES. เครื่องเสมือนใหม่เพิ่มประสิทธิภาพตัวกรองหลายสิบเท่าเมื่อเทียบกับโซลูชันที่มีอยู่

สถาปัตยกรรมเครื่อง BPF

เราจะทำความคุ้นเคยกับสถาปัตยกรรมในลักษณะการทำงานโดยวิเคราะห์ตัวอย่าง อย่างไรก็ตาม ในการเริ่มต้น สมมติว่าเครื่องมีรีจิสเตอร์ 32 บิตสองตัวที่ผู้ใช้สามารถเข้าถึงได้ ซึ่งเป็นตัวสะสม A และการลงทะเบียนดัชนี Xหน่วยความจำ 64 ไบต์ (16 คำ) สำหรับการเขียนและการอ่านในภายหลัง และระบบคำสั่งขนาดเล็กสำหรับการทำงานกับวัตถุเหล่านี้ คำแนะนำในการกระโดดสำหรับการใช้นิพจน์แบบมีเงื่อนไขก็มีอยู่ในโปรแกรมเช่นกัน แต่เพื่อรับประกันความสมบูรณ์ของโปรแกรมในเวลาที่เหมาะสม การกระโดดสามารถทำได้ไปข้างหน้าเท่านั้น กล่าวคือ โดยเฉพาะอย่างยิ่งห้ามมิให้สร้างลูป

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

ข้างต้นจะเพียงพอสำหรับเราที่จะเริ่มดูตัวอย่าง: เราจะทำความคุ้นเคยกับระบบและรูปแบบคำสั่งตามความจำเป็น หากคุณต้องการศึกษาระบบคำสั่งของเครื่องเสมือนทันทีและเรียนรู้เกี่ยวกับความสามารถทั้งหมดของมัน คุณสามารถอ่านบทความต้นฉบับได้ ตัวกรองแพ็คเก็ต BSD และ/หรือครึ่งแรกของไฟล์ เอกสาร/เครือข่าย/filter.txt จากเอกสารเคอร์เนล นอกจากนี้คุณสามารถศึกษาการนำเสนอ libpcap: สถาปัตยกรรมและวิธีการเพิ่มประสิทธิภาพสำหรับการจับแพ็คเก็ตซึ่ง McCanne หนึ่งในผู้เขียน BPF พูดถึงประวัติศาสตร์แห่งการสร้างสรรค์ libpcap.

ตอนนี้เรามาดูตัวอย่างที่สำคัญทั้งหมดของการใช้ BPF แบบคลาสสิกบน Linux: tcpdump (libpcap), วินาทีคอมป์, xt_bpf, cls_bpf.

tcpdump

การพัฒนา BPF ดำเนินการควบคู่ไปกับการพัฒนาส่วนหน้าสำหรับการกรองแพ็กเก็ตซึ่งเป็นยูทิลิตี้ที่รู้จักกันดี tcpdump. และเนื่องจากนี่คือตัวอย่างที่เก่าแก่และมีชื่อเสียงที่สุดของการใช้ BPF แบบคลาสสิกซึ่งมีอยู่ในระบบปฏิบัติการหลายระบบ เราจึงจะเริ่มศึกษาเทคโนโลยีด้วยสิ่งนี้

(ฉันรันตัวอย่างทั้งหมดในบทความนี้บน Linux 5.6.0-rc6. ผลลัพธ์ของคำสั่งบางคำสั่งได้รับการแก้ไขเพื่อให้อ่านง่ายขึ้น)

ตัวอย่าง: การสังเกตแพ็กเก็ต IPv6

สมมติว่าเราต้องการดูแพ็กเก็ต IPv6 ทั้งหมดบนอินเทอร์เฟซ eth0. เพื่อทำสิ่งนี้เราสามารถรันโปรแกรมได้ tcpdump ด้วยตัวกรองแบบง่ายๆ ip6:

$ sudo tcpdump -i eth0 ip6

ในกรณีนี้ tcpdump รวบรวมตัวกรอง ip6 ลงในไบต์โค้ดสถาปัตยกรรม BPF และส่งไปยังเคอร์เนล (ดูรายละเอียดในส่วน Tcpdump: กำลังโหลด). ตัวกรองที่โหลดจะถูกรันสำหรับทุกแพ็กเก็ตที่ส่งผ่านอินเทอร์เฟซ eth0. หากตัวกรองส่งคืนค่าที่ไม่ใช่ศูนย์ nจากนั้นขึ้นไป n ไบต์ของแพ็กเก็ตจะถูกคัดลอกไปยังพื้นที่ผู้ใช้และเราจะเห็นมันในเอาต์พุต tcpdump.

BPF สำหรับเด็ก ตอนที่ XNUMX: BPF แบบคลาสสิก

ปรากฎว่าเราสามารถค้นหาได้อย่างง่ายดายว่ารหัสไบต์ใดถูกส่งไปยังเคอร์เนล tcpdump ด้วยความช่วยเหลือของ tcpdumpถ้าเรารันมันด้วยตัวเลือก -d:

$ sudo tcpdump -i eth0 -d ip6
(000) ldh      [12]
(001) jeq      #0x86dd          jt 2    jf 3
(002) ret      #262144
(003) ret      #0

ในบรรทัดศูนย์เรารันคำสั่ง ldh [12]ซึ่งย่อมาจาก “load into register A ครึ่งคำ (16 บิต) อยู่ที่ที่อยู่ 12” และคำถามเดียวคือเรากำลังพูดถึงหน่วยความจำประเภทใด คำตอบก็คือ ณ x เริ่มต้น (x+1)ไบต์ที่ XNUMX ของแพ็กเก็ตเครือข่ายที่วิเคราะห์ เราอ่านแพ็กเก็ตจากอินเทอร์เฟซอีเทอร์เน็ต eth0, และนี่ วิธีว่าแพ็กเก็ตมีลักษณะเช่นนี้ (เพื่อความง่าย เราถือว่าไม่มีแท็ก VLAN ในแพ็กเก็ต):

       6              6          2
|Destination MAC|Source MAC|Ether Type|...|

ดังนั้นหลังจากดำเนินการตามคำสั่งแล้ว ldh [12] ในทะเบียน A จะมีสนาม Ether Type — ประเภทของแพ็กเก็ตที่ส่งในเฟรมอีเทอร์เน็ตนี้ ในบรรทัดที่ 1 เราเปรียบเทียบเนื้อหาของการลงทะเบียน A (ประเภทบรรจุภัณฑ์)ค 0x86dd, และนี่ และมี ประเภทที่เราสนใจคือ IPv6 ในบรรทัดที่ 1 นอกเหนือจากคำสั่งการเปรียบเทียบแล้วยังมีอีกสองคอลัมน์ - jt 2 и jf 3 — เครื่องหมายที่คุณต้องไปหากการเปรียบเทียบสำเร็จ (A == 0x86dd) และไม่สำเร็จ ดังนั้นในกรณีที่สำเร็จ (IPv6) เราจะไปที่บรรทัด 2 และในกรณีที่ไม่สำเร็จ - ไปที่บรรทัด 3 ที่บรรทัด 3 โปรแกรมสิ้นสุดด้วยรหัส 0 (อย่าคัดลอกแพ็กเก็ต) ในบรรทัด 2 โปรแกรมสิ้นสุดด้วยรหัส 262144 (คัดลอกแพ็คเกจสูงสุด 256 กิโลไบต์ให้ฉัน)

ตัวอย่างที่ซับซ้อนมากขึ้น: เราดูแพ็กเก็ต TCP ตามพอร์ตปลายทาง

มาดูกันว่าตัวกรองจะคัดลอกแพ็กเก็ต TCP ทั้งหมดด้วยพอร์ตปลายทาง 666 ในลักษณะใด เราจะพิจารณาเคส IPv4 เนื่องจากเคส IPv6 นั้นง่ายกว่า หลังจากศึกษาตัวอย่างนี้แล้ว คุณสามารถสำรวจตัวกรอง IPv6 ด้วยตัวคุณเองเป็นแบบฝึกหัด (ip6 and tcp dst port 666) และตัวกรองสำหรับกรณีทั่วไป (tcp dst port 666). ดังนั้นตัวกรองที่เราสนใจจะมีลักษณะดังนี้:

$ sudo tcpdump -i eth0 -d ip and tcp dst port 666
(000) ldh      [12]
(001) jeq      #0x800           jt 2    jf 10
(002) ldb      [23]
(003) jeq      #0x6             jt 4    jf 10
(004) ldh      [20]
(005) jset     #0x1fff          jt 10   jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 16]
(008) jeq      #0x29a           jt 9    jf 10
(009) ret      #262144
(010) ret      #0

เรารู้อยู่แล้วว่าบรรทัด 0 และ 1 ทำอะไร ในบรรทัดที่ 2 เราได้ตรวจสอบแล้วว่านี่คือแพ็กเก็ต IPv4 (Ether Type = 0x800) และโหลดลงในทะเบียน A ไบต์ที่ 24 ของแพ็กเก็ต หน้าตาแพ็คเกจของเราก็ประมาณนี้

       14            8      1     1
|ethernet header|ip fields|ttl|protocol|...|

ซึ่งหมายความว่าเราโหลดเข้าสู่การลงทะเบียน A ฟิลด์โปรโตคอลของส่วนหัว IP ซึ่งเป็นตรรกะ เนื่องจากเราต้องการคัดลอกเฉพาะแพ็กเก็ต TCP เราเปรียบเทียบโปรโตคอลกับ 0x6 (IPPROTO_TCP) บนบรรทัดที่ 3

ในบรรทัดที่ 4 และ 5 เราโหลด halfwords ที่อยู่ตามที่อยู่ 20 และใช้คำสั่ง jset ตรวจสอบว่ามีการตั้งค่าหนึ่งในสามหรือไม่ ธง - สวมหน้ากากอนามัยที่ออกให้ jset บิตที่สำคัญที่สุดสามบิตจะถูกล้าง สองในสามบิตบอกเราว่าแพ็กเก็ตเป็นส่วนหนึ่งของแพ็กเก็ต IP ที่กระจัดกระจายหรือไม่ และหากเป็นเช่นนั้น จะเป็นแฟรกเมนต์สุดท้ายหรือไม่ บิตที่สามถูกสงวนไว้และต้องเป็นศูนย์ เราไม่ต้องการตรวจสอบแพ็คเก็ตที่ไม่สมบูรณ์หรือเสียหาย ดังนั้นเราจึงตรวจสอบทั้งสามบิต

บรรทัดที่ 6 น่าสนใจที่สุดในรายการนี้ การแสดงออก ldxb 4*([14]&0xf) หมายความว่าเราโหลดเข้าสู่การลงทะเบียน X สี่บิตที่มีนัยสำคัญน้อยที่สุดในไบต์ที่สิบห้าของแพ็กเก็ตคูณด้วย 4 สี่บิตที่มีนัยสำคัญน้อยที่สุดในไบต์ที่สิบห้าคือฟิลด์ ความยาวส่วนหัวของอินเทอร์เน็ต ส่วนหัว IPv4 ซึ่งเก็บความยาวของส่วนหัวเป็นคำ ดังนั้นคุณต้องคูณด้วย 4 สิ่งที่น่าสนใจคือนิพจน์ 4*([14]&0xf) เป็นการกำหนดรูปแบบการระบุที่อยู่พิเศษที่สามารถใช้ได้เฉพาะในแบบฟอร์มนี้และสำหรับการลงทะเบียนเท่านั้น X, เช่น. เราไม่สามารถพูดได้เช่นกัน ldb 4*([14]&0xf) ไม่ ldxb 5*([14]&0xf) (เราสามารถระบุได้เฉพาะออฟเซ็ตอื่นเท่านั้น เช่น ldxb 4*([16]&0xf)). เป็นที่ชัดเจนว่ารูปแบบการระบุที่อยู่นี้ถูกเพิ่มเข้าไปใน BPF อย่างแม่นยำเพื่อที่จะรับ X (การลงทะเบียนดัชนี) ความยาวส่วนหัว IPv4

ดังนั้นในบรรทัดที่ 7 เราจึงพยายามโหลดครึ่งคำที่ (X+16). โปรดจำไว้ว่า 14 ไบต์ถูกครอบครองโดยส่วนหัวของ Ethernet และ X มีความยาวของส่วนหัว IPv4 เราเข้าใจว่าใน A โหลดพอร์ตปลายทาง TCP แล้ว:

       14           X           2             2
|ethernet header|ip header|source port|destination port|

ในที่สุด ในบรรทัดที่ 8 เราจะเปรียบเทียบพอร์ตปลายทางกับค่าที่ต้องการ และในบรรทัดที่ 9 หรือ 10 เราจะส่งคืนผลลัพธ์ - ไม่ว่าจะคัดลอกแพ็กเก็ตหรือไม่ก็ตาม

Tcpdump: กำลังโหลด

ในตัวอย่างก่อนหน้านี้ เราไม่ได้เจาะลึกรายละเอียดเกี่ยวกับวิธีการโหลดโค้ดไบต์ BPF ลงในเคอร์เนลเพื่อการกรองแพ็คเก็ต พูด, พูดแบบทั่วไป, พูดทั่วๆไป, tcpdump ย้ายไปยังหลายระบบและสำหรับการทำงานกับตัวกรอง tcpdump ใช้ห้องสมุด libpcap. สั้น ๆ เพื่อวางตัวกรองบนอินเทอร์เฟซโดยใช้ libpcap, คุณต้องทำต่อไปนี้:

  • สร้างคำอธิบายประเภท pcap_t จากชื่ออินเทอร์เฟซ: pcap_create,
  • เปิดใช้งานอินเทอร์เฟซ: pcap_activate,
  • รวบรวมตัวกรอง: pcap_compile,
  • เชื่อมต่อตัวกรอง: pcap_setfilter.

เพื่อดูว่ามีการทำงานอย่างไร pcap_setfilter นำมาใช้ใน Linux ที่เราใช้ strace (บางบรรทัดถูกลบออก):

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

ในเอาต์พุตสองบรรทัดแรกที่เราสร้างขึ้น ซ็อกเก็ตดิบ เพื่ออ่านเฟรมอีเทอร์เน็ตทั้งหมดและผูกเข้ากับอินเทอร์เฟซ eth0. จาก ตัวอย่างแรกของเรา เรารู้ว่าไส้กรอง ip จะประกอบด้วยคำสั่ง BPF สี่คำสั่ง และในบรรทัดที่สาม เราจะดูว่าใช้ตัวเลือกอย่างไร SO_ATTACH_FILTER การโทรของระบบ setsockopt เราโหลดและเชื่อมต่อตัวกรองความยาว 4 นี่คือตัวกรองของเรา

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

ความจริงที่ซ่อนอยู่

เวอร์ชันที่สมบูรณ์กว่านี้เล็กน้อยของเอาต์พุตจะมีลักษณะดังนี้:

$ sudo strace -f -e trace=%network tcpdump -p -i eth0 ip
socket(AF_PACKET, SOCK_RAW, 768)        = 3
bind(3, {sa_family=AF_PACKET, sll_protocol=htons(ETH_P_ALL), sll_ifindex=if_nametoindex("eth0"), sll_hatype=ARPHRD_NETROM, sll_pkttype=PACKET_HOST, sll_halen=0}, 20) = 0
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=1, filter=0xbeefbeefbeef}, 16) = 0
recvfrom(3, 0x7ffcad394257, 1, MSG_TRUNC, NULL, NULL) = -1 EAGAIN (Resource temporarily unavailable)
setsockopt(3, SOL_SOCKET, SO_ATTACH_FILTER, {len=4, filter=0xb00bb00bb00b}, 16) = 0
...

ตามที่กล่าวไว้ข้างต้น เราโหลดและเชื่อมต่อตัวกรองของเราเข้ากับช่องเสียบที่บรรทัดที่ 5 แต่จะเกิดอะไรขึ้นในบรรทัดที่ 3 และ 4 ปรากฎว่าสิ่งนี้ libpcap ดูแลเรา - เพื่อให้ผลลัพธ์ของตัวกรองของเราไม่รวมแพ็กเก็ตที่ไม่เป็นไปตามนั้นไลบรารี เชื่อมต่อ ตัวกรองจำลอง ret #0 (ปล่อยแพ็กเก็ตทั้งหมด) เปลี่ยนซ็อกเก็ตเป็นโหมดไม่บล็อกและพยายามลบแพ็กเก็ตทั้งหมดที่อาจเหลือจากตัวกรองก่อนหน้า

โดยรวมแล้ว ในการกรองแพ็คเกจบน Linux โดยใช้ BPF แบบคลาสสิก คุณต้องมีตัวกรองในรูปแบบของโครงสร้างที่คล้ายกัน struct sock_fprog และซ็อกเก็ตแบบเปิด หลังจากนั้นจึงสามารถติดตัวกรองเข้ากับซ็อกเก็ตได้โดยใช้การเรียกระบบ setsockopt.

ที่น่าสนใจคือสามารถติดตัวกรองเข้ากับซ็อกเก็ตใดก็ได้ ไม่ใช่แค่แบบดิบเท่านั้น ที่นี่ ตัวอย่าง โปรแกรมที่ตัดทั้งหมดยกเว้นสองไบต์แรกออกจากดาตาแกรม UDP ขาเข้าทั้งหมด (ฉันเพิ่มความคิดเห็นในโค้ดเพื่อไม่ให้บทความเกะกะ)

รายละเอียดเพิ่มเติมเกี่ยวกับการใช้งาน setsockopt สำหรับการเชื่อมต่อตัวกรอง โปรดดู ปลั๊กไฟ(7)แต่เกี่ยวกับการเขียนตัวกรองของคุณเองเช่น struct sock_fprog โดยไม่มีความช่วยเหลือ tcpdump เราจะพูดคุยในส่วนนี้ การเขียนโปรแกรม BPF ด้วยมือของเราเอง.

Classic BPF และศตวรรษที่ XNUMX

BPF ถูกรวมอยู่ใน Linux ในปี 1997 และยังคงเป็นเครื่องมือมาเป็นเวลานาน libpcap โดยไม่มีการเปลี่ยนแปลงพิเศษใด ๆ (แน่นอนว่าการเปลี่ยนแปลงเฉพาะ Linux มันเป็นแต่พวกเขาไม่ได้เปลี่ยนภาพรวมทั่วโลก) สัญญาณร้ายแรงแรกที่ BPF จะมีการพัฒนาเกิดขึ้นในปี 2011 เมื่อ Eric Dumazet เสนอ แก้ไขซึ่งเพิ่ม Just In Time Compiler ให้กับเคอร์เนล - โปรแกรมแปลสำหรับการแปลงรหัสไบต์ BPF เป็นเนทิฟ x86_64 รหัส.

คอมไพเลอร์ JIT เป็นคนแรกในห่วงโซ่การเปลี่ยนแปลง: ในปี 2012 ปรากฏ ความสามารถในการเขียนตัวกรองสำหรับ วินาทีโดยใช้ BPF ในเดือนมกราคม 2013 มี เพิ่ม โมดูล xt_bpfซึ่งช่วยให้คุณเขียนกฎเกณฑ์ได้ iptables ด้วยความช่วยเหลือของ BPF และในเดือนตุลาคม 2013 ก็เป็นเช่นนั้น เพิ่ม ยังเป็นโมดูล cls_bpfซึ่งช่วยให้คุณเขียนตัวแยกประเภทการรับส่งข้อมูลโดยใช้ BPF

เราจะดูตัวอย่างทั้งหมดนี้โดยละเอียดมากขึ้นเร็วๆ นี้ แต่ก่อนอื่น มันจะมีประโยชน์สำหรับเราในการเรียนรู้วิธีการเขียนและคอมไพล์โปรแกรมที่กำหนดเองสำหรับ BPF เนื่องจากความสามารถที่จัดทำโดยไลบรารี libpcap จำกัด (ตัวอย่างง่ายๆ: สร้างตัวกรองแล้ว libpcap สามารถส่งคืนได้เพียงสองค่า - 0 หรือ 0x40000) หรือโดยทั่วไป ในกรณีของ seccomp จะไม่สามารถใช้ได้

การเขียนโปรแกรม BPF ด้วยมือของเราเอง

มาทำความรู้จักกับรูปแบบไบนารีของคำสั่ง BPF กันดีกว่า มันง่ายมาก:

   16    8    8     32
| code | jt | jf |  k  |

แต่ละคำสั่งใช้พื้นที่ 64 บิต โดย 16 บิตแรกเป็นโค้ดคำสั่ง จากนั้นจะมีการเยื้องแปดบิตสองอัน jt и jfและ 32 บิตสำหรับอาร์กิวเมนต์ Kซึ่งมีวัตถุประสงค์แตกต่างกันไปในแต่ละคำสั่ง เช่น คำสั่ง retซึ่งจบโปรแกรมก็มีโค้ด 6และค่าที่ส่งคืนจะนำมาจากค่าคงที่ K. ในภาษา C คำสั่ง BPF เดี่ยวจะแสดงเป็นโครงสร้าง

struct sock_filter {
        __u16   code;
        __u8    jt;
        __u8    jf;
        __u32   k;
}

และโปรแกรมทั้งหมดจะอยู่ในรูปแบบโครงสร้าง

struct sock_fprog {
        unsigned short len;
        struct sock_filter *filter;
}

ดังนั้นเราจึงสามารถเขียนโปรแกรมได้แล้ว (เช่น เรารู้โค้ดคำสั่งจาก [1]). ตัวกรองจะมีลักษณะเช่นนี้ ip6 ของ ตัวอย่างแรกของเรา:

struct sock_filter code[] = {
        { 0x28, 0, 0, 0x0000000c },
        { 0x15, 0, 1, 0x000086dd },
        { 0x06, 0, 0, 0x00040000 },
        { 0x06, 0, 0, 0x00000000 },
};
struct sock_fprog prog = {
        .len = ARRAY_SIZE(code),
        .filter = code,
};

โปรแกรม prog เราสามารถใช้การโทรได้อย่างถูกกฎหมาย

setsockopt(sk, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog))

การเขียนโปรแกรมในรูปแบบของรหัสเครื่องนั้นไม่สะดวกนัก แต่บางครั้งก็จำเป็น (เช่น การดีบัก การสร้างการทดสอบหน่วย การเขียนบทความเกี่ยวกับ Habré เป็นต้น) เพื่อความสะดวกในไฟล์ <linux/filter.h> มีการกำหนดมาโครตัวช่วย - ตัวอย่างเดียวกับข้างต้นสามารถเขียนใหม่เป็นได้

struct sock_filter code[] = {
        BPF_STMT(BPF_LD|BPF_H|BPF_ABS, 12),
        BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, ETH_P_IPV6, 0, 1),
        BPF_STMT(BPF_RET|BPF_K, 0x00040000),
        BPF_STMT(BPF_RET|BPF_K, 0),
}

อย่างไรก็ตามตัวเลือกนี้ไม่สะดวกนัก นี่คือสิ่งที่โปรแกรมเมอร์เคอร์เนล Linux ให้เหตุผลและอยู่ในไดเร็กทอรี tools/bpf เคอร์เนลคุณสามารถค้นหาแอสเซมเบลอร์และดีบักเกอร์สำหรับการทำงานกับ BPF แบบคลาสสิก

ภาษาแอสเซมบลีคล้ายกับเอาต์พุตการดีบักมาก tcpdumpแต่นอกจากนี้เรายังสามารถระบุป้ายกำกับสัญลักษณ์ได้ ตัวอย่างเช่น นี่คือโปรแกรมที่จะดรอปแพ็กเก็ตทั้งหมดยกเว้น TCP/IPv4:

$ cat /tmp/tcp-over-ipv4.bpf
ldh [12]
jne #0x800, drop
ldb [23]
jneq #6, drop
ret #-1
drop: ret #0

ตามค่าเริ่มต้น แอสเซมเบลอร์จะสร้างโค้ดในรูปแบบ <количество инструкций>,<code1> <jt1> <jf1> <k1>,...สำหรับตัวอย่างของเรากับ TCP มันจะเป็น

$ tools/bpf/bpf_asm /tmp/tcp-over-ipv4.bpf
6,40 0 0 12,21 0 3 2048,48 0 0 23,21 0 1 6,6 0 0 4294967295,6 0 0 0,

เพื่อความสะดวกของโปรแกรมเมอร์ C สามารถใช้รูปแบบเอาต์พุตที่แตกต่างกัน:

$ tools/bpf/bpf_asm -c /tmp/tcp-over-ipv4.bpf
{ 0x28,  0,  0, 0x0000000c },
{ 0x15,  0,  3, 0x00000800 },
{ 0x30,  0,  0, 0x00000017 },
{ 0x15,  0,  1, 0x00000006 },
{ 0x06,  0,  0, 0xffffffff },
{ 0x06,  0,  0, 0000000000 },

ข้อความนี้สามารถคัดลอกลงในคำจำกัดความของโครงสร้างประเภทได้ struct sock_filterอย่างที่เราทำในตอนต้นของส่วนนี้

ส่วนขยาย Linux และ netsniff-ng

นอกเหนือจาก BPF มาตรฐานแล้ว Linux และ tools/bpf/bpf_asm สนับสนุนและ ชุดที่ไม่ได้มาตรฐาน. โดยพื้นฐานแล้ว คำสั่งจะใช้ในการเข้าถึงฟิลด์ของโครงสร้าง struct sk_buffซึ่งอธิบายแพ็กเก็ตเครือข่ายในเคอร์เนล อย่างไรก็ตาม ยังมีคำแนะนำประเภทอื่นๆ อีกด้วย เช่น ldw cpu จะโหลดเข้าทะเบียน A ผลลัพธ์ของการรันฟังก์ชันเคอร์เนล raw_smp_processor_id(). (ในเวอร์ชันใหม่ของ BPF ส่วนขยายที่ไม่ได้มาตรฐานเหล่านี้ได้รับการขยายเพื่อให้โปรแกรมมีชุดตัวช่วยเคอร์เนลสำหรับการเข้าถึงหน่วยความจำ โครงสร้าง และสร้างเหตุการณ์) นี่คือตัวอย่างที่น่าสนใจของตัวกรองที่เราคัดลอกเฉพาะ ส่วนหัวของแพ็กเก็ตลงในพื้นที่ผู้ใช้โดยใช้ส่วนขยาย poff, ชดเชยเพย์โหลด:

ld poff
ret a

ไม่สามารถใช้ส่วนขยาย BPF ได้ tcpdumpแต่นี่เป็นเหตุผลที่ดีที่จะทำความคุ้นเคยกับแพ็คเกจยูทิลิตี้ netsniff-ngซึ่งมีโปรแกรมขั้นสูงเหนือสิ่งอื่นใด netsniff-ngซึ่งนอกเหนือจากการกรองโดยใช้ BPF แล้ว ยังมีเครื่องมือสร้างปริมาณข้อมูลที่มีประสิทธิภาพและล้ำหน้ากว่าอีกด้วย tools/bpf/bpf_asmแอสเซมเบลอร์ BPF ถูกเรียก bpfc. แพคเกจประกอบด้วยเอกสารที่มีรายละเอียดค่อนข้างมาก โปรดดูลิงก์ท้ายบทความด้วย

วินาที

ดังนั้นเราจึงรู้วิธีการเขียนโปรแกรม BPF ที่มีความซับซ้อนตามอำเภอใจแล้ว และพร้อมที่จะดูตัวอย่างใหม่ อย่างแรกคือเทคโนโลยี seccomp ซึ่งอนุญาตให้ใช้ตัวกรอง BPF เพื่อจัดการชุดและชุดของอาร์กิวเมนต์การเรียกของระบบที่มีให้ กระบวนการที่กำหนดและลูกหลานของมัน

seccomp เวอร์ชันแรกถูกเพิ่มเข้าไปในเคอร์เนลในปี 2005 และไม่ได้รับความนิยมมากนัก เนื่องจากมีตัวเลือกเดียวเท่านั้น - เพื่อจำกัดชุดการเรียกของระบบที่ใช้ได้กับกระบวนการดังต่อไปนี้: read, write, exit и sigreturnและกระบวนการที่ละเมิดกฎก็ถูกฆ่าโดยใช้ SIGKILL. อย่างไรก็ตาม ในปี 2012 seccomp ได้เพิ่มความสามารถในการใช้ตัวกรอง BPF ซึ่งช่วยให้คุณสามารถกำหนดชุดของการเรียกของระบบที่อนุญาต และแม้แต่ทำการตรวจสอบข้อโต้แย้งของพวกเขา (ที่น่าสนใจคือ Chrome เป็นหนึ่งในผู้ใช้กลุ่มแรกๆ ของฟังก์ชันนี้ และบุคลากร Chrome กำลังพัฒนากลไก KRSI โดยใช้ BPF เวอร์ชันใหม่และอนุญาตให้ปรับแต่งโมดูลความปลอดภัยของ Linux ได้) ลิงก์ไปยังเอกสารเพิ่มเติมมีอยู่ในตอนท้าย ของบทความ

โปรดทราบว่ามีบทความเกี่ยวกับการใช้ seccomp ในฮับอยู่แล้ว บางทีอาจมีบางคนต้องการอ่านบทความเหล่านี้ก่อน (หรือแทน) อ่านส่วนย่อยต่อไปนี้ ในบทความ คอนเทนเนอร์และการรักษาความปลอดภัย: seccomp ให้ตัวอย่างการใช้ seccomp ทั้งเวอร์ชัน 2007 และเวอร์ชันที่ใช้ BPF (ตัวกรองถูกสร้างขึ้นโดยใช้ libseccomp) พูดถึงการเชื่อมต่อของ seccomp กับ Docker และยังให้ลิงก์ที่มีประโยชน์มากมาย ในบทความ แยก daemons ด้วย systemd หรือ “คุณไม่จำเป็นต้องมี Docker สำหรับสิ่งนี้!” โดยเฉพาะอย่างยิ่งครอบคลุมถึงวิธีการเพิ่มบัญชีดำหรือบัญชีขาวของการเรียกระบบสำหรับ daemons ที่กำลังรัน systemd

ต่อไปเราจะดูวิธีการเขียนและโหลดตัวกรองสำหรับ seccomp ใน Bare C และการใช้ไลบรารี libseccomp และข้อดีข้อเสียของแต่ละตัวเลือกมีอะไรบ้าง และสุดท้าย มาดูกันว่าโปรแกรม seccomp ใช้งานอย่างไร strace.

การเขียนและการโหลดตัวกรองสำหรับ seccomp

เรารู้วิธีการเขียนโปรแกรม BPF อยู่แล้ว ดังนั้นก่อนอื่นเรามาดูอินเทอร์เฟซการเขียนโปรแกรม seccomp กันก่อน คุณสามารถตั้งค่าตัวกรองที่ระดับกระบวนการ และกระบวนการย่อยทั้งหมดจะสืบทอดข้อจำกัด ทำได้โดยใช้การเรียกของระบบ seccomp(2):

seccomp(SECCOMP_SET_MODE_FILTER, flags, &filter)

ที่ไหน &filter - นี่คือตัวชี้ไปยังโครงสร้างที่เราคุ้นเคยอยู่แล้ว struct sock_fprog, เช่น. โปรแกรมบีพีเอฟ

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

struct seccomp_data {
    int   nr;
    __u32 arch;
    __u64 instruction_pointer;
    __u64 args[6];
};

ที่นี่ nr คือหมายเลขของการเรียกระบบที่จะเรียกใช้ arch - สถาปัตยกรรมปัจจุบัน (เพิ่มเติมด้านล่างนี้) args - อาร์กิวเมนต์การโทรของระบบสูงสุดหกข้อและ instruction_pointer เป็นตัวชี้ไปยังคำสั่งพื้นที่ผู้ใช้ที่ทำการเรียกระบบ ดังนั้น เช่น การโหลดหมายเลขโทรศัพท์ของระบบลงในรีจิสเตอร์ A เราต้องพูด

ldw [0]

มีคุณสมบัติอื่น ๆ สำหรับโปรแกรม seccomp เช่น บริบทสามารถเข้าถึงได้โดยการจัดตำแหน่งแบบ 32 บิตเท่านั้น และคุณไม่สามารถโหลดครึ่งคำหรือไบต์ได้ - เมื่อพยายามโหลดตัวกรอง ldh [0] การโทรของระบบ seccomp จะกลับมา EINVAL. ฟังก์ชั่นตรวจสอบตัวกรองที่โหลด seccomp_check_filter() เมล็ด (สิ่งที่ตลกคือในคอมมิตดั้งเดิมที่เพิ่มฟังก์ชัน seccomp พวกเขาลืมเพิ่มการอนุญาตในการใช้คำสั่งกับฟังก์ชันนี้ mod (ส่วนที่เหลือของการหาร) และตอนนี้ไม่พร้อมใช้งานสำหรับโปรแกรม seccomp BPF นับตั้งแต่มีการเพิ่ม จะแตก เอบีไอ)

โดยพื้นฐานแล้วเรารู้ทุกอย่างแล้วในการเขียนและอ่านโปรแกรม seccomp โดยปกติตรรกะของโปรแกรมจะถูกจัดเรียงเป็นรายการสีขาวหรือสีดำของการเรียกของระบบ เช่น โปรแกรม

ld [0]
jeq #304, bad
jeq #176, bad
jeq #239, bad
jeq #279, bad
good: ret #0x7fff0000 /* SECCOMP_RET_ALLOW */
bad: ret #0

ตรวจสอบบัญชีดำของการเรียกระบบสี่สายหมายเลข 304, 176, 239, 279 การเรียกของระบบเหล่านี้คืออะไร? เราไม่สามารถพูดได้แน่ชัด เนื่องจากเราไม่รู้ว่าโปรแกรมถูกเขียนขึ้นโดยใช้สถาปัตยกรรมใด ดังนั้นผู้เขียน seccomp เสนอ เริ่มต้นโปรแกรมทั้งหมดด้วยการตรวจสอบสถาปัตยกรรม (สถาปัตยกรรมปัจจุบันจะถูกระบุในบริบทเป็นฟิลด์ arch โครงสร้าง struct seccomp_data). เมื่อตรวจสอบสถาปัตยกรรมแล้ว จุดเริ่มต้นของตัวอย่างจะมีลักษณะดังนี้:

ld [4]
jne #0xc000003e, bad_arch ; SCMP_ARCH_X86_64

จากนั้นหมายเลขโทรศัพท์ของระบบของเราก็จะได้ค่าที่แน่นอน

เราเขียนและโหลดตัวกรองสำหรับการใช้ seccomp libseccomp

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

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

#include <seccomp.h>
#include <unistd.h>
#include <err.h>

static int sys_numbers[] = {
        __NR_mount,
        __NR_umount2,
       // ... еще 40 системных вызовов ...
        __NR_vmsplice,
        __NR_perf_event_open,
};

int main(int argc, char **argv)
{
        scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);

        for (size_t i = 0; i < sizeof(sys_numbers)/sizeof(sys_numbers[0]); i++)
                seccomp_rule_add(ctx, SCMP_ACT_TRAP, sys_numbers[i], 0);

        seccomp_load(ctx);

        execvp(argv[1], &argv[1]);
        err(1, "execlp: %s", argv[1]);
}

ขั้นแรกเรากำหนดอาร์เรย์ sys_numbers จำนวนการโทรของระบบมากกว่า 40 หมายเลขที่จะบล็อก จากนั้นเริ่มต้นบริบท ctx และบอกห้องสมุดว่าเราต้องการอนุญาตอะไร (SCMP_ACT_ALLOW) การเรียกของระบบทั้งหมดตามค่าเริ่มต้น (การสร้างบัญชีดำง่ายกว่า) จากนั้น เราจะเพิ่มการโทรของระบบทั้งหมดจากบัญชีดำทีละรายการ เพื่อตอบสนองต่อการเรียกของระบบจากรายการ เราขอ SCMP_ACT_TRAPในกรณีนี้ seccomp จะส่งสัญญาณไปยังกระบวนการ SIGSYS พร้อมคำอธิบายว่าการเรียกของระบบใดที่ละเมิดกฎ ในที่สุดเราก็โหลดโปรแกรมลงในเคอร์เนลโดยใช้ seccomp_loadซึ่งจะคอมไพล์โปรแกรมและแนบไปกับกระบวนการโดยใช้การเรียกของระบบ seccomp(2).

เพื่อให้การคอมไพล์สำเร็จ โปรแกรมจะต้องเชื่อมโยงกับไลบรารี libseccompตัวอย่างเช่น:

cc -std=c17 -Wall -Wextra -c -o seccomp_lib.o seccomp_lib.c
cc -o seccomp_lib seccomp_lib.o -lseccomp

ตัวอย่างการเปิดตัวที่ประสบความสำเร็จ:

$ ./seccomp_lib echo ok
ok

ตัวอย่างของการเรียกของระบบที่ถูกบล็อก:

$ sudo ./seccomp_lib mount -t bpf bpf /tmp
Bad system call

เราใช้ straceสำหรับรายละเอียด:

$ sudo strace -e seccomp ./seccomp_lib mount -t bpf bpf /tmp
seccomp(SECCOMP_SET_MODE_FILTER, 0, {len=50, filter=0x55d8e78428e0}) = 0
--- SIGSYS {si_signo=SIGSYS, si_code=SYS_SECCOMP, si_call_addr=0xboobdeadbeef, si_syscall=__NR_mount, si_arch=AUDIT_ARCH_X86_64} ---
+++ killed by SIGSYS (core dumped) +++
Bad system call

เราจะรู้ได้อย่างไรว่าโปรแกรมถูกยกเลิกเนื่องจากการใช้ระบบที่ผิดกฎหมาย mount(2).

ดังนั้นเราจึงเขียนตัวกรองโดยใช้ไลบรารี libseccompปรับโค้ดที่ไม่สำคัญให้เหมาะสมเป็นสี่บรรทัด ในตัวอย่างข้างต้น หากมีการเรียกของระบบจำนวนมาก เวลาดำเนินการจะลดลงอย่างเห็นได้ชัด เนื่องจากการตรวจสอบเป็นเพียงรายการการเปรียบเทียบเท่านั้น สำหรับการเพิ่มประสิทธิภาพ libseccomp เพิ่งมี รวมแพทช์ซึ่งเพิ่มการสนับสนุนสำหรับแอตทริบิวต์ตัวกรอง SCMP_FLTATR_CTL_OPTIMIZE. การตั้งค่าคุณลักษณะนี้เป็น 2 จะแปลงตัวกรองเป็นโปรแกรมค้นหาแบบไบนารี

หากคุณต้องการดูว่าตัวกรองการค้นหาแบบไบนารีทำงานอย่างไร ลองดูที่ สคริปต์ง่ายๆซึ่งสร้างโปรแกรมดังกล่าวในแอสเซมเบลอร์ BPF โดยการกดหมายเลขเรียกของระบบ เช่น:

$ echo 1 3 6 8 13 | ./generate_bin_search_bpf.py
ld [0]
jeq #6, bad
jgt #6, check8
jeq #1, bad
jeq #3, bad
ret #0x7fff0000
check8:
jeq #8, bad
jeq #13, bad
ret #0x7fff0000
bad: ret #0

คุณจะไม่สามารถเขียนอะไรได้เร็วกว่าปกติ เนื่องจากโปรแกรม BPF ไม่สามารถทำการเยื้องข้ามได้ (เราทำไม่ได้ เช่น jmp A หรือ jmp [label+X]) ดังนั้น การเปลี่ยนภาพทั้งหมดจึงเป็นแบบคงที่

วินาทีและ strace

ทุกคนรู้ถึงประโยชน์ใช้สอย strace เป็นเครื่องมือที่ขาดไม่ได้ในการศึกษาพฤติกรรมของกระบวนการบน Linux อย่างไรก็ตาม หลายคนก็เคยได้ยินเรื่องนี้เช่นกัน ปัญหาด้านประสิทธิภาพ เมื่อใช้ยูทิลิตี้นี้ ความจริงก็คือว่า strace ดำเนินการโดยใช้ ptrace(2)และในกลไกนี้ เราไม่สามารถระบุได้ว่าชุดของการเรียกระบบใดที่เราต้องหยุดกระบวนการ เช่น คำสั่ง

$ time strace du /usr/share/ >/dev/null 2>&1

real    0m3.081s
user    0m0.531s
sys     0m2.073s

и

$ time strace -e open du /usr/share/ >/dev/null 2>&1

real    0m2.404s
user    0m0.193s
sys     0m1.800s

จะถูกประมวลผลในเวลาเดียวกันโดยประมาณ แม้ว่าในกรณีที่สอง เราต้องการติดตามการเรียกของระบบเพียงครั้งเดียว

ทางเลือกใหม่ --seccomp-bpfเพิ่มไปยัง strace เวอร์ชัน 5.3 ช่วยให้คุณเร่งกระบวนการได้หลายครั้งและเวลาเริ่มต้นภายใต้การติดตามของการเรียกระบบหนึ่งครั้งนั้นเทียบได้กับเวลาของการเริ่มต้นปกติแล้ว:

$ time strace --seccomp-bpf -e open du /usr/share/ >/dev/null 2>&1

real    0m0.148s
user    0m0.017s
sys     0m0.131s

$ time du /usr/share/ >/dev/null 2>&1

real    0m0.140s
user    0m0.024s
sys     0m0.116s

(ที่นี่ แน่นอนว่า มีการหลอกลวงเล็กน้อยว่าเราไม่ได้ติดตามการเรียกระบบหลักของคำสั่งนี้ หากเรากำลังติดตาม เช่น newfsstatแล้ว strace จะเบรกแรงพอๆ กับไม่มีเลย --seccomp-bpf.)

ตัวเลือกนี้ทำงานอย่างไร? ปราศจากเธอ strace เชื่อมต่อกับกระบวนการและเริ่มใช้งาน PTRACE_SYSCALL. เมื่อกระบวนการที่ได้รับการจัดการออกการเรียกระบบ (ใดๆ) การควบคุมจะถูกถ่ายโอนไปยัง straceซึ่งดูที่อาร์กิวเมนต์การเรียกของระบบและรันด้วย PTRACE_SYSCALL. หลังจากผ่านไประยะหนึ่ง กระบวนการจะเสร็จสิ้นการเรียกของระบบ และเมื่อออกจากกระบวนการนั้น การควบคุมจะถูกถ่ายโอนอีกครั้ง straceซึ่งดูค่าที่ส่งคืนและเริ่มกระบวนการโดยใช้ PTRACE_SYSCALLและอื่นๆ

BPF สำหรับเด็ก ตอนที่ XNUMX: BPF แบบคลาสสิก

อย่างไรก็ตาม ด้วย seccomp กระบวนการนี้สามารถปรับให้เหมาะสมได้ตรงตามที่เราต้องการ กล่าวคือหากเราต้องการดูเฉพาะการเรียกของระบบ Xจากนั้นเราก็สามารถเขียนตัวกรอง BPF ที่ต้องการได้ X ส่งกลับค่า SECCOMP_RET_TRACEและสำหรับการโทรที่ไม่สนใจเรา - SECCOMP_RET_ALLOW:

ld [0]
jneq #X, ignore
trace: ret #0x7ff00000
ignore: ret #0x7fff0000

ในกรณีนี้ strace เริ่มต้นกระบวนการเป็น PTRACE_CONTตัวกรองของเราจะประมวลผลสำหรับการเรียกของระบบแต่ละครั้ง หากการเรียกของระบบไม่เป็นเช่นนั้น Xจากนั้นกระบวนการก็จะดำเนินต่อไป แต่ถ้าเป็นเช่นนี้ Xจากนั้น seccomp จะถ่ายโอนการควบคุม straceซึ่งจะดูข้อโต้แย้งและเริ่มกระบวนการเช่นนี้ PTRACE_SYSCALL (เนื่องจาก seccomp ไม่มีความสามารถในการรันโปรแกรมเมื่อออกจากการเรียกของระบบ) เมื่อการเรียกของระบบกลับมา strace จะเริ่มกระบวนการใหม่โดยใช้ PTRACE_CONT และจะรอข้อความใหม่จาก seccomp

BPF สำหรับเด็ก ตอนที่ XNUMX: BPF แบบคลาสสิก

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

รายละเอียดเพิ่มเติมเล็กน้อยเกี่ยวกับวิธีการที่แน่นอน strace ใช้งานได้กับ seccomp สามารถหาได้จาก รายงานล่าสุด. สำหรับเรา ข้อเท็จจริงที่น่าสนใจที่สุดคือ BPF แบบคลาสสิกที่แสดงโดย seccomp ยังคงใช้อยู่ในปัจจุบัน

xt_bpf

ตอนนี้เรากลับไปสู่โลกของเครือข่ายกัน

ความเป็นมา: นานมาแล้วในปี 2007 แกนหลักคือ เพิ่ม โมดูล xt_u32 สำหรับเน็ตฟิลเตอร์ มันถูกเขียนโดยการเปรียบเทียบกับตัวแยกประเภทการจราจรที่เก่าแก่ยิ่งกว่านั้น cls_u32 และอนุญาตให้คุณเขียนกฎไบนารี่ที่กำหนดเองสำหรับ iptables โดยใช้การดำเนินการง่ายๆ ต่อไปนี้: โหลด 32 บิตจากแพ็คเกจและดำเนินการชุดการดำเนินการทางคณิตศาสตร์กับกฎเหล่านั้น ตัวอย่างเช่น,

sudo iptables -A INPUT -m u32 --u32 "6&0xFF=1" -j LOG --log-prefix "seen-by-xt_u32"

โหลดส่วนหัว IP 32 บิต เริ่มต้นที่ช่องว่างภายใน 6 และใช้มาสก์กับส่วนหัวเหล่านั้น 0xFF (ใช้ไบต์ต่ำ) สนามนี้ protocol ส่วนหัว IP และเราเปรียบเทียบกับ 1 (ICMP) คุณสามารถรวมการตรวจสอบหลายรายการไว้ในกฎเดียวได้ และคุณยังสามารถดำเนินการโอเปอเรเตอร์ได้อีกด้วย @ — ย้าย X ไบต์ไปทางขวา ตัวอย่างเช่นกฎ

iptables -m u32 --u32 "6&0xFF=0x6 && 0>>22&0x3C@4=0x29"

ตรวจสอบว่าหมายเลขลำดับ TCP ไม่เท่ากันหรือไม่ 0x29. ฉันจะไม่ลงรายละเอียดเพิ่มเติมเนื่องจากเป็นที่ชัดเจนว่าการเขียนกฎดังกล่าวด้วยมือนั้นไม่สะดวกนัก ในบทความ BPF - รหัสไบต์ที่ถูกลืมมีลิงก์หลายลิงก์พร้อมตัวอย่างการใช้งานและการสร้างกฎ xt_u32. ดูลิงก์ท้ายบทความนี้ด้วย

ตั้งแต่ปี 2013 โมดูลแทนโมดูล xt_u32 คุณสามารถใช้โมดูลที่ใช้ BPF ได้ xt_bpf. ใครก็ตามที่อ่านมาจนถึงตอนนี้ควรมีความชัดเจนเกี่ยวกับหลักการของการดำเนินการ: เรียกใช้ BPF bytecode เป็นกฎ iptables คุณสามารถสร้างกฎใหม่ได้ เช่น:

iptables -A INPUT -m bpf --bytecode <байткод> -j LOG

ที่นี่ <байткод> - นี่คือโค้ดในรูปแบบเอาต์พุตแอสเซมเบลอร์ bpf_asm โดยค่าเริ่มต้น เช่น

$ cat /tmp/test.bpf
ldb [9]
jneq #17, ignore
ret #1
ignore: ret #0

$ bpf_asm /tmp/test.bpf
4,48 0 0 9,21 0 1 17,6 0 0 1,6 0 0 0,

# iptables -A INPUT -m bpf --bytecode "$(bpf_asm /tmp/test.bpf)" -j LOG

ในตัวอย่างนี้ เรากำลังกรองแพ็กเก็ต UDP ทั้งหมด บริบทสำหรับโปรแกรม BPF ในโมดูล xt_bpfแน่นอนว่าจะชี้ไปที่ข้อมูลแพ็กเก็ต ในกรณีของ iptables ไปยังจุดเริ่มต้นของส่วนหัว IPv4 ส่งคืนค่าจากโปรแกรม BPF บูลีนที่ไหน false หมายถึงแพ็กเก็ตไม่ตรงกัน

เป็นที่ชัดเจนว่าโมดูล xt_bpf รองรับตัวกรองที่ซับซ้อนมากกว่าตัวอย่างด้านบน มาดูตัวอย่างจริงจาก Cloudfare กัน จนกระทั่งเมื่อไม่นานมานี้พวกเขาใช้โมดูลนี้ xt_bpf เพื่อป้องกันการโจมตี DDoS ในบทความ ขอแนะนำเครื่องมือ BPF พวกเขาอธิบายว่าพวกเขาสร้างตัวกรอง BPF อย่างไร (และทำไม) และเผยแพร่ลิงก์ไปยังชุดยูทิลิตี้สำหรับสร้างตัวกรองดังกล่าว เช่น การใช้ยูทิลิตี้ bpfgen คุณสามารถสร้างโปรแกรม BPF ที่ตรงกับการสืบค้น DNS สำหรับชื่อได้ habr.com:

$ ./bpfgen --assembly dns -- habr.com
ldx 4*([0]&0xf)
ld #20
add x
tax

lb_0:
    ld [x + 0]
    jneq #0x04686162, lb_1
    ld [x + 4]
    jneq #0x7203636f, lb_1
    ldh [x + 8]
    jneq #0x6d00, lb_1
    ret #65535

lb_1:
    ret #0

ในโปรแกรมเราโหลดเข้า register ก่อน X จุดเริ่มต้นของที่อยู่บรรทัด x04habrx03comx00 ภายในดาตาแกรม UDP จากนั้นตรวจสอบคำขอ: 0x04686162 <-> "x04hab" เป็นต้น

หลังจากนั้นไม่นาน Cloudfare ก็เผยแพร่โค้ดคอมไพเลอร์ p0f -> BPF ในบทความ ขอแนะนำคอมไพเลอร์ p0f BPF พวกเขาพูดถึง p0f คืออะไรและวิธีแปลงลายเซ็น p0f เป็น BPF:

$ ./bpfgen p0f -- 4:64:0:0:*,0::ack+:0
39,0 0 0 0,48 0 0 8,37 35 0 64,37 0 34 29,48 0 0 0,
84 0 0 15,21 0 31 5,48 0 0 9,21 0 29 6,40 0 0 6,
...

ขณะนี้ไม่ได้ใช้ Cloudfare อีกต่อไป xt_bpfเนื่องจากพวกเขาย้ายไปที่ XDP - หนึ่งในตัวเลือกสำหรับการใช้ BPF เวอร์ชันใหม่ โปรดดู L4Drop: การบรรเทา XDP DDoS.

cls_bpf

ตัวอย่างสุดท้ายของการใช้ BPF แบบคลาสสิกในเคอร์เนลคือตัวแยกประเภท cls_bpf สำหรับระบบย่อยการควบคุมการรับส่งข้อมูลใน Linux ซึ่งเพิ่มเข้ามาใน Linux เมื่อปลายปี 2013 และแทนที่แนวคิดแบบโบราณ cls_u32.

อย่างไรก็ตาม เราจะไม่อธิบายงานนี้อีกต่อไป cls_bpfเนื่องจากจากมุมมองของความรู้เกี่ยวกับ BPF แบบคลาสสิกสิ่งนี้จะไม่ให้อะไรเลยแก่เรา - เราคุ้นเคยกับฟังก์ชันทั้งหมดแล้ว นอกจากนี้ ในบทความถัดไปที่พูดถึง Extended BPF เราจะพบกับตัวแยกประเภทนี้มากกว่าหนึ่งครั้ง

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

ถึงเวลาบอกลา BPF แบบคลาสสิกและมองไปสู่อนาคต

ลาก่อน BPF แบบคลาสสิก

เราดูว่าเทคโนโลยี BPF ซึ่งพัฒนาขึ้นในช่วงต้นทศวรรษที่ 32 ประสบความสำเร็จมาเป็นเวลาถึงหนึ่งในสี่ของศตวรรษได้อย่างไร และพบการใช้งานใหม่ๆ จนกระทั่งถึงจุดสิ้นสุด อย่างไรก็ตาม คล้ายกับการเปลี่ยนจากเครื่องสแต็กเป็น RISC ซึ่งเป็นแรงผลักดันในการพัฒนา BPF แบบคลาสสิก ในช่วงปี 64 มีการเปลี่ยนแปลงจากเครื่อง XNUMX บิตเป็น XNUMX บิต และ BPF แบบคลาสสิกเริ่มล้าสมัย นอกจากนี้ความสามารถของ BPF แบบคลาสสิกยังมีข้อ จำกัด มากและนอกเหนือจากสถาปัตยกรรมที่ล้าสมัยแล้ว - เราไม่สามารถบันทึกสถานะระหว่างการเรียกไปยังโปรแกรม BPF ได้ ไม่มีความเป็นไปได้ของการโต้ตอบกับผู้ใช้โดยตรง ไม่มีความเป็นไปได้ในการโต้ตอบ ด้วยเคอร์เนล ยกเว้นการอ่านฟิลด์โครงสร้างในจำนวนที่จำกัด sk_buff และการเปิดตัวฟังก์ชันตัวช่วยที่ง่ายที่สุด คุณไม่สามารถเปลี่ยนเนื้อหาของแพ็กเก็ตและเปลี่ยนเส้นทางได้

ที่จริงแล้ว ปัจจุบันสิ่งที่เหลืออยู่ของ BPF แบบคลาสสิกใน Linux คืออินเทอร์เฟซ API และภายในเคอร์เนล โปรแกรมแบบคลาสสิกทั้งหมด ไม่ว่าจะเป็นตัวกรองซ็อกเก็ตหรือตัวกรอง seccomp จะถูกแปลเป็นรูปแบบใหม่โดยอัตโนมัติ ซึ่งก็คือ Extended BPF (เราจะพูดถึงว่าสิ่งนี้เกิดขึ้นได้อย่างไรในบทความถัดไป)

การเปลี่ยนไปใช้สถาปัตยกรรมใหม่เริ่มขึ้นในปี 2013 เมื่อ Alexey Starovoitov เสนอแผนการอัปเดต BPF ในปี 2014 แพตช์ที่เกี่ยวข้อง เริ่มปรากฏให้เห็น ในแกนกลาง เท่าที่ฉันเข้าใจ แผนเดิมเป็นเพียงการปรับสถาปัตยกรรมและคอมไพเลอร์ JIT ให้ทำงานได้อย่างมีประสิทธิภาพมากขึ้นบนเครื่อง 64 บิต แต่การปรับให้เหมาะสมเหล่านี้กลับกลายเป็นจุดเริ่มต้นของบทใหม่ในการพัฒนา Linux

บทความเพิ่มเติมในชุดนี้จะครอบคลุมถึงสถาปัตยกรรมและการประยุกต์ใช้เทคโนโลยีใหม่ ซึ่งเดิมเรียกว่า BPF ภายใน จากนั้นจึงขยาย BPF และตอนนี้เรียกว่า BPF

การอ้างอิง

  1. Steven McCanne และ Van Jacobson, "ตัวกรองแพ็คเก็ต BSD: สถาปัตยกรรมใหม่สำหรับการจับแพ็คเก็ตระดับผู้ใช้", https://www.tcpdump.org/papers/bpf-usenix93.pdf
  2. Steven McCanne, "libpcap: สถาปัตยกรรมและวิธีการเพิ่มประสิทธิภาพสำหรับการจับแพ็คเก็ต", https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
  3. tcpdump, libpcap: https://www.tcpdump.org/
  4. บทช่วยสอนการแข่งขัน IPtable U32.
  5. BPF - รหัสไบต์ที่ถูกลืม: https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
  6. ขอแนะนำเครื่องมือ BPF: https://blog.cloudflare.com/introducing-the-bpf-tools/
  7. bpf_cls: http://man7.org/linux/man-pages/man8/tc-bpf.8.html
  8. ภาพรวม seccomp: https://lwn.net/Articles/656307/
  9. https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
  10. habr: คอนเทนเนอร์และการรักษาความปลอดภัย: seccomp
  11. habr: การแยก daemons ด้วย systemd หรือ “คุณไม่จำเป็นต้องใช้ Docker สำหรับสิ่งนี้!”
  12. Paul Chaignon, "strace --seccomp-bpf: ดูภายใต้ประทุน", https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
  13. netsniff-ng: http://netsniff-ng.org/

ที่มา: will.com

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