
डेविड द्वारा खोजे और दस्तावेज़ीकृत किए गए CVE-2022-1015 और 1016 का स्पेनिश अनुवाद।
यह README.md डेविड के ब्लॉग का अनुवाद है। डेविड ने लिनक्स कर्नेल में CVE-1015 और 1016 पाए। आप मूल दस्तावेज़ पढ़ने के लिए उनकी वेबसाइट पर जा सकते हैं।
यहाँ उनके सोशल मीडिया लिंक हैं:
2 अप्रैल 2022 को प्रकाशित।
ये समस्याएँ उबंटू और RHEL के नवीनतम संस्करणों की डिफ़ॉल्ट कॉन्फ़िगरेशन में शोषण योग्य होनी चाहिए। मैंने CVE-2022-1015 के लिए अपना प्रूफ-ऑफ-कॉन्सेप्ट (PoC) आर्क लिनक्स के कर्नेल संस्करण 5.16-rc3 को लक्ष्य करके लिखा।
यह दस्तावेज़ उन लोगों के लिए है जिन्हें कार्यक्षमता और सुरक्षा के संदर्भ में लिनक्स कर्नेल का बुनियादी ज्ञान है। मैंने इस दस्तावेज़ को उन लोगों के लिए भी अनुकूल बनाने की कोशिश की जिन्हें नेटवर्किंग स्टैक का ज्ञान नहीं है, ताकि यह सभी के लिए सुलभ हो सके।
यहाँ एक पढ़ने की मार्गदर्शिका है:
फरवरी के मध्य में, Google के सुरक्षा कार्यक्रम ने घोषणा की कि वे अपना kCTF पुरस्कार कार्यक्रम जारी रखेंगे, जो nsjail सैंडबॉक्स में अविशेषाधिकार प्राप्त प्रक्रियाओं से रूट उपयोगकर्ता तक विशेषाधिकार वृद्धि करने वाले लिनक्स कर्नेल एक्सप्लॉइट के लिए $31,337 से $91,337 तक का पुरस्कार प्रदान करता है।
एक गरीब छात्र होने के नाते, इसने स्वाभाविक रूप से मेरा ध्यान आकर्षित किया। यह पहली बार था जब मैं "वास्तविक दुनिया" की कमजोरी की तलाश कर रहा था, लेकिन अपनी टीम के साथ CTF खेलने के अपने साहसिक कार्यों में, मैं सुरक्षा के संदर्भ में लिनक्स कर्नेल से परिचित हो गया था। घंटों और घंटों के बाद बहुत कम या बिना किसी प्रगति के (लेकिन लिनक्स के बारे में अधिक ज्ञान के साथ) मैं nf_tables मॉड्यूल में कुछ कमजोरियाँ खोजने में सफल रहा।
दुख की बात है कि दिन के अंत में, मुझे पता चला कि यह मॉड्यूल Google के kCTF नियमों में मौजूद नहीं था (इसलिए मुझे इन दो कमजोरियों के लिए कोई पुरस्कार नहीं मिला)। लेकिन जाहिर है, मैंने फिर भी उन्हें रिपोर्ट किया और CVE-2022-1015 के लिए एक LPE (स्थानीय विशेषाधिकार वृद्धि) एक्सप्लॉइट लिखा।
ठीक है, तो आपने तय कर लिया है कि आप लिनक्स में कुछ कमजोरियाँ खोजेंगे। अब क्या? लिनक्स एक विशाल परियोजना है, और पेड़ों के लिए जंगल न देख पाना काफी आसान है (आप विवरणों पर इतना ध्यान केंद्रित करते हैं कि आप वास्तव में महत्वपूर्ण चीजों की दृष्टि खो देते हैं, आपके पास स्थिति का कोई समग्र दृश्य नहीं होता है)। इसे और बदतर बनाने के लिए, कई भाग दस्तावेजित नहीं हैं और आपको यह समझने के लिए बहुत सारा कोड पढ़ना होगा कि क्या हो रहा है।
मैंने लिनक्स सुरक्षा मॉडल का विस्तृत दृष्टिकोण प्राप्त करने का प्रयास करके शुरुआत की। एक बग ढूँढना एक बात है; लेकिन एक अच्छा बग ढूँढना बिल्कुल अलग बात है। आखिरकार, सभी बग समान नहीं बनाए गए हैं:
FS_USERNS_MOUNT निर्दिष्ट करता है, जिस स्थिति में आप उन्हें user namespace में माउंट कर सकते हैं।CAP_SYS_ADMIN या CAP_NET_ADMIN की आवश्यकता होती है।
/proc/config.gz से एक्सेस किया जा सकता है। मॉड्यूल को (=m) में लोड किया जा सकता है या अलग से संकलित किया जा सकता है और रनटाइम पर लोड किया जा सकता है (=y)।/proc/modules और का उपयोग कर सकते हैं, लेकिन वे हमेशा विश्वसनीय नहीं होते, क्योंकि मॉड्यूल को कर्नेल में गतिशील रूप से लोड किया जा सकता है ( )।ये प्रतिबंध हमें उन फ़ाइल सिस्टम की सीमाओं को जानने में मदद करते हैं जिनमें हम कमजोरियाँ खोज सकते हैं। मुझे लगता है कि अपने इच्छित लक्ष्य पर हमले की योजना बनाने में अपना समय लेना एक अच्छा विचार है।
मैंने उपरोक्त बिंदु के बारे में अपना सबक सीखा है। जैसा कि मैंने उल्लेख किया, nf_tables मॉड्यूल kCTF द्वारा प्रस्तुत इंस्टेंस में लोड नहीं था। मैं शुरू से ही इसका एहसास कर सकता था और निराशा से बच सकता था :p। दूसरी ओर, यदि मुझे पहले इसका एहसास हो जाता तो आप शायद अब यह ब्लॉग नहीं पढ़ रहे होते, मुझे लगता है कि आखिरकार चीजें अच्छी ही हुईं।
एक स्पष्टीकरण कि क्यों COS, Google's container-optimized Linux fork, में nf_tables नहीं था, यहाँ और यहाँ पाया जा सकता है।
उपरोक्त बिंदुओं का मूल्यांकन करने के बाद, मैंने फैसला किया कि शुरू करने का मेरा सबसे अच्छा तरीका शायद नेटवर्किंग स्रोत कोड को देखना होगा। वहाँ कई दिलचस्प कार्यक्षमताओं के लिए CAP_NET_ADMIN की आवश्यकता होती है, लेकिन जैसा कि मैंने उल्लेख किया, यह वास्तव में कोई समस्या नहीं है। इसके विपरीत, मुझे संदेह है कि जिन घटकों को विशेष क्षमताओं की आवश्यकता होती है वे आमतौर पर कम सुरक्षित होते हैं, क्योंकि कर्नेल डेवलपर्स को सुरक्षा की झूठी भावना हो सकती है।
मैंने उस फ़ाइल सिस्टम को चुनने का भी प्रयास किया जिसके बारे में मैं और अधिक जानना चाहता था; इस तरह, भले ही आपको कोई बग न मिले, फिर भी आप बहुत सी दिलचस्प चीजें सीख सकेंगे।
मैंने कई नेटवर्किंग फ़ाइल सिस्टम की जाँच की, लेकिन मुझे कुछ भी महत्वपूर्ण नहीं मिला। net/ उपनिर्देशिका में नेविगेट करने के बाद, मेरी मुलाकात nf_tables मॉड्यूल से हुई। यह थोड़ा जटिल लग रहा था, इसलिए मैंने इसके बारे में जानने के लिए कुछ समय लेने का फैसला किया।
नेटफिल्टर (net/netfilter) कर्नेल में एक काफी बड़ा नेटवर्किंग फ़ाइल सिस्टम सबसिस्टम है। संक्षेप में, नेटफिल्टर नेटवर्किंग मॉड्यूल के माध्यम से हुक रखता है जिसमें अन्य मॉड्यूल हैंडलर पंजीकृत कर सकते हैं। जब कोई हुक पहुँचा जाता है, तो नियंत्रण उन हैंडलर को सौंप दिया जाता है, और वे अपनी संबंधित नेटवर्क पैकेट संरचना के साथ काम कर सकते हैं। हैंडलर पैकेट को स्वीकार, छोड़ और संशोधित कर सकते हैं।
nf_tables API (net/netfilter/nf_tables_api.c) में कुछ घंटों के नेविगेशन के बाद यह जानने के लिए कि यह वास्तव में कैसे काम करता है, मैंने उपयोगकर्ता द्वारा भेजे गए रजिस्टरों के तार्किक सत्यापन पर एक नज़र डालने का फैसला किया, और मुझे कुछ संदिग्ध व्यवहार मिला। यह सोचने के बाद कि क्या मैं पागल हो रहा हूँ या नहीं, मैंने अपने द्वारा पाई गई कमजोरी को ट्रिगर करने का प्रयास करने के लिए एक छोटा PoC (प्रूफ-ऑफ-कॉन्सेप्ट) लिखा: एक कमजोरी जिसे OOB या आउट-ऑफ-बाउंड्स के रूप में जाना जाता है, जो स्टैक मेमोरी को पढ़ने और लिखने की अनुमति देती है।
कर्नेल पतों को लीक करने का एक तरीका खोजने के बाद, पॉइंटर मेमोरी पर नियंत्रण लेना काफी आसान था। थोड़े से ROP (रिटर्न-ओरिएंटेड प्रोग्रामिंग) के बाद, रूट विशेषाधिकारों वाला शेल एक वास्तविकता बन गया।
हर बार जब किसी एक्सप्रेशन की init रूटीन को नेटलिंक उपयोगकर्ता संदेश से एक रजिस्टर पार्स करने की आवश्यकता होती है, तो nft_parse_register_load या nft_parse_register_store रूटीन को कॉल किया जाता है, यह इस बात पर निर्भर करता है कि यह एक स्रोत रजिस्टर है या एक गंतव्य रजिस्टर है। मैंने कुछ टिप्पणियाँ जोड़ी हैं:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
`*_store` वेरिएंट लगभग समान हैं, सिवाय इसके कि वे कुछ शर्तों के तहत *verbdict* में लिखने की अनुमति देते हैं।
अंतिम सत्यापन की समीक्षा करने के बाद, यहाँ वास्तव में कुछ गड़बड़ है:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
यह एक integer overflow प्रतीत होता है, क्या आपको नहीं लगता? यदि हम reg को ऐसा मान दे सकें जो 4 से गुणा करने पर overflow उत्पन्न करे जब इसमें len जोड़ा जाए, तो हम शर्तों को पूरा कर सकते हैं। nft_parse_register_load में, reg का अंतिम महत्वपूर्ण बाइट अभी भी पॉइंटर u8 *sreg पर लिखा जाता है, जो हमारे nft_expr पर पड़ता है, जिसे बाद में एक इंडेक्स के रूप में उपयोग किया जाता है।```c
*sreg = reg;
क्या सच में हम कर सकते हैं? `reg` रूटीन की मान्यता में एक `enum nft_registers` है, वैसे भी। हम `0x00000001` से `0xfffffffb` तक के मान पास कर सकते हैं, `nft_parse_register` की सीमा; लेकिन क्या `nft_validate_register_load` में `reg` 32-बिट मान होगा? यह ज्ञात है कि कंपाइलर *enum types* को सिकोड़ सकते हैं यदि कोई छोटा प्रकार सभी मानों को प्रस्तुत कर सकता है। हम एक दूसरी राय लेते हैं।
GCC मैनुअल से प्राप्त:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
¿TL;DR? यह ABI और संभावित ऑप्टिमाइज़ेशन स्तर पर निर्भर करता है। मुझे इस बात का कोई ठोस प्रमाण नहीं मिला कि लिनक्स बिल्ड में यह विकल्प डिफ़ॉल्ट रूप से सक्रिय है।
लेकिन असेंबलर कभी झूठ नहीं बोलता। आइए एक नज़र डालते हैं:```objdump.x86asm
0000000000001b60 <nft_parse_register_load>:
1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5>
1b65: 55 push rbp
1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4]
1b69: 0f c8 bswap eax
1b6b: 89 c7 mov edi,eax
1b6d: 8d 48 fc lea ecx,[rax-0x4]
1b70: c1 e7 04 shl edi,0x4
1b73: 48 89 e5 mov rbp,rsp
1b76: c1 ef 02 shr edi,0x2
1b79: 83 f8 04 cmp eax,0x4
1b7c: 89 f8 mov eax,edi
1b7e: 0f 47 c1 cmova eax,ecx
1b81: 85 d2 test edx,edx
1b83: 74 13 je 1b98 <nft_parse_register_load+0x38>
1b85: 83 f8 03 cmp eax,0x3
1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38>
1b8a: 8d 14 82 lea edx,[rdx+rax*4]
1b8d: 83 fa 50 cmp edx,0x50
1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f>
1b92: 88 06 mov BYTE PTR [rsi],al
1b94: 5d pop rbp
1b95: 31 c0 xor eax,eax
1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea
1b9d: 5d pop rbp
1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde
1ba4: 5d pop rbp
1ba5: c3 ret
फ़ंक्शन कॉल काफी अच्छी तरह से संरेखित हैं। महत्वपूर्ण संचालन `1b8a` में हैं:```objdump.x86asm
lea edx, [rdx+rax*4]
cmp edx, 0x50
ja 1b9f <nft_parse_register_load+0x3f>
mov BYTE PTR [rsi], al
rax ntf_parse_register का परिणाम है, rdx दिए गए len का मान है, और rsi पॉइंटर sreg है। हमने पहले ही अपनी शंकाओं का समाधान कर लिया है।
nft_parse_register_store समान व्यवहार दर्शाता है। जब तक रजिस्टर stack में रहते हैं, हमारी OOB भेद्यता स्पष्ट रूप से stack के सापेक्ष होगी। यह अच्छा है, क्योंकि कुछ भाग्य से, हम सीधे मेमोरी को अधिलेखित और वापस कर सकते हैं।
एक भेद्य इनपुट का उदाहरण देने के लिए, 0xfffffffb का एक रजिस्टर और 0x20 की लंबाई, 0xfffffffb * 4 + 0x20 = 0x0c < 0x50 का मूल्यांकन करेगा। सत्यापन के बाद, (u8)0xfffffffb = 0xfb को *sreg पर लिखा जाएगा।
हालाँकि एक समस्या है: क्या ऐसे एक्सप्रेशन मौजूद हैं जो हमें ऐसी लंबाई का उपयोग करने देते हैं जो जोड़ने पर overflow का कारण बन सकती है? थोड़ी खोजबीन के बाद, मैंने पाया कि nft_bitwise और nft_payload आपको अपनी लंबाई 0x00 से 0xff तक इनपुट करने की अनुमति देते हैं। कई अन्य एक्सप्रेशन स्थिर और बहुत छोटी लंबाई रखते प्रतीत होते हैं।
फिलहाल यह आशाजनक लगता है। अगला कदम इन exploit primitives (एक exploit के दौरान प्राप्त सामान्य क्षमता) को लेना और उनका उपयोग करना है।
यदि हम यह परिभाषित कर सकते हैं कि हमारा exploit हमें किस प्रकार की शक्ति दे सकता है, तो इस भेद्यता का शोषण करना आसान होना चाहिए। इसलिए, कृपया थोड़ा धैर्य रखें क्योंकि हम कुछ अंकगणित देखने जा रहे हैं।
हमारे overflow के लिए रजिस्टर गुणन के लिए तीन बिंदु हैं जिनका उपयोग किया जा सकता है, क्योंकि यह 4 = 2^2 से गुणा किया जाता है: 2^32 - 1, 2^31 - 1 और 2^30 - 1 (क्रमशः 0xffffffff, 0x7fffffff, और 0x3fffffff)। ये मान तब तक घटते रह सकते हैं जब तक हम अपनी अधिकतम अनुमत लंबाई नहीं जोड़ते, चार से गुणा करने के बाद इसके परिणामस्वरूप overflow नहीं होगा। ध्यान देने वाली एक और बात यह है कि हम 0xfffffffb से अधिक मानों का उपयोग नहीं कर सकते, जैसा कि पहले उल्लेख किया गया है।
एक विशिष्ट लंबाई देते हुए, वे न्यूनतम महत्वपूर्ण बाइट मान जो इस लंबाई का उपयोग करके overflow की अनुमति दे सकते हैं, हमारे OOB इंडेक्स का अंतराल बनाएंगे जिनका उपयोग किया जा सकता है।
आखिरकार, इससे कोई फर्क नहीं पड़ता कि किन overflow बिंदुओं का उपयोग किया जाता है। उदाहरण के लिए, LSB (सबसे कम महत्वपूर्ण बिट) 0xf0 वाले निम्नलिखित मानों को लें:```
0xfffffff0 * 4 = 0xffffffc0
0x7ffffff0 * 4 = 0xffffffc0
0x3ffffff0 * 4 = 0xffffffc0
अब से, हम `0x7fffffff` के करीब रजिस्टर मानों का उपयोग करेंगे।
पहले हमने `nft_payload` और `nft_bitwise` के बारे में बात की थी। इन अभिव्यक्तियों के कुछ गुण हैं:
* `nft_payload` केवल *OOB* लेखन कर सकता है, जबकि `nft_bitwise` *OOB* लेखन और पठन दोनों कर सकता है।
* `nft_payload` 0xff बाइट्स तक मनमाने डेटा का *OOB* लेखन कर सकता है।
* `nft_bitwise` वास्तव में केवल `0x40` बाइट्स तक मनमाने डेटा लिख सकता है और केवल `0x40` बाइट्स डेटा पढ़ सकता है जो रजिस्टर स्पेस के *stack* में स्थित है।
* `nft_bitwise` को एक `sreg` और एक `dreg` की आवश्यकता होती है, जिन्हें समान लंबाई मान के साथ मान्यता पास करनी होती है।
* हमारे पास केवल `0x40` बाइट्स रजिस्टर स्पेस है, इसलिए हम स्पेस रजिस्टर से पढ़ना या लिखना चाहते हैं, लेकिन हम `0x40` से अधिक लंबाई के साथ मान्यता पास नहीं कर सकते।
हम `nft_bitwise` के लिए बड़ा लंबाई मान उपयोग कर सकते हैं, लेकिन इसका मतलब है कि `sreg` और `dreg` को सीमा से बाहर होना होगा, जो हमारे उद्देश्यों के लिए बहुत उपयोगी नहीं होगा। इसलिए, अभी हम `0x40` की लंबाई के साथ काम करेंगे।
यह सब ध्यान में रखते हुए, हम किस प्रकार के *exploits* का उपयोग कर सकते हैं?
`nft_bitwise` की अधिकतम लंबाई `0x40` है। इसका मतलब है कि चार से गुणा किया गया रजिस्टर मान कम से कम `0xffffffc0` होना चाहिए। चार से गुणा करने पर हमें सबसे बड़ा मान `0xfffffffb` मिल सकता है, और चूंकि `0xfffffffb + 0x40 = 0x3b <= 0x50` है, यह मान्यता पास कर लेगा।
`0x7ffffff0 * 4 = 0xffffffc0`: निचली सीमा `0xf0` है।
`0x7fffffff * 4 = 0xfffffffb`: ऊपरी सीमा `0xff` है।
[*बाइट ऑफसेट*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)) में अनुवाद करते हुए:```
0xc1 * 4 = 0x304
0xeb * 4 + 0xff = 0x4ab
nft_payload ऑफ़सेट [0x304, 0x4ab] के माध्यम से struct nft_regs से बाहर लिख सकता है।
अब जब यह सब स्पष्ट हो गया है, तो वास्तव में इन ऑफ़सेट पर स्टैक में क्या है?
nft_do_chain रूटीन को कई कोड पथों के माध्यम से कॉल किया जा सकता है। बहुत सारे कारक हैं जो nft_do_chain के स्टैक फ्रेम से पहले स्टैक के आकार को बदल देंगे:
चाहे चेन हुक इनपुट हो या आउटपुट।
इनपुट के रूप में कॉन्फ़िगर किया गया चेन हुक है, तो हुक सॉफ़्टिर्क संदर्भ में संबंधित नेटवर्क डिवाइस के साथ सक्रिय होगा।आउटपुट के रूप में कॉन्फ़िगर किया गया चेन हुक है, तो हुक send* सिस्टम कॉल के संदर्भ में सक्रिय होगा।हम जिस प्रोटोकॉल का उपयोग कर रहे हैं।
मुझे लगता है कि आप प्रोटोकॉल, इंटरफेस और हुक स्थानों के विभिन्न संयोजनों का उपयोग करके कॉल स्टैक के कई रूप प्राप्त कर सकते हैं। फिलहाल हम UDP पैकेट के साथ आउटपुट के रूप में कॉन्फ़िगर किए गए चेन हुक का उपयोग करेंगे।

जब एक UDP पैकेट आउटपुट पर कॉन्फ़िगर किए गए हुक तक पहुँचता है तो nft_do_chain में स्टैक लेआउट और सीमा से बाहर की पहुँच
एक स्थिर एक्सप्लॉइट बनाने में सक्षम होने के लिए, पहले हमें कर्नेल इमेज का पता फ़िल्टर करना होगा।
कर्नेल इमेज के पते में 9 बिट्स की एन्ट्रॉपी होती है (संदेशों के एक सेट से पहले मौजूद अनिश्चितता का माप, जिसमें से केवल एक ही प्राप्त होगा), जिसका अर्थ है कि 512 अलग-अलग स्थान हैं जहाँ कर्नेल लोड किया जा सकता है। आपके हमले के परिदृश्य के आधार पर, एक्सप्लॉइट के ठीक से काम करने की संभावना 1/512 है; लेकिन यह बेहतर होगा यदि हम इससे अधिक स्थिर एक्सप्लॉइट प्राप्त कर सकें।
सबसे सरल कदम यह है कि हम अपनी सीमा से बाहर पढ़ने की क्षमता का उपयोग करने का प्रयास करें जो nft_bitwise ने हमें दी है ताकि स्टैक से कुछ डेटा को हमारे रजिस्टरों में कॉपी किया जा सके। चूँकि कुल अंतराल जिसे हम पढ़ सकते हैं उसकी लंबाई 0x7c बाइट्स है, कर्नेल पते के वहाँ होने की काफी अच्छी संभावना है।

nft_bitwise का सीमा से बाहर का दायरा
आज हमारा दिन है! दो हैं:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
इसे रजिस्टरों में लिखना एक बात है, लेकिन उन्हें निकालना दूसरी बात है। जांच करने के बाद, ऐसा प्रतीत होता है कि जब `nft_do_chain` चल रहा होता है तो रजिस्टरों को सीधे पढ़ने का कोई आसान तरीका नहीं है।
मेरी मूल रिपोर्ट [email protected] पर, मुझे नेटफिल्टर के एक अनुरक्षक द्वारा `nft_dynset` एक्सप्रेशन के बारे में बताया गया, जो [*डायनामिक सेट*](https://en.wikipedia.org/wiki/Dynamic_set) के लिए समर्थन रखता है जो एक प्रकार के डेटाबेस की तरह कार्य कर सकता है जो विभिन्न `nft_do_chain` निष्पादनों के माध्यम से लिख और पढ़ सकता है। जाहिरा तौर पर, `nft_payload` में भी स्वयं पैकेट में लिखने की क्षमता है, मुझे इसका एहसास नहीं हुआ।
इसके बजाय, मैंने अपने [*साइड-चैनल हमले*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral) के साथ जारी रखने का निर्णय लिया। `nf_tables` की प्रकृति के कारण, आप दुष्प्रभाव उत्पन्न कर सकते हैं। वास्तव में, आप कह सकते हैं कि ये दुष्प्रभाव भी नहीं हैं, बल्कि प्राथमिक प्रभाव हैं।
कर्नेल मेमोरी पते के मान के आधार पर पैकेट को गिराने या स्वीकार करने वाले नियम बनाकर, जिसे हम कॉपी कर रहे हैं, धीरे-धीरे हम यह अनुमान लगा सकते हैं कि मान क्या है, यह जांच कर कि क्या हमारे द्वारा भेजे गए पैकेट भी प्राप्त हुए।
1. एक UDP *सॉकेट* बनाएं जो `127.0.0.1:9999` पर पैकेट प्राप्त करता है:
* इसे एक अलग थ्रेड में पैकेट प्राप्त करने चाहिए।
* प्रत्येक प्राप्त पैकेट के लिए एक संदेश वापस भेजा जाना चाहिए।
2. एक नियम जोड़ें:
1. कर्नेल पते को `nft_bitwise` के साथ रजिस्टरों में कॉपी करें।
2. पते की तुलना एक स्थिरांक से करने के लिए `nft_cmp_expr` का उपयोग करें।
3. यदि तुलना सत्य है तो पैकेट गिराएं।
3. `127.0.0.1:9999` पर एक UDP पैकेट भेजें
1. हम यह निर्धारित कर सकते हैं कि कर्नेल पते के बारे में थोड़ी जानकारी इस आधार पर कि क्या हमें वापस संदेश मिला।
4. उपयुक्त मानों के साथ 2 और 3 को दोहराएं जब तक कि आपके पास स्वयं जानकारी निर्धारित करने के लिए पर्याप्त जानकारी न हो।

अभी भी कुछ चेतावनियाँ हैं। उदाहरण के लिए, हमारे द्वारा प्राप्त पैकेट को बिना किसी पूर्व सूचना के भी गिराया जा सकता है। इसे कम करने के लिए, हम शोर कम करने की एक विधि जोड़ सकते हैं, जिसके लिए हमें एक *बेस चेन* और एक *सहायक नियमित चेन* की आवश्यकता होगी।
*Rule in base chain:*
| # | अभिव्यक्ति | तर्क | टिप्पणी |
| --- | -------------------- | -------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------- |
| 0 | `nft_payload` | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | पैकेट के गंतव्य पोर्ट को रजिस्टर 8 में लिखें। |
| 1 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999 | गंतव्य पोर्ट की तुलना `9999` से करें, और यदि परिणाम समान नहीं है तो `NFT_BREAK` लौटाएँ। |
| 2 | `nft_payload` | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8 | पैकेट के पहले आठ बाइट्स को रजिस्टर 8 में लिखें। |
| 3 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de | पहले आठ बाइट्स की तुलना मैजिक मान से करें, और यदि समान नहीं है तो `NFT_BREAK` लौटाएँ। |
| 4 | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain | चूंकि नियम अभी भी मूल्यांकन कर रहा है, शर्तों का मिलना आवश्यक है, और हमारी *सहायक चेन* को कॉल करें। |
*Rule in auxiliary chain:*
| # | अभिव्यक्ति | तर्क | टिप्पणी |
| --- | --------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | `nft_bitwise` | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | सीमा से बाहर पढ़ने का उपयोग करके कर्नेल पते को रजिस्टरों में लिखें, `SHIFT_AMT` बिट्स द्वारा स्थानांतरित करके वांछित पते के बाइट को सही रजिस्टर में प्राप्त करें। |
| 1 | `nft_cmp` | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND | कर्नेल पते के बाइट की तुलना `COMPARAND` से करें, यदि परिणाम समान नहीं है तो `NFT_BREAK` लौटाएँ। |
| 2 | `nft_immediate` | verdict=NFT_DROP | यदि पता बाइट `COMPARAND` से बड़ा है तो पैकेट गिराएँ। |
गंतव्य पोर्ट की जाँच करके और हेडर के पहले आठ आंतरिक बाइट्स की तुलना एक मैजिक मान से करके, हम उन पैकेटों के लिए दुष्प्रभावों को सक्रिय कर सकते हैं जिन्हें हम चाहते हैं।
`COMPARAND` को गतिशील रूप से बदलकर हम कर्नेल पते के बाइट को `0(log(n))` समय में खोजने के लिए बाइनरी सर्च कर सकते हैं। `SHIFT_AMT` को अगले आठ के गुणकों में गतिशील रूप से बदलकर हम अगले मेमोरी बाइट पर जा सकते हैं और फिर से शुरू कर सकते हैं।
#### 4.3.1 फिल्टर स्यूडो-कोड
मेमोरी पते को फ़िल्टर करने के लिए थोड़ा पाइथन कोड। मजेदार बात यह है कि मैं इसे आसानी से पाइथन में लागू कर सकता था। याद रखें कि आपको हमेशा अपने एक्सप्लॉइट्स को सी में कर्नेल के लिए नहीं बनाना है :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''
def leak_byte(pos):
s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
s.settimeout(200) # 200ms debería ser más que suficiente
s.bind(("127.0.0.1", 1234))
# buscar los límites
low = 0, high = 255
while True:
mid = (low + high) // 2
# si encontramos el valor, lo regresamos
if low == high:
s.close()
return mid
set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)
# Enviar el paquete y activar la auxiliary chain
s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))
# El hilo secundario regresa a 127.0.0.1:1234
res = s.recvfrom(0x2000)
if not res:
'''
nuestro paquete fue soltado
ya que no se regresó nada en los 200ms
lo que significa que
byte to leak >= mid
el byte a filtrar es mayor o igual a mid (127)
'''
low = mid
else:
'''
[sanity check o prueba de cordura]
se usa para evaluar rápidamente si
el valor a calcular es siquiera posible
https://es.wikipedia.org/wiki/Prueba_de_cordura
'''
if res != b"MSG_OK":
print("Something went wrong")
return None
'''
Nuestro paquete fue aceptado, lo que
significa que
byte to leak < mid
byte a filtrar es menor a mid (127)
'''
high = mid - 1
leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]
अब जब हमने लीक प्राप्त कर लिया है, मनमाना कोड निष्पादन बहुत आसान होना चाहिए। nft_payload की सीमा से बाहर लेखन को स्टैक के लिए एक श्रृंखलाबद्ध RoP हमला लिखने में सक्षम होना चाहिए, है ना?
नहीं। हमें बहुत अधिक भाग्य नहीं मिला, कम से कम इस विशेष कर्नेल में। nft_payload की सीमा से बाहर लेखन लगभग पूरी तरह से udp_sendmsg रूटीन के स्टैक फ्रेम के साथ संरेखित होता है। udp_sendmsg का पता रजिस्टरों के सापेक्ष ऑफ़सेट +0x2f8 पर स्थित है, यह स्थान nft_payload या nft_bitwise के साथ पहुंचने के लिए बहुत कम है (हम ऑफ़सेट +0x304 से शुरू करके लिख सकते हैं, बहुत करीब...)। inet_sendmsg का पता ऑफ़सेट +0x4a8 पर स्थित है। तकनीकी रूप से हम इसे प्राप्त कर सकते हैं (और निचले तीन बाइट्स को ओवरराइट कर सकते हैं), लेकिन वहाँ एक स्टैक कैनरी (एक तकनीक जिसका उपयोग स्टैक बफर ओवरफ्लो का पता लगाने के लिए किया जाता है, इससे पहले कि दुर्भावनापूर्ण कोड निष्पादित हो सके) पते +0x0458 पर है जिसे हमें भी ओवरराइट करना होगा। यह स्पष्ट रूप से कर्नेल को क्रैश कर देगा, इसलिए ऐसा करना कोई विकल्प नहीं है।
मैंने इस विधि का उपयोग कर्नेल के एक अन्य बिल्ड में किया, लेकिन ऐसा लगता है कि इस ब्लॉग के लिए मैं जिस कर्नेल का उपयोग कर रहा हूँ, उसके लिए भी ऐसा करना थोड़ा कठिन होगा।
अब, शायद हम udp_sendmsg में स्थानीय चरों को ओवरराइट करने के लिए कुछ कृत्रिम स्टैक फ्रेम हैकिंग कर सकते हैं। हम वर्डिक्ट चेन पॉइंटर को ओवरराइट करने का भी प्रयास कर सकते हैं, रजिस्टरों के मान का उपयोग करते हुए, उदा. 0x7fffff00 (मुझे लगता है कि यह एक शानदार तकनीक हो सकती है; चुनौती को ध्यान में रखते हुए)।
आइए हम उस बेस चेन हुक को बदलने का प्रयास करें जिसका हम उपयोग कर रहे थे। हम एक output चेन का उपयोग कर रहे थे, यदि हम इसे input में बदल दें तो क्या होगा?

एक भेजा गया UDP पैकेट इनपुट हुक तक पहुँचने पर nft_do_chain में सीमा से बाहर पहुँच का आरेख
यह थोड़ा बेहतर दिखता है! हम __netif_receive_skb_one_core के फ्रेम के रिटर्न पते (ऑफ़सेट +0x328) को ओवरराइट कर सकते हैं, जो __netif_receive_skb पर वापस जाता है। चूँकि यह हमारे nft_payload की सीमा से बाहर पहुँच की ऊँचाई के अपेक्षाकृत करीब है, हम अपने OOB इंडेक्स (सीमा से बाहर पहुँच) को सीधे इस रिटर्न पते पर इंगित कर सकते हैं, स्टैक कैनरी को ऑफ़सेट +0x310 पर दरकिनार करते हुए। ऑफ़सेट +0x328 इंडेक्स 0xca में अनुवाद करता है।
रिटर्न पते के ओवरराइट को सक्रिय करने के लिए, हम तालिका में एक नई input चेन बनाते हैं, और इसमें एक नियम जोड़ते हैं जिसमें एक nft_payload है जो पैकेट के आंतरिक हेडर से इंडेक्स 0xca पर 0xff बाइट्स लिखता है। फिर हम पेलोड के साथ एक पैकेट भेजते हैं, और बूम।

🥳 🥳 🥳 🥳 🥳
/proc/kallsymsrequest_module