原文链接:https://blog.immunityinc.com/p/writing-a-linux-kernel-remote-in-2022/
在这篇博客中,我们研究了 2022 年远程利用 Linux 内核的情况,重点介绍了主要障碍以及与本地利用的异同。
概述
在 Appgate 威胁咨询服务,我们专注于攻击性安全研究,以跟上不断发展的网络安全形势。我们相信了解进攻技术和趋势可以提供出色的防守解决方案。
在这篇文章中,我的目标是通过我们的威胁咨询服务团队最近发现的远程堆栈溢出 ( CVE-2022-0435 )来阐明远程内核利用。我在处理这个错误时注意到,与我们在本地权限提升 (LPE) 空间中拥有的大量高质量文章相比,内核远程可用的信息相当稀少。
通过介绍编写这个远程内核所涉及的各个步骤,我希望既能提供一些关于远程 Linux 内核利用的新见解,又能强调与本地利用的异同。
背景
作为背景,当我在 12 月加入 Appgate 威胁咨询服务时,我的团队负责人建议我从探索两个巧妙的 Linux 错误开始。两者都有现有的 LPE 概念验证 (PoC),但是其中一个是堆溢出,它也恰好可以远程访问[0],尽管没有远程 PoC。
当然,作为一名新员工并想给人留下好印象,我谨慎行事并坚持使用 LPE……不,我在开谁的玩笑?当然,我无法抗拒远程内核利用的诱惑!
谈论试炼(尽管完全是自己造成的),但后来出现了一个新漏洞和很多断点,我很高兴能够写这篇文章,目的是分享我的一些工作并为一个很棒的社区做出贡献.
设置场景
好了,寒暄到此为止,让我们来了解一些技术吧!正如我之前提到的,我们将研究我在 CVE-2022-0435 上的工作,因此最好介绍一下它到底是什么。
CVE-2022-0435 是 Linux 内核的透明进程间通信 (TIPC) 网络模块中可远程触发的堆栈溢出。我们基本上能够将攻击者控制大小的有效载荷发送到目标,在那里它将被 memcpy’d 到内核堆栈上的 272 字节缓冲区中——这不是一个糟糕的原语,对吧?
还有比这更多的细微差别,我们将很快讨论有效负载限制,但这是它的一般要点。有关 TIPC 的更多信息和对 bug 的更详细了解,您可以查看我的上一篇文章[1]。
我们发送的“有效负载”伪装成域记录,在 TIPC 节点之间传输以共享网络拓扑视图。这是结构定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define MAX_MON_DOMAIN 64 ... /* struct tipc_mon_domain: domain record to be transferred between peers * @len: actual size of domain record * @gen: current generation of sender's domain * @ack_gen: most recent generation of self's domain acked by peer * @member_cnt: number of domain member nodes described in this record * @up_map: bit map indicating which of the members the sender considers up * @members: identity of the domain members */ struct tipc_mon_domain { u16 len; u16 gen; u16 ack_gen; u16 member_cnt; u64 up_map; u32 members[MAX_MON_DOMAIN]; }; |
但是山姆,那个结构显然是 272 字节?你的假设是正确的!这就是漏洞所在。当模块接收到这些域记录时tipc_mon_rcv()
,它不会检查member_cnt <= MAX_MON_DOMAIN
;这实质上允许攻击者提交这些弹性对象,就好像我们已经提交了u32 members[member_cnt]
.
稍后,当新的域记录被缓存时,TIPC 将这个任意大小的域记录复制到一个预计最多 272 字节的堆栈缓冲区中——这就是我们的堆栈溢出。
有了这些,让我们来谈谈我们的有效载荷的约束:
len
,gen
,ack_gen
和member_cnt
值都受到约束- 但是,
up_map
并且members
可以是任意的 - 有效载荷大小由我们用来发送有效载荷的协议的最大传输单元 (MTU)定义
len
并受其约束;member_cnt
虽然我们稍后会谈到,如果我们想将执行干净地交还给内核,这可能会更短 - 如果我们还没有引起内核恐慌,我们可以多次触发这个错误
Pwning 词汇表
为了使这堵文字墙更容易访问,这里有一些相关技术/缓解措施以及常用术语的资源:
- LPE:本地权限升级:利用具有本地用户访问权限的目标
- RCE:远程代码执行:远程利用目标
- ROP:面向返回的编程;参见 Code Arcana 的 ROP 简介(2013)
$RAX
:x86_64
易失性寄存器;常用于存储返回值$RIP
:x86_64
指令指针寄存器;指向下一条要执行的指令;“获得$RIP
控制” = 控制流劫持$RSP
:x86_64
堆栈指针;指向当前栈帧的顶部- 堆栈粉碎:用于利用堆栈缓冲区溢出的技术的术语;由 Aleph One 1998 年的 Phrack 文章“Smashing The Stack For Fun and Profit”永垂不朽
比赛计划
好的,所以我们有一个非常好的远程堆栈溢出原语,但是我们从这里开始呢?在我们想出一个游戏计划之前,让我们评估一下我们很可能在相对最新的内核上遇到的相关缓解措施:
KASLR
: 内核地址空间布局随机化将在引导时随机化内核的位置,这也许不足为奇。对我们来说,这意味着每次系统启动时内核函数的地址都会相差一个伪随机偏移量。注意内核的顺序没有改变,只是内核基地址 [2]CONFIG_STACKPROTECTOR
:也称为堆栈金丝雀,此缓解在函数序言期间向堆栈添加一个伪随机值,并检查它在结语中是否完好无损。这个金丝雀将位于缓冲区和返回地址之间,这意味着任何溢出都会首先破坏这个金丝雀值。如果内核发现金丝雀发生了变化,就会导致内核恐慌。游戏结束![3]SMEP
&SMAP
:Supervisor Mode Execution Prevention 和 Supervisor Mode Access Prevention 分别阻止我们在内核上下文/环 0 中运行时执行用户模式代码和取消引用用户模式指针。 [4]
如果我们用远程堆栈溢出成功覆盖返回地址并获得执行控制权,我们还需要考虑我们将劫持的执行上下文。与大多数本地漏洞利用场景不同,我们不会在进程上下文中操作,而是会发现自己处于中断上下文中。
在本地利用中,我们通常会通过系统调用到达内核中易受攻击的代码路径,其中用户模式进程可以请求内核代表它执行特权操作(稍后我们将对此进行更多讨论)。Linux 中的进程在内核中通过 跟踪struct task_struct
,它驻留在分配给每个进程的内核堆栈上。
当内核代表用户模式进程工作时,它在调用进程的相应内核堆栈上运行,即在进程上下文中。当我们远程攻击目标时,没有关联的进程。
相反,我们向目标发送一个数据包,然后由网卡接收。网卡然后产生一个中断来表示一个数据包已经到达。然后执行流程将暂停,相应的中断处理程序将运行。在我们的例子中,触发堆栈溢出的网络堆栈在此中断上下文中运行。
从利用上下文来看,这意味着当我们在内核中执行时,没有关联的用户空间供我们提升权限和注入有效负载(例如,远程侦听器)。此外,中断上下文有其自身的一些怪癖,例如没有上下文切换(因此没有睡眠、调度或用户内存访问)。
哎呀,事情现在开始看起来有点复杂了。无论如何,我们拥有制定利用此远程堆栈溢出的游戏计划所需的所有信息:
- 信息泄漏:首先,我们需要泄漏内核地址以绕过 KASLR 以及堆栈金丝雀以绕过 CONFIG_STACKPROTECTOR
- Shellcode 执行:在我们控制了返回地址之后,我们希望利用我们的控制流劫持原语来获得更灵活的东西;shellcode执行会很好
- 转向进程上下文:接下来我们需要从中断上下文转向进程上下文,所以我们实际上有一个用户空间来运行我们的有效负载
- Win:切换到进程上下文后,我们只需要将我们的有效负载注入根进程,瞧!
信息泄露
dmesg
? 系统调用中未初始化的堆栈变量?msg_msg
恶作剧?很长一段时间以来,普遍的共识是 KASLR 更令人讨厌,而不是严重缓解本地利用[5]。然而,远程开发的动态变化很大;攻击面大大减少。
突然间,表面缩小到我们可以远程访问的网络堆栈部分。即使我们发现了一个错误,我们也需要能够通过网络将泄漏返回给我们。当我们考虑堆栈金丝雀时,这尤其困难,因为我们将范围进一步限制为网络堆栈中的堆栈泄漏。
也许不足为奇的是,远程 KASLR 泄漏的可用资源,更不用说金丝雀泄漏,相当稀少。最近一个值得注意的例子是“从 IP ID 到设备 ID 和 KASLR 绕过” (CVE-2019-10639)。
好像找到一个内核地址加上金丝雀泄漏并将其返回给我们还不够困难,我们还需要一种方法来识别目标上运行的内核版本。没有这个,我们就无法真正处理KASLR
泄漏问题,因为内核符号的偏移量因图像而异。
对于这篇文章的范围,我们将假设目标上没有KASLR
+ CONFIG_STACKPROTECTOR
,或者存在具有可用远程泄漏的内核版本。毋庸置疑,在我们的四分比赛计划中,第一是最难克服的障碍。
Shellcode 执行
现在是时候陷入困境了!使用KASLR
&CONFIG_STACKPROTECTOR
平方,我们能够触发我们的远程堆栈溢出并覆盖返回地址而不会导致内核恐慌。
如果我们查看易受攻击函数的汇编结尾,在返回地址之前还会从堆栈中弹出几个寄存器,这意味着我们也可以完全控制这些值。这是一个x86_64
例子:
1 2 3 4 5 6 7 8 9 10 |
tipc_mon_rcv: ... <+191>: pop rbx <+192>: pop r12 <+194>: pop r13 <+196>: pop r14 <+198>: pop r15 <+200>: pop rbp <+201>: ret |
现在我们能够劫持控制流,我们想用它做什么?现在,让我们专注于我们不能做的事情:
- 由于我们在中断上下文中并且我们是远程攻击者,因此没有我们可以跳转到的用户模式 shellcode;不是这样
SMEP
&SMAP
无论如何会让我们现在触摸用户模式。 NX Stack
:在现代 CPU 中,内存管理单元 (MMU) 提供了各种功能。与我们相关的是“NX 位”(不可执行位)的存在,它允许 CPU 将内存页标记为不可执行。对我们来说不幸的是,内核使用此功能使我们的内核堆栈不可执行。
这意味着我们不能只在我们的堆栈有效负载中包含一些 shellcode 并跳转到那个,除非我们能够调用一个名为set_memory_x()
? 多亏了一种称为面向返回编程 (ROP) 的利用技术,我们可以做到这一点。通过对指令指针的控制,我们可以调用其他合法内核代码片段,将它们链接在一起以进行我们的竞标。
因此,这里的目标是创建一个基本上执行以下伪代码的 ROP 链:
1 2 |
call set_memory_x($RSP & ~0xfff, 1) jmp $RSP |
函数签名是set_memory_x(unsigned long addr, int numpages)
,其中addr
必须是页面对齐的地址。因此,为了找到我们的堆栈有效负载所在的页面,我们将页面掩码应用于$RSP
.
使用时髦参数的原因set_memory_x()
是它必须采用页面对齐指针,然后将页面数设置为可执行文件。为了获取我们当前所在页面的起始地址,我们应用了页面掩码(~0xfff
对于 4k 页面)。
通过利用我们可以控制的寄存器和ropper
挖掘代表性内核映像中的小工具之类的工具,我们可以获得一个可执行堆栈,我们可以跳转到执行。让我们花点时间想象一下我们的struct tipc_mon_receive
有效载荷是什么样的:
惊人的!现在我们有了一个可执行堆栈,我们有了更多的灵活性,因为我们可以直接在我们的有效负载中包含 shellcode,这将由内核运行。我们现在有空间专注于转向流程上下文。
清理
让我们不要超越自己!当然,我们有 shellcode 执行,但是如果我们一交回执行,内核就崩溃了,那么弹出一个 root 有效载荷有什么用呢?
在我们继续之前,我们需要确保在触发堆栈溢出后我们可以干净地将执行交还给内核。这不仅对确定漏洞利用的可行性很重要,而且还可能对有效负载施加额外的限制——我们越早知道这些越好!
为了在我们利用漏洞后将执行返回内核并让事情再次顺利运行,我们需要在劫持一切之前了解执行状态。我们要:
- 分析堆栈前/后溢出;我们正在破坏哪个函数的堆栈帧?
- 确定我们可以返回的最近的未破坏堆栈帧是什么
- 确定我们跳过返回的函数中是否释放了任何锁。如果是这样,我们需要自己释放这些
$RSP
在我们选择一个返回地址后,确保$RBP
和任何其他必需的寄存器都是正确的
如果我们看一下易受攻击的函数中的回溯,我们可以开始回答以下一些问题:
1 2 3 4 5 6 7 8 9 10 |
[#1] 0xffffffffc072dd4c → tipc_mon_rcv(net=<optimised out>, data=0xffff88810dba7176, dlen=<optimised out>, addr=<optimised out>, state=0xffff88810035848a, bearer_id=<optimised out>) [#2] 0xffffffffc07274e6 → tipc_link_proto_rcv(l=0xffff888100358400, skb=0xffff888102036c00, xmitq=0xffffc900000e4cc8) [#3] 0xffffffffc0727f4e → tipc_link_rcv(l=0xffff888100358400, skb=0xffff888102036c00, xmitq=0xffffc900000e4cc8) [#4] 0xffffffffc07388a1 → tipc_rcv(net=0xffffffff830e5640 <init_net>, skb=<optimised out>, b=<optimised out>) [#5] 0xffffffffc071ffe9 → tipc_l2_rcv_msg(skb=0xffff888102036c00, dev=<optimised out>, pt=<optimised out>, orig_dev=<optimised out>) [#6] 0xffffffff819f3b67 → __netif_receive_skb_list_ptype(orig_dev=0xffff888102062000, pt_prev=0xffff88810bc3da80, head=0xffffc900000e4d68) [#7] 0xffffffff819f3b67 → __netif_receive_skb_list_ptype(orig_dev=0xffff888102062000, pt_prev=0xffff88810bc3da80, head=0xffffc900000e4d68) [#8] 0xffffffff819f3b67 → __netif_receive_skb_list_core(head=0xffff888102062a60, pfmemalloc=<optimised out>) [#9] 0xffffffff819f3d4e → __netif_receive_skb_list(head=0xffff888102062a60) |
在考虑了我们的 ROP 链、shellcode 和清理代码的一般大小要求并将它们与我们的回溯中的堆栈帧的大小进行比较之后,我们至少知道我们将要使用 clobber#2
和#3
& #4
。这意味着我们实际上希望将执行权移交给#5
以后。
但是,在破坏#2
&期间#4
,我们实际上释放了 TIPC 对象上的一些重要锁,因此我们需要自己释放这些锁。由于 TIPC 是一个可加载的模块,我们的KASLR
泄漏在这里对我们没有帮助,所以要释放这些锁,我们需要从堆栈中获取一个 TIPC 地址。
这给我们留下了#5
作为我们最新的返回点,作为带有 TIPC 引用的最后一个堆栈帧。然后我们可以分析这个和后续函数来检查他们期望的寄存器值。我们现在也知道我们需要调整的数量$RSP
。$RBP
收集到上述要求后,我们现在知道了对我们的有效负载大小的限制,以便安全地交回执行,我们可以相应地计划下一个阶段。
还值得注意的是,我们可以多次抛出这个漏洞。虽然 ROP 链在第一次抛出时可能会占用合理的空间部分,但由于堆栈是确定性的,我们只需使其可执行一次。随后的 throws 可以直接跳转到$RSP
,为我们的 shellcode 提供更多空间。
枢轴处理上下文
好吧,我们在哪里?我们已经有了一个可执行堆栈,是时候编写一些 shellcode 了。除了我们上面提到的强制清理之外,我们还想找到一种方法来转移到进程上下文,所以我们实际上有一个用户空间来 pwn。
“我们将如何转向流程环境?” 我听到你问。好吧,答案是:系统调用挂钩。早些时候,我们简单地谈到了系统调用促进了用户空间和内核之间的交互这一事实,所以在哪里更好地为自己找到一个进程。
系统调用入门
让我们仔细看看在 Linux 中系统调用是如何处理的,以及我们如何能够利用它来发挥我们的优势。对于不熟悉的人来说,系统调用是用户进程和内核之间的基本接口。如果用户进程需要内核代表它做某事,它可以通过系统调用询问。
我们介绍了中断的概念,例如当你的网卡接收到一个数据包时,它会产生一个硬件中断。系统调用也是中断。它们是可以由用户空间进程生成的特定软件中断。
x86_64
这可以通过syscall
指令完成,根据手册:
“SYSCALL 调用特权级别 0 的 OS 系统调用处理程序。它通过从 IA32_LSTAR MSR 加载 RIP 来实现(在将 SYSCALL 之后的指令地址保存到 RCX 之后)。(WRMSR 指令确保 IA32_LSTAR MSR 始终包含规范地址。)”——英特尔指令集参考
总结一下过程:
- 用户空间进程将参数放入寄存器,系统调用号放入
$RAX
并执行syscall
指令 - 内核在 IDT 中找到系统调用中断的中断处理程序,
entry_SYSCALL_64()
在我们的示例中 - 此时,用户模式寄存器被保存到堆栈中,定义为
struct pt_regs
- 传入的系统调用号
$RAX
用作sys_call_table
查找正确处理程序的索引 - 运行正确的系统调用处理程序,我们从堆栈中获取并恢复用户模式寄存器,并将结果通过
$RAX
系统调用挂钩
恢复营业!就像我提到的,我们到进程上下文的路径是通过使用一种通用技术实现的:系统调用挂钩。计划是sys_call_table
用我们自己的代码覆盖其中的一个函数指针,因此下次运行该系统调用时,它会运行我们的代码。
除了,与一些更典型的用例不同,我们将使用我们的钩子将用户模式有效负载注入到调用我们钩子系统调用的不幸进程中。
这很好,因为作为远程攻击者,我们对目标上运行的进程没有太大影响,但我们可以自信地假设某些系统调用将被定期调用。
就这样,我们赢了!好吧,好吧,没那么容易。我们还有更多的障碍需要跨越。首先,为了挂钩我们选择的系统调用,我们需要在我们的 shellcode 中做几件事:
- 我们需要
kmalloc
一些内存才能让我们的钩子和用户模式有效负载存活;然后我们需要set_memory_x()
我们的钩子 set_memory_w()
接下来,我们需要sys_call_table
禁用写保护位$CR0
- 现在我们可以覆盖
sys_call_table
上钩了!
很近!现在剩下的就是编写我们真正的钩子,瞧。这里的目的是使用我们的用户模式有效负载注入进程,然后将控制权传递回原始系统调用处理程序,就像什么都没发生一样。
系统调用挂钩的好处在于,系统调用处理程序被传递了一个指向struct pt_regs
(上面的步骤 3)的指针,这使我们可以访问用户空间寄存器。这意味着我们可以完全覆盖指令指针。
我们还可以访问current
task_struct
,这意味着我们不必满足于更少;我们可以检查uid
当前进程以确保我们只注入到根进程中。没有 unpriv’d shell,非常感谢。
总的来说,我们的钩子看起来像这样:
getuid()
确保我们正在挂钩根进程调用;如果不是根则忽略mmap()
和我们有效载荷的用户空间中的可执行区域copy_to_user()
我们的用户空间有效载荷- 调整
struct pt_regs
以pt_regs->ip
指向我们的用户空间有效负载 - 成功的清理钩子
- 将执行返回到原始处理程序
获胜
现在一切都取决于我们的想象。我们有一个在根进程中运行的任意用户空间有效负载,隐藏在我们自己的小区域中mmap()
。只记得清理并返回执行!
在我们的示例中,用户态有效负载可能类似于以下伪代码:
1 2 3 4 5 6 |
fork() if (parent) repair_registers() // E.g. RBP, RSP jmp_old_ip() // hand back execution to original instruction ptr else CANVAS_callback() // establish connection with the CANVAS server |
缓解措施
在我们结束之前,如果我不涉及针对 CVE-2022-0435 等漏洞的现有缓解措施,那我就失职了。所以,这里有一个相关缓解措施的小词汇表,包括我们已经提到的一些:
KASLR
&CONFIG_STACKPROTECTOR
在现代系统上默认启用,并且考虑到有限的表面,都对远程攻击者施加了严格的信息泄漏要求CONFIG_FORTIFY_SRC
:自 2021 年夏季[6]起,向 Linux 内核添加了严格memcpy()
的边界检查。本质上,当我们知道[7]fortify_panic()
时,如果我们尝试这样做,这种缓解将触发memcpy(dst, src, size)
size > sizeof(dst/src)
CONFIG_FG_KASLR
:正如我们所提到的KASLR
,值得注意的是,函数粒度 KASLR 通过在每个函数级别随机化内核的位置,而不是单个 KASLR 幻灯片,更进一步- 有几种架构/编译器特定的实现旨在通过保护前向边缘(想想函数指针)和/或后向边缘(想想返回地址)来提供控制流完整性(CFI )。这些旨在减轻控制流劫持技术,例如我们在本文中看到的 ROP 技术。
关于缓解问题,我衷心推荐@a13xp0p0v的Linux Kernel Defense Map项目,以揭开 Linux 内核安全状态的神秘面纱,这是一种简洁的图形格式!
结论
因此,我们有它!使用最近的漏洞编写内核远程的故障。希望这篇文章提供了一些关于内核利用的见解,并强调了与本地利用的一些差异。
能够分享我在 Apppgate Threat Advisory Services 所做的一些令人难以置信的工作真是太好了。如果有人有任何问题、建议或更正,请通过samuel.page@appgate.com与我联系。
谢谢阅读!
信用
我想向我的团队负责人阿尔弗雷多·佩索利( Alfredo Pesoli )大声疾呼,感谢他们一路上的鼓励、支持和帮助。多亏了他和团队,我才能写出像这样很酷的东西。
我还想参考 Dan Rosenberg 的 2011 年演讲“远程内核利用剖析”,作为开始远程利用路径的重要帮助。
还可以参照:https://blog.immunityinc.com/p/a-remote-stack-overflow-in-the-linux-kernel/
参考文献
- https://www.sentinelone.com/labs/tipc-remote-linux-kernel-heap-overflow-allows-arbitrary-code-execution/
- https://blog.immunityinc.com/p/a-remote-stack-overflow-in-the-linux-kernel/
- https://lwn.net/Articles/569635/
- https://cateee.net/lkddb/web-lkddb/STACKPROTECTOR.html
- https://en.wikipedia.org/wiki/Supervisor_Mode_Access_Prevention
- https://forums.grsecurity.net/viewtopic.php?f=7&t=3367
- https://lwn.net/Articles/864521/
- https://github.com/torvalds/linux/blob/master/include/linux/fortify-string.h#L212