
PoC لـ CVE-2026-53360: قراءة/كتابة خارج الحدود في الذاكرة الديناميكية (heap) مشغلة بواسطة الضيف في معالجة تغيير حالة الصفحة (PSC) في KVM SEV-SNP.
إثبات مفهوم لقراءة وكتابة خارج حدود الكومة في معالجة تغيير حالة الصفحة (PSC) لـ SEV-SNP في KVM. يقوم ضيف SEV-SNP ضار بجعل نواة المضيف تجتاز مصفوفة إدخالات PSC إلى ما بعد نهاية تخصيص البلاطة (slab) الخاصة بها. يؤدي ذلك إلى تسريب تخطيط كائنات kmalloc-cg-32 المجاورة وكتابة قيمة صغيرة مسيطر عليها فيها، ويمكن للضيف تكرار ذلك بقدر ما يشاء.
الشرح الكامل: https://cyberstan.co.uk/sev-snp-oob/
| CVE | CVE-2026-53360 |
| المكون | دعم مضيف KVM SNP، arch/x86/kvm/svm/sev.c |
| قُدِّم | 9b54e248d264 (أول معالجة PSC لـ KVM SNP، مايو 2024، ~v6.10) |
| أُصْلِح | db3f219 (الرئيسي، مايو 2026، Cc: stable)، معنون بـ Fixes: 4af663c |
| أُبْلِغ | [email protected]، 8 أبريل 2026 |
| المتأثر | مسار مضيف SEV-SNP فقط. لا يمكّن KVM PSC لضيوف SEV-ES العاديين. |
يمكن لأي ضيف SEV-SNP إفساد كومة نواة المضيف وقراءة معلومات حول تخطيطها عن طريق إرسال طلب PSC مشوه. هذا هو الاتجاه من الضيف إلى المضيف: تم بناء SEV-SNP لحماية الضيف من مضيف غير موثوق، ولكن لا يزال على المضيف الدفاع عن نفسه ضد ضيف ضار، وهذا المعالج لا يفعل ذلك.
هذا يتطلب أجهزة SEV-SNP حقيقية. لا يمكن إعادة إنتاجه على Intel، ولن يعطيك المحاكاة الافتراضية المتداخلة ضيف SNP.
الأجهزة:
*.metal، Hetzner AX، وما شابه).نواة المضيف:
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KVM=y
CONFIG_KVM_AMD=y
CONFIG_KVM_AMD_SEV=y
CONFIG_CRYPTO_DEV_SP_PSP=y
kvm_amd.sev=1 kvm_amd.sev_es=1 kvm_amd.sev_snp=1 kasan_multi_shot
cat /sys/module/kvm_amd/parameters/sev_snp # Y
ls /dev/sev # /dev/sev
مساحة مستخدم المضيف:
git clone https://github.com/AMDESE/qemu.git
cd qemu && git checkout snp-latest
mkdir build && cd build
../configure --target-list=x86_64-softmmu && make -j$(nproc)
الضيف:
build-essential و linux-headers-$(uname -r) حتى تتمكن من بناء الوحدة فيه.يتواصل ضيوف SEV-SNP مع المضيف من خلال GHCB، وهي صفحة مشتركة بحجم 4 كيلوبايت. يحدد طلب PSC SW_EXITCODE إلى SVM_VMGEXIT_PSC (0x80000010)، ويوجه SW_SCRATCH إلى واصف، ويضع طول الواصف في SW_EXITINFO2.
الواصف هو struct psc_buffer: رأس بحجم 8 بايت متبوع بمصفوفة من إدخالات بحجم 8 بايت. لا يوجد حقل عدد صريح. يعالج المضيف الإدخالات من hdr->cur_entry إلى hdr->end_entry، وكلاهما مسيطر عليه من قبل الضيف.
struct psc_hdr {
u16 cur_entry;
u16 end_entry;
u32 reserved;
} __packed; /* 8 bytes */
struct psc_entry {
u64 cur_page : 12;
u64 gfn : 40;
u64 operation : 4;
u64 pagesize : 1;
u64 reserved : 7;
} __packed; /* 8 bytes */
من المفترض أن يحتفظ ضيف GHCB v2+ بمنطقة الخدش الخاصة به داخل المخزن المؤقت المشترك لـ GHCB بحجم 2032 بايت، حتى يتمكن المضيف من إعادة استخدام تعيينه الحالي. يتسع لـ (2032 - 8) / 8 = 253 إدخالًا، وهو مصدر الحد الأقصى للبروتوكول VMGEXIT_PSC_MAX_COUNT (253). هذا الرقم منطقي فقط عندما يكون المخزن المؤقت هو بالفعل المخزن المؤقت المشترك.
إذا وجه الضيف منطقة الخدش خارج GHCB، لا يمكن للمضيف استخدام تعيينه، لذا يقوم setup_vmgexit_scratch() بتخصيص مخزن مؤقت منفصل بالحجم الذي طلبه الضيف. لا ينبغي لـ SNP أن يسلك هذا المسار أبدًا، لكن لا شيء يمنعه:
scratch_va = kvzalloc(len, GFP_KERNEL_ACCOUNT); /* len == exit_info_2, يتحكم به الضيف */
يأتي len مباشرة من الضيف، ويضع GFP_KERNEL_ACCOUNT التخصيص في ذاكرات التخزين المؤقت kmalloc-cg-N المحسوبة على مستوى المجموعة. اطلب exit_info_2 = 24 وستحصل على تخصيص بحجم 24 بايت في فتحة 32 بايت kmalloc-cg-32: مساحة للرأس بالإضافة إلى إدخالين. كل شيء بعد entries[1] هو ذاكرة كائن آخر.
ثم يتحقق snp_begin_psc() من عدد الإدخالات مقابل ثابت البروتوكول، وليس مقابل المخزن المؤقت الذي خصصه بالفعل:
idx_end = hdr->end_entry;
if (idx_end >= VMGEXIT_PSC_MAX_COUNT) { /* يتحقق من 253، وليس حجم المخزن المؤقت */
snp_complete_psc(svm, ...);
return 1;
}
for (idx = idx_start; idx <= idx_end; idx++) {
entry_start = entries[idx]; /* خارج الحدود بمجرد idx >= 2 */
...
}
مع مخزن مؤقت بحجم 24 بايت، يوجد إدخالان فقط، لكن التحقق يسمح بـ end_entry حتى 252. اضبطها على 252 وستجتاز الحلقة حوالي 2 كيلوبايت بعد التخصيص، عبر كائنات البلاطة المجاورة.
كل خطوة تتجاوز النهاية تعيد تفسير الـ 8 بايت التالية من ذاكرة البلاطة كـ psc_entry وتشغيلها خلال كود PSC. يعطي ذلك ثلاثة أشياء:
entry.gfn و entry.operation من ذاكرة لم يمتلكها المخزن المؤقت أبدًا. هذا هو قراءة خارج حدود البلاطة التي يلتقطها KASAN.KVM_HC_MAP_GPA_RANGE، يكتب كود الإكمال مرة أخرى في نفس الفتحة خارج الحدود: entries[idx].cur_page = entry.pagesize ? 512 : 1. واحدة من قيمتين صغيرتين في البتات الـ 12 المنخفضة لكلمة يختارها الضيف، قابلة للتكرار.SW_EXITINFO2 عن الفهرس الذي توقف عنده. رفع end_entry واحدًا تلو الآخر يسرب، فتحة بعد فتحة، ما إذا كانت الذاكرة المجاورة قد فُككت إلى no-op أو إلى شيء فشل، وهو ما يكفي لإيجاد حدود الكائنات ومعرفة الصفر من غير الصفر.كل VMGEXIT يعيد تخصيص المخزن المؤقت للخدش، لذا تهبط الطلبات المتكررة في فتحات قائمة حرة مختلفة وتسمح للضيف بالمسح عبر الجيران بدلاً من البقاء عالقًا مع واحد. مجتمعة، يعطي هذا كشف تخطيط الكومة، والكتابة المحدودة أعلاه، واستخدام بعد التحرير عبر الطلبات.
trigger.c هو وحدة نواة ضيف. قم بتحميلها داخل ضيف SEV-SNP وسيقود أربع مراحل ضد المضيف من insmod واحد:
cur_page في جار صفري وتأكيد أن طلبًا لاحقًا يتجاوزه.end_entry=200 وتقيس مدى وصول القراءة خارج الحدود قبل أن تصطدم ببيانات غير صفرية.entries[3..10] خارج الحدود، كل منها يسبب تقرير KASAN على المضيف.تخصص الوحدة صفحة، وتضع علامة عليها كمفكوكة التشفير باستخدام set_memory_decrypted()، وتستخدمها كمنطقة خدش، وتبني طلب GHCB PSC يدويًا. تخرج بـ -EAGAIN حتى لا تبقى محملة.
اضبط مسارات OVMF والقرص حسب إعدادك:
qemu-system-x86_64 \
-enable-kvm -cpu EPYC-v4 \
-machine q35,confidential-guest-support=sev0,memory-backend=ram1 \
-object memory-backend-memfd,id=ram1,size=4G \
-object sev-snp-guest,id=sev0,cbitpos=51,reduced-phys-bits=1,policy=0x30000 \
-smp 4 -m 4G \
-bios OVMF_SNP.fd \
-drive file=guest.qcow2,format=qcow2,if=virtio \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
-device virtio-net-pci,netdev=net0 \
-nographic
انسخ trigger.c و Makefile إلى الضيف، ثم:
make
insmod trigger.ko
تتحقق الوحدة من SEV-SNP عبر CPUID أولاً وترفض التشغيل في أي مكان آخر. تشغل مراحلها الأربعة وتفرغ نفسها (يعيد init القيمة -EAGAIN، لذا لا تبقى أبدًا مقيمة).
على المضيف:
dmesg | grep -E "KASAN|BUG|snp_begin_psc"
الإخراج المتوقع: