Berkeley Packet Filters (BPF) เป็นเทคโนโลยีเคอร์เนล Linux ที่อยู่ในหน้าแรกของสื่อสิ่งพิมพ์ด้านเทคโนโลยีภาษาอังกฤษมาหลายปีแล้ว การประชุมเต็มไปด้วยรายงานเกี่ยวกับการใช้และการพัฒนา BPF David Miller ผู้ดูแลระบบย่อยเครือข่าย Linux กล่าวถึงคำพูดของเขาที่ Linux Plumbers 2018
ยังไม่มีคำอธิบายที่เป็นระบบของ BPF ใน Habré ดังนั้นในชุดบทความ ฉันจะพยายามพูดถึงประวัติความเป็นมาของเทคโนโลยี อธิบายเครื่องมือสถาปัตยกรรมและการพัฒนา และร่างขอบเขตการใช้งานและแนวปฏิบัติของการใช้ BPF บทความนี้เป็นศูนย์ในซีรีส์นี้ จะบอกเล่าประวัติศาสตร์และสถาปัตยกรรมของ BPF แบบคลาสสิก และยังเปิดเผยความลับของหลักการปฏิบัติงานอีกด้วย tcpdump
, seccomp
, strace
และอีกมากมาย
การพัฒนา BPF ถูกควบคุมโดยชุมชนเครือข่าย Linux แอปพลิเคชันหลักที่มีอยู่ของ 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 ซึ่งเป็นผลงานของผลิตภัณฑ์ที่มีชื่อเสียงเช่น
สถาปัตยกรรมเครื่อง BPF
เราจะทำความคุ้นเคยกับสถาปัตยกรรมในลักษณะการทำงานโดยวิเคราะห์ตัวอย่าง อย่างไรก็ตาม ในการเริ่มต้น สมมติว่าเครื่องมีรีจิสเตอร์ 32 บิตสองตัวที่ผู้ใช้สามารถเข้าถึงได้ ซึ่งเป็นตัวสะสม A
และการลงทะเบียนดัชนี X
หน่วยความจำ 64 ไบต์ (16 คำ) สำหรับการเขียนและการอ่านในภายหลัง และระบบคำสั่งขนาดเล็กสำหรับการทำงานกับวัตถุเหล่านี้ คำแนะนำในการกระโดดสำหรับการใช้นิพจน์แบบมีเงื่อนไขก็มีอยู่ในโปรแกรมเช่นกัน แต่เพื่อรับประกันความสมบูรณ์ของโปรแกรมในเวลาที่เหมาะสม การกระโดดสามารถทำได้ไปข้างหน้าเท่านั้น กล่าวคือ โดยเฉพาะอย่างยิ่งห้ามมิให้สร้างลูป
รูปแบบทั่วไปในการสตาร์ทเครื่องมีดังนี้ ผู้ใช้สร้างโปรแกรมสำหรับสถาปัตยกรรม BPF และใช้งาน บาง กลไกเคอร์เนล (เช่น การเรียกของระบบ) โหลดและเชื่อมต่อโปรแกรมด้วย ถึงบางคน ไปยังตัวสร้างเหตุการณ์ในเคอร์เนล (ตัวอย่างเช่น เหตุการณ์คือการมาถึงของแพ็กเก็ตถัดไปบนการ์ดเครือข่าย) เมื่อมีเหตุการณ์เกิดขึ้น เคอร์เนลจะรันโปรแกรม (เช่น ในล่าม) และหน่วยความจำของเครื่องจะสอดคล้องกัน ถึงบางคน ขอบเขตหน่วยความจำเคอร์เนล (เช่น ข้อมูลของแพ็กเก็ตขาเข้า)
ข้างต้นจะเพียงพอสำหรับเราที่จะเริ่มดูตัวอย่าง: เราจะทำความคุ้นเคยกับระบบและรูปแบบคำสั่งตามความจำเป็น หากคุณต้องการศึกษาระบบคำสั่งของเครื่องเสมือนทันทีและเรียนรู้เกี่ยวกับความสามารถทั้งหมดของมัน คุณสามารถอ่านบทความต้นฉบับได้ libpcap
: สถาปัตยกรรมและวิธีการเพิ่มประสิทธิภาพสำหรับการจับแพ็คเก็ต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 และส่งไปยังเคอร์เนล (ดูรายละเอียดในส่วน eth0
. หากตัวกรองส่งคืนค่าที่ไม่ใช่ศูนย์ n
จากนั้นขึ้นไป n
ไบต์ของแพ็กเก็ตจะถูกคัดลอกไปยังพื้นที่ผู้ใช้และเราจะเห็นมันในเอาต์พุต tcpdump
.
ปรากฎว่าเราสามารถค้นหาได้อย่างง่ายดายว่ารหัสไบต์ใดถูกส่งไปยังเคอร์เนล 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
, และนี่
6 6 2
|Destination MAC|Source MAC|Ether Type|...|
ดังนั้นหลังจากดำเนินการตามคำสั่งแล้ว ldh [12]
ในทะเบียน A
จะมีสนาม Ether Type
— ประเภทของแพ็กเก็ตที่ส่งในเฟรมอีเทอร์เน็ตนี้ ในบรรทัดที่ 1 เราเปรียบเทียบเนื้อหาของการลงทะเบียน A
(ประเภทบรรจุภัณฑ์)ค 0x86dd
, และนี่ 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
ในบรรทัดที่ 4 และ 5 เราโหลด halfwords ที่อยู่ตามที่อยู่ 20 และใช้คำสั่ง jset
ตรวจสอบว่ามีการตั้งค่าหนึ่งในสามหรือไม่ jset
บิตที่สำคัญที่สุดสามบิตจะถูกล้าง สองในสามบิตบอกเราว่าแพ็กเก็ตเป็นส่วนหนึ่งของแพ็กเก็ต IP ที่กระจัดกระจายหรือไม่ และหากเป็นเช่นนั้น จะเป็นแฟรกเมนต์สุดท้ายหรือไม่ บิตที่สามถูกสงวนไว้และต้องเป็นศูนย์ เราไม่ต้องการตรวจสอบแพ็คเก็ตที่ไม่สมบูรณ์หรือเสียหาย ดังนั้นเราจึงตรวจสอบทั้งสามบิต
บรรทัดที่ 6 น่าสนใจที่สุดในรายการนี้ การแสดงออก ldxb 4*([14]&0xf)
หมายความว่าเราโหลดเข้าสู่การลงทะเบียน X
สี่บิตที่มีนัยสำคัญน้อยที่สุดในไบต์ที่สิบห้าของแพ็กเก็ตคูณด้วย 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
.
ที่น่าสนใจคือสามารถติดตัวกรองเข้ากับซ็อกเก็ตใดก็ได้ ไม่ใช่แค่แบบดิบเท่านั้น ที่นี่
รายละเอียดเพิ่มเติมเกี่ยวกับการใช้งาน setsockopt
สำหรับการเชื่อมต่อตัวกรอง โปรดดู struct sock_fprog
โดยไม่มีความช่วยเหลือ tcpdump
เราจะพูดคุยในส่วนนี้
Classic BPF และศตวรรษที่ XNUMX
BPF ถูกรวมอยู่ใน Linux ในปี 1997 และยังคงเป็นเครื่องมือมาเป็นเวลานาน libpcap
โดยไม่มีการเปลี่ยนแปลงพิเศษใด ๆ (แน่นอนว่าการเปลี่ยนแปลงเฉพาะ Linux x86_64
รหัส.
คอมไพเลอร์ JIT เป็นคนแรกในห่วงโซ่การเปลี่ยนแปลง: ในปี 2012 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;
}
ดังนั้นเราจึงสามารถเขียนโปรแกรมได้แล้ว (เช่น เรารู้โค้ดคำสั่งจาก 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
ภาษาแอสเซมบลีคล้ายกับเอาต์พุตการดีบักมาก 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
ใน 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()
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 ช่วยให้คุณสามารถควบคุมผลลัพธ์ได้อย่างสมบูรณ์ แต่ในขณะเดียวกัน บางครั้งมันก็ดีกว่าที่จะมีโค้ดแบบพกพาและ/หรือแบบอ่านได้ ห้องสมุดจะช่วยเราในเรื่องนี้
ตัวอย่างเช่น ลองเขียนโปรแกรมที่รันไฟล์ไบนารี่ที่ผู้ใช้เลือก โดยก่อนหน้านี้ได้ติดตั้งบัญชีดำของการเรียกระบบจาก
#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 จะแปลงตัวกรองเป็นโปรแกรมค้นหาแบบไบนารี
หากคุณต้องการดูว่าตัวกรองการค้นหาแบบไบนารีทำงานอย่างไร ลองดูที่
$ 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
และอื่นๆ
อย่างไรก็ตาม ด้วย 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
เมื่อใช้ตัวเลือก --seccomp-bpf
มีข้อจำกัดสองประการ ประการแรก คุณจะไม่สามารถเข้าร่วมกระบวนการที่มีอยู่แล้วได้ (ตัวเลือก -p
โครงการ strace
) เนื่องจาก seccomp ไม่รองรับ ประการที่สอง ไม่มีความเป็นไปได้ ไม่ ดูที่กระบวนการลูก เนื่องจากตัวกรอง seccomp ได้รับการสืบทอดโดยกระบวนการลูกทั้งหมดโดยไม่มีความสามารถในการปิดใช้งานสิ่งนี้
รายละเอียดเพิ่มเติมเล็กน้อยเกี่ยวกับวิธีการที่แน่นอน strace
ใช้งานได้กับ 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
. ฉันจะไม่ลงรายละเอียดเพิ่มเติมเนื่องจากเป็นที่ชัดเจนว่าการเขียนกฎดังกล่าวด้วยมือนั้นไม่สะดวกนัก ในบทความ 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 ในบทความ 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 ในบทความ
$ ./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 เวอร์ชันใหม่ โปรดดู
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 แพตช์ที่เกี่ยวข้อง
บทความเพิ่มเติมในชุดนี้จะครอบคลุมถึงสถาปัตยกรรมและการประยุกต์ใช้เทคโนโลยีใหม่ ซึ่งเดิมเรียกว่า BPF ภายใน จากนั้นจึงขยาย BPF และตอนนี้เรียกว่า BPF
การอ้างอิง
- Steven McCanne และ Van Jacobson, "ตัวกรองแพ็คเก็ต BSD: สถาปัตยกรรมใหม่สำหรับการจับแพ็คเก็ตระดับผู้ใช้",
https://www.tcpdump.org/papers/bpf-usenix93.pdf
- Steven McCanne, "libpcap: สถาปัตยกรรมและวิธีการเพิ่มประสิทธิภาพสำหรับการจับแพ็คเก็ต",
https://sharkfestus.wireshark.org/sharkfest.11/presentations/McCanne-Sharkfest'11_Keynote_Address.pdf
tcpdump
,libpcap
:https://www.tcpdump.org/ บทช่วยสอนการแข่งขัน IPtable U32 .- BPF - รหัสไบต์ที่ถูกลืม:
https://blog.cloudflare.com/bpf-the-forgotten-bytecode/
- ขอแนะนำเครื่องมือ BPF:
https://blog.cloudflare.com/introducing-the-bpf-tools/
bpf_cls
:http://man7.org/linux/man-pages/man8/tc-bpf.8.html
- ภาพรวม seccomp:
https://lwn.net/Articles/656307/
https://github.com/torvalds/linux/blob/master/Documentation/userspace-api/seccomp_filter.rst
habr: คอนเทนเนอร์และการรักษาความปลอดภัย: seccomp habr: การแยก daemons ด้วย systemd หรือ “คุณไม่จำเป็นต้องใช้ Docker สำหรับสิ่งนี้!” - Paul Chaignon, "strace --seccomp-bpf: ดูภายใต้ประทุน",
https://fosdem.org/2020/schedule/event/debugging_strace_bpf/
netsniff-ng
:http://netsniff-ng.org/
ที่มา: will.com