
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 है जिसमें अनाम nft_set पर lookup अभिव्यक्ति है, और जहाँ अनाम 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() that points to nft_lookup_destroy()
nf_tables_destroy_set()
nft_set_destroy()
kvfree() that deallocates memory used by `nft_set`
NFT_MSG_DELSETELEM संचालन को संसाधित करने से पहले, जहाँ निम्नलिखित कॉल के दौरान nft_trans_elem_set() के माध्यम से डीलोकेट किए गए nft_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));
उपरोक्त भेद्यता का शोषण करने के लिए Linux कर्नेल से पृष्ठभूमि कार्यकर्ता थ्रेड से निष्पादित nf_tables_trans_destroy_work() के साथ प्रतिस्पर्धा जीतने की आवश्यकता होती है। ऐसा प्रतीत होता है कि यह व्यावहारिक शोषण को जटिल बनाता है, इससे पहले कि हम मौजूदा शमन उपायों पर विचार करें, जैसे कि कर्नेल स्लैब आवंटक का सख्तीकरण, कर्नेल एड्रेस स्पेस लेआउट रैंडमाइजेशन (KASLR) और विशेष रूप से कंट्रोल-फ्लो इंटीग्रिटी। हालाँकि, संलग्न 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() के एकल आह्वान के भीतर बुलाया जाता है जो Linux कर्नेल से पृष्ठभूमि कार्यकर्ता थ्रेड से निष्पादित होता है। इसके अलावा, डीलोकेट किया गया मेमोरी चंक आमतौर पर केवल उसी CPU कोर से पुन: उपयोग के लिए उपलब्ध होता है।
nf_tables_trans_destroy_work() के साथ प्रतिस्पर्धा करते समय, हम पृष्ठभूमि कार्यकर्ता थ्रेड के लिए nf_tables_rule_destroy() और nf_tables_set_elem_destroy() को कॉल करने के बीच एक नियंत्रित विलंब जोड़कर अपनी संभावनाओं में सुधार करते हैं। इसके लिए हम बड़ी संख्या में तत्वों वाले एक अन्य nft_set को नष्ट करने के लिए एक अतिरिक्त संचालन सम्मिलित करते हैं। इसके अतिरिक्त, हम अन्य सभी CPU कोर को व्यस्त रखते हैं, जिससे पृष्ठभूमि कार्यकर्ता थ्रेड के एक विशिष्ट CPU कोर पर निर्धारित होने की संभावना होती है, ताकि हम उसी CPU कोर से एक नई संरचना आवंटित करने का प्रयास कर सकें, ठीक उसके बाद जब वह nf_tables_rule_destroy() के तहत nft_set को डीलोकेट करता है। हमारा लक्ष्य nft_set के मेमोरी स्थान का पुन: उपयोग करने के लिए एक अलग प्रकार का एक नया nft_set आवंटित करना है जो nf_tables_rule_destroy() के तहत डीलोकेट किया गया था।
नए nft_set प्रकार को set->ops->elemsize के लिए एक अलग मान का उपयोग करने के लिए चुना जाता है। इसलिए जब पृष्ठभूमि कार्यकर्ता थ्रेड अंततः NFT_MSG_DELSETELEM संचालन को संसाधित करने के लिए nf_tables_set_elem_destroy() को कॉल करता है, तो यह अपने 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 हेडर तैयार करते समय हम अभी भी सीमा से बाहर के ऑफसेट का उपयोग कर सकते हैं, ताकि आसन्न मेमोरी चंक्स में कुछ मान्य nft_expr पर expr->ops->destroy() को कॉल किया जा सके।
इसके लिए हम नियंत्रित NFTA_LOG_PREFIX के साथ nft_log अभिव्यक्तियों का छिड़काव (spray) करते हैं। expr->ops->destroy() कॉल होने पर वह nft_log->prefix तब nft_log_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_log->prefix मेमोरी चंक का पुन: उपयोग करने और nft_log अभिव्यक्ति को नष्ट करने के लिए nft_object->udata आवंटित करते हैं। यह nft_object->udata मेमोरी को डीलोकेट करता है, लेकिन अब हम NULL बाइट्स पर प्रतिबंधों के बिना मेमोरी सामग्री लीक करने के लिए nft_object->udata हैंगिंग पॉइंटर का उपयोग कर सकते हैं।
अगले चरणों के लिए उपयुक्त संरचनाओं की तलाश में, हमने nft_dynset_new() से आवंटित nft_expr पर निर्णय लिया। ये nft_log->prefix और nft_object->udata के समान स्लैब में रहते हैं। और साथ ही, हमारे पास आवंटन आकार पर उचित नियंत्रण है, जिससे बाद में यदि आवश्यक हो तो हम आसानी से विभिन्न आकारों के स्लैब के बीच स्विच कर सकते हैं।
इन संरचनाओं का उपयोग करने के लिए, हम nft_dynset अभिव्यक्ति के साथ पैकेट फ़िल्टर बनाते हैं। और जब हम लूपबैक इंटरफ़ेस पर कोई पैकेट भेजते हैं, तो nft_dynset अभिव्यक्ति संबंधित nft_set के लिए नए तत्व बनाने के लिए nft_dynset_new() को कॉल करती है। बनाए गए तत्व निम्नलिखित प्रकारों की स्टेटफुल अभिव्यक्तियाँ हैं:
nft_counter कर्नेल मेमोरी में nf_tables.ko का स्थान प्राप्त करने के लिए।
संरचना में nf_tables.ko कर्नेल मॉड्यूल में nft_counter_ops का एक पॉइंटर शामिल है। हम nft_object->udata पढ़कर इस पॉइंटर को लीक करते हैं।
nft_quota मनमानी मेमोरी रीड और राइट के लिए।
हम nft_quota->consumed पॉइंटर को संशोधित करने के लिए बार-बार nft_object->udata को डीलोकेट और पुनः आवंटित कर सकते हैं। इसके बाद, हम 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) >=
nft_quota->consumed को संशोधित करने के लिए।
हम कर्नेल कोर का बेस पता प्राप्त करने के लिए उपरोक्त मनमानी मेमोरी रीड का उपयोग करते हैं। और फिर हम "/sbin/modprobe" पथनाम के "sbin" उपस्ट्रिंग को संशोधित करने के लिए आगे बढ़ते हैं, ताकि इसे "/tmp" से बदल दिया जाए। परिणामी पथनाम "//tmp/modprobe" का उपयोग तब कर्नेल द्वारा रूट विशेषाधिकारों के साथ एक प्रक्रिया शुरू करने के लिए किया जाता है, जहाँ हम फ़ाइल सामग्री को नियंत्रित करते हैं।
ध्यान दें कि हमने कंट्रोल-फ्लो इंटीग्रिटी को बायपास करने के लिए कोई जानबूझकर प्रयास नहीं किया। हालाँकि, शोषण के प्रत्येक चरण के लिए, हमने सचेत रूप से सबसे लचीली और सबसे मजबूत प्रिमिटिव चुनीं। पता चला, कि हमारे चयन ने किसी भी ऐसे प्रिमिटिव से परहेज किया जो संभावित रूप से कंट्रोल-फ्लो इंटीग्रिटी द्वारा अवरुद्ध किए जा सकते थे। अब हम परीक्षण के साथ पुष्टि करने के लिए उत्सुक हैं कि परिणामी शोषण वास्तव में कंट्रोल-फ्लो इंटीग्रिटी शमन वाले सिस्टम के खिलाफ काम करता है।