คอมพิวเตอร์ Windows อินเทอร์เน็ต

Man syscalls (2): การเรียกระบบ Linux ลินุกซ์ - syscalls. การเรียกระบบใน Linux Interrupts ในสถาปัตยกรรม x86


ระบบเรียก

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

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

ลืมไฟล์ /proc และไฟล์อุปกรณ์ เป็นเพียงรายละเอียดเล็กๆ น้อยๆ กระบวนการสื่อสารเคอร์เนลจริงที่ใช้โดยกระบวนการทั้งหมดคือการเรียกระบบ เมื่อกระบวนการร้องขอบริการจากเคอร์เนล (เช่น การเปิดไฟล์ การเริ่มกระบวนการใหม่ หรือการร้องขอหน่วยความจำเพิ่ม) กลไกนี้จะถูกนำมาใช้ หากคุณต้องการเปลี่ยนพฤติกรรมของเคอร์เนลในลักษณะที่น่าสนใจ ที่นี่คือที่ที่เหมาะสม อีกอย่าง ถ้าอยากดูว่าโปรแกรมเรียกใช้ระบบอะไร ให้รัน: strace .

โดยทั่วไป กระบวนการไม่สามารถเข้าถึงเคอร์เนลได้ ไม่สามารถเข้าถึงหน่วยความจำเคอร์เนลและไม่สามารถเรียกใช้ฟังก์ชันเคอร์เนลได้ ฮาร์ดแวร์ของ CPU บังคับใช้สถานะนี้ (ด้วยเหตุผลบางอย่างเรียกว่า 'โหมดป้องกัน') การเรียกระบบเป็นข้อยกเว้นสำหรับกฎทั่วไปนี้ กระบวนการเติมการลงทะเบียนด้วยค่าที่เหมาะสมแล้วเรียกคำสั่งพิเศษที่ข้ามไปที่ ตำแหน่งที่กำหนดไว้ล่วงหน้าในเคอร์เนล (แน่นอน มันถูกอ่านโดยกระบวนการของผู้ใช้ แต่ไม่ได้เขียนทับโดยพวกเขา) ภายใต้ CPU ของ Intel สิ่งนี้ทำได้ผ่านการขัดจังหวะ 0x80 ฮาร์ดแวร์รู้ว่าเมื่อคุณข้ามไปยังตำแหน่งนี้ คุณจะไม่ทำงานอีกต่อไป ในโหมดจำกัดผู้ใช้ คุณทำงานเป็นเคอร์เนลของระบบปฏิบัติการแทน ดังนั้น คุณจึงได้รับอนุญาตให้ทำทุกอย่างที่คุณต้องการ

ตำแหน่งในเคอร์เนลที่กระบวนการสามารถข้ามไปเรียกว่า system_call โพรซีเดอร์ที่อยู่ตรงนั้นจะตรวจสอบหมายเลขเรียกของระบบ ซึ่งบอกเคอร์เนลว่ากระบวนการต้องการอะไร จากนั้นจะค้นหาตารางการเรียกระบบ (sys_call_table) เพื่อค้นหาที่อยู่ของฟังก์ชันเคอร์เนลที่จะโทร จากนั้นฟังก์ชันที่ต้องการจะถูกเรียกและหลังจากที่ส่งกลับค่าแล้วจะมีการตรวจสอบหลายครั้งในระบบ ผลลัพธ์จะถูกส่งกลับไปยังกระบวนการ (หรือไปยังกระบวนการอื่นหากกระบวนการสิ้นสุดลง) หากคุณต้องการดูโค้ดที่ทำทั้งหมดนี้อยู่ในไฟล์ต้นฉบับ arch/< architecture >/kernel/entry.S หลังบรรทัด ENTRY(system_call)

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

ซอร์สโค้ดที่ให้ไว้ที่นี่เป็นตัวอย่างของโมดูลดังกล่าว เราต้องการ "สอดแนม" ผู้ใช้บางคน และส่งข้อความผ่าน printk ทุกครั้งที่ผู้ใช้เปิดไฟล์ เราแทนที่การเรียกไฟล์ระบบเปิดด้วยฟังก์ชันของเราเองที่เรียกว่า our_sys_open ฟังก์ชันนี้จะตรวจสอบ uid (id ผู้ใช้) ของกระบวนการปัจจุบัน และหากเท่ากับ uid ที่เรากำลังสอดแนมอยู่ ให้เรียก printk เพื่อแสดงชื่อไฟล์ที่จะเปิด จากนั้นเรียกใช้ฟังก์ชัน open ดั้งเดิมด้วยพารามิเตอร์เดียวกัน อันที่จริงแล้วเป็นการเปิดไฟล์

ฟังก์ชัน init_module เปลี่ยนตำแหน่งที่เหมาะสมใน sys_call_table และเก็บตัวชี้เดิมไว้ในตัวแปร ฟังก์ชัน cleanup_module ใช้ตัวแปรนี้เพื่อกู้คืนทุกอย่างกลับสู่สภาวะปกติ วิธีการนี้เป็นอันตราย เนื่องจากมีความเป็นไปได้ที่โมดูลสองโมดูลจะเปลี่ยนการเรียกระบบเดียวกัน ลองนึกภาพว่าเรามีสองโมดูล A และ B การเรียกระบบเปิดของโมดูล A จะเรียกว่า A_open และการเรียกของโมดูล B เดียวกันจะเรียกว่า B_open ตอนนี้เคอร์เนลที่ฉีด syscall ถูกแทนที่ด้วย A_open ซึ่งจะเรียก sys_open ดั้งเดิมเมื่อเสร็จสิ้นสิ่งที่ต้องทำ จากนั้น B จะแทรกลงในเคอร์เนลและแทนที่การเรียกของระบบด้วย B_open ซึ่งจะเรียกสิ่งที่คิดว่าเป็นการเรียกระบบดั้งเดิม แต่จริงๆ แล้วเป็น A_open

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

ฉันเห็นสองวิธีในการป้องกันปัญหานี้ ขั้นแรก: คืนค่าการเรียกเป็นค่าดั้งเดิมของ sys_open ขออภัย sys_open ไม่ได้เป็นส่วนหนึ่งของตารางเคอร์เนลของระบบใน /proc/ksyms ดังนั้นเราจึงไม่สามารถเข้าถึงได้ อีกวิธีหนึ่งคือการใช้ตัวนับลิงก์เพื่อป้องกันไม่ให้โมดูลถูกยกเลิกการโหลด นี่เป็นสิ่งที่ดีสำหรับโมดูลปกติ แต่ไม่ดีสำหรับโมดูล "การศึกษา"

/* syscall.c * * ตัวอย่างการเรียกระบบ "ขโมย" */ /* ลิขสิทธิ์ (C) 1998-99 โดย Ori Pomerantz */ /* ไฟล์ส่วนหัวที่จำเป็น */ /* มาตรฐานในโมดูลเคอร์เนล */ #include /* เรากำลังดำเนินการเคอร์เนลอยู่ */ #include /* โดยเฉพาะโมดูล */ /* จัดการกับ CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #รวม /* รายการการเรียกของระบบ */ /* สำหรับโครงสร้างปัจจุบัน (กระบวนการ) เราต้องการ * สิ่งนี้เพื่อให้รู้ว่าใครคือผู้ใช้ปัจจุบัน */ #รวม /* ใน 2.2.3 /usr/include/linux/version.h มีมาโคร * สำหรับสิ่งนี้ แต่ 2.0.35 ไม่มี - ดังนั้นฉันจึงเพิ่ม * ที่นี่หากจำเป็น */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a ,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* ตารางการเรียกระบบ (ตารางฟังก์ชัน) เรา * เพียงแค่กำหนดสิ่งนี้เป็นภายนอก และเคอร์เนลจะ * เติมให้เราเมื่อเราอยู่ใน insmod"ed */ extern void *sys_call_table; /* UID ที่เราต้องการสอดแนม - จะถูกเติมจากบรรทัดคำสั่ง * */ int uid; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* ตัวชี้ไปยังการเรียกระบบเดิม เหตุผล * เราเก็บสิ่งนี้ไว้ แทนที่จะเรียกใช้ฟังก์ชันเดิม * (sys_open) เป็นเพราะอาจมีคนอื่น * แทนที่การเรียกของระบบก่อนหน้าเรา ฟังก์ชันในโมดูลนั้น - และมัน * อาจถูกลบก่อนที่เราจะเป็น * เป็นตัวแปรคงที่ดังนั้นจึงไม่ถูกส่งออก */ asmlinkage int (*original_call)(const char *, int, int); /* ด้วยเหตุผลบางอย่างใน 2.2.3 ปัจจุบัน -> uid ให้ * ศูนย์กับฉัน ไม่ใช่ ID ผู้ใช้จริง ฉันพยายามค้นหาสิ่งที่ * ผิดพลาด แต่ฉันทำไม่ได้ในเวลาอันสั้นและ * ฉันขี้เกียจ - ดังนั้นฉันจะใช้การเรียกของระบบเพื่อรับ * uid วิธีที่กระบวนการจะทำ * * ด้วยเหตุผลบางอย่าง หลังจากที่ฉันคอมไพล์เคอร์เนลใหม่ ปัญหา * ก็หายไป */ asmlinkage int (*getuid_call)(); /* ฟังก์ชันที่เราจะแทนที่ sys_open (ฟังก์ชัน * เรียกเมื่อคุณเรียกการเรียกระบบเปิด) ด้วย หากต้องการ * ค้นหาต้นแบบที่แน่นอนด้วยจำนวนและประเภท * ของอาร์กิวเมนต์ เราจะค้นหาฟังก์ชันดั้งเดิมก่อน * (มัน) s ที่ fs/open.c) * * ตามทฤษฎีแล้ว นี่หมายความว่าเราผูกติดอยู่กับ * เคอร์เนลเวอร์ชันปัจจุบัน ในทางปฏิบัติการเรียกระบบ * แทบไม่มีการเปลี่ยนแปลง (มันจะทำลายล้าง * และต้องการให้โปรแกรมคอมไพล์ใหม่เนื่องจากการเรียกของระบบ * เป็น ส่วนต่อประสานระหว่างเคอร์เนลและกระบวนการ *).*/ asmlinkage int our_sys_open(ชื่อไฟล์ const char *, แฟล็ก int, โหมด int) ( int i = 0; char ch; /* ตรวจสอบว่านี่คือผู้ใช้ที่เรากำลังสอดแนมอยู่หรือไม่ */ if (uid == getuid_call()) ( /* getuid_call เป็นการเรียกระบบ getuid * ซึ่งให้ uid ของผู้ใช้ที่ * รันกระบวนการซึ่งเรียกระบบ * call เราได้ */ /* รายงานไฟล์ ถ้าเกี่ยวข้อง */ printk("เปิดไฟล์โดย %d: ", uid); do ( #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, filename+i); #else ch = get_user(filename+ i ); #endif i++; printk("%c", ch); ) while (ch != 0); printk("\n"); ) /* เรียก sys_open ดั้งเดิม - ไม่เช่นนั้นเราจะสูญเสีย * ความสามารถในการเปิด ไฟล์ */ ส่งคืน original_call(ชื่อไฟล์ แฟล็ก โหมด); ) /* เริ่มต้นโมดูล - แทนที่การเรียกระบบ */ int init_module() ( /* คำเตือน - สายเกินไปสำหรับตอนนี้ แต่อาจจะสำหรับ * ในครั้งต่อไป .. */ printk("ฉัน" อันตราย ฉันหวังว่าคุณจะทำ "); printk("sync before you insmod"ed me.\n"); printk("คู่หูของฉัน cleanup_module() เท่ากัน"); printk("อันตรายกว่านี้ ถ้า\n"); printk("คุณให้ความสำคัญกับระบบไฟล์ของคุณ มันจะทำ"); printk("เป็น \"ซิงค์; rmmod\" \n"); printk("เมื่อคุณลบโมดูลนี้\n"); /* เก็บตัวชี้ไปยังฟังก์ชันดั้งเดิมใน * original_call จากนั้นแทนที่การเรียกของระบบ * ในตารางการเรียกระบบด้วย our_sys_open */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open; /* หากต้องการรับที่อยู่ของฟังก์ชันสำหรับระบบ * call foo ให้ไปที่ sys_call_table[__NR_foo] */ printk("กำลังสอดแนม UID:%d\n", uid); /* รับการเรียกระบบสำหรับ getuid */ getuid_call = sys_call_table[__NR_getuid]; กลับ 0; ) /* Cleanup - ยกเลิกการลงทะเบียนไฟล์ที่เหมาะสมจาก /proc */ void cleanup_module() ( /* เรียกระบบกลับมาเป็นปกติ */ if (sys_call_table[__NR_open] != our_sys_open) ( printk("มีคนอื่นเล่นด้วย "); printk("open system call\n"); printk("The system may be left in "); printk("an unstable state.\n"); ) sys_call_table[__NR_open] = original_call; )

โดยส่วนใหญ่แล้ว รหัสสำหรับการเรียกระบบคือหมายเลข __NR_xxx ซึ่งกำหนดไว้ใน /usr/include/asm/unistd.h, สามารถพบได้ในซอร์สโค้ดของเคอร์เนลลินุกซ์ในฟังก์ชัน sys_xxx(). (สามารถดูตารางการโทรสำหรับ i386 ได้ใน /usr/src/linux/arch/i386/เคอร์เนล/entry.S.) มีข้อยกเว้นหลายประการสำหรับกฎนี้ สาเหตุหลักมาจากการที่ระบบเก่าส่วนใหญ่ถูกแทนที่ด้วยใหม่ และไม่มีระบบใด ๆ บนแพลตฟอร์มที่มีการจำลอง OS ที่เป็นกรรมสิทธิ์ เช่น parisc, sparc, sparc64 และ alpha มีการเรียกระบบเพิ่มเติมมากมาย mips64 ยังมีการเรียกระบบ 32 บิตครบชุด

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

*ปัจจุบันมีสามเวอร์ชั่นที่แตกต่างกัน สถานะ(2): sys_stat() (สถานที่ __NR_oldstat), sys_newstat() (สถานที่ __NR_stat) และ sys_stat64() (สถานที่ __NR_stat64) ส่วนหลังกำลังใช้งานอยู่ สถานการณ์ที่คล้ายกันกับ lstat(2) และ fstat(2). * กำหนดในทำนองเดียวกัน __NR_oldolduname, __NR_oldunameและ __NR_unameสำหรับการโทร sys_olduname(), sys_uname() และ sys_newuname(). * Linux 2.0 มีเวอร์ชันใหม่ vm86(2) กระบวนการทางนิวเคลียร์แบบใหม่และแบบเก่าเรียกว่า sys_vm86old() และ sys_vm86(). * Linux 2.4 มีเวอร์ชันใหม่ getrlimit(2) กระบวนการนิวเคลียร์แบบใหม่และแบบเก่าเรียกว่า sys_old_getrlimit() (สถานที่ __NR_getrlimit) และ sys_getrlimit() (สถานที่ __NR_ugetrlimit). * ใน Linux 2.4 ขนาดของฟิลด์ ID ผู้ใช้และกลุ่มได้เพิ่มขึ้นจาก 16 เป็น 32 บิต มีการเพิ่มการเรียกระบบหลายครั้งเพื่อรองรับการเปลี่ยนแปลงนี้ (เช่น chown32(2), getuid32(2), getgroups32(2), setresuid32(2)) เลิกใช้การโทรก่อนหน้านี้ที่มีชื่อเดียวกัน แต่ไม่มีส่วนต่อท้าย "32" * Linux 2.4 เพิ่มการรองรับสำหรับการเข้าถึงไฟล์ขนาดใหญ่ (ซึ่งขนาดและออฟเซ็ตไม่พอดีกับ 32 บิต) ในแอปพลิเคชันบนสถาปัตยกรรม 32 บิต สิ่งนี้จำเป็นต้องเปลี่ยนแปลงการเรียกระบบที่ทำงานกับขนาดไฟล์และออฟเซ็ต เพิ่มการเรียกระบบต่อไปนี้: fcntl64(2), getdents64(2), stat64(2), statfs64(2), truncate64(2) และคู่หูของพวกเขาที่จัดการ file descriptor หรือลิงก์สัญลักษณ์ การเรียกระบบเหล่านี้ลบล้างการเรียกระบบแบบเก่า ซึ่งยกเว้นการเรียก "stat" มีชื่อเหมือนกัน แต่ไม่มีส่วนต่อท้าย "64"

บนแพลตฟอร์มที่ใหม่กว่าที่มีการเข้าถึงไฟล์แบบ 64 บิตเท่านั้น และ UID/GID แบบ 32 บิต (เช่น alpha, ia64, s390x, x86-64) มีการเรียกใช้ระบบสำหรับ UID/GID และการเข้าถึงไฟล์เพียงเวอร์ชันเดียวเท่านั้น บนแพลตฟอร์ม (โดยปกติคือแพลตฟอร์ม 32 บิต) ที่มีการเรียก *64 และ *32 เวอร์ชันอื่นๆ จะล้าสมัย

* ความท้าทาย rt_sig*เพิ่มในเคอร์เนล 2.2 เพื่อรองรับสัญญาณเรียลไทม์เพิ่มเติม (ดู สัญญาณ(7)). การเรียกระบบเหล่านี้เลิกใช้การเรียกระบบเก่าที่มีชื่อเดียวกัน แต่ไม่มีคำนำหน้า "rt_" * ในระบบโทร เลือก(2) และ mmap(2) มีการใช้อาร์กิวเมนต์ตั้งแต่ห้ารายการขึ้นไป ซึ่งทำให้เกิดปัญหาในการพิจารณาว่าอาร์กิวเมนต์ถูกส่งผ่านบน i386 อย่างไร เป็นผลให้ในขณะที่สถาปัตยกรรมอื่นเรียก sys_select() และ sys_mmap() การแข่งขัน __NR_selectและ __NR_mmap, บน i386 พวกเขาสอดคล้องกับ old_select() และ old_mmap() (ขั้นตอนโดยใช้ตัวชี้ไปยังกลุ่มอาร์กิวเมนต์) ปัจจุบันไม่มีปัญหาในการส่งข้อโต้แย้งเกินห้าข้ออีกต่อไปและมี __NR__newselectซึ่งตรงกับ sys_select() และสถานการณ์เดียวกันกับ __NR_mmap2.

เนื้อหานี้เป็นการดัดแปลงบทความชื่อเดียวกันโดย Vladimir Meshkov ซึ่งตีพิมพ์ในวารสาร "System Administrator"

เนื้อหานี้เป็นสำเนาบทความของ Vladimir Meshkov จากนิตยสาร "System Administrator" บทความเหล่านี้สามารถพบได้ที่ลิงค์ด้านล่าง นอกจากนี้ยังมีการเปลี่ยนแปลงตัวอย่างบางส่วนของข้อความต้นฉบับของโปรแกรม - ปรับปรุง ปรับปรุงแล้ว (ตัวอย่าง 4.2 ได้รับการแก้ไขอย่างมาก เนื่องจากต้องมีการสกัดกั้นการเรียกระบบที่แตกต่างกันเล็กน้อย) URLs: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/ อัพโหลดแล้ว/a3. pdf

มีคำถามหรือไม่? แล้วคุณอยู่ที่นี่: [ป้องกันอีเมล]

  • 2. โมดูลเคอร์เนลที่โหลดได้
  • 4. ตัวอย่างของการดักฟังระบบตาม LKM
    • 4.1 ปิดการสร้างไดเร็กทอรี

1. มุมมองทั่วไปของสถาปัตยกรรมลินุกซ์

มุมมองทั่วไปส่วนใหญ่ทำให้เราเห็นรูปแบบสองระดับของระบบ เคอร์เนล<=>progs ตรงกลาง (ด้านซ้าย) คือเคอร์เนลของระบบ เคอร์เนลโต้ตอบโดยตรงกับฮาร์ดแวร์คอมพิวเตอร์ โดยแยกโปรแกรมแอปพลิเคชันออกจากคุณลักษณะทางสถาปัตยกรรม เคอร์เนลมีชุดบริการที่จัดเตรียมไว้สำหรับโปรแกรมแอปพลิเคชัน บริการเคอร์เนลรวมถึงการดำเนินการ I/O (การเปิด การอ่าน การเขียน และการจัดการไฟล์) การสร้างและจัดการกระบวนการ การซิงโครไนซ์ และการสื่อสารระหว่างกระบวนการ แอปพลิเคชันทั้งหมดร้องขอบริการเคอร์เนลผ่านการเรียกของระบบ

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

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

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

/* ที่มา 1.0 */ #include main () ( int fd; char buf; /* เปิดไฟล์ - รับลิงค์ (file descriptor) fd */ fd = open("file1",O_RDONLY); /* อ่าน 80 ตัวอักษรในบัฟเฟอร์ buf */ read( fd, buf , sizeof(buf)); /* ปิดไฟล์ */ close(fd); ) /* EOF */ รายการการเรียกระบบ OS Linux ทั้งหมดมีอยู่ใน /usr/include/asm/unistd.h . ทีนี้มาดูกลไกการเรียกระบบในตัวอย่างนี้ เมื่อคอมไพเลอร์ตรงตามฟังก์ชัน open() เพื่อเปิดไฟล์ แปลงเป็นโค้ดแอสเซมเบลอร์ โหลดหมายเลขการเรียกระบบที่สอดคล้องกับฟังก์ชันนี้และพารามิเตอร์ในโปรเซสเซอร์รีจิสเตอร์แล้วจึงเรียกอินเตอร์รัปต์ 0x80 ค่าต่อไปนี้ถูกโหลดลงในการลงทะเบียนโปรเซสเซอร์:

  • ไปที่การลงทะเบียน EAX - จำนวนการโทรของระบบ ดังนั้น สำหรับกรณีของเรา หมายเลขโทรศัพท์ของระบบคือ 5 (ดู __NR_open)
  • ไปยังการลงทะเบียน EBX - พารามิเตอร์แรกของฟังก์ชัน (สำหรับ open() จะเป็นตัวชี้ไปยังสตริงที่มีชื่อไฟล์ที่กำลังเปิดอยู่
  • ไปยังการลงทะเบียน ECX - พารามิเตอร์ที่สอง (สิทธิ์การเข้าถึงไฟล์)
พารามิเตอร์ที่สามถูกโหลดลงในการลงทะเบียน EDX ในกรณีนี้เราไม่มี ในการดำเนินการเรียกระบบใน OS Linux จะใช้ฟังก์ชัน system_call ซึ่งกำหนดไว้ (ขึ้นอยู่กับสถาปัตยกรรมในกรณีนี้คือ i386) ในไฟล์ /usr/src/linux/arch/i386/kernel/entry.S ฟังก์ชันนี้เป็นจุดเริ่มต้นสำหรับการเรียกระบบทั้งหมด เคอร์เนลตอบสนองต่อการขัดจังหวะ 0x80 โดยการเรียกใช้ฟังก์ชัน system_call ซึ่งเป็นตัวจัดการการขัดจังหวะ 0x80 โดยพื้นฐาน

เพื่อให้แน่ใจว่าเรามาถูกทางแล้ว มาดูโค้ดของฟังก์ชัน open() ในไลบรารีระบบ libc กัน:

# gdb -q /lib/libc.so.6 (gdb) disas open ดัมพ์ของรหัสแอสเซมเบลอร์สำหรับฟังก์ชันเปิด: 0x000c8080 : โทร 0x1082be< __i686.get_pc_thunk.cx >0x000c8085 : เพิ่ม $0x6423b,%ecx 0x000c808b : cmpl $0x0.0x1a84(%ecx) 0x000c8092 : jne 0xc80b1 0x000c8094 : กด %ebx 0x000c8095 : mov 0x10(%esp,1),%edx 0x000c8099 : mov 0xc(%esp,1),%ecx 0x000c809d : mov 0x8(%esp,1),%ebx 0x000c80a1 : mov $0x5,%eax 0x000c80a6 : int $0x80 ... ดังที่คุณเห็นในบรรทัดสุดท้าย พารามิเตอร์จะถูกส่งไปยังการลงทะเบียน EDX, ECX, EBX และการลงทะเบียน EAX ล่าสุดจะเต็มไปด้วยหมายเลขเรียกของระบบ ซึ่งอย่างที่เราทราบแล้วคือ 5 .

ตอนนี้กลับไปที่กลไกการเรียกระบบ ดังนั้นเคอร์เนลจึงเรียกตัวจัดการการขัดจังหวะ 0x80 - ฟังก์ชัน system_call System_call จะส่งสำเนาของการลงทะเบียนที่มีพารามิเตอร์การโทรไปยังสแต็กโดยใช้มาโคร SAVE_ALL และเรียกใช้ฟังก์ชันระบบที่ต้องการด้วยคำสั่งเรียก ตารางตัวชี้ไปยังฟังก์ชันเคอร์เนลที่ใช้การเรียกระบบจะอยู่ในอาร์เรย์ sys_call_table (ดูไฟล์ arch/i386/kernel/entry.S) หมายเลขเรียกของระบบที่อยู่ในการลงทะเบียน EAX คือดัชนีในอาร์เรย์นี้ ดังนั้น หาก EAX มีค่า 5 ฟังก์ชันเคอร์เนล sys_open() จะถูกเรียก เหตุใดจึงจำเป็นต้องใช้มาโคร SAVE_ALL คำอธิบายที่นี่ง่ายมาก เนื่องจากฟังก์ชันระบบเคอร์เนลเกือบทั้งหมดเขียนด้วยภาษา C จึงมองหาพารามิเตอร์ในสแต็ก และพารามิเตอร์จะถูกผลักไปที่สแต็กด้วย SAVE_ALL! ค่าส่งคืนของการเรียกระบบจะถูกเก็บไว้ในการลงทะเบียน EAX

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

2. โมดูลเคอร์เนลที่โหลดได้

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

แต่ละ LKM ประกอบด้วยสองหน้าที่หลัก (ขั้นต่ำ):

  • ฟังก์ชันการเริ่มต้นโมดูล เรียกเมื่อโหลด LKM ลงในหน่วยความจำ: int init_module(void) ( ... )
  • ฟังก์ชั่นยกเลิกการโหลดโมดูล: ถือเป็นโมฆะ cleanup_module(เป็นโมฆะ) ( ... )
นี่คือตัวอย่างโมดูลที่ง่ายที่สุด: /* Source 2.0 */ #include int init_module(void) ( printk("Hello World\n"); return 0; ) void cleanup_module(void) ( printk("Bye\n"); ) /* EOF */ คอมไพล์และโหลดโมดูล การโหลดโมดูลลงในหน่วยความจำทำได้โดยใช้คำสั่ง insmod และการดูโมดูลที่โหลดด้วยคำสั่ง lsmod: # gcc -c -DMODULE -I /usr/src/linux/include/ src-2.0.c # insmod src-2.0.o คำเตือน: กำลังโหลด src-2.0 .o จะทำให้เคอร์เนลเสีย: ไม่มีใบอนุญาต โหลดโมดูล src-2.0 พร้อมคำเตือน # dmesg | tail -n 1 สวัสดีชาวโลก # lsmod | grep src src-2.0 336 0 (ไม่ได้ใช้) # rmmod src-2.0 # dmesg | tail -n 1 บาย

3. อัลกอริธึมสำหรับการสกัดกั้นการเรียกระบบตาม LKM

ในการใช้งานโมดูลที่สกัดกั้นการเรียกของระบบ จำเป็นต้องกำหนดอัลกอริธึมการสกัดกั้น อัลกอริทึมมีดังต่อไปนี้:
  • บันทึกตัวชี้ไปยังการโทรเดิม (ดั้งเดิม) เพื่อให้สามารถกู้คืนได้
  • สร้างฟังก์ชันที่ใช้การเรียกระบบใหม่
  • แทนที่การโทรในตารางการเรียกระบบ sys_call_table เช่น ตั้งค่าตัวชี้ที่สัมพันธ์กับการเรียกระบบใหม่
  • เมื่อสิ้นสุดการทำงาน (เมื่อยกเลิกการโหลดโมดูล) ให้เรียกคืนการเรียกระบบเดิมโดยใช้ตัวชี้ที่บันทึกไว้ก่อนหน้านี้
การติดตามช่วยให้คุณค้นหาว่าการเรียกระบบใดที่เกี่ยวข้องกับการทำงานของแอปพลิเคชันผู้ใช้ โดยการติดตาม คุณสามารถกำหนดได้ว่าระบบใดควรถูกสกัดกั้นเพื่อควบคุมแอปพลิเคชัน # ltrace -S ./src-1.0 ... open("file1", 0, 01 SYS_open("file1", 0, 01) = 3<... open resumed>) = 3 อ่าน(3, SYS_read(3, "123\n", 80) = 4<... read resumed>"123\n", 80) = 4 ปิด(3 SYS_close(3) = 0<... close resumed>) = 0 ... ตอนนี้เรามีข้อมูลเพียงพอที่จะเริ่มศึกษาตัวอย่างการใช้งานโมดูลที่สกัดกั้นการเรียกของระบบ

4. ตัวอย่างของการดักฟังระบบตาม LKM

4.1 ปิดการสร้างไดเร็กทอรี

เมื่อไดเร็กทอรีถูกสร้างขึ้น ฟังก์ชันเคอร์เนล sys_mkdir จะถูกเรียก พารามิเตอร์คือสตริงที่มีชื่อของไดเร็กทอรีที่จะสร้าง พิจารณารหัสที่สกัดกั้นการเรียกของระบบที่เกี่ยวข้อง /* ที่มา 4.1 */ #include #รวม #รวม /* ส่งออกตารางการเรียกของระบบ */ ถือเป็นโมฆะภายนอก *sys_call_table; /* กำหนดตัวชี้เพื่อจัดเก็บการโทรดั้งเดิม */ int (*orig_mkdir)(const char *path); /* สร้างการเรียกระบบของเราเอง การเรียกของเราไม่ทำอะไรเลย แค่คืนค่า null */ int own_mkdir(const char *path) ( return 0; ) /* ระหว่างการเริ่มต้นโมดูล ให้บันทึกตัวชี้ไปที่การโทรเดิมและแทนที่การเรียกของระบบ */ int init_module(void) ( orig_mkdir =sys_call_table; sys_call_table=own_mkdir; printk("sys_mkdir Replace\n"); return(0); ) /* เมื่อยกเลิกการโหลด ให้เรียกคืนการโทรเดิม */ void cleanup_module(void) ( sys_call_table=orig_mkdir; printk("sys_mkdir ย้ายกลับ \n "); ) /* EOF */ หากต้องการรับโมดูลอ็อบเจ็กต์ ให้รันคำสั่งต่อไปนี้และเรียกใช้การทดลองบางอย่างบนระบบ: # gcc -c -DMODULE -I/usr/src/linux/include/ src-3.1 c # dmesg | tail -n 1 sys_mkdir แทนที่ # mkdir test # ls -ald test ls: ทดสอบ: ไม่มีไฟล์หรือไดเรกทอรีดังกล่าว # rmmod src-3.1 # dmesg | tail -n 1 sys_mkdir ย้ายกลับ # mkdir test # ls -ald test drwxr-xr-x 2 root root 4096 2003-12-23 03:46 test อย่างที่คุณเห็น คำสั่ง "mkdir" ไม่ทำงาน หรือค่อนข้างไม่มีอะไร เกิดขึ้น การถอดโมดูลออกก็เพียงพอที่จะคืนค่าการทำงานของระบบ ที่ได้ทำไว้ข้างต้น

4.2 การซ่อนรายการไฟล์ในไดเร็กทอรี

มาดูกันว่าการเรียกระบบใดมีหน้าที่อ่านเนื้อหาของไดเร็กทอรี ในการทำเช่นนี้ เราจะเขียนส่วนทดสอบอื่นที่อ่านไดเร็กทอรีปัจจุบัน: /* Source 4.2.1 */ #include #รวม int main() ( DIR *d; struct dirent *dp; d = opendir("."); dp = readdir(d); return 0; ) /* EOF */ รับไฟล์ปฏิบัติการและติดตาม: # gcc -o src -3.2.1 src-3.2.1.c # ltrace -S ./src-3.2.1 ... opendir("." SYS_open(".", 100352, 010005141300) = 3 SYS_fstat64(3, 0xbffff79c, 0x4014c2c0, 3, 0xbffff874) = 0 SYS_fcntl64(3, 2, 1, 1, 0x4014c2c0) = 0 SYS_brk(NULL) = 0x080495 = 0x0806a5f4 SYS_brk(NULL) = 0x0806a5f4 SYS_brk(0x0806b000) = 0x0806b000<... opendir resumed>) = 0x08049648 readdir(0x08049648 SYS_getdents64(3, 0x08049678, 4096, 0x40014400, 0x4014c2c0) = 528<... readdir resumed>) = 0x08049678 ... ให้ความสนใจกับบรรทัดสุดท้าย เนื้อหาของไดเร็กทอรีถูกอ่านโดยฟังก์ชัน getdents64 (getdents เป็นไปได้ในเคอร์เนลอื่น) ผลลัพธ์จะถูกเก็บไว้เป็นรายการโครงสร้างประเภท struct dirent และฟังก์ชันจะคืนค่าความยาวของรายการทั้งหมดในไดเร็กทอรี เรามีความสนใจในสองสาขาของโครงสร้างนี้:
  • d_reclen - ขนาดบันทึก
  • d_name - ชื่อไฟล์
เพื่อซ่อนรายการไฟล์เกี่ยวกับไฟล์ (กล่าวอีกนัยหนึ่งทำให้มองไม่เห็น) คุณต้องสกัดกั้นการเรียกระบบ sys_getdents64 ค้นหารายการที่เกี่ยวข้องในรายการโครงสร้างที่ได้รับและลบออก พิจารณารหัสที่ดำเนินการนี้ (ผู้เขียนรหัสดั้งเดิมคือ Michal Zalewski): /* แหล่ง 4.2.2 */ #include #รวม #รวม #รวม #รวม #รวม #รวม #รวม โมฆะภายนอก *sys_call_table; int (*orig_getdents)(u_int fd, struct dirent *dirp, นับ u_int); /* กำหนดการเรียกระบบของเรา */ int own_getdents(u_int fd, struct dirent *dirp, u_int count) ( unsigned int tmp, n; int t; struct dirent64 ( int d_ino1,d_ino2; int d_off1,d_off2_reclen short ไม่ได้ลงชื่อ char d_type; char d_name; ) *dirp2, *dirp3; /* ชื่อของไฟล์ที่เราต้องการซ่อน */ char hide = "file1"; /* กำหนดความยาวของรายการในไดเร็กทอรี */ tmp = (*orig_getdents )(fd,dirp ,count); if (tmp>0) ( /* จัดสรรหน่วยความจำสำหรับโครงสร้างเคอร์เนลสเปซและคัดลอกเนื้อหาของไดเร็กทอรีลงในไดเร็กทอรี */ dirp2 = (struct dirent64 *)kmalloc(tmp,GFP_KERNEL) ; copy_from_user(dirp2,dirp,tmp) ; /* เรียกใช้โครงสร้างที่สองและบันทึกค่าของความยาวของรายการในไดเร็กทอรี */ dirp3 = dirp2; t = tmp; /* เริ่มค้นหาไฟล์ของเรา */ ในขณะที่ (t >0) ( /* อ่านความยาวของรายการแรกและกำหนดความยาวที่เหลือของรายการในไดเร็กทอรี */ n = dirp3->d_reclen; t -= n; /* ตรวจสอบว่าชื่อไฟล์จากรายการปัจจุบันตรงกับชื่อไฟล์หรือไม่ เรากำลังมองหา */ if (strstr((char *)&(dirp3->d_name), (char *)&hide) != NULL) ( /* ถ้าใช่ ให้เขียนทับรายการและคำนวณค่าใหม่สำหรับความยาวของรายการในไดเร็กทอรี */ memcpy(dirp3, (char *)dirp3+dirp3->d_reclen, t) ; tmp -=n; ) /* วางตัวชี้ไปที่รายการถัดไปและค้นหาต่อไป */ dirp3 = (struct dirent64 *)((char *)dirp3+dirp3->d_reclen); ) /* ส่งคืนผลลัพธ์และหน่วยความจำว่าง */ copy_to_user(dirp,dirp2,tmp); ฟรี (dirp2); ) /* ส่งคืนค่าความยาวของรายการในไดเร็กทอรี */ return tmp; ) /* ฟังก์ชันการเริ่มต้นและยกเลิกการโหลดโมดูลมีรูปแบบมาตรฐาน */ int init_module(void) ( orig_getdents = sys_call_table; sys_call_table=own_getdents; return 0; ) void cleanup_module() ( sys_call_table=orig_getdents; ) /* EOF */ หลังจากรวบรวม โค้ดสังเกตว่า "file1" หายไปได้อย่างไร ซึ่งเป็นสิ่งที่เราต้องการพิสูจน์

5. วิธีการเข้าถึงโดยตรงไปยังพื้นที่ที่อยู่เคอร์เนล /dev/kmem

ให้เราพิจารณาในทางทฤษฎีก่อนว่าการสกัดกั้นดำเนินการอย่างไรโดยวิธีการเข้าถึงโดยตรงไปยังพื้นที่ที่อยู่ของเคอร์เนลแล้วดำเนินการนำไปใช้จริง

การเข้าถึงโดยตรงไปยังพื้นที่ที่อยู่เคอร์เนลนั้นมาจากไฟล์อุปกรณ์ /dev/kmem ไฟล์นี้แสดงพื้นที่ที่อยู่เสมือนที่มีอยู่ทั้งหมด รวมถึงพาร์ติชั่นสว็อป (พื้นที่สว็อป) ในการทำงานกับไฟล์ kmem จะใช้ฟังก์ชันระบบมาตรฐาน - open(), read(), write() การเปิด /dev/kmem ในวิธีมาตรฐาน เราสามารถอ้างถึงที่อยู่ในระบบโดยตั้งค่าเป็นออฟเซ็ตในไฟล์นี้ วิธีนี้ได้รับการพัฒนาโดย Silvio Cesare

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

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

ในการกำหนดที่อยู่ของตาราง sys_call_table คุณต้องคำนวณที่อยู่ของฟังก์ชัน system_call ก่อน เนื่องจากฟังก์ชันนี้เป็นตัวจัดการการขัดจังหวะ มาดูวิธีจัดการกับการขัดจังหวะในโหมดที่ได้รับการป้องกัน

ในโหมดจริง เมื่อลงทะเบียนอินเตอร์รัปต์ โปรเซสเซอร์จะเข้าถึงตารางเวกเตอร์อินเตอร์รัปต์ ซึ่งอยู่ที่จุดเริ่มต้นของหน่วยความจำเสมอและมีแอดเดรสสองคำของตัวจัดการอินเตอร์รัปต์ ในโหมดที่ได้รับการป้องกัน อะนาล็อกของตารางเวกเตอร์ขัดจังหวะคือตารางอธิบายการขัดจังหวะ (IDT) ซึ่งอยู่ในระบบปฏิบัติการโหมดที่ได้รับการป้องกัน เพื่อให้โปรเซสเซอร์เข้าถึงตารางนี้ได้ ต้องโหลดที่อยู่ของมันลงใน Interrupt Descriptor Table Register (IDTR) ตาราง IDT ประกอบด้วยตัวอธิบายตัวจัดการการขัดจังหวะ ซึ่งโดยเฉพาะอย่างยิ่ง รวมถึงที่อยู่ของพวกเขา ตัวอธิบายเหล่านี้เรียกว่าเกตเวย์ (gates) โปรเซสเซอร์ที่ลงทะเบียนการขัดจังหวะแล้วดึงเกตเวย์จาก IDT ตามหมายเลขกำหนดที่อยู่ของตัวจัดการและโอนการควบคุมไป

ในการคำนวณที่อยู่ของฟังก์ชัน system_call จากตาราง IDT จำเป็นต้องแยกอินเทอร์รัปต์เกต int $0x80 ออก และจากนั้นให้ระบุแอดเดรสของตัวจัดการที่เกี่ยวข้อง เช่น ที่อยู่ของฟังก์ชัน system_call ในฟังก์ชัน system_call ตาราง system_call_table สามารถเข้าถึงได้โดยคำสั่ง call<адрес_таблицы>(,%eax,4). เมื่อพบ opcode (ลายเซ็น) ของคำสั่งนี้ในไฟล์ /dev/kmem เราจะพบที่อยู่ของตารางการเรียกระบบด้วย

เพื่อตรวจสอบ opcode ลองใช้ดีบักเกอร์และแยกส่วนฟังก์ชัน system_call:

# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call ดัมพ์ของรหัสแอสเซมเบลอร์สำหรับฟังก์ชัน system_call: 0xc0194cbc : กด %eax 0xc0194cbd : cld 0xc0194cbe : กด %es 0xc0194cbf : กด %ds 0xc0194cc0 : กด %eax 0xc0194cc1 : กด %ebp 0xc0194cc2 : กด %edi 0xc0194cc3 : กด %esi 0xc0194cc4 : กด %edx 0xc0194cc5 : กด %ecx 0xc0194cc6 : กด %ebx 0xc0194cc7 : mov $0x18,%edx 0xc0194ccc : mov %edx,%ds 0xc0194cce : mov %edx,%es 0xc0194cd0 : mov $0xffffe000,%ebx 0xc0194cd5 : และ %esp,%ebx 0xc0194cd7 : testb $0x2.0x18(%ebx) 0xc0194cdb : jne 0xc0194d3c 0xc0194cdd : cmp $0x10e,%eax 0xc0194ce2 : จ๋า 0xc0194d69 0xc0194ce8 : โทร *0xc02cbb0c(,%eax,4) 0xc0194cef : mov %eax,0x18(%esp,1) 0xc0194cf3 : nop สิ้นสุดการถ่ายโอนข้อมูลแอสเซมเบลอร์ บรรทัด "call *0xc02cbb0c(,%eax,4)" เป็นการเรียกไปยังตาราง sys_call_table ค่า 0xc02cbb0c คือที่อยู่ของตาราง (มีแนวโน้มว่าตัวเลขของคุณจะต่างกัน) รับ opcode ของคำสั่งนี้: (gdb) x/xw system_call+44 0xc0194ce8 : 0x0c8514ff เราพบ opcode คำสั่ง sys_call_table แล้ว เท่ากับ \xff\x14\x85 4 ไบต์ที่ตามมาคือที่อยู่ของตาราง คุณสามารถตรวจสอบได้โดยป้อนคำสั่ง: (gdb) x/xw system_call+44+3 0xc0194ceb : 0xc02cbb0c ดังนั้น ค้นหาลำดับ \xff\x14\x85 ในไฟล์ /dev/kmem และอ่าน 4 ไบต์ที่ตามมา เราได้ที่อยู่ของตารางการเรียกระบบ sys_call_table เมื่อทราบที่อยู่ของมัน เราก็สามารถรับเนื้อหาของตารางนี้ (ที่อยู่ของฟังก์ชันระบบทั้งหมด) และเปลี่ยนที่อยู่ของการเรียกระบบด้วยการสกัดกั้น

พิจารณารหัสเทียมที่ดำเนินการสกัดกั้น:

readaddr(old_syscall, scr + SYS_CALL*4, 4); writeaddr(new_syscall, scr + SYS_CALL*4, 4); ฟังก์ชัน readaddr จะอ่านที่อยู่การเรียกของระบบจากตารางการเรียกของระบบ และเก็บไว้ในตัวแปร old_syscall แต่ละรายการในตาราง sys_call_table ใช้เวลา 4 ไบต์ ที่อยู่ที่ต้องการจะอยู่ที่ offset sct + SYS_CALL*4 ในไฟล์ /dev/kmem (ในที่นี้ sct คือที่อยู่ของตาราง sys_call_table SYS_CALL คือหมายเลขซีเรียลของการเรียกระบบ) ฟังก์ชัน writeaddr จะเขียนทับที่อยู่ของการเรียกระบบ SYS_CALL ด้วยที่อยู่ของฟังก์ชัน new_syscall และการเรียกทั้งหมดไปยังการเรียกระบบ SYS_CALL จะได้รับบริการโดยฟังก์ชันนี้

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

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

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

แต่ในการใช้อัลกอริทึมนี้ เราจำเป็นต้องมีที่อยู่ของฟังก์ชัน kmalloc คุณสามารถค้นหาได้หลายวิธี วิธีที่ง่ายที่สุดคืออ่านที่อยู่นี้จากไฟล์ System.map หรือพิจารณาโดยใช้ดีบักเกอร์ gdb (พิมพ์ &kmalloc) หากเคอร์เนลเปิดใช้งานการสนับสนุนโมดูลอยู่ สามารถกำหนดที่อยู่ kmalloc ได้โดยใช้ฟังก์ชัน get_kernel_syms() ตัวเลือกนี้จะกล่าวถึงต่อไป หากไม่มีการสนับสนุนสำหรับโมดูลเคอร์เนล ที่อยู่ของฟังก์ชัน kmalloc จะต้องถูกค้นหาโดย opcode ของคำสั่งเรียก kmalloc ซึ่งคล้ายกับที่ทำในตาราง sys_call_table

ฟังก์ชัน kmalloc รับพารามิเตอร์สองตัว: ขนาดของหน่วยความจำที่ร้องขอและตัวระบุ GFP ในการค้นหา opcode เราจะใช้ตัวดีบั๊กและแยกส่วนฟังก์ชันเคอร์เนลที่มีการเรียกฟังก์ชัน kmalloc

# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register ดัมพ์ของรหัสแอสเซมเบลอร์สำหรับฟังก์ชัน inter_module_register: 0xc01a57b4 : กด %ebp 0xc01a57b5 : กด %edi 0xc01a57b6 : กด %esi 0xc01a57b7 : กด %ebx 0xc01a57b8 : ย่อย $0x10,%esp 0xc01a57bb : mov 0x24(%esp,1),%ebx 0xc01a57bf : mov 0x28(%esp,1),%esi 0xc01a57c3 : mov 0x2c(%esp,1),%ebp 0xc01a57c7 : movl $0x1f0,0x4(%esp,1) 0xc01a57cf : movl $0x14,(%esp,1) 0xc01a57d6 : โทร 0xc01bea2a ... ไม่ว่าฟังก์ชั่นจะทำอะไร สิ่งสำคัญในนั้นคือสิ่งที่เราต้องการ - การเรียกใช้ฟังก์ชัน kmalloc ให้ความสนใจกับบรรทัดสุดท้าย ขั้นแรก พารามิเตอร์จะถูกโหลดลงในสแต็ก (การลงทะเบียน esp ชี้ไปที่ด้านบนสุดของสแต็ก) จากนั้นการเรียกใช้ฟังก์ชันจะตามมา ตัวระบุ GFP ถูกโหลดลงในสแต็กก่อน ($0x1f0,0x4(%esp,1) สำหรับเคอร์เนลเวอร์ชัน 2.4.9 และสูงกว่า ค่านี้คือ 0x1f0 ค้นหา opcode สำหรับคำสั่งนี้: (gdb) x/xw inter_module_register+ 19 0xc01a57c7 : 0x042444c7 หากเราพบ opcode นี้ เราสามารถคำนวณที่อยู่ของฟังก์ชัน kmalloc เมื่อมองแวบแรก ที่อยู่ของฟังก์ชันนี้เป็นอาร์กิวเมนต์ของคำสั่งการโทร แต่นี่ไม่เป็นความจริงทั้งหมด ต่างจากฟังก์ชัน system_call ในที่นี้ คำสั่งไม่ใช่ที่อยู่ kmalloc แต่เป็นการชดเชยที่สัมพันธ์กับที่อยู่ปัจจุบัน เราจะตรวจสอบสิ่งนี้โดยกำหนด opcode ของการเรียกคำสั่ง 0xc01bea2a: (gdb) x/xw inter_module_register+34 0xc01a57d6 : 0x01924fe8 ไบต์แรกคือ e8 ซึ่งเป็น opcode ของคำสั่งการโทร ค้นหาค่าของอาร์กิวเมนต์ของคำสั่งนี้: (gdb) x/xw inter_module_register+35 0xc01a57d7 : 0x0001924f ตอนนี้ถ้าเราเพิ่มที่อยู่ปัจจุบัน 0xc01a57d6 ออฟเซ็ต 0x0001924f และ 5 ไบต์ของคำสั่ง เราจะได้ที่อยู่ที่ต้องการของฟังก์ชัน kmalloc - 0xc01bea2a

สรุปการคำนวณทางทฤษฎีและโดยใช้เทคนิคข้างต้น เราจะสกัดกั้นการเรียกระบบ sys_mkdir

6. ตัวอย่างการสกัดกั้นโดยใช้ /dev/kmem

/* แหล่งที่มา 6.0 */ #include #รวม #รวม #รวม #รวม #รวม #รวม #รวม /* หมายเลขเรียกของระบบที่จะสกัดกั้น */ #define _SYS_MKDIR_ 39 #define KMEM_FILE "/dev/kmem" #define MAX_SYMS 4096 /* IDTR register format description */ struct ( unsigned short limit; unsigned int base; ) __attribute__ ((บรรจุแล้ว) ) idtr; /* คำอธิบายรูปแบบประตูขัดจังหวะตาราง IDT */ struct ( short off1 ที่ไม่ได้ลงชื่อ; sel short ที่ไม่ได้ลงชื่อ; ถ่านที่ไม่ได้ลงนาม แฟล็ก; off2 short ที่ไม่ได้ลงนาม; ) __attribute__ ((บรรจุ)) idt; /* คำอธิบายของโครงสร้างสำหรับฟังก์ชัน get_kmalloc */ struct kma_struc ( ulong (*kmalloc) (uint, int); // - ที่อยู่ของฟังก์ชัน kmalloc ขนาด int // - ขนาดของหน่วยความจำเพื่อจัดสรรแฟล็ก int // - ค่าสถานะ สำหรับเมล็ด > 2.4.9 = 0x1f0 (GFP) ulong mem; ) __attribute__ ((บรรจุ)) kmalloc; /* ฟังก์ชันที่จัดสรรเฉพาะบล็อกของหน่วยความจำในพื้นที่ที่อยู่เคอร์เนล */ int get_kmalloc(struct kma_struc *k) ( k->mem = k->kmalloc(k->size, k->flags); คืนค่า 0 ; ) /* ฟังก์ชันที่ส่งคืนที่อยู่ของฟังก์ชัน (จำเป็นสำหรับการค้นหา kmalloc) */ ulong get_sym(char *n) ( แท็บ struct kernel_sym; int numsyms; int i; numsyms = get_kernel_syms(NULL); if (numsyms > MAX_SYMS || numsyms< 0) return 0; get_kernel_syms(tab); for (i = 0; i < numsyms; i++) { if (!strncmp(n, tab[i].name, strlen(n))) return tab[i].value; } return 0; } /* Наша новая системная функция, ничего не делает;) */ int new_mkdir(const char *path) { return 0; } /* Читает из /dev/kmem с offset size данных в buf */ static inline int rkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset){ printf("lseek err\n"); return 0; } if (read(fd, buf, size) != size) return 0; return size; } /* Аналогично, но только пишет в /dev/kmem */ static inline int wkm(int fd, uint offset, void *buf, uint size) { if (lseek(fd, offset, 0) != offset) return 0; if (write(fd, buf, size) != size) return 0; return size; } /* Читает из /dev/kmem данные размером 4 байта */ static inline int rkml(int fd, uint offset, ulong *buf) { return rkm(fd, offset, buf, sizeof(ulong)); } /* Аналогично, но только пишет */ static inline int wkml(int fd, uint offset, ulong buf) { return wkm(fd, offset, &buf, sizeof(ulong)); } /* Функция для получения адреса sys_call_table */ ulong get_sct(int kmem) { ulong sys_call_off; // - адрес обработчика // прерывания int $0x80 (функция system_call) char *p; char sc_asm; asm("sidt %0" : "=m" (idtr)); if (!rkm(kmem, idtr.base+(8*0x80), &idt, sizeof(idt))) return 0; sys_call_off = (idt.off2 << 16) | idt.off1; if (!rkm(kmem, sys_call_off, &sc_asm, 128)) return 0; p = (char *)memmem(sc_asm, 128, "\xff\x14\x85", 3) + 3; printf("call for sys_call_table at %08x\n",p); if (p) return *(ulong *)p; return 0; } /* Функция для определения адреса функции kmalloc */ ulong get_kma(ulong pgoff) { uint i; unsigned char buf, *p, *p1; int kmemz; ulong ret; ret = get_sym("kmalloc"); if (ret) { printf("\nZer gut!\n"); return ret; } kmemz = open("/dev/kmem", O_RDONLY); if (kmemz < 0) return 0; for (i = pgoff+0x100000; i < (pgoff + 0x1000000); i += 0x10000){ if (!rkm(kmemz, i, buf, sizeof(buf))) return 0; p1=(char *)memmem(buf,sizeof(buf),"\x68\xf0\x01\x00",4); if(p1) { p=(char *)memmem(p1+4,sizeof(buf),"\xe8",1)+1; if (p) { close(kmemz); return *(unsigned long *)p+i+(p-buf)+4; } } } close(kmemz); return 0; } int main() { int kmem; // !! - пустые, нужно подставить ulong get_kmalloc_size; // - размер функции get_kmalloc !! ulong get_kmalloc_addr; // - адрес функции get_kmalloc !! ulong new_mkdir_size; // - размер функции-перехватчика!! ulong new_mkdir_addr; // - адрес функции-перехватчика!! ulong sys_mkdir_addr; // - адрес системного вызова sys_mkdir ulong page_offset; // - нижняя граница адресного // пространства ядра ulong sct; // - адрес таблицы sys_call_table ulong kma; // - адрес функции kmalloc unsigned char tmp; kmem = open(KMEM_FILE, O_RDWR, 0); if (kmem < 0) return 0; sct = get_sct(kmem); page_offset = sct & 0xF0000000; kma = get_kma(page_offset); printf("OK\n" "page_offset\t\t:\t0x%08x\n" "sys_call_table\t:\t0x%08x\n" "kmalloc()\t\t:\t0x%08x\n", page_offset,sct,kma); /* Найдем адрес sys_mkdir */ if (!rkml(kmem, sct+(_SYS_MKDIR_*4), &sys_mkdir_addr)) { printf("Cannot get addr of %d syscall\n", _SYS_MKDIR_); perror("er: "); return 1; } /* Сохраним первые N байт вызова sys_mkdir */ if (!rkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Cannot save old %d syscall!\n", _SYS_MKDIR_); return 1; } /* Перепишем первые N байт, функцией get_kmalloc */ if (!wkm(kmem, sys_mkdir_addr,(void *)get_kmalloc_addr, get_kmalloc_size)) { printf("Can"t overwrite our syscall %d!\n",_SYS_MKDIR_); return 1; } kmalloc.kmalloc = (void *) kma; //- адрес функции kmalloc kmalloc.size = new_mkdir_size; //- размер запращевоемой // памяти (размер функции-перехватчика new_mkdir) kmalloc.flags = 0x1f0; //- спецификатор GFP /* Выполним сис. вызов sys_mkdir, тем самым выполним нашу функцию get_kmalloc */ mkdir((char *)&kmalloc,0); /* Востановим оригинальный вызов sys_mkdir */ if (!wkm(kmem, sys_mkdir_addr, tmp, get_kmalloc_size)) { printf("Can"t restore syscall %d !\n",_SYS_MKDIR_); return 1; } if (kmalloc.mem < page_offset) { printf("Allocated memory is too low (%08x < %08x)\n", kmalloc.mem, page_offset); return 1; } /* Оторбразим результаты */ printf("sys_mkdir_addr\t\t:\t0x%08x\n" "get_kmalloc_size\t:\t0x%08x (%d bytes)\n\n" "our kmem region\t\t:\t0x%08x\n" "size of our kmem\t:\t0x%08x (%d bytes)\n\n", sys_mkdir_addr, get_kmalloc_size, get_kmalloc_size, kmalloc.mem, kmalloc.size, kmalloc.size); /* Разместим в пространстве ядра наш новый сис. вызво */ if(!wkm(kmem, kmalloc.mem, (void *)new_mkdir_addr, new_mkdir_size)) { printf("Unable to locate new system call !\n"); return 1; } /* Перепишем таблицу sys_call_table на наш новый вызов */ if(!wkml(kmem, sct+(_SYS_MKDIR_*4), kmalloc.mem)) { printf("Eh ..."); return 1; } return 1; } /* EOF */ Скомпилируем полученый код и определим адреса и размеры функций get_kmalloc и new_mkdir. Запускать полученое творение рано! Для вычисления адресов и размеров воспользуемся утилитой objdump: # gcc -o src-6.0 src-6.0.c # objdump -x ./src-6.0 >ดัมพ์ มาเปิดไฟล์ดัมพ์กันเถอะ และค้นหาข้อมูลที่เราสนใจ: 080485a4 g F .text 00000032 get_kmalloc 080486b1 g F .text 0000000a new_mkdir ตอนนี้ มาเพิ่มค่าเหล่านี้ในโปรแกรมของเรากัน: ulong get_kmalloc_size=0x32; ulong get_kmalloc_addr=0x080485a4 ; ulong new_mkdir_size=0x0a; ulong new_mkdir_addr=0x080486b1; ตอนนี้ขอคอมไพล์โปรแกรมใหม่ เมื่อเปิดใช้งานเพื่อดำเนินการ เราจะสกัดกั้นการเรียกระบบ sys_mkdir การเรียกใช้ sys_mkdir ทั้งหมดจะถูกจัดการโดยฟังก์ชัน new_mkdir

สิ้นสุดกระดาษ/EOP

ประสิทธิภาพของโค้ดจากทุกส่วนได้รับการทดสอบบนเคอร์เนล 2.4.22 ในการจัดทำรายงาน มีการใช้วัสดุจากไซต์งาน