Linux内核中的侧信道攻击利用方式研究

逆向病毒分析 1年前 (2023) admin
299 0 0

本文主要介绍一种名为Pspray的侧信道攻击手法,由 Yoochan Lee 在Black Hat 2022 Europe 提出。Pspray的核心思路是使用 SLAB 分配时间来了解内存分配状态,从而提高漏洞利用成功几率。


01
背景知识

Linux 在内存管理子系统中实现了 SLAB、SLUB 和SLOB 三种分配器,它们主要用于小内存对象分配,设计的目的是为了解决伙伴系统分配带来的内存碎片化问题。

在操作系统管理的虚拟内存中,用于内存管理的最小单位是页,大多数传统的架构是4KB。由于进程每次申请分配4KB是不现实的,比如分配几个字节或几十个字节,这时需要中间机制来管理页面的微型内存。

        为此,内核实现了一个分配器来管理页中碎片内存的分配和回收。可以把分配器理解为一个零售供应商:它收购大量的库存(4KB大小的页),然后在模块需要时分成小块出售。这种分配的基本版就是SLAB。

1.1 SLAB

当内核子系统为对象请求、释放数据时,主要的开销在于初始化、销毁过程,而不是为对象分配的内存。如果有一组经常使用的内核对象,那么可以将它们保存在一个可快速使用的地方,使整个过程更有效率。这就是SLAB的原理:分配器跟踪这些块,称为缓存,当收到为某种类型的数据对象分配内存的请求时,它可以立即使用已经分配过的来满足请求。这种情况下,SLAB是内存中包含预先分配的内存块的一个或多个连续页面。
SLAB 可能存在以下状态之一:
  • empty:SLAB 中所有对象都是空闲的
  • partial:SLAB 中包含被分配的对象和空闲对象
  • full:SLAB 中所有对象都被分配
分配器的目的是尽可能快的处理请求,因此跟踪过程至关重要,这个过程通过缓存完成,而每种对象类型都有一个缓存。

1.2 SLUB

SLUB 是 SLAB 的变体,旨在实现更好的调试、更少的碎片和更好的性能。它沿用 SLAB 的基本功能,优化了 SLAB 中多处理器的设计缺陷。自从2008年Linux 2.6.23以来SLUB被设置为默认分配器。
接下来会观察SLUB的实现细节,并通过常用的场景给出示例。
SLAB 中的对象通过链表互相连接,这样分配器总是可以找到下一个空闲对象,而不需要关心已经使用的数据:


Linux内核中的侧信道攻击利用方式研究



SLUB 和 SLAB 不同:指向下一个空闲对象的指针直接存储在对象本身内部的结构体中,并不需要额外的内存空间进行存储,且保证SLAB功能100%的利用效率。在某些特殊情况,指针存储在对象结构体中的一个偏移量里面,这根据不同平台的 CPU 而定。

Linux内核中的侧信道攻击利用方式研究

上图中objsize表示对象自身的大小,offset是next指针之前的空间大小,size是总大小。所有的这些信息,以及更多的信息,都存储在一个kmem_cache结构体中,它的结构体定义如下:

/*
 * Slab cache management.
 */

struct kmem_cache {
 struct kmem_cache_cpu __percpu *cpu_slab;
 /* Used for retrieving partial slabs, etc. */
 slab_flags_t flags;
 unsigned long min_partial;
 unsigned int size; /* The size of an object including metadata */
 unsigned int object_size;/* The size of an object without metadata */
 unsigned int offset; /* Free pointer offset */
 ......
 struct kmem_cache_node *node[MAX_NUMNODES];
}

每个对象有且只有一个kmem_cache,并且该对象的所有slab都由相同的kmem_cache管理,这些结构体通过双向链表互相链接,可以通过导出的slab_caches变量从内核中的任何位置访问。slab_caches定义如下:

extern struct list_head slab_caches;  // list_head用于管理双向链表

kmem_cache结构体中,存储了两种指针以跟踪对象:一个kmem_cache_node数组,是结构体最后一个成员struct kmem_cache_node *node[MAX_NUMNODES] ;另一个是指向kmem_cache_cpu的指针,结构体第一个成员struct kmem_cache_cpu __percpu *cpu_slab
  • 以下是kmem_cache_node结构体定义。它跟踪不活动的 partial 和 full 的对象,在空闲的情况被访问,或者当活动的 SLAB 被填满时用另一个 partial 替换它。

/*
 * The slab lists for all objects.
 */

struct kmem_cache_node {
 spinlock_t list_lock;

#ifdef CONFIG_SLAB
 struct list_head slabs_partial; /* partial list first, better asm code */
 struct list_head slabs_full;
 struct list_head slabs_free;
 unsigned long total_slabs; /* length of all slab lists */
 unsigned long free_slabs; /* length of free slab list only */
 unsigned long free_objects;
 unsigned int free_limit;
 unsigned int colour_next; /* Per-node cache coloring */
 struct array_cache *shared; /* shared per node */
 struct alien_cache **alien; /* on other nodes */
 unsigned long next_reap; /* updated without locking */
 int free_touched;  /* updated without locking */
#endif

#ifdef CONFIG_SLUB
 unsigned long nr_partial;
 struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
 atomic_long_t nr_slabs;
 atomic_long_t total_objects;
 struct list_head full;
#endif
#endif

};

  • kmem_cache_cpu结构体管理活动的SLAB,它只有一个,并且与当前的CPU相关(不同的处理器有不同的缓存)。下一次申请始终由freelist字段指向的 SLAB 返回:

struct kmem_cache_cpu {
 void **freelist; /* Pointer to next available object */
 unsigned long tid; /* Globally unique transaction id */
 struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
 struct page *partial; /* Partially allocated frozen slabs */
#endif
#ifdef CONFIG_SLUB_STATS
 unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};

以下是kmem_cache结构体成员的关系图:

Linux内核中的侧信道攻击利用方式研究

1.3 SLUB的分配过程

SLUB 的分配流程大致如下:首先从 kmem_cache_cpu 中分配,如果没有则从 kmem_cache_cpu 的 partial 链表分配,如果还没有则从 kmem_cache_node 中分配,如果kmem_cache_node 中也没有,则需要向伙伴系统申请内存。

Linux内核中的侧信道攻击利用方式研究

SLUB的分配接口是kmem_cache_malloc()。其分配object的流程大概如下:
  1. 首先在kmem_cache_cpu所使用的 SLAB 中查找 free object,如果当前 SLAB 中有 free object,则返回这个object。
  2. 如果当前 SLAB 没有free object,就要看 SLUB 是否开启了kmem_cache_cpu的partial队列,如果开启了partial队列,就在partial队列中查看有没有free object的 SLAB,如果有的话就选定这个SLAB,并返回其free object。
  3. 如果kmem_cache_cpu的partial链表中也没有拥有free object的SLAB,则在kmem_cache_node中查找。
  4. 如果kmem_cache_node中的SLAB有free object,则选定这个SLAB并返回free object。
  5. 如果kmem_cache_node中也没有free object,则需要向伙伴系统申请内存,使用kmem_cache_create()制作新的SLAB。
总结上述分配流程,在SLAB有free object时,分配是最快速的,相反,向伙伴系统申请内存是最慢的,这一信息对于Pspray至关重要。

1.4 分配的随机性

Linux内核为了缓解OOB漏洞带来的危害,可以通过 CONFIG_SLAB_FREELIST_RANDOM 编译配置来开启或关闭分配对象的随机性。开启或关闭此配置时,分配顺序发生变化:

Linux内核中的侧信道攻击利用方式研究


02
常规的利用手法

常见的 Linux 内核漏洞一般分为三种:OOB、UAF和 Double Free。下面简要介绍这三种漏洞利用的方式,以及一些失败情景。

Out-Of-Bounds

OOB是指某些边界检查不严谨导致的越界访问,一般为堆溢出的形式。在利用时,攻击者可以通过放置两个相邻的内存对象,一个为被攻击的对象,一个为受控制的目标对象,可以通过堆喷的方式布局目标对象和被攻击的对象,如果运气足够好,让它们彼此相邻,然后触发越界访问,泄露或者覆盖一些关键数据,以此达到控制数据或执行流。
但利用有一定的失败几率,失败原因是由于分配器的随机性,因为攻击者很难将易受攻击对象和目标对象并排放置,而且攻击者无法知道 freelist 的顺序。因此,攻击者无法知道在使用堆喷射时,SLAB 中已经填充了多少目标对象。

UAF和DF

为了利用 UAF,攻击者需要将易受攻击的对象和目标对象放置在同一位置。在利用时,首先分配易受攻击的对象和一个额外的对象,然后释放易受攻击的对象。接下来将目标对象分配到相同的虚拟地址,也就是最初的易受攻击对象存放的位置。 最后访问释放的易受攻击对象。
Double-Free (DF) 漏洞会释放一个已经被释放的对象。 为了利用 DF 漏洞,需要两个对象,即受攻击对象和目标对象。第一次使用 free 分配和释放易受攻击的对象。然后将受害对象分配到与易受攻击对象相同的地址,并使用 double free 释放受害对象。 此时,由于使用 free 释放了受害对象,留下了指向受害对象的指针,即悬挂指针。 然后将目标对象分配到与易受攻击对象和受害者对象相同的地址。 最后,可以使用悬挂指针访问和修改目标对象。
对于UAF和DF的利用也会失败。根据漏洞的不同,分配并不总是只分配一个对象。 例如CVE-2018-6555,它在 kmalloc-96 中为易受攻击的对象分配了 12 个附加对象。在分配漏洞对象时,分配给 page 的 SLAB 被完全填充,这时相应的 SLAB 移动到 full-list,page 被清空。 然后在分配附加对象时,内核会填充 page。 也就是说,CPU page 被更改为另一个 SLAB,附加对象会从新的 CPU page中分配。 最后如果尝试分配目标对象,就无法重新分配到原来的地址,因为 CPU 的 page 发生了变化,将会从新的里面分配。

03
Pspray利用手法

总结以上利用失败的主要原因,主要是无法获得有关 SLAB 分配情况,Pspray就可以帮助我们了解分配状态。我们可以通过分配时间来判断是否新建了SLAB,从而推断 SLAB 的分配状态,提高利用成功率。
在前面介绍了 SLUB 分配的流程,根据其分配的速度,大致可以将分配过程划分为三个路径:fast path、medium path 和 slow path。如下图:

Linux内核中的侧信道攻击利用方式研究

  • fast path 表示从 freelist 直接分配返回的路径,是最快的分配方式。
  • medium path 表示从 partial 分配的路径,如上图中 medium path # 1、 # 2 和 # 3,表示分配速度中等。
  • slow path 表示从伙伴系统算法创建新的 SLAB 进行分配的路径,因为要创建新的 SLAB,所以分配速度最慢。
分配器这么设计其实是为了优化提高分配效率,尽量以最少的开销执行有效路径。作者对这三条执行路径性能做了测试,通过修改 slab_alloc_node 函数的代码(因为它是kmalloc的主要功能)进行调用次数统计,然后使用100次 msgsnd 系统调用观察其调用次数:fast path 平均 459 个循环,medium path # 1、 # 2 和 # 3 分别显示 676、1,191 和 1,848 个循环,slow path 是最慢的,平均 6,048 个循环,存在显着性能差距。这意味着,如果攻击者可以精确测量分配的性能,则攻击者可以判断分配采用了哪条路径,从而在 SLUB 分配器的内部行为上建立时序侧信道。
fast path 和 medium path 使用 freelist 和 partial,因此我们无法得知它们当前到底分配了多少个对象,但 slow path 是会新创建一个 SLAB,然后从其中进行分配,也就是说,我们可以预测 slow path中新 SLAB 的分配情况。
在利用 slow path 这个特点时,还需要满足一些条件。首先需要找到一个系统调用,并确保非特权用户可以调用。然后这个系统调用可以分配一个对象,用于向内核申请分配。最后这个系统调用耗费的时间不能过长,避免影响测试结果。根据这些条件,找到了一些不同大小对象的系统调用:

Linux内核中的侧信道攻击利用方式研究


Out-Of-Bounds

对于上面提到的OOB利用失败的原因,主要是没法获取两个相邻的对象。Pspray利用方式可以提高这种几率,步骤如下:
  1. 假如现在有一个正在使用的SLAB A,其中的分配情况我们无法得知,但我们可以利用slow path的特点,通过分配的快慢速度来判断当前SLAB是否已满,当满时将会创建新的SLAB B。
  2. 我们通过在SLAB B中堆喷与目标大小相同的对象,来统计得到一个新的SLAB的数量N。SLAB B满之后会创建一个新的SLAB C。
  3. 在SLAB C中,我们分配N-1个受害对象,并在其中创建一个用于OOB的对象,只要这个OOB对象不在SLAB的末尾,那么就可以修改到其中一个受害对象。
  4. 最后就是常规的OOB利用。


UAF和DF

对于UAF和DF利用失败的原因,主要是因为不在同一 SLAB 下造成的。那么我们可以通过 Pspray 来让分配都处在同一个 SLAB 下:
  1. 首先也是通过slow path的特点,通过分配的快慢来判断当前 SLAB 是否已满,满时会创建新的 SLAB B。
  2. 在 SLAB 中创建受害对象和用于 UAF 的对象,就保证了它们处于同一个 SLAB 中,之后是正常的 UAF 或 DF 利用手法。


Pspray利用评估

为了评估Pspray利用的成功率,作者测试了kmalloc 64-4096大小之间的对象,并测试了6个真实漏洞,得到下面结果。可以看出Pspray确实提高了漏洞利用成功的几率,特别是OOB类型的漏洞,另外对于真实漏洞的利用成功率也提高了不少:

Linux内核中的侧信道攻击利用方式研究

Linux内核中的侧信道攻击利用方式研究

但在使用Pspray利用方式时,也会受到来自系统调度方面的噪音导致利用失败。对于UAF和DF,噪音主要在释放和分配过程之间。而对于OOB利用时,主要噪音来自两方面:
  1. CPU 切换:CPU切换时,CPU page 将更改为另一个 CPU page,从而阻碍分配器将两个对象放置在相邻的位置或将两个对象放置在同一地址。 但是可以使用 Linux 系统调用 pthread_setaffinity_np来阻止它。 这个系统调用固定进程,限制它只在 CPU set 上运行。 也就是说使用固定 CPU 可以减轻 CPU 切换引起的噪音。
  2. 上下文切换:上下文切换时可能由其他进程分配了一个对象,然后在利用 OOB 漏洞时,会发现两个对象不相邻。可以通过更高的调度优先级来缓解,但这种做法也很难完全抑制上下文切换。


Reference

https://zhuanlan.zhihu.com/p/382056680

https://www.cnblogs.com/unr4v31/p/15815505.html

https://www.usenix.org/system/files/sec23summer_79-lee-prepub.pdf

       

原文始发于微信公众号(山石网科安全技术研究院):Linux内核中的侧信道攻击利用方式研究

版权声明:admin 发表于 2023年5月5日 下午2:01。
转载请注明:Linux内核中的侧信道攻击利用方式研究 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...