อธิบายเจาะลึกเทคนิคยกระดับสิทธิ์ใหม่บนลินุกซ์ “SUDO_INJECT”

เมื่อช่วงสงกรานต์ที่ผ่านมา นักวิจัยด้านความปลอดภัย chaignc จากทีม HexpressoCTF ได้มีเปิดเผยเทคนิคใหม่ในการโจมตี sudo ในระบบปฏิบัติการลินุกซ์เพื่อช่วยยกระดับสิทธิ์ของบัญชีผู้ใช้งานปัจจุบันให้มีสิทธิ์สูงขึ้นภายใต้ชื่อการโจมตีว่า SUDO_INJECT

ในบล็อกนี้ ทีมตอบสนองการโจมตีและภัยคุกคาม (Intelligent Response) จะมาอธิบายถึงรายละเอียดการทำงานของ sudo ซึ่งทำให้เกิดเป็นช่องโหว่แบบ In-depth Vulnerability Analysis เพื่อความเข้าใจในสาเหตุการเกิดขึ้นของช่องโหว่นี้กันครับ

สิทธิ์ของผู้ใช้งาน user ถูกยกระดับเป็น root หลังโจมตีสำเร็จ

ทำความเข้าใจ Exploit

จากไฟล์ exploit ซึ่งปรากฎในโครงการของผู้ค้นพบช่องโหว่ เราจะมาทำความเข้าใจ exploit ซึ่งทำให้เราได้สิทธิ์ root ที่อยู่ในไฟล์ exploit.sh กันก่อนว่ามันพยายามทำอะไรกับระบบบ้าง

โค้ดของ exploit ซึ่งอยู่ในไฟล์ exploit.sh

เมื่อถูกรัน ไฟล์ exploit.sh จะมีการดำเนินการดังต่อไปนี้

  1. คำสั่ง sudo ซึ่งมีการระบุพารามิเตอร์ -S จะทำการรอรับรหัสผ่านจาก STDIN ซึ่งถูกส่งต่อมาจากคำสั่ง echo จากนั้น PID ของโปรเซสปัจจุบันจะถูกนำมาแสดง
  2. ไฟล์ activate_sudo_token ถูกคัดลอกไปยังพาธ /tmp/ ก่อนจะถูกแก้ไขสิทธิ์ให้สามารถถูกเอ็กซีคิวต์โดยบัญชีผู้ใช้งานใดก็ได้
  3. จากนั้นสคริปต์จะทำการรวบรวม PID ของโปรเซส shell ที่ทำงานอยู่และถูกรันโดยบัญชีผู้ใช้งานที่มีสิทธิ์ปกติ ยกเว้น PID ของโปรเซสปัจจุบัน
  4. คำสั่ง echo แรกภายใต้ลูปของตัวแปร pid จะทำการแสดงข้อความ "Inject process" พร้อมด้วยค่า PID ที่จะดำเนินการ ชื่อของคำสั่งที่เกี่ยวข้องกับโปรเซสจะถูกแสดงจากไฟล์ /proc/[PID]/comm
  5. คำสั่ง echo ที่สองภายใต้ลูปของตัวแปร pid จะแบ่งออกเป็นสี่ส่วน โดยจะอธิบายจากหลังมาหน้า ได้แก่
    1. ส่วนแรกหลังเครื่องหมาย pipe จะเป็นการเรียกใช้ GNU Debugger หรือ GDB โดยการระบุไม่ให้มีการแสดงข้อความต้อนรับ (พารามิเตอร์ -q), ไม่ต้องโหลดคอนฟิกที่มีอยู่แล้วจากไฟล์ ~/.gdbinit (พารามิเตอร์ -n) และให้ทำการดีบั๊กโปรเซสจากค่า PID ที่ระบุตามลูป
    2. ส่วนที่สองก่อนเครื่องหมาย pipe จะเป็นคำสั่งซึ่งถูกส่งไปยัง STDIN ของโปรเซส GDB โดยเป็นคำสั่งให้ GDB ทำการเรียกใช้ฟังก์ชันที่มีอยู่ในโปรแกรม (คำสั่ง call)
    3. ส่วนที่สามซึ่งเป็นพารามิเตอร์ของคำสั่ง call ของ GDB จะเป็นการบอกให้ GDB ทำการเรียกใช้ฟังก์ชัน system() ในโปรเซสที่ทำการดีบั๊กอยู่ ฟังก์ชัน system() จะทำการสร้างโปรเซสลูกเพื่อรันคำสั่งที่ถูกส่งเข้ามาอีกต่อนึง
    4. ส่วนที่สี่ซึ่งเป็นพารามิเตอร์ของฟังก์ชัน system() จะเป็นคำสั่งซึ่งได้ผลลัพธ์แบบเดียวกับในขั้นตอนที่ 1 แต่จะมีการรันโปรแกรมชื่อ activate_sudo_token พร้อมทั้งระบุพาธ /var/lib/sudo/ts/* ให้กับตัวโปรแกรมด้วย
  6. โปรแกรมจะดำเนินการจนกว่าจะครบลูปของตัวแปร pid ที่รวบรวมมา

ในภาพรวมนั้นสคริปต์ exploit.sh จะมีการรันคำสั่ง sudo ที่ไม่สำเร็จก่อน จากนั้นจึงทำการค้นหา PID ของ shell ที่รันอยู่ก่อนจะพยายามรันคำสั่งภายใต้ PID ของโปรเซสนั้นๆ ผ่านทาง GDB การรันคำสั่งภายใต้ขอบเขตของโปรแกรมนั้นมีจุดประสงค์เพื่อดำเนินการบางอย่างกับไฟล์ที่ตำแหน่ง /var/lib/sudo/ts/* ด้วยโปรแกรม activate_sudo_token

มีอะไรอยู่ในไฟล์ activate_sudo_token?

ดูเหมือนจุดสำคัญที่อยู่ในไฟล์ exploit.sh นั้นจะอยู่ที่ไฟล์ activate_sudo_token ซึ่งถูกรันภายใต้ขอบเขตของโปรเซสอื่นๆ เราจะมาดูกันว่า activate_sudo_token นั้นมีหน้าที่อะไร

ฟังก์ชัน activate_token ในไฟล์ activate_sudo_token ซึ่งทำหน้าที่หลักในการขโมยสิทธิ์

ไฟล์หรือโปรแกรม activate_sudo_token ถูกคอมไพล์มาจาก activate_sudo_token.c ในไดเรกทอรี extra_tools โดยฟังก์ชันซึ่งอยู่ภายในไฟล์ activate_sudo_token.c ทำให้เราสามารถทราบถึงการทำงานของโปรแกรมได้ดังต่อไปนี้

  • เมื่อเริ่มต้นการทำงาน ฟังก์ชัน get_proc_info() จะทำการเก็บข้อมูลเกี่ยวกับโปรเซสจาก PID ปัจจุบัน โดยเก็บไว้ที่ตัวแปร pinfo_self ซึ่งใช้สตรัคเจอร์ข้อมูลชื่อ procinfo
  • ฟังก์ชัน activate_token() จะถูกเรียกใช้กับไฟล์ที่อยู่ในพาธในลักษณะของ glob ได้แก่ /var/run/sudo/ts/*, /var/lib/sudo/ts/* และ /run/sudo/ts/*
  • ฟังก์ชัน activate_token() เมื่อเริ่มต้นจะทำการตรวจสอบพาธในลักษณะของ glob ก่อนว่ามีไฟล์อยู่หรือไม่ ถ้ามีไฟล์อยู่ในพาธดังกล่าว ไฟล์แต่ละไฟล์ซึ่งในพาธจะถูกนำไปดำเนินการต่อ
  • การดำเนินการต่อไฟล์แต่ละไฟล์ที่อยู่ในพาธที่ถูกส่งมายังฟังก์ชัน activate_token() ฟังก์ชันจะทำการอ่านไฟล์โดยอ้างอิงสตรัคเจอร์ในตัวแปร header สตรัคเจอร์นี้เป็นส่วนหัวของฟอร์แมต sudoers_timestamp ซึ่งเป็นปลั๊กอินของ sudoers ทำหน้าที่ในการทำแคชข้อมูลการพิสูจน์ตัวตน เพื่อให้ในระยะเวลาที่กำหนดนั้น ผู้ใช้งานไม่จำเป็นที่จะต้องพิสูจน์ตัวตนด้วยรหัสผ่านเมื่อเรียกใช้งานคำสั่ง sudo การอ่านไฟล์จะถูกดำเนินการโดยฟังก์ชัน read() ที่กำหนดจำนวนไบต์ในการอ่านไว้เท่ากับ sizeof(header) ข้อมูลของไฟล์จะถูกเก็บไว้ที่ตำแหน่งของ header
  • หลังจากข้อมูลที่ถูกอ่านขนาด sizeof(header) ถูกนำไปเก็บไว้ที่ตำแหน่งของ header แล้ว โปรแกรมจะเข้าสู่กระบวนการแก้ไขค่าที่เก็บอยู่ในตำแหน่งของ header โดยมีขั้นตอนดังต่อไปนี้
    • แก้ไขค่าที่ตำแหน่ง header.flags เป็น 0 อ้างอิงจาก sudoers_timestamp ตำแหน่งนี้จะเกี่ยวข้องกับ Time stamp flag ชื่อ TS_DISABLED ไว้กำหนดสถานะของ timestamp และ TS_ANYUID ไว้จำกัดรูปแบบการใช้ค่า UID หลังจากนั้นเขียนทับการแก้ไขนี้ลงในข้อมูลที่อยู่ที่ตำแหน่งของ header
    • โปรแกรมจะทำการตรวจสอบเวอร์ชันของข้อมูลที่อ่านโดยอ้างอิงจากสตรัคเจอร์ sudoers_timestamp โดยหากเป็นกรณีที่ข้อมูลที่อ่านนั้นเป็นเวอร์ชัน 1 โปรแกรมจะมีอ่านข้อมูลส่วนถัดมาต่อจากส่วน header (ใช้ชื่อเป็น entry) โดยอ้างอิงสตรัคเจอร์ชื่อ timestamp_entry_v1 ก่อนทำการแก้ไขค่า entry.ts.tv_sec เป็นเวลาที่โปรเซส activate_token ถูกเริ่มการทำงานหน่วยเป็นวินาที และแก้ไขค่า entry.ts.tv_nsec ในหน่วยนาโนวินาทีให้เป็น 0 หลังจากนั้นเขียนทับการแก้ไขนี้ลงในข้อมูลที่อยู่ที่ตำแหน่งของ entry
    • เช่นเดียวกันกับในกรณีที่ข้อมูลที่อ่านนั้นเป็นเวอร์ชัน 1 เงื่อนไขที่ข้อมูลที่อ่านนั้นไม่เป็นเวอร์ชันหนึ่งจะมีการแก้ไขในลักษณะเดียวกัน แต่แตกต่างกันในจุดของการอ้างอิงสตรัคเจอร์หรือตำแหน่งในการแก้ไขซึ่งใช้เป็น timestamp_entry แทน

โดยสรุปแล้วสิ่งที่เกิดขึ้นเมื่อ activate_sudo_token ถูก inject ให้เข้าไปรันภายใต้ขอบเขตของโปรแกรม shell แต่ละโปรแกรมผ่านทาง GDB นั้นคือการเข้าไปแก้ไขค่าที่เกี่ยวข้องกับ timestamp ของ sudoers_timestamp ของทุกๆ ส่วนของข้อมูลที่ปรากฎอยู่ที่ตำแหน่ง /var/lib/sudo/ts/* นั่นเอง

มีอะไรอยู่ในพาธ /var/lib/sudo/ts/* ?

จากการวิเคราะห์ไฟล์ activate_sudo_token.c ทำให้เราสามารถเห็นภาพชัดเจนขึ้นได้ว่า ไฟล์ที่อยู่ภายใต้ /var/lib/sudo/ts/* มีส่วนสำคัญต่อการยกระดับสิทธิ์และอาจเป็นไปได้ว่ามีช่องโหว่บางอย่างอยู่ในพาธนี้ ดังนั้นในส่วนต่อไปเราจะมาดูถึงความสำคัญของไฟล์ในพาธนี้กันครับ

พาธ /var/lib/sudo/ts/* รวมไปถึงพาธพี่น้องอย่าง /var/run/sudo/ts/* และ /run/sudo/ts/* เป็นพาธที่เกี่ยวข้องกับปลั๊กอินหนึ่งของ sudoers อ้างอิงจากหน้าคู่มือการใช้งานของ sudoers และ sudoers_timestamp พาธดังกล่าวนั้นมีหน้าที่ในการเก็บไฟล์สำหรับการบันทึกไฟล์ timestamp ที่ใช้ในการยืนยันตัวตนกับคำสั่ง sudo เพื่อจุดประสงค์ในการทำแคชหรือบันทึกเซสชันการพิสูจน์ตัวตนกับ sudo

การเกิดขึ้นของไฟล์ของปลั๊กอิน sudoers_timestamp

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

อ้างอิงจากรูปภาพด้านบน เมื่อผู้ใช้งานเรียกใช้คำสั่ง sudo vim และมีการพิสูจน์ตัวตนที่ถูกต้อง เซสชันนี้จะถูกบันทึกไว้ในไฟล์ที่พาธ /var/run/sudo/ts/<user> เมื่อผู้ใช้งานมีการเรียกใช้คำสั่งอีกครั้งภายในเวลาที่กำหนดแล้ว โปรแกรม sudo จะไม่มีการสอบถามรหัสผ่านอีก

สตรัคเจอร์ของ sudoers_timestamp

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

การเปลี่ยนแปลงค่าของสตรัคเจอร์ timestamp_entry ของ sudoers_timestamp สามารถวิเคราะห์ได้ตามพฤติกรรมของการเรียกใช้ sudo โดยไฟล์ /var/run/sudo/ts/<user> จะยังไม่ถูกสร้างจนกว่าจะมีการเรียกใช้คำสั่ง sudo ดังนั้นเราจะมีสังเกตพฤติกรรมที่เกิดขึ้นจากสองกรณี ได้แก่ กรณีที่กระบวนการยืนยันตัวตนกับ sudo นั้นถูกต้องและสมบูรณ์ กับกรณีที่การยืนยันตัวตนกับ sudo นั้นไม่ถูกต้องส่งผลให้ไม่สมบูรณ์

พฤติกรรมของ sudoers_timestamp

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

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

ts: The actual time stamp. A monotonic time source (which does not move backward) is used if the system supports it. Where possible, sudoers uses a monotonic timer that increments even while the system is suspended. The value of ts is updated each time a command is run via sudo. If the difference between ts and the current time is less than the value of the timestamp_timeout option, no password is required. - Sudoers Time Stamp Manual

แม้ทั้งสองกรณีจะมีค่าของ flags, sid และ ttydev ที่ต่างกัน แต่ค่าที่แท้จริงที่มีผลให้เกิดเงื่อนในการยกระดับสิทธิ์ได้คือค่า ts ซึ่งหากแก้ไขให้ความแตกต่างระหว่างค่าในฟิลด์ ts กับเวลาปัจจุบันนั้นน้อยกว่าค่า timestamp_timeout ของ sudo ผู้ใช้งานที่เรียกใช้คำสั่ง sudo ใน sid ที่ระบุไว้จะไม่ต้องระบุรหัสผ่าน ทำให้เกิดเงื่อนไขของการยกระดับสิทธิ์ได้ตามที่ activate_sudo_token พยายามเข้าไปแก้ไขค่าตรงส่วนนี้ของโปรเซส shell ถูกโปรเซสที่กำลังทำงานอยู่ในระบบ

Exploit พยายามเข้าไปแก้ไข sudoers_timestamp ของโปรเซส shell เพื่อให้สามารถใช้ sudo -i โดยไม่ต้องระบุรหัสผ่าน

ค่าของ sudo_timestamps ก่อนเริ่มรัน exploit โดยมี session ของ sudo ที่สำเร็จและไม่สำเร็จตามเงื่อนไขของการ exploit

ค่าภายใน sudoers_timestamp หลังจาก exploit สำเร็จ สังเกตได้ว่าโปรเซสที่มี PID (ค่า SID ในสตรัคเจอร์) เป็น 27651 หรือ bash ที่รัน exploit ถูกแก้ไข timestamp_entry ทำให้รัน sudo -i ได้โดยไม่ต้องใส่รหัสผ่าน

เงื่อนไขการโจมตีช่องโหว่

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

  1. บัญชีผู้ใช้งานที่เรามีสิทธิ์ในปัจจุบันจะต้องมีโปรเซสที่กำลังรันโดยใช้ sudo ที่พิสูจน์ตัวตนถูกต้องอยู่ก่อนแล้ว และต้องเป็นบัญชีผู้ใช้งานที่มีค่า UID เดียวกัน
  2. อ้างอิงจากแพตช์ด้านความปลอดภัยเกี่ยวกับ ptrace เนื่องจากการโจมตีนั้นอาศัยการ inject เข้าไปในโปรเซสอื่นๆ เราจึงจำเป็นต้องยกเลิกแพตช์ด้านความปลอดภัยของ ptrace ก่อนโดยการเพิ่มการตั้งค่าชื่อ ptrace_scope ที่พาธ /proc/sys/kernel/yama ให้มีค่าเป็น 0

การตรวจจับการโจมตีและการป้องกัน

  1. การจรวจจับการโจมตีสามารถทำได้โดยการสังเกตความผิดปกติในไฟล์ sudoers_timestamp โดยสามารถใช้ได้ทั้งโปรแกรม read_sudo_token_forensic หรือโปรแกรม unpacking.py ของไอ-ซีเคียว ทั้งนี้เนื่องจากข้อมูลดังกล่าวเก็บอยู่ในรูปแบบของ tmpfs ซึ่งเป็น volatile data ผู้วิเคราะห์ควรเพิ่มความระมัดระวังในการเก็บข้อมูลและตรวจสอบด้วย
  2. ตรวจสอบว่า kernel configuration นั้นไม่ได้มีการตั้งค่าให้ kernel.yama.ptrace_scope มีค่าเป็น 0 โดยจะต้องเป็น 1 เท่านั้น
  3. อัปเดตเวอร์ชันของ sudo ให้เป็นเวอร์ชันล่าสุดซึ่งมีการแก้ไขช่องโหว่นี้แล้ว