Linux 内核加密算法:潜在利用?

信息安全 密码学 linux 核心
2021-08-22 15:26:03

我对 linux 内核的 arch/x86/crypto 分支中的许多加密算法感兴趣:

http://lxr.free-electrons.com/source/arch/x86/crypto/

还有这个:

http://lxr.free-electrons.com/source/crypto/

其中许多算法都像 AVX 和 XMM 寄存器这样的寄存器,我不确定内核是否总是从以前的上下文中重新初始化它(如果实现的话,性能会很重)(请提供对内核源代码的引用)。 因此,如果没有,如果一个内核模块正在执行计算,然后可以从查看 XMM 寄存器的用户模式应用程序中嗅出中间密钥,这不是风险吗?

此外,如果有人可以修改这些寄存器,它也可能破坏 AES 计算,如果计算可以被中断并且调度传递给用户模式应用程序。

进一步概括上述两种情况:

如果进程 A 要使用 XMM 寄存器 - 无论是在用户模式还是内核模式下,在进程切换到进程 B 期间,这些 XMM 寄存器值应该被备份和切换。因此,如果有 10 个进程都使用 XMM 寄存器,那么每个进程的上下文都将包含 XMM 信息。
并且由于这些必须在切换期间在内核中完成,有人可以帮助指出显示已完成的内核源代码吗?

在这里可以找到与此类似的问题(未回答):

https://stackoverflow.com/questions/10879546/user-to-kernel-transition-and-xmm-register-state

3个回答

其中许多算法都像 AVX 和 XMM 寄存器这样的寄存器,我不确定内核是否总是从以前的上下文重新初始化它(如果实现的话,性能会很重)

哦,但确实如此。至少,对于使用 SSE/XMM 的进程。可以在硬件中禁用对 SSE/XMM 寄存器的访问(通过 CR4.OSFXSR),这也使得任务切换对于根本不使用 SSE/XMM 的进程更加有效。

因此,如果没有,如果一个内核模块正在执行计算,然后可以从查看 XMM 寄存器的用户模式应用程序中嗅出中间密钥,这不是风险吗?

在返回用户空间之前,内核会非常小心地清理内核代码可能触及的所有内容。

它可能不一定保存/恢复,在某些情况下它可能只是用零值清除。但一切都得到恢复或消毒。

在哪里完成这是一个不同的问题。默认假设可能是内核不接触这些高级寄存器,因此责任可能在于使用它们的模块(至少对于系统调用而言;任务切换可能比系统调用稍微多一些)。

好的,让我们浏览一下内核代码。我在某种程度上同意@DepressedDaniel 的观点,即内核在清理寄存器时可能非常小心,进程切换是内核代码中的关键时刻(可能是最关键的时刻),因此需要非常注意确保代码安全。

另一方面,MMX/SSE/SSE2/3DNOW/MMXEXT寄存器是不经常使用的浮点寄存器。我将所有这些扩展放在同一条船上,因为它们都是简单的长寄存器以及少数操作码来操作它们。在进程切换期间,保存和恢复一堆甚至没有使用的浮点寄存器会对性能造成很大的影响,因此除非需要,否则内核不会这样做。那么问题就变成了“内核如何知道它需要处理 MMX 寄存器?” . 那是应该出现在 中的东西thread.flags,我们很快就会讲到。

在搜索了寄存器后,我发现它们的用处比我最初想象的要多:

  • RAID 代码使用MM0-MM7,您甚至可以看到它在 x86_64 机器上使用 MM0-MM15 的位置(也有非 SSE 实现)。
  • AMD 的3DNOW 代码仍在内核中,尽管我怀疑它是否用于 x86_64
  • KVM使用 MMX sutff,因为它可能需要模拟它
  • 有趣的是,我在加密模块中找不到任何 MMX 用途(不过我并没有用力搜索)。

进程切换部分,乍一看似乎没有使用 MMX 寄存器。但是,当然,这不可能是真的,因为我们很清楚我们可以将 RAID 或 KVM 进程从 CPU 中切换出来并返回。进程切换的主要功能是(并且几乎一直是)switch_to()在那里我们可以看到:

#define switch_to(prev,next,last) do { \
     if (ia64_psr(task_pt_regs(prev))->mfh && ia64_is_local_fpu_owner(prev)) { \
             ia64_psr(task_pt_regs(prev))->mfh = 0;                  \
             (prev)->thread.flags |= IA64_THREAD_FPH_VALID; \
             __ia64_save_fpu((prev)->thread.fph); \
     } \
     __switch_to(prev, next, last); \
     /* "next" in old context is "current" in new context */ \
     if (unlikely((current->thread.flags & IA64_THREAD_MIGRATION) && \
                  (task_cpu(current) != \
                  task_thread_info(current)->last_cpu))) { \
             platform_migrate(current); \
             task_thread_info(current)->last_cpu = task_cpu(current); \
     } \
} while (0)

并且该pt_regs结构不包含有关 MM0-5 的任何内容,但包含有关 MM6-11 的内容(在最后)。

那么,什么给了?内核在哪里处理其他寄存器?关键__switch_to()是上面文件中略高的过程(好吧,宏,如果你想学究气的话):

#define __switch_to(prev,next,last) do { \
    if (IA64_HAS_EXTRA_STATE(prev)) \
            ia64_save_extra(prev); \
    if (IA64_HAS_EXTRA_STATE(next)) \
            ia64_load_extra(next); \
    ia64_psr(task_pt_regs(next))->dfh = !ia64_is_local_fpu_owner(next); \
    (last) = ia64_switch_to((next)); \
} while (0)

并在ia64_save_extra()线程内部检查和处理标志。

还要注意,在上面ia64_fph_disable()总是被调用。其他ia64_fph_*过程的代码就在禁用宏的下方。

最后一个技巧似乎是,在 x86_64 上MM0-5,x86_64 CPU 认为寄存器是易失的,因此它们与 RAX、RCX 和 RDX 一起保存在普通的 x86 上,这可能是在直接处理 MMX 内容的代码中完成的(对于维基百科链接,我很抱歉,我找不到 ABI 文档)。

查看这个分支:arch/x86/kernel/fpu(它处理所有 x86 的 FPU 特定的东西):

阅读可以提供答案的评论:

http://lxr.free-electrons.com/source/arch/x86/kernel/fpu/init.c

/*
 * FPU context switching strategies:
 *
 * Against popular belief, we don't do lazy FPU saves, due to the
 * task migration complications it brings on SMP - we only do
 * lazy FPU restores.
 *
 * 'lazy' is the traditional strategy, which is based on setting
 * CR0::TS to 1 during context-switch (instead of doing a full
 * restore of the FPU state), which causes the first FPU instruction
 * after the context switch (whenever it is executed) to fault - at
 * which point we lazily restore the FPU state into FPU registers.
 *
 * Tasks are of course under no obligation to execute FPU instructions,
 * so it can easily happen that another context-switch occurs without
 * a single FPU instruction being executed. If we eventually switch
 * back to the original task (that still owns the FPU) then we have
 * not only saved the restores along the way, but we also have the
 * FPU ready to be used for the original task.
 *
 * 'lazy' is deprecated because it's almost never a performance win
 * and it's much more complicated than 'eager'.
 *
 * 'eager' switching is by default on all CPUs, there we switch the FPU
 * state during every context switch, regardless of whether the task
 * has used FPU instructions in that time slice or not. This is done
 * because modern FPU context saving instructions are able to optimize
 * state saving and restoration in hardware: they can detect both
 * unused and untouched FPU state and optimize accordingly.

因此,重复解释的内容:

一个。LAZY模式:FPU不会一直恢复/保存,只有在使用的时候才会恢复,使用FPU也会重置CR0:TS中的一个flag,所以我们不需要一直检测FPU寄存器的使用情况. 但是这种模式不是默认的,因为时间节省/性能增强并不显着,而且算法变得非常复杂,从而增加了处理开销。

湾。EAGER 模式:这是默认模式。FPU 总是为每个上下文切换保存和恢复。但同样有硬件功能可以检测是否使用了长链的 FPU 寄存器 - 无论使用哪个,只有该寄存器将被保存/恢复,因此它的硬件效率很高。

要做到这一点绝非易事,因为这意味着在 2015 年编写了 208 个补丁:

https://lwn.net/Articles/643235/

保存所有 FPU - XMM、MMX、SSE、SSE2 等的指令称为 FXSAVE、FNSAVE、FSAVE:

http://x86.renejeschke.de/html/file_module_x86_id_128.html

linux内核中的开销以87个周期为基准。

https://lwn.net/Articles/643235/

这些优化的保存方式也可以在下面的评论中找到:

 * When executing XSAVEOPT (or other optimized XSAVE instructions), if
 * a processor implementation detects that an FPU state component is still
 * (or is again) in its initialized state, it may clear the corresponding
 * bit in the header.xfeatures field, and can skip the writeout of registers
 * to the corresponding memory layout.
 *
 * This means that when the bit is zero, the state component might still contain
 * some previous - non-initialized register state.

为了检测内核在使用 FPU 时触发,我们可以在 KGDB 中的 fpstate_sanitize_xstate 上设置断点,内核堆栈跟踪如下:

Thread 441 hit Breakpoint 1, fpstate_sanitize_xstate (fpu=0xffff8801e7a2ea80) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/fpu/xstate.c:111
111 {
#0  fpstate_sanitize_xstate (fpu=0xffff8801e7a2ea80) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/fpu/xstate.c:111
#1  0xffffffff8103b183 in copy_fpstate_to_sigframe (buf=0xffff8801e7a2ea80, buf_fx=0x7f73ad4fe3c0, size=<optimized out>) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/fpu/signal.c:178
#2  0xffffffff8102e207 in get_sigframe (frame_size=440, fpstate=0xffff880034dcbe10, regs=<optimized out>, ka=<optimized out>) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/signal.c:247
#3  0xffffffff8102e703 in __setup_rt_frame (regs=<optimized out>, set=<optimized out>, ksig=<optimized out>, sig=<optimized out>) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/signal.c:413
#4  setup_rt_frame (regs=<optimized out>, ksig=<optimized out>) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/signal.c:627
#5  handle_signal (regs=<optimized out>, ksig=<optimized out>) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/signal.c:671
#6  do_signal (regs=0xffff880034dcbf58) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/kernel/signal.c:714
#7  0xffffffff8100320c in exit_to_usermode_loop (regs=0xffff880034dcbf58, cached_flags=4) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/entry/common.c:248
#8  0xffffffff81003c6e in prepare_exit_to_usermode (regs=<optimized out>) at /build/linux-FvcHlK/linux-4.4.0/arch/x86/entry/common.c:283

使用“info thread 441”(见上文),您会发现“Xorg”是上述堆栈跟踪的发起者,但除此之外,大多数进程不使用 FPU。

从堆栈跟踪中,“get_sigframe()”似乎是第一个分析 FPU 使用情况的函数:

if (fpu->fpstate_active) {
        unsigned long fx_aligned, math_size;

        sp = fpu__alloc_mathframe(sp, 1, &fx_aligned, &math_size);
        *fpstate = (struct _fpstate_32 __user *) sp;
        if (copy_fpstate_to_sigframe(*fpstate, (void __user *)fx_aligned,
                            math_size) < 0)
                return (void __user *) -1L;
}

所以基本上这里发生的事情是将 FPU 信息复制到用户空间堆栈指针(即“sp”)。

所以总而言之,这部分 FPU 保存/复制/恢复逻辑仅在使用 FPU 时触发。