Cross-Cache Attack 学习笔记

henry Lv4

Cross Cache Attack

Reference:

VERITAS501师傅 –> Cross Cache Attack

CVE-2022-29582 –> an io_uring vulnerability

kfree 调用链

1
2
3
4
kfree() / kmem_cache_free()
slab_free()
do_slab_free()
__slab_free()

kfree 源码分析

​ 先调用 virt_to_head_page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;

//....
page = virt_to_head_page(x); //取出 page
if (unlikely(!PageSlab(page))) { //判断 page 是否为 slab page
unsigned int order = compound_order(page);

BUG_ON(!PageCompound(page));
kfree_hook(object);
mod_node_page_state(page_pgdat(page), NR_SLAB_UNRECLAIMABLE,
-(1 << order));
__free_pages(page, order);
return;
}
//多数情况会来到这里 slab_free
slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);
}

slab_free

​ 该函数是对 do_slab_free 的封装,核心函数在内部

1
2
3
4
5
6
7
8
9
static __fastpath_inline
void slab_free(struct kmem_cache *s, struct slab *slab, void *object,
unsigned long addr)
{
memcg_slab_free_hook(s, slab, &object, 1);

if (likely(slab_free_hook(s, object, slab_want_init_on_free(s))))
do_slab_free(s, slab, object, object, 1, addr);
}

do_slab_free

​ 这个函数主要干了两件事,第一检查当前释放的 object 所在的 page 是不是 cpu 的 active page,如果是则直接设置 free_list pointer,这种方式称为快速路径;第二是在检查到不满足快速路径的条件后,调用 __slab_free() 触发 slowpath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static __always_inline void do_slab_free(struct kmem_cache *s,
struct page *page, void *head, void *tail,
int cnt, unsigned long addr)
{
void *tail_obj = tail ? : head;
struct kmem_cache_cpu *c;
unsigned long tid;
redo:
/*
* Determine the currently cpus per cpu slab.
* The cpu may change afterward. However that does not matter since
* data is retrieved via this pointer. If we are on the same cpu
* during the cmpxchg then the free will succeed.
*/
do {
tid = this_cpu_read(s->cpu_slab->tid);
c = raw_cpu_ptr(s->cpu_slab);
} while (IS_ENABLED(CONFIG_PREEMPT) &&
unlikely(tid != READ_ONCE(c->tid)));

/* Same with comment on barrier() in slab_alloc_node() */
barrier();

if (likely(page == c->page)) {
void **freelist = READ_ONCE(c->freelist);

set_freepointer(s, tail_obj, freelist); //被当前 active page 回收

if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
freelist, tid,
head, next_tid(tid)))) {

note_cmpxchg_failure("slab_free", s, tid);
goto redo;
}
stat(s, FREE_FASTPATH);
} else
__slab_free(s, page, head, tail_obj, cnt, addr); //进入 slow path

}

​ 这个函数中有两个非常重要的结构体,即 kmem_cache_cpukmem_cache,在 struct kmem_cache 结构体的头部可以看到有一个 kmem_cache_cpu 指针,同时 cpu_slab 成员前面还跟了一个 __percpu 参数,这意味着每个 cpu 都有一个 cpu_slab 结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* 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 */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
#endif
struct kmem_cache_order_objects oo;

/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */

//....
#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif
//....
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
//....
struct kmem_cache_node *node[MAX_NUMNODES];
};

kmem_cache_cpu 中的 page 即为前面提到的 active page,freelist 是这个 active page 中的 freelist 指针,partial 中存放的是非满的 page。

1
2
3
4
5
6
7
8
9
10
11
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
};

调用链图(重要)

这里再次回到我们的主题,即 Cross cache attack,我们需要知道如何才能使 victim object 所属的 slab 被 slub 分配器回收,该部分功能事实上是由 discard_slab 函数负责完成的,__slab_free() 到 discard_slab 的调用链如下:

nipaste_2024-04-08_21-13-0

这张图非常重要,理解了这张图就理解了 Cross cache attack 的核心,希望读者可以仔细品味。

从上图中可以看到,为使得 page 能够进入 put_cpu_partial 中,需要该 page 满足两个条件:

(1)当前 page 不为 active page

(2)当前 page 不处于 partial list 中,即它是一个 full page(该 page 中所有的 object 都已被分配出去)

用原文作者的话总结来说就是:只有一个非 active 的满 page 尝试释放其中的一个 object 时才会进入 put_cpu_partial()。(将这段话理解了在尝试阅读下面的内容)

__slab_free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static void __slab_free(struct kmem_cache *s, struct page *page,
void *head, void *tail, int cnt,
unsigned long addr)

{
void *prior;
int was_frozen;
struct page new;
unsigned long counters;
struct kmem_cache_node *n = NULL;
unsigned long flags;

//....
do {
if (unlikely(n)) {
spin_unlock_irqrestore(&n->list_lock, flags);
n = NULL;
}
prior = page->freelist;
counters = page->counters;
set_freepointer(s, tail, prior);
// frozen和counters是union关系,这一步就设置了new.frozen
new.counters = counters;
// frozen是指page在partial list中
was_frozen = new.frozen;
new.inuse -= cnt; // page中多少个object在被使用
// 如果当前page为满状态,则没有freelist,所以prior == NULL,
// 且因为是满状态,所以也不在partial中,因此 was_frozen == 0
if ((!new.inuse || !prior) && !was_frozen) {

if (kmem_cache_has_cpu_partial(s) && !prior) {
new.frozen = 1; // 需要执行这一步

} else { /* Needs to be taken off a list */

n = get_node(s, page_to_nid(page));
spin_lock_irqsave(&n->list_lock, flags);

}
}

} while (!cmpxchg_double_slab(s, page,
prior, counters,
head, new.counters,
"__slab_free"));

if (likely(!n)) {

/*
* If we just froze the page then put it onto the
* per cpu partial list.
*/
if (new.frozen && !was_frozen) { //前面的 new.frozen 在这里起作用
put_cpu_partial(s, page, 1); //需要执行到这一步
stat(s, CPU_PARTIAL_FREE);
}
/*
* The list lock was not taken therefore no list
* activity can be necessary.
*/
if (was_frozen)
stat(s, FREE_FROZEN);
return;
}
//....
}

put_cpu_partial

如前面调用图所示,该函数主要负责检查 partial_list 中的 pobjects 个数是否超过了阈值,若没有超过会将目标 page 放入 partial list,否则就会调用目标函数 unfreeze_partials(),将当前 cpu partial 链表中的 page 转移到 Node 管理的 partial 链表尾部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// >>> mm/slub.c:2389
/* 2389 */ static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
/* 2390 */ {
/* 2391 */ #ifdef CONFIG_SLUB_CPU_PARTIAL
/* 2392 */ struct page *oldpage;
/* 2393 */ int pages;
/* 2394 */ int pobjects;
/* 2395 */
/* 2396 */ preempt_disable();
/* 2397 */ do {
/* 2398 */ pages = 0;
/* 2399 */ pobjects = 0;
/* 2400 */ oldpage = this_cpu_read(s->cpu_slab->partial);
/* 2401 */
/* 2402 */ if (oldpage) {
/* 2403 */ pobjects = oldpage->pobjects;
/* 2404 */ pages = oldpage->pages;
// partial list 是否满了,如果满了,走下面if中的逻辑
// pobjects 为当前的partial链表中free object的count,后者为count的阈值
// #define slub_cpu_partial(s) ((s)->cpu_partial)
/* 2405 */ if (drain && pobjects > slub_cpu_partial(s)) {
/* 2406 */ unsigned long flags;
------
/* 2411 */ local_irq_save(flags);
// 调用目标函数 unfreeze_partials()
/* 2412 */ unfreeze_partials(s, this_cpu_ptr(s->cpu_slab)); // <--- 目标!
------
/* 2419 */ }
/* 2420 */
// 正常逻辑,将目标page加入partial list中
/* 2421 */ pages++;
/* 2422 */ pobjects += page->objects - page->inuse;
/* 2423 */
/* 2424 */ page->pages = pages;
/* 2425 */ page->pobjects = pobjects;
/* 2426 */ page->next = oldpage;

unfreeze_partials

unfreeze_partials 会将 CPU 的 partial 链表中的非空 page 转移到 Node 管理的 partial 链表尾部。而对于那些空的 page,会调用 discard_slab 进行释放,即为最终 Cross Cache Attack 的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// >>> mm/slub.c:2321
/* 2321 */ static void unfreeze_partials(struct kmem_cache *s,
/* 2322 */ struct kmem_cache_cpu *c)
/* 2323 */ {
/* 2324 */ #ifdef CONFIG_SLUB_CPU_PARTIAL
/* 2325 */ struct kmem_cache_node *n = NULL, *n2 = NULL;
/* 2326 */ struct page *page, *discard_page = NULL;
/* 2327 */
/* 2328 */ while ((page = slub_percpu_partial(c))) {
/* 2329 */ struct page new;
/* 2330 */ struct page old;
/* 2331 */
/* 2332 */ slub_set_percpu_partial(c, page);
------
/* 2343 */ do {
/* 2344 */
/* 2345 */ old.freelist = page->freelist;
/* 2346 */ old.counters = page->counters;
/* 2347 */ VM_BUG_ON(!old.frozen);
/* 2348 */
/* 2349 */ new.counters = old.counters;
/* 2350 */ new.freelist = old.freelist;
/* 2351 */
/* 2352 */ new.frozen = 0;
/* 2353 */
/* 2354 */ } while (!__cmpxchg_double_slab(s, page,
/* 2355 */ old.freelist, old.counters,
/* 2356 */ new.freelist, new.counters,
/* 2357 */ "unfreezing slab"));
/* 2358 */
// 当前page为空,且node的partial数不小于最小值(一般都满足)
// 就会将此page加入到discard page的列表中
/* 2359 */ if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
/* 2360 */ page->next = discard_page;
/* 2361 */ discard_page = page;
/* 2362 */ } else {
/* 2363 */ add_partial(n, page, DEACTIVATE_TO_TAIL);
/* 2364 */ stat(s, FREE_ADD_PARTIAL);
/* 2365 */ }
/* 2366 */ }
------
// 将discard page列表中的page依次通过discard_slab()释放
/* 2371 */ while (discard_page) {
/* 2372 */ page = discard_page;
/* 2373 */ discard_page = discard_page->next;
/* 2374 */
/* 2375 */ stat(s, DEACTIVATE_EMPTY);
/* 2376 */ discard_slab(s, page);
/* 2377 */ stat(s, FREE_SLAB);
/* 2378 */ }
/* 2379 */ #endif /* CONFIG_SLUB_CPU_PARTIAL */
/* 2380 */ }

利用过程

如果想释放一个 slab page,这里我们以 filp 结构体为例进行说明,该结构体用于管理文件的打开和关闭等操作,做法如下:

1. 查看基本信息

1
2
3
4
5
6
~ # sudo cat /sys/kernel/slab/filp/object_size  # 每个object的大小
256
~ # sudo cat /sys/kernel/slab/filp/objs_per_slab # 每个slab中可容纳多少object
16
~ # sudo cat /sys/kernel/slab/filp/cpu_partial # cpu partial list最大阈值
13

2. 堆喷,收割 cache 在 kernel 中的内存碎片

**3. ** 申请 (cpu_partial + 1) * objs_per_slab = (13 + 1) * 16 个object

小概率情况下上面总共申请的所有 object 会刚好处于 14 个 page 中,因为在这之前 page 中总有之前分配的object 被占用。所以上面分配的所有 object 大多数情况下会分布在 15 个 slab 中,且第 15 个 slab 时非满的状态。

nipaste_2024-04-08_21-13-0

4. 申请 objs_per_slab - 1 = 15 个 object

这样可以使得上面的第15个 slab 处于满 objec 的状态,同时第 16 个 object 处于非满的状态。

nipaste_2024-04-09_15-02-4

5. 申请一个漏洞object,后续用来UAF

**6. 申请 **objs_per_slab + 1 = 17 个object

这样就可以让上面的第 16 slag 为 full page,并制造出第 17 个 slab

mage-2023030809275177

7. 触发 victim object 的 UAF

从前面的调用图可以知道,由于漏洞object所在的第16个slab是满的,因此会触发put_cpu_partial(),但由于cpu partial list 非满,所以还不会进入unfreeze_partials()

nipaste_2024-04-09_15-02-4

8. 将 victim object 的前后 16 个 object 进行释放,让第 16 个 slab 进入到全空状态

注意这里即使第 16 个 slab 为全空,其依然会在 cpu partial_list 链表中

9. 将 1~14 个 slab 中各释放一个 object,将每个 slab 总全满变为半满状态

由于从一开始的信息知道 cpu partial list 最大阈值为13,这时就会导致最后几个slab在释放时,如果遇到 cpu partial_list 超过了阈值,此时就会触发 put_cpu_partial 将我们之前的全空第 16 个slab 被回收。

demo

https://github.com/veritas501/cross_page_attack_demo

原文作者写了一个demo,验证了该技巧,可以学习一下

  • Title: Cross-Cache Attack 学习笔记
  • Author: henry
  • Created at : 2024-04-10 23:02:07
  • Updated at : 2024-04-10 23:06:19
  • Link: https://henrymartin262.github.io/2024/04/10/Cross-Cache-Attack/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments