- 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()คืนค่า-EDEADLKremove_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 ล็อกก่อนและกลายเป็นเป้าหมายของ requeuef_wait: futex ปกติที่ waiter รอด้วยFUTEX_WAIT_REQUEUE_PI
- ลำดับการทริกเกอร์มีดังนี้
- waiter ล็อก
f_pi_chainแล้วถูกบล็อกที่FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target)และrt_mutex_waiterถูกวางไว้บน kernel stack ของตัวเอง - owner ล็อก
f_pi_targetแล้วถูกบล็อกที่f_pi_chainซึ่ง waiter ถืออยู่ - เธรด main เรียก
FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target)
- waiter ล็อก
- เมื่อ proxy requeue พยายามเชื่อม waiter เข้ากับ
f_pi_targetวงจรwaiter → f_pi_target → owner → f_pi_chain → waiterจะปิดครบ - การค้นหา PI chain คืนค่า
-EDEADLKและดำเนินการ rollback ที่ผิดพลาด ทำให้ waiter ตื่นขึ้นมาพร้อม danglingpi_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 ของ treetask: ตั้งเป็น&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
- พบ waiter ปลอมจาก
- ใช้ประโยชน์จากคุณสมบัติที่เมื่อลบ node root ที่มีลูกเพียงตัวเดียว ลูกตัวนั้นจะถูกเขียนลงในสล็อต root
- หากระบุ
lockเป็นtarget - 8ข้อมูลรอบ ๆ จะถูกตีความเป็นฟิลด์ของrt_mutex_baseดังนี้target - 8:wait_lockที่ต้องถูกอ่านว่าอยู่ในสถานะไม่ล็อกtarget:waiters.rb_root.rb_nodeที่จะถูกเขียนทับtarget + 8:waiters.rb_leftmosttarget + 16:owner
- ผลลัพธ์คือการเขียนครั้งเดียวที่ถูกดำเนินการคือ
*(uint64_t *)target = W0_BASE - ที่อยู่เป้าหมายต้องเป็นไปตามเงื่อนไขโดยประมาณดังนี้
- 32 บิตล่างของ
target - 0x08ต้องเป็น0 - ค่า 64 บิตของ
target + 0x08ต้องเป็น0 - ค่าของ owner pointer ที่
target + 0x10หลังตัด flag บิตล่างออกแล้วต้องเป็น0
- 32 บิตล่างของ
- หาก 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.80offset ที่เกี่ยวข้องคือ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ในข้อมูลเคอร์เนล หรือก็คือโหมดการเข้าถึงของ sysctlcore_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 ทั้งหมดและผลลัพธ์
- การโจมตีดำเนินตามลำดับต่อไปนี้
- ใช้
prefetchรั่ว kernel image slide และที่อยู่ฐาน physmap - ใช้ GhostLock ทิ้ง
rt_mutex_waiterที่เป็น dangling ไว้ในpi_blocked_onของ waiter - ใช้
PR_SET_MM_MAPเพื่อใช้เฟรม kernel stack เดิมซ้ำและสร้าง waiter ปลอม - ใช้การลบ rtmutex rb-tree เพื่อบันทึกพอยน์เตอร์ CEA ลงใน
inet6_protos[IPPROTO_UDP] - วาง
inet6_protocolปลอม, pivot slot และ ROP stack ใน CEA - เรียก handler ที่ถูกเขียนทับด้วยแพ็กเก็ต IPv6 UDP loopback
- ใช้ 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แล้วประมวลผลตามลำดับต่อไปนี้- ล็อก
waiter_task->pi_lock - นำ waiter ออกจากคิว rtmutex
- ตั้ง
waiter_task->pi_blocked_on = NULL - ส่ง
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 นี้
- exploit พึ่งพาการซ้อนทับกันอย่างกำหนดได้ระหว่างเฟรม waiter ที่ถูกปล่อยแล้วกับเฟรม
-
STATIC_USERMODE_HELPERSTATIC_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 ความคิดเห็น
ความเห็นจาก Hacker News
มีการทดสอบบนอุปกรณ์ 3 เครื่องที่ใช้ Android 9·13·16 และ Firefox เวอร์ชันต่ำกว่า 150 คนละเวอร์ชัน โดย 2 เครื่องติดลูปบูตจนต้องเข้าโหมดกู้คืน ส่วนอีก 1 เครื่องดับไปเลย เดโมจะเปลี่ยนวอลเปเปอร์ของอุปกรณ์ Pixel ที่รองรับ และสามารถดูหน้าทดสอบได้ที่ IonStack
เวลาจะเปิดบล็อกหรือเว็บสุ่มบนอุปกรณ์ส่วนตัว ควรติดตั้งเบราว์เซอร์ตระกูล Chromium อย่าง Chromite แยกจากเบราว์เซอร์หลัก แล้วปิด JavaScript กับตัวถอดรหัสวิดีโอแบบเร่งฮาร์ดแวร์ที่มักถูกโจมตีบ่อยใน flags จากนั้นใช้โหมดอ่านกับเว็บที่แสดงผลพัง ๆ จะปลอดภัยกว่า หรือจะใช้งานผ่านแท็บเล็ตเฉพาะทางอีกเครื่องก็ได้
adbเพื่อตรวจสอบเมื่อเข้า หน้าทดสอบ ก็มีเอาต์พุตแสดงในแท็บ Firefox ดูเหมือนโค้ด proof of concept จะทำงานแล้ว แต่หลังจากนั้นโทรศัพท์ค้างและไม่รับอินพุตใด ๆ เลย มีเพียงการรีสตาร์ตที่ยังใช้ได้ จึงสงสัยว่าในสภาพที่ดูเหมือน kernel ค้างนั้น เหตุใดจึงยังตอบสนองต่อเหตุการณ์รีสตาร์ตได้ หน้าจอยังค้างเปิดอยู่พร้อมแสดงผลลัพธ์บางส่วน ก่อนที่สกรีนเซฟเวอร์จะเริ่มทำงาน
ขอชื่นชมอย่างมากต่อบรรดานักวิจัยความปลอดภัยที่ไม่ปล่อย สคริปต์ยกระดับสิทธิ์แบบ local privilege escalation แบบ zero-day ที่ใครก็หยิบไปใช้ได้ทันที ต่างจาก copyfail
มีการลองทำ local privilege escalation (LPE) บน Rocky Linux 9 อยู่หลายชั่วโมงแต่โชคดีที่ไม่สำเร็จ ถ้าไม่มีเวลามากหรือไม่ได้เก่งมากจริง ๆ ก็ดูเหมือนจะเอาไปใช้โจมตีจริงบนดิสโทรฝั่งองค์กรได้ยาก
สงสัยว่าช่องโหว่นี้จะทำให้ ปลดล็อก bootloader ได้หรือไม่ แม้ในโทรศัพท์ที่ปกติปลดไม่ได้ ถ้าทำได้จริงก็น่าจะเป็นหนึ่งในเหตุการณ์ที่ยิ่งใหญ่ที่สุดของระบบนิเวศ Android
น่าจะใส่คำว่า LPE ซึ่งหมายถึง local privilege escalation ไว้ในชื่อเรื่อง คนส่วนใหญ่จะได้สบายใจแล้วกลับไปใช้วันหยุดสุดสัปดาห์ต่อ
แต่การโจมตีครั้งนี้สามารถทำงานได้แม้จะอยู่ใน โปรเซสที่ถูก sandbox อย่างเข้มงวด เช่นโปรเซสเบราว์เซอร์ที่แยกตัวของ Firefox ผู้โจมตีจึงแค่ต้องเชื่อมการโจมตีสองขั้นเข้าด้วยกัน คือใช้ช่องโหว่ JavaScript เพื่อรันโค้ดภายใน sandbox ที่ถูกแยกไว้ แล้วใช้ช่องโหว่นี้ไต่ขึ้นไปถึง kernel mode ดังนั้นต้องอัปเดตทั้ง Firefox และ Linux kernel
สะดุดหูตั้งแต่ประโยคที่ว่า “Google จ่าย 92,337 ดอลลาร์ เป็นรางวัล kernelCTF”
สงสัยว่านี่หมายความว่าแอป Android สามารถรัน native code ผ่าน NDK แล้วเอาไปขอ สิทธิ์ root ได้เลยหรือไม่ และ SELinux จะช่วยป้องกันได้หรือเปล่า
แม้จะสามารถ backport แพตช์ไปยัง kernel รุ่นเก่าได้ แต่ในบันทึกการอัปเดตของสมาร์ตโฟนก็มักไม่ค่อยระบุ CVE ชัดเจน ทำให้เครื่องมือตรวจสอบช่องโหว่แทบเป็นหนทางเดียวในการยืนยัน ถ้าแอปจาก Play Store หรือจากภายนอกถูกเจาะ ก็สามารถได้สิทธิ์ root ทันที ดังนั้นหลักการเรื่องความเชื่อถือและการตรวจสอบก่อนติดตั้งยังคงสำคัญ
ในอนาคตอาจมีการเพิ่มการตรวจนี้เข้าไปในทุกระดับของ Google Play Integrity จนแอปจำนวนมากติดตั้งไม่ได้บนโทรศัพท์ที่ยังไม่แพตช์ ส่วนฝั่งเบราว์เซอร์ซึ่งหลีกเลี่ยงเว็บสุ่มและโฆษณาได้ยากนั้นยิ่งร้ายแรงกว่า เพราะการหนีออกจาก sandbox สามารถข้ามการแยกแอปได้ด้วย คล้ายกับ JailbreakMe บน iOS
ตลอด 15 ปีที่ผ่านมา บน Linux ใดก็ตาม แอปที่สามารถรัน native code ได้ก็จะมีทางยกระดับเป็นสิทธิ์ root ได้จนกว่าจะได้รับ kernel update
ข้อเท็จจริงที่ว่า GhostLock ถูกนำเข้าใน Linux 2.6.39 และเพิ่งถูกแก้ใน Linux 7.1 นั้นชวนช็อกมาก
รู้สึกเหมือนเคยอ่านคอมเมนต์พวกนี้ตั้งแต่เมื่อวานแล้ว แต่เวลาเขียนกลับแสดงว่าไม่เกิน 10 ชั่วโมง จึงสงสัยว่าระบบแสดงเวลาของ HN ผิดพลาดหรือไม่
โพสต์นี้อยู่บนสุดของรายการ “underwater” ที่ตรวจดูทุกวัน ซึ่งก็คือรายการโพสต์ที่ได้คะแนนโหวตสูงแต่ด้วยเหตุผลบางอย่างไม่ได้ขึ้นหน้าแรก จึงถูกนำมาแสดงอีกครั้ง มันดูแปลกก็จริง แต่จนถึงตอนนี้ก็ยังไม่มีทางเลือกอื่นที่ทำให้สับสนน้อยกว่านี้