
CVE-2023-32233
الكود المتأثر مصدره من نواة لينكس الرسمية من https://kernel.org/ وهو جزء من مكون Netfilter nf_tables (net/netfilter/nf_tables_api.c).
يسمح Netfilter nf_tables بتحديث تكوينه كعملية ذرية. عند استخدام هذه الميزة، ترسل تطبيقات وضع المستخدم طلبات دفعية تحتوي على قائمة من العمليات الأساسية. ثم يقوم Netfilter nf_tables بمعالجة جميع العمليات داخل الدفعة كمعاملة واحدة. عند معالجة الدفعة، يتحقق Netfilter nf_tables من تحديثات حالة التكوين لضمان أن كل عملية أساسية متتالية صالحة، ويراعي أيضًا تحديثات الحالة من جميع العمليات السابقة داخل الدفعة. ومع ذلك، فإن الفحص المطبق حاليًا غير كافٍ.
في سيناريونا المحدد، نبدأ بتكوين Netfilter nf_tables يحتوي على nft_rule بتعبير lookup على nft_set مجهول، وحيث يحتوي nft_set المجهول على بعض العناصر. بعد ذلك، نرسل طلبًا دفعيًا يحتوي على العمليتين الأساسيتين التاليتين:
NFT_MSG_DELRULE لحذف nft_rule.lookup و nft_set المجهول.NFT_MSG_DELSETELEM لحذف أي من عناصر nft_set المجهول المحذوف.يقبل الإصدار الحالي من Netfilter nf_tables الطلب الدفعي أعلاه. ثم يستدعي nf_tables_commit_release() التي تُلحق الموارد المحررة بـ nf_tables_destroy_list. تتم بعد ذلك معالجة nf_tables_destroy_list بواسطة nf_tables_trans_destroy_work() التي تقوم أولاً بتحرير الموارد المتعلقة بعملية NFT_MSG_DELRULE عن طريق استدعاء:
nft_commit_release()
nf_tables_rule_destroy()
nf_tables_expr_destroy()
expr->ops->destroy() التي تشير إلى nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() التي تحرر الذاكرة المستخدمة بواسطة `nft_set`
قبل معالجة عملية NFT_MSG_DELSETELEM، حيث يتم الوصول إلى مرجع nft_set المحرر عبر nft_trans_elem_set() أثناء الاستدعاءات التالية:
nft_commit_release()
nf_tables_set_elem_destroy()
nft_set_elem_ext()
داخل nft_set_elem_ext() أعلاه، يتم الوصول إلى موقع الذاكرة الخاص بـ nft_set المحرر لتحديد موقع nft_set_ext:
static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
void *elem)
{
return elem + set->ops->elemsize;
}
للعمليات التالية. لذلك كلما تم إفساد قيمة set->ops->elemsize، يمكن تفسير موقع ذاكرة غير متوقع كقائمة من nft_expr ليتم تدميرها:
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem);
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
يتطلب استغلال الثغرة أعلاه الفوز بسباق مع nf_tables_trans_destroy_work() التي تنفذ من سلسلة خلفية تابعة لنواة لينكس. يبدو أن هذا يعقد الاستغلال العملي حتى قبل اعتبار التخفيفات الموجودة، مثل تقوية مخصص الشظايا (slab allocator) للنواة، وتوزيع عشوائي لمساحة عنوان النواة (KASLR)، وخاصة سلامة تدفق التحكم (Control-Flow Integrity). ومع ذلك، يثبت إثبات المفهوم (PoC) المرفق أنه لا يزال من الممكن تحقيق استغلال موثوق بشكل معقول في الممارسة.
من أجل استغلال الثغرة، نحتاج إلى تعديل محتوى الذاكرة من nft_set بعد تحريرها تحت nf_tables_rule_destroy()، ولكن قبل استخدامها تحت nf_tables_set_elem_destroy(). يتم استدعاء كل من nf_tables_rule_destroy() و nf_tables_set_elem_destroy() داخل استدعاء واحد لـ nf_tables_trans_destroy_work() التي تنفذ من سلسلة خلفية تابعة للنواة. علاوة على ذلك، فإن كتلة الذاكرة المحررة تكون عادة متاحة لإعادة الاستخدام فقط من نفس نواة المعالج (CPU core).
عند المنافسة مع nf_tables_trans_destroy_work()، نحسن فرصنا بإضافة تأخير متحكم به لسلسلة الخلفية بين استدعاءاتها لـ nf_tables_rule_destroy() و nf_tables_set_elem_destroy(). لذلك نقوم بإدراج عملية إضافية لتدمير nft_set آخر يحتوي على عدد كبير من العناصر. بالإضافة إلى ذلك، نبقي جميع نوى المعالج الأخرى مشغولة، بحيث يتم جدولة سلسلة الخلفية على نواة معينة، حتى نتمكن من محاولة تخصيص بنية جديدة من نفس النواة بعد تحرير nft_set تحت nf_tables_rule_destroy(). هدفنا هو تخصيص nft_set جديد من نوع مختلف لإعادة استخدام موقع الذاكرة الخاص بـ nft_set المحرر تحت nf_tables_rule_destroy().
يتم اختيار نوع nft_set الجديد لاستخدام قيمة مختلفة لـ set->ops->elemsize. لذلك عندما تستدعي سلسلة الخلفية أخيرًا nf_tables_set_elem_destroy() لمعالجة عملية NFT_MSG_DELSETELEM، فإنها تفسر وسيط elem بشكل غير صحيح، بحيث يكون nft_set_ext *ext المُفسد بعد بضعة بايتات من الموقع الصحيح. هذا يعني أن حقل بيانات معين يتحكم فيه المستخدم من nft_set_ext الأصلي يتم تفسيره الآن كرؤوس، مما ينتج عنه ارتباك في النوع (type confusion).
إحدى طرق إساءة استخدام هذا الارتباك في النوع هي عن طريق صياغة رؤوس nft_set_ext المُفسدة بقيم إزاحة بحيث تفسر nf_tables_set_elem_destroy() محتوى أي كتل ذاكرة مجاورة كقائمة من nft_expr لتدميرها عبر الاستدعاءات التالية:
nft_set_elem_expr_destroy()
__nft_set_elem_expr_destroy()
nf_tables_expr_destroy()
expr->ops->destroy()
في هذه المرحلة من الاستغلال، لا نملك بعد تفاصيل تخطيط ذاكرة النواة. لذلك ليس من الممكن صياغة عناوين مؤشر مطلقة. ومع ذلك، عند صياغة رؤوس nft_set_ext المُفسدة، لا يزال بإمكاننا استخدام إزاحات خارج النطاق، بحيث يتم استدعاء expr->ops->destroy() على nft_expr صالحة معينة في كتل الذاكرة المجاورة.
لهذا نقوم برش تعبيرات nft_log، مع NFTA_LOG_PREFIX متحكم به. يتم تحرير nft_log->prefix بواسطة nft_log_destroy() بمجرد استدعاء expr->ops->destroy():
static void nft_log_destroy(const struct nft_ctx *ctx,
const struct nft_expr *expr)
{
struct nft_log *priv = nft_expr_priv(expr);
struct nf_loginfo *li = &priv->loginfo;
if (priv->prefix != nft_log_null_prefix)
kfree(priv->prefix);
لاحظ أنه لا يزال بإمكاننا الوصول إلى هذه الذاكرة بل وحتى تحريرها مرة أخرى عبر المرجع الآخر من تعبير nft_log المرشوش.
بالإضافة إلى ذلك، يمكننا أيضًا التحكم في حجم nft_log->prefix، بحيث يمكن تخصيصها من أي من الشظايا kmalloc-{8, ..., 192}. أخيرًا، يتم تفسير الذاكرة المشار إليها كسلسلة من الأحرف بواسطة النواة، لذلك لا داعي للقلق بشأن التلف عندما نضع كائنات مختلفة فوقها. هذا يعني انتهاء اللعبة بشكل أساسي.
إحدى المضايقات هي أن أي أحرف NULL تنهي nft_log->prefix، لذلك لا يمكننا القراءة بعد بايتات NULL عند تسريب محتوى الذاكرة. يتم معالجة هذا في الخطوة التالية، حيث نخصص nft_object->udata لإعادة استخدام كتلة ذاكرة nft_log->prefix وتدمير تعبير nft_log. هذا يحرر ذاكرة nft_object->udata، ولكن الآن لا يزال بإمكاننا استخدام المؤشر المعلق nft_object->udata لتسريب محتوى الذاكرة دون قيود على بايتات NULL.
بالنظر إلى هياكل مناسبة للخطوات التالية، قررنا استخدام nft_expr المخصصة من nft_dynset_new(). هذه تعيش في نفس الشظايا مثل nft_log->prefix و nft_object->udata. وأيضًا، لدينا تحكم معقول في حجم التخصيص، بحيث يمكننا لاحقًا التبديل بسهولة بين شظايا بأحجام مختلفة إذا لزم الأمر.
لاستخدام هذه الهياكل، نقوم بإنشاء مرشح حزم مع تعبير nft_dynset. وعندما نرسل أي حزم عبر واجهة الحلقة الخلفية (loopback)، يقوم تعبير nft_dynset باستدعاء nft_dynset_new() لإنشاء عناصر جديدة لـ nft_set المرتبط. العناصر التي تم إنشاؤها هي تعبيرات ذات حالة من الأنواع التالية:
nft_counter للحصول على موقع nf_tables.ko في ذاكرة النواة.
يحتوي الهيكل على مؤشر إلى nft_counter_ops في وحدة النواة nf_tables.ko. نقوم بتسريب هذا المؤشر عن طريق قراءة nft_object->udata.
nft_quota للقراءة والكتابة العشوائية للذاكرة.
يمكننا تحرير وإعادة تخصيص nft_object->udata بشكل متكرر لتعديل مؤشر nft_quota->consumed. بعد ذلك، نقوم بعملية NFT_MSG_GETSETELEM التي تستدعي nft_quota_do_dump() لقراءة محتوى الذاكرة المشار إليها وتمرير النتيجة كخاصية NFTA_QUOTA_CONSUMED في النتيجة. أما بالنسبة للكتابة، فنحن ببساطة نرسل حزمًا عبر واجهة الحلقة الخلفية، حيث تستدعي nft_quota_do_eval():
static inline bool nft_overquota(struct nft_quota *priv,
const struct sk_buff *skb)
{
return atomic64_add_return(skb->len, priv->consumed) >=
نستخدم قراءة الذاكرة العشوائية أعلاه للحصول على العنوان الأساسي للنواة. ثم ننتقل إلى تعديل السلسلة الفرعية "sbin" من اسم المسار "/sbin/modprobe"، بحيث يتم استبدالها بـ "/tmp". يتم استخدام اسم المسار الناتج "//tmp/modprobe" بعد ذلك بواسطة النواة لبدء عملية بصلاحيات الجذر، حيث نتحكم في محتوى الملف.
لاحظ أننا لم نبذل جهدًا متعمدًا لتجاوز سلامة تدفق التحكم (Control-Flow Integrity). ومع ذلك، في كل خطوة من خطوات الاستغلال، اخترنا بوعي أكثر البدائيات مرونة وقوة. اتضح أن اختيارنا تجنب بأي شكل من الأشكال أيًا من البدائيات التي يمكن أن يتم حظرها بواسطة سلامة تدفق التحكم. نحن الآن فضوليون لتأكيد من خلال الاختبار أن الاستغلال الناتج يعمل بالفعل ضد الأنظمة التي تحتوي على تخفيفات سلامة تدفق التحكم.
لتعديل nft_quota->consumed.