หนังสือ "BPF สำหรับ Linux Monitoring"

หนังสือ "BPF สำหรับ Linux Monitoring"สวัสดีชาวคาโบร! เครื่องเสมือน BPF เป็นหนึ่งในองค์ประกอบที่สำคัญที่สุดของเคอร์เนล Linux การใช้งานอย่างเหมาะสมจะช่วยให้วิศวกรระบบสามารถค้นหาข้อบกพร่องและแก้ไขปัญหาที่ซับซ้อนที่สุดได้ คุณจะได้เรียนรู้วิธีการเขียนโปรแกรมที่ติดตามและแก้ไขพฤติกรรมของเคอร์เนล วิธีใช้โค้ดอย่างปลอดภัยเพื่อตรวจสอบเหตุการณ์ในเคอร์เนล และอื่นๆ อีกมากมาย David Calavera และ Lorenzo Fontana จะช่วยคุณปลดล็อกพลังของ BPF ขยายความรู้ของคุณเกี่ยวกับการเพิ่มประสิทธิภาพ เครือข่าย ความปลอดภัย - ใช้ BPF เพื่อตรวจสอบและแก้ไขพฤติกรรมของเคอร์เนล Linux - แทรกโค้ดเพื่อตรวจสอบเหตุการณ์เคอร์เนลอย่างปลอดภัย โดยไม่ต้องคอมไพล์เคอร์เนลใหม่หรือรีบูตระบบ — ใช้ตัวอย่างโค้ดที่สะดวกใน C, Go หรือ Python - ควบคุมโดยเป็นเจ้าของวงจรชีวิตของโปรแกรม BPF

ความปลอดภัยของเคอร์เนล Linux คุณสมบัติและ Seccomp

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

Linux Security Modules (LSM) เป็นเฟรมเวิร์กที่จัดเตรียมชุดฟังก์ชันที่สามารถใช้เพื่อปรับใช้โมเดลความปลอดภัยต่างๆ ในลักษณะที่เป็นมาตรฐาน LSM สามารถใช้ได้โดยตรงในแผนผังซอร์สเคอร์เนล เช่น Apparmor, SELinux และ Tomoyo

เริ่มต้นด้วยการพูดคุยเกี่ยวกับความสามารถของ Linux

ความสามารถในการ

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

พิจารณาโปรแกรม Go ชื่อ main.go:

package main
import (
            "net/http"
            "log"
)
func main() {
     log.Fatalf("%v", http.ListenAndServe(":80", nil))
}

โปรแกรมนี้ให้บริการเซิร์ฟเวอร์ HTTP บนพอร์ต 80 (นี่คือพอร์ตสิทธิพิเศษ) โดยปกติแล้วเราจะรันมันทันทีหลังจากการคอมไพล์:

$ go build -o capabilities main.go
$ ./capabilities

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

2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1

capsh (ตัวจัดการเชลล์) เป็นเครื่องมือที่รันเชลล์ด้วยชุดความสามารถเฉพาะ

ในกรณีนี้ ดังที่กล่าวไปแล้ว แทนที่จะให้สิทธิ์รูทแบบเต็ม คุณสามารถเปิดใช้งานการรวมพอร์ตที่มีสิทธิพิเศษได้โดยการจัดเตรียมความสามารถ cap_net_bind_service พร้อมกับทุกสิ่งที่มีอยู่ในโปรแกรมแล้ว เมื่อต้องการทำเช่นนี้ เราสามารถรวมโปรแกรมของเราไว้ใน capsh:

# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' 
   --keep=1 --user="nobody" 
   --addamb=cap_net_bind_service -- -c "./capabilities"

มาทำความเข้าใจทีมนี้กันหน่อย

  • capsh - ใช้ capsh เป็นเชลล์
  • —caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' - เนื่องจากเราจำเป็นต้องเปลี่ยนผู้ใช้ (เราไม่ต้องการเรียกใช้ในฐานะรูท) เราจะระบุ cap_net_bind_service และความสามารถในการเปลี่ยน ID ผู้ใช้จริงจาก รูทถึงใครเลย คือ cap_setuid และ cap_setgid
  • —keep=1 — เราต้องการเก็บความสามารถที่ติดตั้งไว้เมื่อเปลี่ยนจากบัญชีรูท
  • —user=“nobody” — ผู้ใช้ปลายทางที่รันโปรแกรมจะไม่มีใครเลย
  • —addamb=cap_net_bind_service — ตั้งค่าการล้างความสามารถที่เกี่ยวข้องหลังจากเปลี่ยนจากโหมดรูท
  • - -c "./capabilities" - เพียงแค่เรียกใช้โปรแกรม

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

คุณอาจสงสัยว่า +eip หมายถึงอะไรหลังจากระบุความสามารถในตัวเลือก --caps แฟล็กเหล่านี้ใช้เพื่อกำหนดว่าความสามารถ:

- ต้องเปิดใช้งาน (p);

- พร้อมใช้งาน (จ);

- สามารถสืบทอดโดยกระบวนการลูก (i)

เนื่องจากเราต้องการใช้ cap_net_bind_service เราจึงต้องทำเช่นนี้ด้วยแฟล็ก e จากนั้นเราจะเริ่มการทำงานของเชลล์ด้วยคำสั่ง สิ่งนี้จะเรียกใช้ไบนารีความสามารถและเราจำเป็นต้องทำเครื่องหมายด้วยแฟล็ก i สุดท้ายนี้ เราต้องการให้เปิดใช้งานคุณลักษณะนี้ (เราทำสิ่งนี้โดยไม่เปลี่ยน UID) ด้วย p ดูเหมือนว่า cap_net_bind_service+eip

คุณสามารถตรวจสอบผลลัพธ์โดยใช้ ss ลองย่อเอาต์พุตเล็กน้อยเพื่อให้พอดีกับเพจ แต่จะแสดงพอร์ตที่เกี่ยวข้องและ ID ผู้ใช้อื่นที่ไม่ใช่ 0 ในกรณีนี้คือ 65:

# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0

ในตัวอย่างนี้ เราใช้ capsh แต่คุณสามารถเขียนเชลล์โดยใช้ libcap ได้ สำหรับข้อมูลเพิ่มเติม โปรดดูที่ man 3 libcap

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

เพื่อให้เข้าใจถึงความสามารถของโปรแกรมของเราได้ดีขึ้น เราสามารถใช้เครื่องมือที่รองรับ BCC ซึ่งตั้งค่า kprobe สำหรับฟังก์ชันเคอร์เนล cap_capable:

/usr/share/bcc/tools/capable
TIME      UID  PID   TID   COMM               CAP    NAME           AUDIT
10:12:53 0 424     424     systemd-udevd 12 CAP_NET_ADMIN         1
10:12:57 0 1103   1101   timesync        25 CAP_SYS_TIME         1
10:12:57 0 19545 19545 capabilities       10 CAP_NET_BIND_SERVICE 1

เราสามารถบรรลุสิ่งเดียวกันได้โดยใช้ bpftrace กับ kprobe แบบซับเดียวในฟังก์ชันเคอร์เนล cap_capable:

bpftrace -e 
   'kprobe:cap_capable {
      time("%H:%M:%S ");
      printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
    }' 
    | grep -i capabilities

สิ่งนี้จะแสดงผลดังนี้หากความสามารถของโปรแกรมของเราถูกเปิดใช้งานหลังจาก kprobe:

12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1

คอลัมน์ที่ห้าคือความสามารถที่กระบวนการต้องการ และเนื่องจากเอาต์พุตนี้มีเหตุการณ์ที่ไม่ใช่การตรวจสอบ เราจึงเห็นการตรวจสอบที่ไม่ใช่การตรวจสอบทั้งหมดและสุดท้ายคือความสามารถที่จำเป็นโดยตั้งค่าสถานะการตรวจสอบ (สุดท้ายในเอาต์พุต) ตั้งค่าเป็น 1 ความสามารถ สิ่งที่เราสนใจคือ CAP_NET_BIND_SERVICE ซึ่งถูกกำหนดให้เป็นค่าคงที่ในซอร์สโค้ดเคอร์เนลในไฟล์ include/uapi/linux/ability.h พร้อมตัวระบุ 10:

/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">

ความสามารถต่างๆ มักจะเปิดใช้งานในขณะรันไทม์สำหรับคอนเทนเนอร์ เช่น runC หรือ Docker เพื่อให้ทำงานในโหมดไม่มีสิทธิ์ แต่จะอนุญาตเฉพาะความสามารถที่จำเป็นในการรันแอปพลิเคชันส่วนใหญ่เท่านั้น เมื่อแอปพลิเคชันต้องการความสามารถบางอย่าง Docker สามารถให้ได้โดยใช้ --cap-add:

docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy

คำสั่งนี้จะมอบความสามารถ CAP_NET_ADMIN ให้กับคอนเทนเนอร์ ทำให้สามารถกำหนดค่าลิงก์เครือข่ายเพื่อเพิ่มอินเทอร์เฟซ dummy0

ส่วนถัดไปจะแสดงวิธีใช้ฟีเจอร์ต่างๆ เช่น การกรอง แต่การใช้เทคนิคอื่นที่ช่วยให้เราใช้ตัวกรองของเราเองโดยทางโปรแกรมได้

เซคคอม

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

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

วิธีการกรอง Seccomp ขึ้นอยู่กับตัวกรอง BPF ที่ทำงานในโหมด SECOMP_MODE_FILTER และการกรองการโทรของระบบจะดำเนินการในลักษณะเดียวกับแพ็กเก็ต

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

นี่คือลักษณะของโครงสร้าง seccomp_data จากซอร์สโค้ดเคอร์เนลในไฟล์ linux/seccomp.h:

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

ดังที่คุณเห็นจากโครงสร้างนี้ เราสามารถกรองโดยการเรียกของระบบ อาร์กิวเมนต์ หรือทั้งสองอย่างรวมกัน

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

- SECOMP_RET_KILL_PROCESS - ฆ่ากระบวนการทั้งหมดทันทีหลังจากกรองการเรียกของระบบที่ไม่ได้ดำเนินการด้วยเหตุนี้

- SECOMP_RET_KILL_THREAD - ยุติเธรดปัจจุบันทันทีหลังจากกรองการเรียกของระบบที่ไม่ได้ดำเนินการด้วยเหตุนี้

— SECOMP_RET_KILL - นามแฝงสำหรับ SECOMP_RET_KILL_THREAD เหลือไว้สำหรับความเข้ากันได้แบบย้อนหลัง

- SECOMP_RET_TRAP - ห้ามการเรียกของระบบและสัญญาณ SIGSYS (การเรียกระบบไม่ดี) จะถูกส่งไปยังงานที่เรียก

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

- SECCOMP_RET_TRACE - ใช้เพื่อแจ้งเตือนตัวติดตาม ptrace โดยใช้ - PTRACE_O_TRACESECCOMP เพื่อสกัดกั้นเมื่อมีการดำเนินการเรียกระบบเพื่อดูและควบคุมกระบวนการนั้น หากไม่ได้เชื่อมต่อตัวติดตาม ข้อผิดพลาดจะถูกส่งกลับ errno ถูกตั้งค่าเป็น -ENOSYS และการเรียกของระบบจะไม่ดำเนินการ

- SECOMP_RET_LOG - การเรียกของระบบได้รับการแก้ไขและบันทึกแล้ว

- SECOMP_RET_ALLOW - อนุญาตให้เรียกระบบได้ง่าย

ptrace คือการเรียกของระบบเพื่อใช้กลไกการติดตามในกระบวนการที่เรียกว่า Tracee โดยมีความสามารถในการมอนิเตอร์และควบคุมการดำเนินการของกระบวนการ โปรแกรมการติดตามสามารถมีอิทธิพลต่อการดำเนินการและแก้ไขการลงทะเบียนหน่วยความจำของการติดตามได้อย่างมีประสิทธิภาพ ในบริบทของ Seccomp นั้น ptrace จะถูกใช้เมื่อถูกทริกเกอร์โดยรหัสสถานะ SECOMP_RET_TRACE ดังนั้นตัวติดตามจึงสามารถป้องกันไม่ให้การเรียกของระบบดำเนินการและใช้ตรรกะของตัวเองได้

ข้อผิดพลาดของเซคคอม

ในบางครั้ง ขณะทำงานกับ Seccomp คุณจะพบกับข้อผิดพลาดต่างๆ ซึ่งระบุด้วยค่าส่งคืนประเภท SECCOMP_RET_ERRNO หากต้องการรายงานข้อผิดพลาด การเรียกของระบบ seccomp จะส่งกลับ -1 แทนที่จะเป็น 0

ข้อผิดพลาดต่อไปนี้เป็นไปได้:

- EACCESS - ผู้โทรไม่ได้รับอนุญาตให้ทำการโทรของระบบ สิ่งนี้มักจะเกิดขึ้นเนื่องจากไม่มีสิทธิ์ CAP_SYS_ADMIN หรือไม่ได้ตั้งค่า no_new_privs โดยใช้ prctl (เราจะพูดถึงเรื่องนี้ในภายหลัง)

— EFAULT — อาร์กิวเมนต์ที่ส่งผ่าน (args ในโครงสร้าง seccomp_data) ไม่มีที่อยู่ที่ถูกต้อง

— EINVAL — อาจมีสี่เหตุผลที่นี่:

- การดำเนินการที่ร้องขอไม่เป็นที่รู้จักหรือไม่รองรับเคอร์เนลในการกำหนดค่าปัจจุบัน

- ค่าสถานะที่ระบุไม่ถูกต้องสำหรับการดำเนินการที่ร้องขอ

-การดำเนินการรวมถึง BPF_ABS แต่มีปัญหากับออฟเซ็ตที่ระบุ ซึ่งอาจเกินขนาดของโครงสร้าง seccomp_data

- จำนวนคำสั่งที่ส่งไปยังตัวกรองเกินจำนวนสูงสุด

— ENOMEM — หน่วยความจำไม่เพียงพอที่จะรันโปรแกรม

- EONOTSUPP - การดำเนินการระบุว่าด้วย SECOMP_GET_ACTION_AVAIL การดำเนินการนั้นพร้อมใช้งาน แต่เคอร์เนลไม่รองรับการส่งคืนในอาร์กิวเมนต์

— ESRCH — มีปัญหาเกิดขึ้นเมื่อซิงโครไนซ์สตรีมอื่น

- ENOSYS - ไม่มีตัวติดตามแนบมากับการดำเนินการ SECOMP_RET_TRACE

prctl คือการเรียกระบบที่อนุญาตให้โปรแกรมพื้นที่ผู้ใช้จัดการ (ตั้งค่าและรับ) ลักษณะเฉพาะของกระบวนการ เช่น ไบต์ endianness ชื่อเธรด โหมดการคำนวณที่ปลอดภัย (Seccomp) สิทธิ์พิเศษ เหตุการณ์ Perf เป็นต้น

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

ตัวอย่างตัวกรอง BPF Seccomp

ที่นี่เราจะแสดงวิธีการรวมการกระทำทั้งสองที่กล่าวถึงก่อนหน้านี้ กล่าวคือ:

— เราจะเขียนโปรแกรม Seccomp BPF ซึ่งจะใช้เป็นตัวกรองที่มีโค้ดส่งคืนที่แตกต่างกันขึ้นอยู่กับการตัดสินใจ

— โหลดตัวกรองโดยใช้ prctl

ขั้นแรกคุณต้องมีส่วนหัวจากไลบรารีมาตรฐานและเคอร์เนล Linux:

#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>

ก่อนที่จะลองตัวอย่างนี้ เราต้องแน่ใจว่าเคอร์เนลได้รับการคอมไพล์โดย CONFIG_SECOMP และ CONFIG_SECCOMP_FILTER ตั้งค่าเป็น y บนเครื่องที่ใช้งานคุณสามารถตรวจสอบสิ่งนี้ได้:

cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP

โค้ดที่เหลือคือฟังก์ชัน install_filter สองส่วน ส่วนแรกประกอบด้วยรายการคำแนะนำในการกรอง BPF ของเรา:

static int install_filter(int nr, int arch, int error) {
  struct sock_filter filter[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
  };

คำแนะนำถูกตั้งค่าโดยใช้มาโคร BPF_STMT และ BPF_JUMP ที่กำหนดไว้ในไฟล์ linux/filter.h
มาดูคำแนะนำกัน

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch)))) - ระบบโหลดและสะสมจาก BPF_LD ในรูปแบบของคำว่า BPF_W ข้อมูลแพ็กเก็ตจะอยู่ที่ออฟเซ็ตคงที่ BPF_ABS

- BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - ตรวจสอบโดยใช้ BPF_JEQ ว่าค่าสถาปัตยกรรมในค่าคงที่ตัวสะสม BPF_K เท่ากับอาร์คหรือไม่ หากเป็นเช่นนั้น ให้ข้ามที่ออฟเซ็ต 0 ไปยังคำสั่งถัดไป หรือข้ามที่ออฟเซ็ต 3 (ในกรณีนี้) เพื่อทำให้เกิดข้อผิดพลาดเนื่องจากส่วนโค้งไม่ตรงกัน

- BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) - โหลดและสะสมจาก BPF_LD ในรูปแบบของคำว่า BPF_W ซึ่งเป็นหมายเลขการเรียกของระบบที่มีอยู่ในออฟเซ็ตคงที่ของ BPF_ABS

— BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) — เปรียบเทียบหมายเลขการเรียกของระบบกับค่าของตัวแปร nr หากเท่ากัน ให้ไปยังคำสั่งถัดไปและปิดใช้งานการเรียกของระบบ ไม่เช่นนั้นจะอนุญาตการเรียกระบบด้วย SECOMP_RET_ALLOW

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (ข้อผิดพลาด & SECCOMP_RET_DATA)) - ยุติโปรแกรมด้วย BPF_RET และผลที่ตามมาทำให้เกิดข้อผิดพลาด SECCOMP_RET_ERRNO พร้อมตัวเลขจากตัวแปร err

- BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - ยุติโปรแกรมด้วย BPF_RET และอนุญาตให้เรียกใช้ระบบโดยใช้ SECCOMP_RET_ALLOW

SECOMP คือ CBPF
คุณอาจสงสัยว่าเหตุใดจึงใช้รายการคำสั่งแทนอ็อบเจ็กต์ ELF ที่คอมไพล์แล้วหรือโปรแกรม C ที่คอมไพล์ด้วย JIT

มีสองเหตุผลสำหรับเรื่องนี้

• ประการแรก Seccomp ใช้ cBPF (Classic BPF) ไม่ใช่ eBPF ซึ่งหมายความว่า: ไม่มีรีจิสเตอร์ แต่มีเพียงตัวสะสมเพื่อจัดเก็บผลการคำนวณล่าสุด ดังที่เห็นในตัวอย่าง

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

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

if (arch != AUDIT_ARCH_X86_64) {
    return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
    return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;

หลังจากกำหนดโค้ดตัวกรองในโครงสร้าง socket_filter แล้ว คุณต้องกำหนด sock_fprog ที่มีโค้ดและความยาวที่คำนวณได้ของตัวกรอง โครงสร้างข้อมูลนี้จำเป็นเป็นอาร์กิวเมนต์สำหรับการประกาศกระบวนการให้ทำงานในภายหลัง:

struct sock_fprog prog = {
   .len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
   .filter = filter,
};

เหลือเพียงสิ่งเดียวที่ต้องทำในฟังก์ชัน install_filter - โหลดโปรแกรมเอง! ในการดำเนินการนี้ เราใช้ prctl โดยรับ PR_SET_SECOMP เป็นตัวเลือกในการเข้าสู่โหมดการคำนวณที่ปลอดภัย จากนั้นเราบอกให้โหมดโหลดตัวกรองโดยใช้ SECOMP_MODE_FILTER ซึ่งมีอยู่ในตัวแปร prog ประเภท sock_fprog:

  if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
    perror("prctl(PR_SET_SECCOMP)");
    return 1;
  }
  return 0;
}

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

ตอนนี้เราสามารถเรียกใช้ฟังก์ชัน install_filter ได้แล้ว มาบล็อกการเรียกระบบการเขียนทั้งหมดที่เกี่ยวข้องกับสถาปัตยกรรม X86-64 และเพียงแค่ให้สิทธิ์ที่บล็อกความพยายามทั้งหมด หลังจากติดตั้งตัวกรองแล้ว เราจะดำเนินการต่อไปโดยใช้อาร์กิวเมนต์แรก:

int main(int argc, char const *argv[]) {
  if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
   perror("prctl(NO_NEW_PRIVS)");
   return 1;
  }
   install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
  return system(argv[1]);
 }

มาเริ่มกันเลย. ในการคอมไพล์โปรแกรมของเรา เราสามารถใช้ clang หรือ gcc ก็ได้ ไม่ว่าจะเป็นการคอมไพล์ไฟล์ main.c โดยไม่มีตัวเลือกพิเศษ:

clang main.c -o filter-write

ตามที่ระบุไว้ เราได้บล็อกรายการทั้งหมดในโปรแกรมแล้ว เพื่อทดสอบสิ่งนี้ คุณต้องมีโปรแกรมที่แสดงผลอะไรบางอย่าง - ดูเหมือนว่า ls จะเป็นตัวเลือกที่ดี เธอมักจะประพฤติตัวดังนี้:

ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c

มหัศจรรย์! หน้าตาการใช้โปรแกรม wrapper ของเรามีดังนี้: เราเพียงแต่ผ่านโปรแกรมที่เราต้องการทดสอบเป็นอาร์กิวเมนต์แรก:

./filter-write "ls -la"

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

strace -f ./filter-write "ls -la"

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

[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation not permitted)

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

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

กับดัก BPF LSM

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

ในขณะที่เขียนเคอร์เนลมี hook เจ็ดตัวที่เกี่ยวข้องกับโปรแกรม BPF และ SELinux เป็น LSM ในตัวเพียงตัวเดียวที่นำไปใช้งาน

ซอร์สโค้ดสำหรับกับดักอยู่ในแผนผังเคอร์เนลในไฟล์ include/linux/security.h:

extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);

แต่ละคนจะถูกเรียกตามขั้นตอนการดำเนินการที่แตกต่างกัน:

— security_bpf — ทำการตรวจสอบเบื้องต้นของการเรียกระบบ BPF ที่ดำเนินการ

- security_bpf_map - ตรวจสอบว่าเคอร์เนลส่งคืนตัวอธิบายไฟล์สำหรับแผนที่เมื่อใด

- security_bpf_prog - ตรวจสอบเมื่อเคอร์เนลส่งคืนตัวอธิบายไฟล์สำหรับโปรแกรม eBPF

— security_bpf_map_alloc — ตรวจสอบว่าฟิลด์ความปลอดภัยภายในแผนที่ BPF ได้รับการเตรียมใช้งานหรือไม่

- security_bpf_map_free - ตรวจสอบว่าฟิลด์ความปลอดภัยถูกล้างภายในแผนที่ BPF หรือไม่

— security_bpf_prog_alloc — ตรวจสอบว่าฟิลด์ความปลอดภัยถูกเตรียมใช้งานภายในโปรแกรม BPF หรือไม่

- security_bpf_prog_free - ตรวจสอบว่าฟิลด์ความปลอดภัยถูกล้างภายในโปรแกรม BPF หรือไม่

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

สรุป

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

เกี่ยวกับผู้แต่ง

เดวิด คาลาเวร่า เป็น CTO ของ Netlify เขาทำงานในฝ่ายสนับสนุน Docker และมีส่วนในการพัฒนาเครื่องมือ Runc, Go และ BCC รวมถึงโครงการโอเพ่นซอร์สอื่นๆ เป็นที่รู้จักจากผลงานของเขาในโครงการ Docker และการพัฒนาระบบนิเวศปลั๊กอิน Docker David มีความหลงใหลเกี่ยวกับกราฟเปลวไฟมากและมองหาวิธีเพิ่มประสิทธิภาพการทำงานอยู่เสมอ

ลอเรนโซ ฟอนทานา ทำงานในทีมโอเพ่นซอร์สที่ Sysdig ซึ่งเขามุ่งเน้นไปที่ Falco เป็นหลัก ซึ่งเป็นโครงการ Cloud Native Computing Foundation ที่ให้ความปลอดภัยรันไทม์คอนเทนเนอร์และการตรวจจับความผิดปกติผ่านโมดูลเคอร์เนลและ eBPF เขามีความหลงใหลเกี่ยวกับระบบแบบกระจาย เครือข่ายที่กำหนดโดยซอฟต์แวร์ เคอร์เนล Linux และการวิเคราะห์ประสิทธิภาพ

» ดูรายละเอียดหนังสือเพิ่มเติมได้ที่ เว็บไซต์ของผู้จัดพิมพ์
» สารบัญ
» ข้อความที่ตัดตอนมา

สำหรับ Khabrozhiteley ส่วนลด 25% โดยใช้คูปอง - ลินุกซ์

เมื่อชำระเงินค่าหนังสือในรูปแบบกระดาษ หนังสืออิเล็กทรอนิกส์จะถูกส่งทางอีเมล

ที่มา: will.com

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