Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
Pixel_GPU_Exploit — استغلال نواة أندرويد 14 لأجهزة Pixel7/8 Pro | Kitploit
أدوات/GitHubGitHub/0x36/pixel_gpu_exploit
أمان أندرويدتصعيد الامتيازاتتحليل الذاكرة الجنائيتحليل الثغرات الأمنيةالاستغلالالتعلم والتعليماستغلال الملفات الثنائية
GitHub0x36/pixel_gpu_exploit

Pixel_GPU_Exploit

استغلال نواة أندرويد 14 لأجهزة Pixel7/8 Pro

عرض المستودع
55788منذ 2 سنواتتمت المراجعة من قبل Kitploit

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

LPE في نواة معالج Mali GPU

تقدم هذه المقالة تحليلاً متعمقاً لثغرتين في نواة معالج Mali GPU، يمكن الوصول إليهما من بيئة التطبيقات الافتراضية المعزولة (sandbox)، وقد اكتشفتُهما بشكل مستقل وأبلغت جوجل بهما. وتتضمن استغلالاً للنواة (kernel exploit) يحقق قدرات قراءة/كتابة عشوائية في النواة. ونتيجةً لذلك، فإنه يعطّل SELinux ويرفع الامتيازات إلى صلاحيات الجذر (root) على أجهزة Google Pixel 7 و8 Pro التي تعمل بالإصدارات التالية من Android 14:

  • Pixel 8 Pro: google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys
  • Pixel 7: google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (بواسطة m4b4 (Marcel))

الثغرات

يعتمد هذا الاستغلال على ثغرتين: تجاوز عدد صحيح (integer overflow) ناتج عن تصحيح غير مكتمل في أمر ioctl الخاص بـ gpu_pixel_handle_buffer_liveness_update_ioctl، وتسريب معلومات داخل مخازن رسائل تدفق الخط الزمني (timeline stream).

تجاوز سفلي للمخزن المؤقت في gpu_pixel_handle_buffer_liveness_update_ioctl() بسبب تصحيح غير صحيح لتجاوز العدد الصحيح

عالجت جوجل تجاوز العدد الصحيح في أمر ioctl الخاص بـ gpu_pixel_handle_buffer_liveness_update_ioctl في هذا الالتزام. في البداية، عندما أبلغت عن هذه المشكلة، ظننت أن الخطأ ناتج عن مشكلة في التصحيح الموصوف سابقاً. وبعد مراجعة التقرير، أدركت أن تحليلي للثغرة كان غير دقيق. وعلى الرغم من افتراضي الأول بأن التصحيح غير مكتمل، فإنه يحل فعلياً ويمنع حدوث تجاوز سفلي في العملية الحسابية. قادني هذا إلى الاشتباه في أن التغيير لم يُطبَّق في إصدارات الإنتاج. ومع ذلك، على الرغم من أنني أستطيع التسبب في تجاوز سفلي في العملية الحسابية، فلا يمكنني التسبب في تجاوز علوي. يشير هذا إلى أن أمر ioctl قد تم إصلاحه جزئياً، وإن لم يكن بالتصحيح المعروض أعلاه. كشف الفحص باستخدام IDA أنه تم شحن تصحيح غير مكتمل آخر في إصدارات الإنتاج، وهذا التصحيح غير موجود في أي فرع git من وحدة نواة معالج mali gpu.

تم اكتشاف هذه الثغرة لأول مرة في أحدث إصدار من Android وأُبلغ عنها في 19 نوفمبر 2023. وأخبرتني جوجل لاحقاً أنها كانت قد حددتها داخلياً بالفعل، وأنها خصصت لها CVE-2023-48409 في نشرة أمان Android لشهر ديسمبر، وصنفتها كمشكلة مكررة.

على الرغم من أنني تمكنت من التحقق من أن الخطأ قد تم تحديده داخلياً قبل أشهر من تقريري (استناداً إلى تاريخ الالتزام حوالي 30 أغسطس)، إلا أنه لا تزال هناك بعض الالتباسات. وتحديداً، من الغريب أن مستويات تصحيح الأمان (SPL) لشهرَي أكتوبر ونوفمبر على أحدث الأجهزة كانت لا تزال متأثرة بهذه الثغرة — ولم أتحقق من الإصدارات السابقة لهذه. لذلك، لا أستطيع الجزم بشكل قاطع ما إذا كانت هذه مشكلة مكررة فعلاً، وما إذا كان التصحيح المناسب قد جُدول بالفعل لشهر ديسمبر قبل تقديمي، أم كان هناك سهو في معالجة هذه الثغرة.

على أي حال، ما يجعل هذه الثغرة قوية هو ما يلي:

  • المخزن المؤقت info.live_ranges تحت سيطرة المستخدم بالكامل.
  • القيم المتجاوزة هي مدخلات يتحكم بها المستخدم، وبالتالي يمكننا تجاوز العملية الحسابية بحيث يكون مؤشر info.live_ranges عند إزاحة عشوائية قبل بداية عنوان النواة للمخزن buff.
  • حجم التخصيص هو أيضاً مدخل يتحكم به المستخدم، مما يتيح إمكانية طلب تخصيص ذاكرة من أي مُخصِّص شرائح (slab allocator) للأغراض العامة.

تتقاسم هذه الثغرة أوجه تشابه مع ثغرة التجاوز السفلي للمخزن المؤقت DeCxt::RasterizeScaleBiasData() التي اكتشفتُها واستغللتها في نواة iOS 15 في عام 2022.

تسريب مؤشرات النواة في مخازن رسائل تدفق الخط الزمني

يُنفذ معالج Mali GPU تدفق خط زمني (timeline stream) مخصصاً مصمماً لجمع المعلومات وتسلسلها ثم كتابتها في مخزن حلقي (ring buffer) وفق تنسيق محدد. يمكن للمستخدمين استدعاء أمر ioctl kbase_api_tlstream_acquire للحصول على واصف ملف (file descriptor)، مما يمكنهم من القراءة من هذا المخزن الحلقي. تنسيق الرسائل هو كما يلي:

  • ترويسة الحزمة
  • معرّف الرسالة
  • مخزن رسائل متسلسل، حيث يتوقف المحتوى المحدد على معرّف الرسالة.

على سبيل المثال، تقوم الدالة __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait بتسلسل مؤشرات النواة kbase_kcpu_command_queue وdma_fence في مخزن الرسائل، مما يؤدي إلى تسريب مؤشرات النواة إلى عمليات مساحة المستخدم.```c void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait( struct kbase_tlstream *stream, const void *kcpu_queue, const void *fence ) { const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT; const size_t msg_size = sizeof(msg_id) + sizeof(u64) + sizeof(kcpu_queue) + sizeof(fence) ; char *buffer; unsigned long acq_flags; size_t pos = 0;

root@kitploit:~
buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);

pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id));
pos = kbasep_serialize_timestamp(buffer, pos);
pos = kbasep_serialize_bytes(buffer,
	pos, &kcpu_queue, sizeof(kcpu_queue));
pos = kbasep_serialize_bytes(buffer,
	pos, &fence, sizeof(fence));

kbase_tlstream_msgbuf_release(stream, acq_flags);

}

root@kitploit:~
يكشف استغلال إثبات المفهوم عن عنوان كائن `kbase_kcpu_command_queue` من خلال مراقبة معرف الرسالة `KBASE_TL_KBASE_NEW_KCPUQUEUE` الذي يتم إرساله بواسطة الدالة `kbasep_kcpu_queue_new` كلما تم تخصيص كائن طابور kcpu جديد.

أبلغني Google أن الثغرة تم الإبلاغ عنها في مارس 2023 وتم تعيينها  [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) في نشرة الأمان الخاصة به. ومع ذلك، تمكنت من تكرار المشكلة على أحدث أجهزة Pixel التي تم تزويدها بمستويات تصحيح الأمان (SPL) لشهر أكتوبر ونوفمبر، مما يشير إلى أن الإصلاح لم يتم تطبيقه بشكل صحيح أو لم يتم تطبيقه على الإطلاق. بعد ذلك، عالجت Google المشكلة بسرعة في نشرة تحديث الأمان لشهر ديسمبر دون منح الفضل، وأخبرتني لاحقًا أن المشكلة اعتُبرت مكررة. لكن المبرر وراء تصنيف هذه المشكلة على أنها مكررة لا يزال موضع تساؤل.

## الاستغلال
---
إذن لدي ثغرتان مثيرتان للاهتمام. الأولى توفر قدرة قوية على تعديل محتوى أي عنوان نواة محاذٍ لـ 16 بايت يقع قبل عنوان ~buff~ المخصص. أما الثغرة الثانية فتوفر تلميحات حول المواقع المحتملة للكائنات داخل ذاكرة النواة.

### ملاحظات حول قيم buffer_count و live_ranges_count
بفضل السيطرة الكاملة على الحقلين `buffer_count` و `live_ranges_count`، يمكنني اختيار الشريحة (slab) المستهدفة والإزاحة الدقيقة التي أنوي الكتابة إليها. ومع ذلك، فإن اختيار قيم `buffer_count` و `live_ranges_count` يتطلب دراسة متأنية بسبب عدة قيود وعوامل:
- ترتبط القيمتان ببعضهما، ولن يحدث الفائض إلا إذا تم تجاوز جميع الفحوصات المضافة حديثًا.
- إن شرط أن تكون الإزاحة السالبة محاذية لـ 16 بايت يحد من القدرة على الكتابة في أي موقع مختار. ومع ذلك، فهذا ليس عائقًا كبيرًا بشكل عام.
- اختيار إزاحة أكبر يؤدي إلى كتابة كمية كبيرة من البيانات في مناطق ذاكرة قد لا تكون أهدافًا مقصودة. على سبيل المثال، إذا تجاوز حجم التخصيص إلى `0x3004`، فسيتم تعيين مؤشر `live_ranges` على `-0x4000` بايت من مساحة الذاكرة المخصصة لكائن `buff`. ستقوم الدالة `copy_from_user` عندها بكتابة `0x7004` بايت، بناءً على حساب قيمة `update->live_ranges_count` مضروبة في 4. ونتيجة لذلك، ستؤدي هذه العملية إلى قيام بيانات يتحكم بها المستخدم بالكتابة فوق منطقة الذاكرة الواقعة بين مؤشر `live_ranges` وتخصيص `buff`. لذلك، من الضروري التأكد بعناية من عدم الكتابة فوق أي كائنات نظام حساسة داخل هذا النطاق عن طريق الخطأ. ونظرًا لأن العملية تتضمن استدعاء `copy_from_user`، قد يفكر المرء في إحداث خطأ `EFAULT` عن طريق إلغاء تعيين منطقة الذاكرة غير المرغوب فيها بعد مخزن المستخدم المصدر بشكل متعمد، لمنع كتابة البيانات إلى مواقع حساسة. ومع ذلك، فإن هذا الأسلوب غير فعّال، وذلك لأنه إذا فشلت الدالة `raw_copy_from_user`، فسوف تقوم بتصفير البايتات المتبقية في المخزن المؤقت للنواة الوجهة. تم تطبيق هذا السلوك لضمان أنه في حالة النسخ الجزئي بسبب خطأ، لا يحتوي باقي مخزن النواة على بيانات غير مهيأة.```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (!should_fail_usercopy() && likely(access_ok(from, n))) {
		instrument_copy_from_user(to, from, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}

بالنظر إلى ذلك، نحتاج إلى اختيار الكائن الذي سنستبدل محتواه والبيانات التي سنكتبها بعناية.

اختيار الكائن المناسب للكتابة فوقه

نظرًا لأنني عالق مع هذا الفحص غير المريح، تتمثل استراتيجيتي في تحديد كائن لن يُنتج، إذا تم تصفيره، أي نتيجة غير مرغوبة. لكن، قبل أن أصل إلى ذلك، هناك مسألة أخرى يجب التعامل معها. تذكّر عندما قلت في الجزء السابق إنه يمكنني اختيار أي حجم تخصيص وبالتالي أي مُخصِّص من مُخصِّصات الـ slab cache ذات الأغراض العامة لخدمة المخزن المؤقت المخصص؟ هذا غير صحيح، لأن السبب يعود إلى copy_from_user مرة أخرى! يعود السبب إلى التخفيف المعروف بـ CONFIG_HARDENED_USERCOPY. إنه يمنع تحديد حجم لا يطابق حجم الـ slab cache المقابل الذي يخص المخزن المؤقت الوجهة في النواة (والموافق في هذه الحالة لكائن من الكائنات المخصصة في الـ heap). يتحقق مما إذا كانت صفحة المخزن المؤقت صفحة slab، وإذا كان الأمر كذلك، فإنه يسترجع قيمة kmem_cache->size المطابقة ويحدد ما إذا كان الحجم الذي يوفره المستخدم لن يتجاوزها؛ وإلا فإن النواة تتعطل ببساطة بسبب عدم تطابق الأحجام. إذن، بعبارة أخرى، لا يمكنني استهداف الكائنات التي تنتمي إلى المُخصِّص العام، لكن لا يزال بإمكاني استهداف الكائنات ذات الأحجام الكبيرة (أي تلك التي يخدمها مُخصِّص الصفحات (page allocator) مباشرة).

أول فكرة خطرت ببالي كانت استخدام تقنية pipe_buffer، وهي تقنية أنيقة جدًا للحصول على بدائيات قراءة/كتابة عشوائية. لن أخوض في تفاصيل التقنية، لكنني أشجع القراء على قراءة هذه المدونة الرائعة من Interrupt Labs. عند إنشاء كائن pipe، يُنشأ كائن pipe_buffer مبدئيًا في مصفوفة من 16 عنصرًا؛ ومع ذلك، يمكن ضبط حجم المصفوفة باستخدام fcntl(F_SETPIPE_SZ). لذلك، يمكن ضبط تخصيص مصفوفة pipe_buffer بحيث يُخدم من مُخصِّص الصفحات، مما يجعله كائنًا مثاليًا للاستهداف. بعد اختيار كائن pipe_buffer كمرشح للاستهداف، الخطوة التالية نحو تحقيق القراءة/الكتابة في النواة هي الكتابة فوق محتواه باستخدام ثغرة التدفق السفلي (underflow)، مما سيسمح لي بالقراءة/الكتابة من/إلى أي موقع ذاكرة من خلال استبدال الحقل pipe_buffer->page بصفحة ذلك الموقع. لأن الثغرة تسمح لي بكتابة بيانات عشوائية، يمكنني التحكم في المحتوى الكامل لـ'pipe_buffer' بما في ذلك حقل الصفحة الخاص به، وللقيام بذلك، أحتاج إلى تخصيص مصفوفة pipe_buffer قبل كائن kbuff الضعيف، ويجب أن يكونا بجانب بعضهما البعض.

وضع كائنات pipe_buffer وbuff بشكل متجاور

لقد قمت برشّ ذاكرة النواة بعدد كبير من كائنات kbase_kcpu_command_queue ثم تبعتها بمجموعة من مصفوفات pipe_buffer. لا يمكنني ببساطة استخدام مصفوفات pipe_buffer وحدها كمصدر أساسي للرش بسبب القيد الذي تفرضه pipe_max_size. لذلك، قررت أن أبدأ الرش باستخدام كائن kbase_kcpu_command_queue. كان اختيار كائن kbase_kcpu_command_queue لسببين: حجم تخصيصه هو 0x38C8 وبالتالي يتعامل معه مُخصِّص الصفحات، ويمكنني الحصول بشكل حتمي على عنوانه في النواة باستخدام ثغرة تسريب معلومات النواة، مما يجعله كائنًا جيدًا للرش به وكذلك كائنًا جيدًا للاستهداف (كما سنرى في القسم التالي).

كما ذكرت سابقًا، استخدمت fcntl(F_SETPIPE_SZ) لزيادة حجم تخصيص مصفوفة pipe_buffer بحيث يمكن أن يخدمها مُخصِّص الصفحات. لكي أكون أكثر تحديدًا، اخترت أن يكون حجم التخصيص ==0x4000 بايت (4 * PAGE_SIZE)== لكي يتوافق مع تخصيصات kbase_kcpu_command_queue.

الحصول على عنوان struct page

من أجل استخدام pipe_buffer بشكل صحيح، يلزم عنوان صفحة. القدرة على تحديد عنوان النواة لكائن kbase_kcpu_command_queue يمكنني إنشاؤه وتدميره عمدًا تجعله مرشحًا جيدًا للاستخدام، ويمكن الحصول على struct page المطابق له باستخدام virt_to_page.

المحتويات المطلوب كتابتها في pipe_buffer

إذن، كائن pipe_buffer يكون كما يلي:```c struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };

root@kitploit:~
كما ذُكر سابقًا، يجب أن يحتوي حقل `page` على عنوان صفحة صالح. يجب ألا يتجاوز حقلا `offset` و`len` قيمة `PAGE_SIZE`، وإلا ستقوم الأنبوب بزيادة عدّادي head/tail، مما يؤدي إلى استخدام كائن `pipe_buffer` جديد وفقدان السيطرة على مخزن الأنبوب المزيّف (fake pipe buffer).  
أيضًا، يجب أن تكون `flags` مساوية لـ `PIPE_BUF_FLAG_CAN_MERGE` بحيث أنه في استدعاءات `pipe_write` التالية، بدلًا من زيادة عدّاد head بشكل أعمى واستخدام مخزن الأنبوب التالي، يتم أولًا التحقق مما إذا كانت هناك مساحة في `pipe_buffer` الحالي تتسع لطلب الكتابة أم لا، وإذا وُجدت، فسيتم ببساطة إلحاق البيانات بنفس مخزن الأنبوب بدءًا من القيمة المخزنة في حقل `len`.  
لتجنب تعطل الجهاز عند `pipe_buf_confirm`، والذي يتم استدعاؤه من قبل `pipe_write` و`pipe_read`’، يجب أن يكون مؤشر `ops` أيضًا عنوانًا صالحًا في النواة مع تعيين حقل `ops->confirm` إلى _NULL_. يمكنني ببساطة استخدام إزاحة داخل الكائن المُسرَّب `kbase_kcpu_command_queue` تكون قيمتها NULL ولا تتغير تحت أي ظرف.

### اختيار قيمة الإزاحة المثلى للـ underflow
بينما تكون أحجام التخصيص لكل من `buff` و`kbase_kcpu_command_queue` و`pipe_buffer` حوالي ~0x4000~ بايت، اخترت إجراء underflow للمخزن بمقدار **0x8000** بايت. لماذا؟

لنلقِ نظرة سريعة على كيفية تحديث `pipe_buffers` أثناء عمليات القراءة والكتابة. افترض أننا نستطيع تشكيل `pipe_buffer` بالشكل التالي:```c
struct pipe_buffer {
	.page = virt_to_page(addr),
	.offset =  0,
	.len = 0x40,
	.ops = kcpu_addr + 0x50,
	.flags = PIPE_BUF_FLAG_CAN_MERGE,
	unsigned long private = 0
};

بينما تمنح الثغرة القدرة على التحكم التعسفي في محتوى هذا الكائن، فإنها لا تفعل ذلك إلا مرة واحدة فقط لأن الكائن الذي حدث فيه الـ underflow يتم تحريره مباشرة بعد انتهاء استدعاء ioctl. يشكّل هذا في الواقع مشكلة لأنني أحتاج إلى تحديث كائن pipe_buffer يدويًا لإعادة استخدامه، نظرًا لأن كل عملية قراءة/كتابة على الأنبوب:

  • لا يتم تحديث حقل .page؛ يبقى كما هو، وعندما يصبح المخزن المؤقت فارغًا، يتم تحريره، وهو أمر لا أريده أن يحدث لأن حقل .ops غير مضبوط بشكل صحيح.
  • نظرًا لأن pipe_buffer يحدّث حقل .offset عند إجراء القراءة، لذلك لا يمكنني قراءة نفس المنطقة من الذاكرة مرة أخرى.
  • البيانات المكتوبة إلى pipe_buffer ستُلحق بالمخزن المؤقت ابتداءً من قيمة .len (بافتراض أن علم PIPE_BUF_FLAG_CAN_MERGE مضبوط)، ويتم تحديث .len وفقًا لذلك. أي أننا لا نستطيع كتابة البيانات إلى العنوان نفسه مرتين.

نتيجة لذلك، ما لم أُحدّث pipe_buffer بشكل صحيح بعد كل عملية قراءة أو كتابة، لا يمكنني القراءة والكتابة من/إلى نفس الأنبوب في الوقت نفسه. لهذا السبب فإن إحداث underflow بـ 0x8000 بايت هو أكثر عملية بكثير، لأنه بدلاً من استبدال pipe_buffer واحد، سأستبدل مثيلين مختلفين من pipe_buffer لكائني أنبوبين مختلفين: أحدهما سيُستخدم للقراءة والآخر لعمليات الكتابة.```c #define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */

pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;

pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;

root@kitploit:~
إن `pipe_read` عبارة عن مخزن أنبوب وهمي سيُستخدم لقراءة البيانات من الصفحة الهدف بدءًا من `.offset = 0` وحتى `0xfff` بايت، بينما `pipe_write` هو `pipe_buffer` وهمي سيُستخدم لكتابة البيانات بدءًا من `.len = 0` وحتى `0xfff` بايت.
من المهم أيضًا أن نذكر مرة أخرى أن كتابة أكثر من `PAGE_SIZE` بايت ستدفع الأنبوب إلى زيادة عدّاد الرأس (head counter)، وبالتالي استخدام `pipe_buffer` مخصّص حديثًا وفقدان السيطرة على `pipe_write` الوهمي. من ناحية أخرى، فإن إفراغ مخزن `fake_read` (قراءة بيانات بحجم `0xfff` منه) يخبر النواة بتحرير الصفحة الفعلية عبر استدعاء `ops→release`، مما يؤدي إلى انهيار النواة لأنني لا أملك بعد عنوان نص النواة.
رغم أنني نجحت في عزل عمليات القراءة والكتابة على الأنبوب بحيث لا يتعارض تنفيذ كتابة في أحد طرفي الأنبوب مع مخزن الأنبوب الآخر والعكس صحيح، إلا أنني لم أحل المشكلة الأساسية بعد: كيف يمكن تحديث مخزن الأنبوب بشكل موثوق؟ الجواب الواضح الذي تبادر إلى ذهني كان ببساطة تكرار عملية الرش (spray) مرارًا وتكرارًا بعد كل استدعاء قراءة أو كتابة على الأنبوب.  وهذا غير منطقي لأنه سيكون له تأثير كبير على موثوقية الاستغلال. في القسم التالي، سأقسم الهدف إلى هدفين فرعيين: أولاً، سأركز على حقل `.page` فقط، ثم على الحقلين `.len/.offset` بعد ذلك.

### تعديل حقل pipe_buffer→page

لدهشتي، لا يلزمني ولا أحتاج إلى تحديث `.page` إطلاقًا، وذلك لأنني أستطيع الكتابة فوق `pipe_buffer→page` ليشير إلى عنوان صفحة `kbase_kcpu_command_queue` المسرَّبة. لذلك، **كل ما عليّ فعله هو تحرير كائن `kbase_kcpu_command_queue` وتراكبه مع كائن `pipe_buffer` جديد. أجل! الآن لديّ `pipe_buffer→page` يشير إلى كائن `pipe_buffer` شرعي!
استبدال `kbase_kcpu_command_queue` بـ `pipe_buffer` يمنحنا القدرة على التعامل مع مخزن أنبوب شرعي دون الحاجة إلى تحديث حقل `.page` بانتظام. ومع ذلك، لا يزال يتعين عليّ التعامل مع الحقلين `.len` و`.offset`.

### تعديل حقلي pipe_buffer→len/offset

كما ذكرت سابقًا، فإن إجراء عمليات القراءة/الكتابة على الأنبوب يحدّث الحقلين `.len` و`.offset`، مما يجعل عمليات القراءة/الكتابة اللاحقة على نفس الصفحة غير قابلة للاستخدام حتى لو تمت عبر الأنبوبين المنفصلين. إليك حيلة أخرى: **هناك تقنية لقراءة/كتابة البيانات دون حتى لمس الحقلين `.len/.offset`!**.  ومن الممكن تحقيق ذلك عن طريق إحداث خطأ (fault) في استدعاءات `copy_page_from_iter` و`copy_page_to_iter` الخاصة بـ `pipe_read/write`! نعم، تمامًا مثل `copy_to/from_user`، تقوم `copy_page_to/from_iter` بنسخ البيانات من/إلى مساحة المستخدم التي يتم تمريرها عبر بنية `iov_iter`، ويمكن إحداث خطأ فيها.

لمواصلة المثال السابق، إذا أردنا كتابة 8 بايتات من البيانات إلى عنوان معيّن، يجب أن يكون حجم المخزن الموفَّر في مساحة المستخدم مساويًا لـ 8، متبوعًا بمنطقة ذاكرة غير معيّنة أو غير قابلة للقراءة، ثم نمرر `9` كوسيط حجم إلى استدعاء النظام `write` للإشارة إلى كمية البيانات التي نريد كتابتها.ستكتب هذه العملية 8 بايتات وتفشل عند البايت _التاسع_ لأنها تصادف موقع ذاكرة غير معيّن/غير قابل للقراءة. ونتيجة لذلك، تكون البيانات قد كُتبت فعليًا إلى المخزن الوجهة في النواة ولم يتم تعديل حقل `.len`. ستعود دالة النواة `pipe_write` فحسب دون تحديث حقل `buf->len`.```c
		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			if (ret)
				goto out;

			ret = copy_page_from_iter(buf->page, offset, chars, from);
			if (unlikely(ret < chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf->len += ret;
			if (!iov_iter_count(from))
				goto out;
		}

الأمر نفسه ينطبق على عمليات القراءة؛ إذا أردنا قراءة 8 بايتات، اجعل البايت التاسع من المخزن المؤقت غير قابل للقراءة ثم ادّعِ فقط أننا نريد قراءة 9 بايتات، سيتم نسخ البيانات إلى مخزن المستخدم المؤقت دون تغيير حقل .offset. نتيجةً لذلك، يمكننا إجراء عمليات قراءة/كتابة غير محدودة على أي عنوان ذاكرة في النواة دون الحاجة إلى المرور المتكرر بعملية الرش (spray).

الحصول على صلاحيات الجذر

الآن وبعد أن أصبح لديّ إمكانية قراءة/كتابة عشوائية قوية، قمت بالبحث في جميع struct page داخل مصفوفة VMEMMAP_START لتحديد عنوان بداية نص النواة باستخدام التقنية الموضحة في منشور مدونة Interrupt Labs. ثم أدركت أن init_task يتم تصفيره إلى صفر في تحديثات أندرويد الأمنية لشهر نوفمبر, لذا استخدمت kthreadd_task بدلاً منه. امتلاك عنوان نواة kthreadd_task سمح لي باجتياز قائمة task->tasks والحصول على عنوان نواة المهمة الحالية الخاصة بي current، ثم تصفير بنية cred لتحقيق صلاحيات الجذر.

لاحقًا، أدركت أن فحص جميع عناوين الصفحات كان غير ضروري لأنني كنت أملك بالفعل عنوان نص النواة anon_pipe_buf_ops من كائن pipe_buffer. بهذه المعلومات، تمكنت من استنتاج عنوان قاعدة نص النواة، متجاوزًا بذلك KASLR بشكل فعّال.

تعطيل SELinux

يُعطّل الاستغلال أيضًا SELinux، وبوجود عنوان قاعدة نص النواة، كل ما أحتاجه هو العثور على موقع البنية العامة selinux_state ثم تصفير قيمة .enforcing.

إثبات المفهوم

تم اختبار إثبات المفهوم المرفق بالتقرير على أجهزة Pixel 7 و8 Pro التي تعمل بنظام Android 14 مع تحديثات ASB لشهري أكتوبر ونوفمبر، محققًا معدل نجاح يقارب 100%. من المهم أيضًا الإشارة إلى أن الاستغلال لن يعمل بشكل مباشر على الأجهزة الأخرى بسبب استخدام بعض الإزاحات الثابتة (hardcoded offsets). لإضافة دعم لجهاز جديد، يجب توفير ما يلي:

  • إزاحة kthreadd_task من عنوان قاعدة النواة.
  • إزاحة selinux_state من عنوان قاعدة النواة.
  • إزاحات البنى task_struct->cred , task_struct->pid وtask_struct->tasks.
  • إزاحة anon_pipe_buf_ops من عنوان قاعدة النواة.

التجميع

لتجميع الاستغلال كملف ثنائي مستقل، استخدم الأمر التالي، ثم استخدم adb shell لتشغيله:```sh $ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog $ adb push poc /data/local/tmp/ $ adb shell /data/local/tmp/poc

root@kitploit:~
يمكنك أيضًا تشغيل الاستغلال عبر تطبيق Android Studio من خلال تضمين هذا الدليل معه، وتأكد من تعطيل تحذيرات C++ غير المفيدة بإضافة `-w -Wno-c++11-narrowing` إلى ملف cmake.

### عرض توضيحي```shell
$ adb logcat  |grep -i EXPLOIT
11-28 16:04:12.500  7989  7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563  7989  7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000  from context (0x0)
11-28 16:04:18.441  7989  7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000  from context (0xff)
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444  7989  7989 E EXPLOIT : 10 00 39 01 89 FF FF FF  10 00 39 01 89 FF FF FF  | ..9.......9.....
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.445  7989  7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446  7989  7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462  7989  7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463  7989  7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF  00 00 00 00 30 00 00 00  | @..&........0...
11-28 16:04:18.463  7989  7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF  10 00 00 00 00 00 00 00  | p7..............
11-28 16:04:18.463  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00                           | ........
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102  7989  7989 E EXPLOIT : [+] Cleanup  ... OK
تنزيل الأداة