2 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • GhostLock (CVE-2026-43499) เป็นช่องโหว่เคอร์เนลที่ถูกนำเข้ามาใน Linux 2.6.39 และแก้ไขใน 7.1 โดยผู้โจมตีภายในเครื่องที่ไม่มีสิทธิ์พิเศษสามารถใช้เพียง system call สำหรับ threading ทั่วไปเพื่อก่อให้เกิด stack UAF และนำไปใช้เพื่อ ยกระดับสิทธิ์เป็น root และหลบหนีออกจากคอนเทนเนอร์ ได้
  • remove_waiter() ในเส้นทางพร็อกซี Requeue-PI ล้าง pi_blocked_on ของ current แทนที่จะเป็น task ที่กำลังรอจริง ทำให้ task ที่กลับสู่ user space แล้วยังคงมี พอยน์เตอร์ที่ชี้ไปยัง stack frame ที่ถูกปล่อยแล้ว อยู่
  • สร้างวงจรการพึ่งพา PI ด้วย futex สามตัวและเธรดสามตัวเพื่อทำให้เกิดการ rollback แบบ -EDEADLK และจัดวาง rt_mutex_waiter ปลอมใน stack buffer ที่ควบคุมได้ของ PR_SET_MM_MAP เพื่อให้ได้ การเขียนพอยน์เตอร์แบบมีข้อจำกัด
  • exploit ใช้ prefetch เพื่อหา address ฐานของ KASLR และ physmap วางโครงสร้างปลอมกับ ROP stack ใน CPU entry area (CEA) จากนั้นเขียนทับ inet6_protos[IPPROTO_UDP] เพื่อ ยึดการไหลของการควบคุมผ่านแพ็กเก็ต IPv6 UDP loopback
  • ทีมวิจัยได้รับเงิน $92,337 จาก Google kernelCTF ด้วย exploit สำหรับ ยกระดับสิทธิ์และหลบหนีคอนเทนเนอร์ที่เสถียร 97% และ Linux ทุกดิสทริบิวชันที่ยังไม่ได้แพตช์ควรอัปเกรดเป็น LTS ล่าสุด

ขอบเขตผลกระทบและภาพรวมของช่องโหว่

  • GhostLock เป็นช่องโหว่ในเคอร์เนล Linux ที่ VEGA ค้นพบ ผู้ใช้ภายในเครื่องที่ไม่มีสิทธิ์พิเศษสามารถทริกเกอร์ได้โดยไม่ต้องมีสิทธิ์เพิ่มเติมหรือ user namespace
  • ถูกนำเข้ามาจากการปรับปรุง rtmutex ใน 8161239a8bcc และมีขอบเขตผลกระทบตั้งแต่ v2.6.39-rc1 ถึง v7.1-rc1
  • ได้รับการแก้ไขในเดือนเมษายน 2026 ที่ 3bfdc63936dd และการตั้งค่าเคอร์เนลที่จำเป็นมีเพียง CONFIG_FUTEX_PI=y
  • ผู้โจมตีสามารถยกระดับสิทธิ์ได้ผ่านขั้นตอนต่อไปนี้
    • ใช้เพียง system call สำหรับ threading ทั่วไปเพื่อให้ได้ dangling kernel pointer ที่ชี้ไปยังหน่วยความจำ kernel stack
    • สร้าง primitive แบบมีข้อจำกัดที่สามารถเขียนพอยน์เตอร์หรือค่า 0 ขนาด 8 ไบต์ไปยัง address ที่เกือบจะกำหนดเองได้
    • ยึด function table เพื่อควบคุม control flow และได้สิทธิ์ root
  • Linux ทุกดิสทริบิวชันที่ยังไม่ได้แพตช์ได้รับผลกระทบ จึงควรอัปเกรดเป็นเวอร์ชัน LTS ล่าสุด

สาเหตุที่ remove_waiter() เคลียร์ task ผิดตัว

  • remove_waiter() ใน kernel/locking/rtmutex.c เดิมถูกเขียนขึ้นสำหรับเส้นทางที่เธรดที่ถูกบล็อกจัดการสถานะการรอของตัวเองโดยตรง
  • ใน slow path ปกติ current ที่กำลังรันอยู่คือ task เจ้าของ waiter ดังนั้นการล้าง current->pi_blocked_on จึงเป็นพฤติกรรมที่ถูกต้อง
  • ใน เส้นทางพร็อกซี Requeue-PI rt_mutex_start_proxy_lock() จะใส่ rt_mutex_waiter ลงคิวแทน task อื่นที่กำลังหลับอยู่ และเมื่อเกิดข้อผิดพลาดก็จะ rollback สิ่งนี้
    • ในขณะนั้น current คือ requeuer ที่เรียก FUTEX_CMP_REQUEUE_PI
    • waiter จริงคือ task อีกตัวที่หลับอยู่ใน FUTEX_WAIT_REQUEUE_PI
  • เมื่อ __rt_mutex_start_proxy_lock() คืนค่า -EDEADLK remove_waiter() จะลบ waiter ออกจาก lock แต่กลับทำให้เฉพาะ current->pi_blocked_on เป็น NULL
  • pi_blocked_on ของ waiter จริงยังคงชี้ไปยัง rt_mutex_waiter ที่อยู่บน kernel stack ของตัวเอง และเมื่อ waiter กลับสู่ user space แล้ว stack frame ดังกล่าวจะถือว่าถูกปล่อยแล้ว
  • หลังจากนั้น ในจังหวะที่การค้นหา PI chain ผ่าน task ดังกล่าว จะเกิดการ dereference อ็อบเจ็กต์บน stack ที่ถูกปล่อยแล้ว
  • lockdep ตรวจสอบเพียงว่ามี pi_lock ใดถูกจับอยู่หรือไม่ แต่ไม่ตรวจว่าเป็น lock ของใคร จึงจับข้อผิดพลาดนี้ไม่ได้

วงจร futex สามตัวที่ทำให้เกิด rollback แบบ -EDEADLK

  • เพื่อไปถึงเส้นทางข้อผิดพลาด จะต้องสร้างวงจรการพึ่งพา PI ด้วย futex สามตัวและเธรดสามตัว
    • f_pi_chain: PI futex ที่ waiter ล็อกก่อน
    • f_pi_target: PI futex ที่ owner ล็อกก่อนและกลายเป็นเป้าหมายของ requeue
    • f_wait: futex ปกติที่ waiter รอด้วย FUTEX_WAIT_REQUEUE_PI
  • ลำดับการทริกเกอร์มีดังนี้
    1. waiter ล็อก f_pi_chain แล้วถูกบล็อกที่ FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) และ rt_mutex_waiter ถูกวางไว้บน kernel stack ของตัวเอง
    2. owner ล็อก f_pi_target แล้วถูกบล็อกที่ f_pi_chain ซึ่ง waiter ถืออยู่
    3. เธรด main เรียก FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
  • เมื่อ proxy requeue พยายามเชื่อม waiter เข้ากับ f_pi_target วงจร waiter → f_pi_target → owner → f_pi_chain → waiter จะปิดครบ
  • การค้นหา PI chain คืนค่า -EDEADLK และดำเนินการ rollback ที่ผิดพลาด ทำให้ waiter ตื่นขึ้นมาพร้อม dangling pi_blocked_on
  • เงื่อนไขสำคัญคือ requeuer ต้อง rollback ในขณะที่ waiter ยังถืออ็อบเจ็กต์บน stack อยู่ และหลังจากวงจรถูกสร้างครบแล้ว กระบวนการจะดำเนินต่อเอง
  • เมื่อ waiter กลับสู่ user space แล้ว จะไม่มีแรงกดดันด้านเวลาอีกต่อไป และสามารถใช้ sched_setattr() เพื่อทำให้เกิดการค้นหา chain ได้ทุกเมื่อในภายหลัง
  • แม้การจัดวางจะใช้สามเธรด แต่การแข่งขัน UAF เองสามารถ ทริกเกอร์ได้แม้บน CPU เพียงคอร์เดียว

primitive เริ่มต้นที่ stack UAF ให้มา

  • dangling pointer ชี้ไปยัง rt_mutex_waiter ที่เคยอยู่ในเฟรม FUTEX_WAIT_REQUEUE_PI
  • หากวางไบต์ที่ควบคุมได้กลับลงไปที่ระดับความลึกของ stack เดิมใน task เดียวกัน จะทำให้เคอร์เนล dereference มันเป็น rt_mutex_waiter ปลอมได้
  • ขึ้นอยู่กับวิธีวางโครงสร้างปลอม การเข้าถึงหนึ่งครั้งจะให้ primitive หลักสองแบบ
    • สามารถ เขียนพอยน์เตอร์ ไปยัง address ที่เกือบจะกำหนดเองได้ภายใต้เงื่อนไขจำกัด
    • สามารถ เขียนค่า 0 ขนาด 8 ไบต์ ไปยัง address ที่เกือบจะกำหนดเองได้ภายใต้เงื่อนไขจำกัด
  • ก่อนการเขียนจะมีการ dereference พอยน์เตอร์หลายครั้งและการตรวจสอบความสมบูรณ์ แต่หากผ่านเงื่อนไข เคอร์เนลจะไม่ crash หลังการเขียนและกลับทำงานตามปกติ
  • การทำ exploit ให้สมบูรณ์จำเป็นต้องทำให้เกิดการนำ stack frame กลับมาใช้ซ้ำ ผ่านการตรวจโครงสร้างของ waiter ปลอม และเลือกเป้าหมายที่สอดคล้องกับข้อจำกัดของการเขียนทั้งหมด

การนำสแต็กเฟรมที่ถูกปล่อยแล้วกลับมาใช้ซ้ำด้วย PR_SET_MM_MAP

  • waiter จะเรียก prctl(PR_SET_MM, PR_SET_MM_MAP, ...) ทันทีที่กลับจาก system call ของ futex
  • prctl_set_mm_map() จะคัดลอก auxv ที่ผู้ใช้จัดเตรียมไว้ไปยังบัฟเฟอร์สแต็กขนาดคงที่ unsigned long user_auxv[AT_VECTOR_SIZE]
  • เนื่องจากบัฟเฟอร์นี้อยู่ที่ความลึกของสแต็กใกล้เคียงกับ waiter ที่ถูกปล่อยแล้ว บล็อก qword ขนาดใหญ่ที่จัดแนวแล้วและควบคุมได้จึงทับซ้อนอยู่เหนือ rt_mutex_waiter เดิม
  • พื้นที่ทับซ้อนของ auxv ถูกจัดวางดังนี้
    • tree: ทำให้เป็น rb node ที่เมื่อถูกลบแล้วจะดัน pointer ของลูกที่เลือกไว้ W0_BASE ขึ้นเป็น root ของ tree
    • task: ตั้งเป็น &init_task เพื่อให้การ dereference ระหว่างการไล่ chain ผ่านไปได้อย่างปลอดภัย
    • lock: ระบุเป็น &inet6_protos[IPPROTO_UDP] - 8 เพื่อเล็งเป้าหมายการเขียน
    • wake_state: ตั้งเป็น 0
  • วาง auxv ไว้ใน memfd และจัดตำแหน่งให้การคัดลอกข้ามขอบเขต page จากนั้นให้ sibling thread แข่งเรียก fallocate(PUNCH_HOLE) กับ page ด้านหลังระหว่างที่ prctl กำลังทำงาน เพื่อยืดเวลา copy_from_user
  • consumer thread บน CPU อื่นจะเรียก sched_setattr() กับ waiter ระหว่างที่ waiter ปลอมยังคงอยู่บนสแต็ก เพื่อไล่ PI chain
  • system call อื่นที่ใช้ตัวแปร local บนสแต็กขนาดใหญ่และควบคุมได้ เช่น clone, setsockopt, pselect, keyctl ก็สามารถทำหน้าที่เดียวกันได้
  • เลือกใช้ prctl เพราะบัฟเฟอร์มีขนาดใหญ่ จัดแนวแล้ว และไม่ต้องใช้ namespace ส่วน candidate เพิ่มเติมรวมอยู่ใน โค้ด PoC สาธารณะ

สร้างการเขียน pointer แบบมีข้อจำกัดด้วยการลบ rb-tree

  • แม้จะควบคุม waiter ปลอมได้ ก็ไม่ได้ทำให้เกิด arbitrary write เต็มรูปแบบในทันที โดยการไล่ chain จะรันตามเส้นทางต่อไปนี้
    • พบ waiter ปลอมจาก task->pi_blocked_on
    • พบ rt_mutex_base ปลอมจาก fake waiter->lock
    • rt_mutex_dequeue(lock, waiter) ทำการลบ rb-tree จาก lock->waiters
  • ใช้ประโยชน์จากคุณสมบัติที่เมื่อลบ node root ที่มีลูกเพียงตัวเดียว ลูกตัวนั้นจะถูกเขียนลงในสล็อต root
  • หากระบุ lock เป็น target - 8 ข้อมูลรอบ ๆ จะถูกตีความเป็นฟิลด์ของ rt_mutex_base ดังนี้
    • target - 8: wait_lock ที่ต้องถูกอ่านว่าอยู่ในสถานะไม่ล็อก
    • target: waiters.rb_root.rb_node ที่จะถูกเขียนทับ
    • target + 8: waiters.rb_leftmost
    • target + 16: owner
  • ผลลัพธ์คือการเขียนครั้งเดียวที่ถูกดำเนินการคือ *(uint64_t *)target = W0_BASE
  • ที่อยู่เป้าหมายต้องเป็นไปตามเงื่อนไขโดยประมาณดังนี้
    • 32 บิตล่างของ target - 0x08 ต้องเป็น 0
    • ค่า 64 บิตของ target + 0x08 ต้องเป็น 0
    • ค่าของ owner pointer ที่ target + 0x10 หลังตัด flag บิตล่างออกแล้วต้องเป็น 0
  • หาก qword ด้านหน้าดูเหมือน spinlock ที่ถูกล็อกอยู่ trylock จะล้มเหลวและจบการทำงานโดยไม่เขียนอะไร
  • หากค่าด้านหลังชี้ไปยัง top waiter หรือ owner ที่ควบคุมไม่ได้ หรือเป็นค่าที่ไม่ได้ถูก map อาจทำให้ kernel panic ได้
  • W0_BASE ต้องยังคงใช้งานได้จนกว่าการ compare, requeue, การอัปเดต priority และ wakeup แบบไม่มี owner จะเสร็จสิ้น ดังนั้นจึงใช้ direct-map alias ของ CEA

การรั่วไหลด้วย prefetch และ CPU entry area

  • ค้นหาที่อยู่ฐานของ KASLR และ physmap

    • เวลาในการรัน prefetch กับที่อยู่หนึ่ง ๆ จะแตกต่างกันตามว่าที่อยู่นั้นถูก map อยู่ใน page table ปัจจุบันหรือไม่
    • เมื่อ process ที่ไม่มีสิทธิ์พิเศษวัดเวลาในการรันในช่วงที่อยู่ของ kernel ก็สามารถประมาณตำแหน่งที่ถูก map ได้ โดยหลักการละเอียดสรุปไว้ใน บทความ prefetch
    • entropy ของที่อยู่ฐานของ image ของ Linux kernel พื้นฐานมีประมาณ 9 บิต จึงสามารถกู้คืนที่อยู่ฐาน KASLR ได้ด้วยความน่าเชื่อถือเกือบ 100% ผ่านการวัดซ้ำ
    • ตามทฤษฎี CPU ที่มี prefetch และไม่มี KPTI ที่เหมาะสมจะได้รับผลกระทบ แต่ในทางปฏิบัติเป็นเทคนิคที่ใช้เป็นหลักบน x86 ที่ปิด KPTI
    • image ของ kernelCTF ปิด KPTI ไว้ และแม้เปิด KPTI อยู่ หากผสาน prefetch กับ EntryBleed ก็สามารถกู้คืนที่อยู่ฐานของ image kernel ผ่าน trampoline ได้
  • การเลี่ยงการสุ่มที่อยู่ CEA

    • CPU entry area(CEA) คือโครงสร้างต่อ CPU ของ x86 ที่เก็บสแต็กสำหรับการเข้า kernel/การจัดการ exception และบริบท register
    • เมื่อโปรแกรมที่ไม่มีสิทธิ์พิเศษก่อให้เกิด software exception บริบท register ของตัวเองจะถูกบันทึกลงใน pt_regs ของสแต็ก exception ใน CEA ทำให้เกิดหน่วยความจำต่อเนื่องที่ควบคุมได้ประมาณ 120 ไบต์
    • ก่อน Linux 6.2 ที่อยู่ virtual ของ CEA ถูกตรึงไว้ทั้งหมด จึงสามารถใช้ได้โดยตรงกับโครงสร้างปลอม การดูดซับผลข้างเคียงจากการ dereference pointer และการจัดวาง ROP stack
    • หลังจาก Project Zero เผยแพร่ Bringing back the stack attack ตั้งแต่ Linux 6.2 เป็นต้นมา ที่อยู่ virtual ของ CEA ถูกสุ่มอย่างเข้มข้น
    • ที่อยู่ virtual ของ CEA ของแต่ละ CPU ถูกสุ่มต่างกัน แต่ที่อยู่ physical คงที่ ดังนั้นหากรู้ที่อยู่ฐานของ physmap ก็สามารถคำนวณ direct-map alias ได้
    • ผสาน prefetch, การ normalize ขอบเขตของ candidate และการตรวจสอบ page CEA ที่คาดไว้ เพื่อตัด alias รอบข้างออกและหา cea_direct = physmap_base + CPU1_CEA_BASE
    • ในสภาพแวดล้อมบูต 3.5GB ของ kernelCTF LTS 6.12.80 offset ที่เกี่ยวข้องคือ 0x11c517000(+0x1f58)

นำ CEA กลับมาใช้เป็น waiter ปลอมและ object ต่อเนื่อง

  • ก่อนการเขียนครั้งแรก จะวาง waiter และ lock ปลอมที่มีความสอดคล้องในตัวเองไว้ใน W0 ของ CEA
    • ตั้ง task เป็น &init_task
    • ใส่ค่าที่ถูกต้องใน prio
    • ทำให้ wait_lock ของ lock ดูเหมือนอยู่ในสถานะไม่ล็อก
    • จัดวาง owner ให้ผ่าน dequeue, requeue, การอัปเดต priority และ wakeup ได้อย่างปลอดภัย
  • เมื่อการเขียน rb-tree เสร็จสิ้น W0 ไม่จำเป็นต้องเป็น waiter อีกต่อไป จึงสามารถเติม CEA ใหม่เป็นโครงสร้างที่เป้าหมายที่ถูกเขียนทับต้องการได้
  • CEA มีขนาดเล็กประมาณ 120 ไบต์ แต่มีประสิทธิภาพเพราะสามารถวางข้อมูลไว้ที่ที่อยู่ kernel แบบคงที่ที่คำนวณได้
  • NPerm และ kernelsnitch เป็นต้น ก็สามารถทำหน้าที่เดียวกันได้ในพื้นที่ที่กว้างกว่า
  • exploit ใช้พื้นที่ CEA เดียวกันเป็น rt_mutex_waiter ปลอม, lock ปลอม, inet6_protocol, สล็อต JOP/stack pivot และ ROP stack สุดท้าย ทั้งแบบต่อเนื่องหรือพร้อมกัน

การยึดการไหลควบคุมด้วย inet6_protos[IPPROTO_UDP]

  • บน Linux x86_64 ทั่วไป หลังจากได้ที่อยู่ฐานสำหรับ KASLR แล้ว สามารถเลือกเส้นทางสั้น ๆ ที่เขียนทับตารางฟังก์ชันที่ตรงเงื่อนไข หรืออ็อบเจ็กต์ที่มีตารางนั้นอยู่ได้
  • บริเวณรอบ inet6_protos[IPPROTO_UDP] ในพื้นที่ข้อมูลที่เขียนได้ตอบสนองข้อจำกัดที่ต้องการได้อย่างเป็นธรรมชาติ
    • inet6_protos[16] == NULL กลายเป็นสถานะที่ยังไม่ถูกล็อกของ wait_lock ปลอม
    • inet6_protos[17] == &udpv6_protocol คือเป้าหมายจริงที่จะถูกเขียนทับ
    • inet6_protos[18] == NULL กลายเป็น rb_leftmost ปลอม
    • inet6_protos[19] == NULL กลายเป็น owner ปลอม
  • เมื่อเขียนเสร็จ inet6_protos[IPPROTO_UDP] จะชี้ไปยัง inet6_protocol ปลอมภายในหน้า CEA
  • พ่น CEA อีกครั้งแล้วจัดโครงสร้างดังนี้
    • handler: กำหนดเป็น pivot gadget ตัวแรก
    • err_handler: ไม่ได้ใช้
    • flags: ตั้งเป็น INET6_PROTO_NOPOLICY | INET6_PROTO_FINAL
  • เมื่อส่ง แพ็กเก็ต IPv6 UDP loopback ที่ connect ไปยัง ::1 แล้วเขียนข้อมูล เคอร์เนลจะเรียก handler ปลอม ทำให้ควบคุม program counter ได้

Pivot สั้นและการยกระดับสิทธิ์ด้วย DirtyMode

  • สำหรับเป้าหมาย lts-6.12.80 ของ Google kernelCTF ไม่พบ single stack pivot gadget ที่เหมาะสม จึงใช้ load/call เพิ่มเติมเพื่อใส่ที่อยู่ CEA ลงใน rbp แล้ว pivot ด้วย mov rsp, rbp; pop rbp; ret
  • การเขียนทับทั้ง ret2usr หรือ /proc/%P/fd/x ต้องใช้ gadget qword ประมาณ 10 ตัว ซึ่งใหญ่เกินไปเมื่อเทียบกับพื้นที่ CEA ที่จำกัด
  • ในขั้นสุดท้าย ใช้ DirtyMode ซึ่งเปลี่ยนบิตสิทธิ์ด้วยการเขียนเพียงครั้งเดียว แล้วทำขั้นตอนที่เหลือใน user space
  • เป้าหมายการเขียนคือ coredump_sysctls[1].mode ในข้อมูลเคอร์เนล หรือก็คือโหมดการเข้าถึงของ sysctl core_pattern
  • เนื่องจากแชร์ KASLR slide เดียวกับ kernel image จึงคำนวณที่อยู่ได้ และค่าที่ตั้งบิตเขียน ซึ่งเป็นบิตลำดับต่ำสุดตัวที่สอง ก็เพียงพอแล้ว
  • ใช้ chain สั้น ๆ แบบ pop reg; mov [reg], reg; ret เพื่อเปลี่ยนค่า mode แล้วใช้ msleep หยุดเธรดที่ยึดมาไว้อย่างปลอดภัย
  • เมื่อ /proc/sys/kernel/core_pattern กลายเป็นไฟล์ที่ผู้ใช้ทุกคนเขียนได้ โปรเซสที่ไม่มีสิทธิ์พิเศษสามารถเขียน |/proc/%P/fd/666 %P แล้วทำให้ helper crash เพื่อให้เคอร์เนลรันไบนารีของผู้โจมตีด้วยสิทธิ์ root
  • การเขียน rb-tree ครั้งแรกไม่สามารถไปถึง coredump_sysctls[1].mode ได้โดยตรงเนื่องจากข้อจำกัดด้านการจัดวาง ดังนั้นการเปลี่ยน mode จึงทำในขั้น ROP สั้น ๆ

ลำดับการ exploit ทั้งหมดและผลลัพธ์

  • การโจมตีดำเนินตามลำดับต่อไปนี้
    1. ใช้ prefetch รั่ว kernel image slide และที่อยู่ฐาน physmap
    2. ใช้ GhostLock ทิ้ง rt_mutex_waiter ที่เป็น dangling ไว้ใน pi_blocked_on ของ waiter
    3. ใช้ PR_SET_MM_MAP เพื่อใช้เฟรม kernel stack เดิมซ้ำและสร้าง waiter ปลอม
    4. ใช้การลบ rtmutex rb-tree เพื่อบันทึกพอยน์เตอร์ CEA ลงใน inet6_protos[IPPROTO_UDP]
    5. วาง inet6_protocol ปลอม, pivot slot และ ROP stack ใน CEA
    6. เรียก handler ที่ถูกเขียนทับด้วยแพ็กเก็ต IPv6 UDP loopback
    7. ใช้ DirtyMode เปลี่ยนบิต mode ของ core_pattern แล้วจบการยกระดับสิทธิ์ใน user space
  • ในสภาพแวดล้อมระยะไกลของ kernelCTF เส้นทางที่ผสาน CEA กับ DirtyMode สามารถได้แฟล็กในเวลาประมาณ 5 วินาที
  • exploit ทั้งหมดเผยแพร่ใน โปรเจกต์ CyberMeowfia
  • บน Android วิธีใช้เฟรม stack ซ้ำและการเลี่ยง ASLR/CFI จะแตกต่างออกไป และจะกล่าวถึงในบทความภาคต่อแยกต่างหาก

เส้นทางทางเลือกและมาตรการบรรเทา

  • พื้นที่ ROP ที่ใหญ่ขึ้น

    • หน่วยความจำที่อิง NPerm สามารถใช้เป็น stack ปลอมขนาดใหญ่ได้หลังจากยึดการไหลควบคุมแล้ว
    • เส้นทางที่หนักกว่านี้ก็เป็นไปได้ เช่น heap-KASLR leak ของ Lukas Maar แต่จะเพิ่มขั้นตอนและทำให้เวลารันนานขึ้น
    • ใน kernelCTF chain ที่สั้นและเชื่อถือได้มากที่สุดได้เปรียบ จึงใช้การผสาน CEA กับ DirtyMode
  • แพตช์เคอร์เนล

    • แพตช์สุดท้ายจับ pi_lock และล้าง pi_blocked_on โดยอิง waiter->task แทน current
    • remove_waiter() บันทึก waiter_task = waiter->task แล้วประมวลผลตามลำดับต่อไปนี้
      1. ล็อก waiter_task->pi_lock
      2. นำ waiter ออกจากคิว rtmutex
      3. ตั้ง waiter_task->pi_blocked_on = NULL
      4. ส่ง waiter_task แทน current ให้กับ rt_mutex_adjust_prio_chain() ที่ตามมาด้วย
    • แพตช์อีกชุดที่นักวิจัยส่งก่อน v1 ถูกออกแบบให้ caller ส่ง task เจ้าของมาอย่างชัดเจน
      • ในเส้นทางที่ตัวเองถูกบล็อก ให้ส่ง current
      • ใน proxy rollback ให้ส่ง task เป้าหมายของ proxy
      • ล้างเฉพาะเมื่อ pi_blocked_on ยังชี้ไปยัง waiter นั้นอยู่ และป้องกันด้วย pi_lock ของ task
  • RANDOMIZE_KSTACK_OFFSET

    • exploit พึ่งพาการซ้อนทับกันอย่างกำหนดได้ระหว่างเฟรม waiter ที่ถูกปล่อยแล้วกับเฟรม user_auxv ที่ตามมา
    • เมื่อเปิด RANDOMIZE_KSTACK_OFFSET ค่า stack offset จะเปลี่ยนไป ทำให้ขั้นตอนนี้กลายเป็นการเดา 5 บิตที่มีโอกาสประมาณ 1/32
    • ในเป้าหมายทั่วไปสองรายการที่ส่งไป การตั้งค่านี้ปิดเป็นค่าเริ่มต้น ส่วนในเป้าหมายแบบบรรเทาความเสี่ยงเปิดไว้ จึงไม่ได้ใช้เส้นทาง exploit นี้
  • STATIC_USERMODE_HELPER

    • STATIC_USERMODE_HELPER บล็อกเส้นทาง DirtyMode เฉพาะนี้
    • อย่างไรก็ตาม แนวทางเดียวกันสามารถทำให้ทั่วไปขึ้นได้กับการตั้งค่า /proc/sys อื่นที่ควบคุมสิทธิ์เข้าถึงด้วย ctl_table::mode และตารางอยู่ในข้อมูลเคอร์เนลที่เขียนได้และคาดเดาได้

กำหนดการเปิดเผยข้อมูล

  • 18 เมษายน 2026: ส่งช่องโหว่และแพตช์ฉบับร่างไปยัง security@kernel.org
  • 20 เมษายน 2026: ช่องโหว่ได้รับการแก้ไขด้วยแพตช์อื่น
  • 4 พฤษภาคม 2026: แบ็กพอร์ตแพตช์แก้ไข v1
  • 30 มิถุนายน 2026: Google ยืนยันการส่งเข้า kernelCTF
  • 7 กรกฎาคม 2026: เผยแพร่บทวิเคราะห์เชิงเทคนิค
  • ช่องโหว่ที่ VEGA ค้นพบใช้แนวทาง นโยบายเปิดเผยข้อมูล 90+30 วัน มาตรฐาน

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความเห็นจาก Hacker News
  • มีการทดสอบบนอุปกรณ์ 3 เครื่องที่ใช้ Android 9·13·16 และ Firefox เวอร์ชันต่ำกว่า 150 คนละเวอร์ชัน โดย 2 เครื่องติดลูปบูตจนต้องเข้าโหมดกู้คืน ส่วนอีก 1 เครื่องดับไปเลย เดโมจะเปลี่ยนวอลเปเปอร์ของอุปกรณ์ Pixel ที่รองรับ และสามารถดูหน้าทดสอบได้ที่ IonStack
    เวลาจะเปิดบล็อกหรือเว็บสุ่มบนอุปกรณ์ส่วนตัว ควรติดตั้งเบราว์เซอร์ตระกูล Chromium อย่าง Chromite แยกจากเบราว์เซอร์หลัก แล้วปิด JavaScript กับตัวถอดรหัสวิดีโอแบบเร่งฮาร์ดแวร์ที่มักถูกโจมตีบ่อยใน flags จากนั้นใช้โหมดอ่านกับเว็บที่แสดงผลพัง ๆ จะปลอดภัยกว่า หรือจะใช้งานผ่านแท็บเล็ตเฉพาะทางอีกเครื่องก็ได้

    • ตอนนี้ ทดสอบเฉพาะบน Pixel 10 เท่านั้น แต่มี PR อยู่หลายตัวที่พยายามเพิ่มการรองรับอุปกรณ์อื่น สามารถดูได้ที่ https://github.com/NebuSec/CyberMeowfia
    • เมื่อลองพอร์ต kernel exploit ไปยังอุปกรณ์อื่น ก็พบว่ามันไวต่อ วิธีที่คอมไพเลอร์จัดวาง stack frame ในแต่ละ kernel build มาก พอหา stamp pattern และ offset ที่ตรงกับ build นั้นได้แล้ว มันก็ทำงานได้ค่อนข้างเสถียร
    • มีการยอมเสี่ยงรันบน Samsung S26 Ultra และจะเปิดเผยผลทั้งหมดหลังติดตั้ง adb เพื่อตรวจสอบ
      เมื่อเข้า หน้าทดสอบ ก็มีเอาต์พุตแสดงในแท็บ Firefox ดูเหมือนโค้ด proof of concept จะทำงานแล้ว แต่หลังจากนั้นโทรศัพท์ค้างและไม่รับอินพุตใด ๆ เลย มีเพียงการรีสตาร์ตที่ยังใช้ได้ จึงสงสัยว่าในสภาพที่ดูเหมือน kernel ค้างนั้น เหตุใดจึงยังตอบสนองต่อเหตุการณ์รีสตาร์ตได้ หน้าจอยังค้างเปิดอยู่พร้อมแสดงผลลัพธ์บางส่วน ก่อนที่สกรีนเซฟเวอร์จะเริ่มทำงาน
    • ถ้าสามารถนำไป รูท อุปกรณ์ Android ที่ยังรูทไม่ได้ในตอนนี้ได้ก็คงน่าทึ่งมาก จึงอยากรู้ว่ามีวิธีที่เป็นไปได้หรือไม่
    • ช่องโหว่ Firefox ดูเหมือนจะเป็น type confusion ในคอมไพเลอร์ JIT ของ IonMonkey คือ CVE-2026-10702: https://www.sentinelone.com/vulnerability-database/cve-2026-10702/
  • ขอชื่นชมอย่างมากต่อบรรดานักวิจัยความปลอดภัยที่ไม่ปล่อย สคริปต์ยกระดับสิทธิ์แบบ local privilege escalation แบบ zero-day ที่ใครก็หยิบไปใช้ได้ทันที ต่างจาก copyfail
    มีการลองทำ local privilege escalation (LPE) บน Rocky Linux 9 อยู่หลายชั่วโมงแต่โชคดีที่ไม่สำเร็จ ถ้าไม่มีเวลามากหรือไม่ได้เก่งมากจริง ๆ ก็ดูเหมือนจะเอาไปใช้โจมตีจริงบนดิสโทรฝั่งองค์กรได้ยาก

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

  • น่าจะใส่คำว่า LPE ซึ่งหมายถึง local privilege escalation ไว้ในชื่อเรื่อง คนส่วนใหญ่จะได้สบายใจแล้วกลับไปใช้วันหยุดสุดสัปดาห์ต่อ

    • แต่จริง ๆ ก็ไม่ใช่เรื่องที่ควรสบายใจขนาดนั้น ปกติ exploit แบบ local privilege มักหมายถึงการยกระดับจากสิทธิ์ผู้ใช้ทั่วไปไปเป็น root และแอประดับสิทธิ์ปกติก็สร้างความเสียหายได้มากอยู่แล้ว คนเลยมักไม่ตื่นตระหนกมาก
      แต่การโจมตีครั้งนี้สามารถทำงานได้แม้จะอยู่ใน โปรเซสที่ถูก sandbox อย่างเข้มงวด เช่นโปรเซสเบราว์เซอร์ที่แยกตัวของ Firefox ผู้โจมตีจึงแค่ต้องเชื่อมการโจมตีสองขั้นเข้าด้วยกัน คือใช้ช่องโหว่ JavaScript เพื่อรันโค้ดภายใน sandbox ที่ถูกแยกไว้ แล้วใช้ช่องโหว่นี้ไต่ขึ้นไปถึง kernel mode ดังนั้นต้องอัปเดตทั้ง Firefox และ Linux kernel
    • การโจมตีในคอมเมนต์บนสุดดูเหมือนจะได้สิทธิ์ root จาก JavaScript โดยตรง แต่จริง ๆ แล้วเป็นการเชื่อม exploit คนละ 2 ตัว เข้าด้วยกัน
    • ถ้า escape ออกจาก container ได้ ก็ยังน่าจะกระทบคนจำนวนมากอยู่ดีไม่ใช่หรือ
    • เนื่องจากมีการค้นพบช่องโหว่ type confusion ของ Firefox/IonMonkey ไปพร้อมกันด้วย แค่เข้าเว็บไซต์สุ่มก็อาจทำให้ อุปกรณ์ถูกยึดครอง ได้อย่างรวดเร็ว
    • ทุกวันนี้ดูเหมือนน่าจะมี zero-day สะสมไว้ใช้ในสถานการณ์แบบนี้เป็นร้อย ๆ ตัวแล้ว ตั้งแต่ SSH ถึง Node.js มีปัญหาใหม่ออกมาทุกไม่กี่สัปดาห์ ถ้าไม่ได้เอาการสื่อสารทั้งหมดไปไว้หลัง WireGuard ก็แทบต้องมองว่าทุกอย่างเป็นช่องโหว่แบบ remote ไปหมดแล้ว
  • สะดุดหูตั้งแต่ประโยคที่ว่า “Google จ่าย 92,337 ดอลลาร์ เป็นรางวัล kernelCTF”

    • เมื่อดูจากขอบเขตผลกระทบแล้ว มันดูเป็นเงินที่น้อยไปหน่อย จึงสงสัยว่าบริษัทต่าง ๆ จ่ายหนักเฉพาะช่องโหว่ remote exploit เท่านั้นหรือไม่
  • สงสัยว่านี่หมายความว่าแอป Android สามารถรัน native code ผ่าน NDK แล้วเอาไปขอ สิทธิ์ root ได้เลยหรือไม่ และ SELinux จะช่วยป้องกันได้หรือเปล่า

    • โทรศัพท์ที่ไม่ใช่รุ่นเรือธงมักไม่ค่อยได้รับอัปเดต โดยเฉพาะส่วน kernel จึงดูมีโอกาสเป็นไปได้จริงสูง
      แม้จะสามารถ backport แพตช์ไปยัง kernel รุ่นเก่าได้ แต่ในบันทึกการอัปเดตของสมาร์ตโฟนก็มักไม่ค่อยระบุ CVE ชัดเจน ทำให้เครื่องมือตรวจสอบช่องโหว่แทบเป็นหนทางเดียวในการยืนยัน ถ้าแอปจาก Play Store หรือจากภายนอกถูกเจาะ ก็สามารถได้สิทธิ์ root ทันที ดังนั้นหลักการเรื่องความเชื่อถือและการตรวจสอบก่อนติดตั้งยังคงสำคัญ
      ในอนาคตอาจมีการเพิ่มการตรวจนี้เข้าไปในทุกระดับของ Google Play Integrity จนแอปจำนวนมากติดตั้งไม่ได้บนโทรศัพท์ที่ยังไม่แพตช์ ส่วนฝั่งเบราว์เซอร์ซึ่งหลีกเลี่ยงเว็บสุ่มและโฆษณาได้ยากนั้นยิ่งร้ายแรงกว่า เพราะการหนีออกจาก sandbox สามารถข้ามการแยกแอปได้ด้วย คล้ายกับ JailbreakMe บน iOS
    • ถ้า kernel เองถูกเจาะแล้ว SELinux ก็ป้องกันไม่ได้ เทคโนโลยีคอนเทนเนอร์อย่าง Android sandbox หรือ Docker ก็หยุด exploit นี้ไม่ได้เช่นกัน วิธีแยกที่พอใช้งานได้จริงมีเพียง full virtualization เท่านั้น และถ้าใช้ KVM ก็ต้องตั้งอยู่บนสมมติฐานว่าแพตช์ของ CVE-2026-53359 ที่เปิดเผยเมื่อสัปดาห์ก่อน ถูกกระจายครบทุกที่แล้ว
      ตลอด 15 ปีที่ผ่านมา บน Linux ใดก็ตาม แอปที่สามารถรัน native code ได้ก็จะมีทางยกระดับเป็นสิทธิ์ root ได้จนกว่าจะได้รับ kernel update
  • ข้อเท็จจริงที่ว่า GhostLock ถูกนำเข้าใน Linux 2.6.39 และเพิ่งถูกแก้ใน Linux 7.1 นั้นชวนช็อกมาก

  • รู้สึกเหมือนเคยอ่านคอมเมนต์พวกนี้ตั้งแต่เมื่อวานแล้ว แต่เวลาเขียนกลับแสดงว่าไม่เกิน 10 ชั่วโมง จึงสงสัยว่าระบบแสดงเวลาของ HN ผิดพลาดหรือไม่

    • เป็นไปได้มากว่าเกิดจากระบบ re-up ของ HN เมื่อนำโพสต์นี้กลับมาขึ้นใหม่ timestamp ของคอมเมนต์เดิมจึงถูกคำนวณใหม่เป็นเวลาแบบสัมพัทธ์ สามารถดูข้อมูลเกี่ยวกับเรื่องนี้ได้ที่ https://hn.algolia.com/?dateRange=all&page=0&prefix=true&query=by%3Adang%20timestamps%20re-up&sort=byDate&type=comment
      โพสต์นี้อยู่บนสุดของรายการ “underwater” ที่ตรวจดูทุกวัน ซึ่งก็คือรายการโพสต์ที่ได้คะแนนโหวตสูงแต่ด้วยเหตุผลบางอย่างไม่ได้ขึ้นหน้าแรก จึงถูกนำมาแสดงอีกครั้ง มันดูแปลกก็จริง แต่จนถึงตอนนี้ก็ยังไม่มีทางเลือกอื่นที่ทำให้สับสนน้อยกว่านี้
    • บางครั้งก็มีการรวม คอมเมนต์เข้าด้วยกัน ตอนที่รวมข่าวที่คล้ายกันเป็นเรื่องเดียว