在没有内核堆栈的情况下会出现什么安全问题?

What security issues arise in the absence of a kernel stack?

内核代码使用进程的常规堆栈会导致哪些类型的安全问题?

对于非特权用户来说微不足道space 使内核崩溃,并且很容易接管它或“只是”获得 root 权限。

用户-space 对内核堆栈指针的控制(由中断异步使用)破坏了“非特权”代码的任何可能性,假设所述非特权代码是由潜在攻击者控制的机器代码。 或者 user-space 中的错误可能会导致内核崩溃。

使内核崩溃可以像 xor esp,esp / int 0x80 或等待定时器中断一样简单。在 RSP 回绕到 0xFF...8 之后,这可能会导致页面错误,因为它试图将异常帧推送到未映射的页面上。 (内核使用与 user-space 相同的 page-tables;每个 PTE 中都有一个位将其标记为仅内核或不。)尝试传递该页面错误导致失败到另一个页面错误或 GPF,然后 你出现了三重错误

RSP 的控制还允许您使用异常帧轻松覆盖任意内核地址,可能会影响其他内核上发生的事情。

请注意,我使用 int 0x80 而不是 syscall,因为 syscall 跳转到存储在 MSR 中的入口点,而没有 接触内存(或修改 RSP)。在这种情况下,内核理论上可以在做任何事情之前检查它是否有效。但是真正的中断(包括软件中断)在任何内核指令 运行 之前推送 CS:RIP 和 RFLAGS。 在实际的 x86-64 上,. 如果这没有发生,user-space 将控制这些存储的虚拟地址。 (IDK,如果它甚至可以配置使用未修改的用户-space RSP,或者如果 HW 有效地强制拥有内核堆栈/每个任务内核堆栈。)

(通常内核的 syscall 入口点使用 swapgs 和来自 gs:0 的加载或从内核堆栈底部加载内核堆栈指针的东西。)


接管内核(本地提权):

  • 在一个进程中启动多个线程,因此它们都共享同一个虚拟地址space。 (或者使用 POSIX 共享内存或其他任何东西并在那里设置 RSP)。

  • 一个线程将其堆栈指针存储到其他线程可以读取的全局变量中。

  • 该线程进行系统调用; 内核将其堆栈用于内核-space return 地址和数据。选择 open()stat() 这样的 sys_open()sys_stat() 内核函数到 return 需要一些时间,特别是如果它们在磁盘上阻塞 I/O 在路径名解析或访问 inode 期间。

    或者更简单地说,nanosleep。 (休眠的系统调用将 user-space 状态保存在内核堆栈中,在上下文切换回此任务并 return 从调用到schedule().) 在磁盘上阻塞 I/O 不必要地复杂。尽管它确实公开了许多文件系统代码作为寄存器值的可能来源;您可以选择要覆盖的 return 地址。

  • 在发生这种情况时,另一个用户-space线程修改了内存,获得了内核的 RIP/EIP 和堆栈上数据的控制权.即使使用非 executable 内核堆栈,您也可以做很多事情。通过阅读 return 地址,您可以击败内核 ASLR,然后知道如何修改它们以跳转到您想要的任何内核代码。

内核使用与用户space相同的page-table,所以read/write/exec可以在进行系统调用之前由mprotect(PROT_EXEC)设置。 Executable 堆栈页面将使代码注入变得微不足道。但是 the SMEP bit (Supervisor Mode Execution Prevention) introduced in 2010 blocks this, disallowing ring 0 exec of user-space pages (the U/S bit in the page-table entry which will always be set by any page "owned" by user-space). Another more recent blog post.

create_module(2) 系统调用处理程序中进行权限检查后,您仍然可以 ret 到某个地方,以获取从文件系统加载的模块,其中包含 运行 中的代码内核 space。 ROP 攻击的攻击面巨大,因为内核实现了每个系统调用,包括特权调用。更不用说各种内部函数了系统调用和其他东西使用,以及大量的驱动程序代码。


Broadwell 引入了另一个功能,SMAP (Supervisor Mode Access Prevention) 可以抵御这种情况。在活动期间,如果内核试图 读取 用户页面,则会出错。它必须在 copy_to_user()copy_from_user() 附近被禁用,但是通过指向用户 space 内存的 RSP 实现这些功能似乎不太可能。 call 推送 return 地址时会出错。可能在 32 位内核上,您可以在 1:3 user/kernel 拆分上方使用 ESP 进行系统调用,因此只有一些嵌套函数调用会从 1G 内核页面的底部进入最高用户页面。但是,如果 copy_to/from_user 是叶函数(或者在禁用 SMAP 时不进行任何函数调用),我们可能无法攻击它们。

使用 SMAP 使内核崩溃仍然是微不足道的,但它使非 DoS 攻击变得更加困难。 (这就是它在真正的 x86-64 上的目的:将可能的漏洞转化为故障。)仍然,在我们假设的没有内核堆栈的 x86 中,将 RSP 设置为内核地址并进行系统调用(而不是在用户中使用 [RSP] space) 将允许内核指令覆盖内核数据,SMAP 不会停止。请参阅下面的回复:没有多任务处理。


或者如果您实际上不想 运行 在内核模式下编写代码,您可以 ret 编写将您的进程提升为根的代码,设置 EUID = 0 .

当达到 ret 时,您可以通过选择进行的系统调用和传递的参数来控制寄存器中的值。以及哪一层嵌套函数调用覆盖了return中的地址.


请注意,即使在单核机器上,阻塞系统调用也可能使这种攻击成为可能,因为在单核机器上,攻击线程无法 运行 同时使用内核代码。它只需要在受害者系统调用 returns 之前获得预定的核心,这就是阻塞使之成为可能的原因。

在没有多任务处理的玩具系统上(无法进入内核 运行任何其他用户-space代码之前左),您可以做的“全部” 是用堆栈帧覆盖任意内核内存 地址。包括无效的系统调用(比如 Linux 上的 returns -ENOSYS 的 RAX 值)转储用户-space 以已知模式注册内容 然后 return 没有太多干扰更多堆栈 space!!!假设一个 syscall 入口点写了类似 Linux 的东西,它很早就检查了电话号码,没有一堆 call/ret 会乱写垃圾,如果你想的话,你可能不想要它接管而不是崩溃。

一旦您的 syscall return 无效,您就将 RSP 恢复到正常值,然后进行系统调用以利用您刚刚覆盖的任何数据,例如让通常不会成功的系统调用成功。例如chmod + chown 来创建一个 SUID-root executable,或者如果你设法将当前任务的 UID 设置为零然后执行一个新的 shell.