#!/usr/bin/env python3
"""
This was entirely generated by Claude code.

Generate a DCE-RPC Bind / Bind-ACK pcap with a sec_addr that has NO trailing null.

Before the patch  (line 90 original):
  *(begin() + length()) == 0  --> always true (binpac null terminator)
  --> sec_addr is ALWAYS truncated by 1 byte, even for b"ABC" -> "AB"

After the patch (line 90 patched):
  *(begin() + length() - 1) == 0  --> false for b"ABC"
  --> sec_addr is kept as-is: "ABC"

Packets built per flow:
  Pkt 1  b'ABC'      old=b'AB'   new=b'ABC'  <- behavior change
  Pkt 2  b'ABC\\x00'  old=b'ABC'  new=b'ABC'  <- same (no change)
  Pkt 3  b'X'        old=b''     new=b'X'    <- behavior change
"""

import struct

from scapy.all import IP, TCP, Ether, Raw, wrpcap

DCE_RPC_BIND = 0x0B
DCE_RPC_BIND_ACK = 0x0C

# A real-ish transfer syntax UUID (NDR 2.0): 8a885d04-1ceb-11c9-9fe8-08002b104860
NDR_UUID = bytes.fromhex(
    "04 5d 88 8a eb 1c c9 11 9f e8 08 00 2b 10 48 60".replace(" ", "")
)
NDR_VER_MAJ = 2
NDR_VER_MIN = 0

# A dummy abstract syntax UUID
ABS_UUID = bytes(
    [
        0x78,
        0x56,
        0x34,
        0x12,
        0x34,
        0x12,
        0xCD,
        0xAB,
        0xEF,
        0x00,
        0x01,
        0x23,
        0x45,
        0x67,
        0x89,
        0xAB,
    ]
)


def dce_rpc_hdr(ptype, body_len, call_id=1):
    frag_length = 16 + body_len
    return struct.pack(
        "<BBBB4sHHI",
        5,
        0,
        ptype,
        0x03,
        b"\x10\x00\x00\x00",  # packed_drep: little-endian
        frag_length,
        0,
        call_id,
    )


def build_bind():
    """Minimal DCE_RPC_Bind body."""
    max_xmit = 4280
    max_recv = 4280
    assoc = 0

    # ContextList: num_contexts=1, reserved=3, one ContextRequest
    # ContextRequest: id=0, num_syntaxes=1, reserved=1, abstract(20), transfer[1](20)
    context = struct.pack("<HBx", 0, 1)  # id, num_syntaxes, pad
    context += ABS_UUID + struct.pack("<HH", 1, 0)  # abstract syntax
    context += NDR_UUID + struct.pack(
        "<HH", NDR_VER_MAJ, NDR_VER_MIN
    )  # transfer syntax

    body = struct.pack("<HHI", max_xmit, max_recv, assoc)
    body += struct.pack("<Bxxx", 1)  # num_contexts + 3 reserved bytes
    body += context
    return body


def build_bind_ack(sec_addr_bytes):
    """Build DCE_RPC_Bind_Ack body."""
    max_xmit = 4280
    max_recv = 4280
    assoc = 0x12345678

    body = struct.pack("<HHI", max_xmit, max_recv, assoc)
    body += struct.pack("<H", len(sec_addr_bytes))
    body += sec_addr_bytes

    # Pad to 4-byte alignment; sec_addr field starts at offset 10 within body
    off = 10 + len(sec_addr_bytes)
    body += b"\x00" * ((4 - off % 4) % 4)

    # ContextList reply: num_contexts=1 + 3 reserved bytes
    body += struct.pack("<Bxxx", 1)
    # ContextReply: ack_result=0, ack_reason=0, syntax(20 bytes)
    body += struct.pack("<HH", 0, 0)
    body += NDR_UUID + struct.pack("<HH", NDR_VER_MAJ, NDR_VER_MIN)

    return body


def make_flow(sec_addr_bytes, src_ip, sport, seq_c=100, seq_s=200):
    """Return [syn, syn-ack, bind_pkt, bind_ack_pkt] for one flow."""
    dst_ip = "10.0.0.1"
    dport = 135

    def eth_ip(s_ip, d_ip, s_port, d_port, flags, seq, ack, payload=b""):
        return (
            Ether(src="aa:bb:cc:dd:ee:01", dst="aa:bb:cc:dd:ee:02")
            / IP(src=s_ip, dst=d_ip)
            / TCP(sport=s_port, dport=d_port, flags=flags, seq=seq, ack=ack)
            / (Raw(load=payload) if payload else b"")
        )

    bind_body = build_bind()
    bind_payload = dce_rpc_hdr(DCE_RPC_BIND, len(bind_body)) + bind_body

    ack_body = build_bind_ack(sec_addr_bytes)
    ack_payload = dce_rpc_hdr(DCE_RPC_BIND_ACK, len(ack_body)) + ack_body

    pkts = [
        # TCP handshake
        eth_ip(src_ip, dst_ip, sport, dport, "S", seq_c, 0),
        eth_ip(dst_ip, src_ip, dport, sport, "SA", seq_s, seq_c + 1),
        eth_ip(src_ip, dst_ip, sport, dport, "A", seq_c + 1, seq_s + 1),
        # Bind (client -> server)
        eth_ip(src_ip, dst_ip, sport, dport, "PA", seq_c + 1, seq_s + 1, bind_payload),
        # Bind ACK (server -> client)
        eth_ip(
            dst_ip,
            src_ip,
            dport,
            sport,
            "PA",
            seq_s + 1,
            seq_c + 1 + len(bind_payload),
            ack_payload,
        ),
    ]
    return pkts


def main():
    packets = []

    # Flow 1: sec_addr = b'ABC'  (no null) -> old strips to "AB", new keeps "ABC"
    packets += make_flow(b"ABC", src_ip="10.0.0.2", sport=49152)

    # Flow 2: sec_addr = b'ABC\x00' (null)  -> both old and new produce "ABC"
    packets += make_flow(b"ABC\x00", src_ip="10.0.0.3", sport=49153)

    # Flow 3: sec_addr = b'X'    (no null) -> old strips to "",  new keeps "X"
    packets += make_flow(b"X", src_ip="10.0.0.4", sport=49154)

    out = "dce_rpc_bind_ack_test.pcap"
    wrpcap(out, packets)
    print(f"Wrote {len(packets)} packets to {out}")
    print()
    print("Expected sec_addr seen by Zeek's dce_rpc_bind_ack event:")
    print("  Flow 1  b'ABC'       old -> 'AB'   new -> 'ABC'  (behavior change)")
    print("  Flow 2  b'ABC\\x00'   old -> 'ABC'  new -> 'ABC'  (no change)")
    print("  Flow 3  b'X'         old -> ''     new -> 'X'    (behavior change)")


if __name__ == "__main__":
    main()
