Research / Protocol Reverse Engineering

Deconstructing BrählerOS: Reverse Engineering a Parliamentary Protocol from Scratch

April 2026 reverse-engineeringprotocol-analysisdotnetnode.jspowershellbinary-parsing
BrählerOS manages the microphones, speaker lists, and request queues in a national parliament's chamber. It exposes a TCP service on port 400 with a completely undocumented binary protocol. Starting from a running Windows binary with zero documentation, I decompiled the server, cracked the wire format, and shipped a working open-source Node.js client library — all without ever touching a proprietary SDK.

1. Target Overview

BrählerOS (braehlerOS.exe) is a parliamentary conference management platform used in legislative chambers worldwide. The deployment I studied manages approximately 107 delegate DigiMIC microphone units seated in a national parliament. Delegates press their unit to request the floor; the operator grants or removes them from the speaker list in real time.

The system exposes a TCP service called ConfClientServicePort on port 400. Both the operator console (ConfOperator.exe, a Unity desktop application) and the physical delegate processing units speak to this port. It delivers the full conference state on connect and then streams live events as delegates interact with their hardware.

STACK: Server is .NET Framework 4.7.2 (C#). The operator client is a Unity application — meaning all game logic lives in ConfOperator_Data/Managed/Assembly-CSharp.dll, a fully decompilable managed CIL assembly shipped alongside the Unity player binary.

2. Recon

Decompiling the Binaries

braehlerOS.exe is a standard .NET PE. One pass of ilspycmd produced the full server source. Several supporting assemblies were referenced:

  • APATools.dll — low-level transport, framing, and message queueing
  • SMic.Basics.dll — serialisation primitives
  • SMic.Database.dll — entity definitions (conference, delegates, seats)

The Unity operator client (ConfOperator.exe) is a native 64-bit binary and cannot be decompiled directly. But Unity ships all managed C# code as CIL in Assembly-CSharp.dll, which decompiles cleanly. This is where the handshake logic, packet parsers, and command enum lived.

Config Exposure

Found immediately alongside the binary:

<!-- braehlerOS.exe.config -->
<add name="MainDB"
     connectionString="Host=localhost;Port=5432;Database=BDB1_1;Username=braehler;Password=REDACTED" />

Plaintext PostgreSQL credentials in the application config. This gave direct database access for schema validation and for cross-referencing data observed on the wire — enormously useful when trying to confirm whether a parsed field was actually what I thought it was.

3. Wire Protocol

3.1 Framing

The TCP framing scheme, found in the APATools receive loop:

[Int32 LE  cmd      ]
[Int32 LE  payloadLen]
[payloadLen bytes   ]

Every message is self-contained. A payloadLen of zero is valid. The 8-byte header is always present.

3.2 The String Encoding Trap

This is where my first two hours disappeared. There are two incompatible string encoding formats in the codebase. Using the wrong one cascades into every subsequent field offset being wrong.

SMic.Basics.Memstream — used by port 400:

[Int32 charCount][charCount × 2 bytes, UTF-16LE]

APATools.MemStream — used by other services:

[Int32 byteLen][byteLen bytes, UTF-8]

I confirmed the correct format by tracing the code path of port 400 connections specifically through SMicClientConnection.doReceiveLoop, which constructs a SMic.Basics.Memstream reader — not an APATools.MemStream.

3.3 Handshake

The handshake, reconstructed from RequestLogin() in Assembly-CSharp.dll:

Client → Server: ClientIdentification (cmd 58)

WriteString(guid)    →  [Int32 charCount][UTF-16LE bytes]
WriteInt(clientType) →  [Int32]

The GUID is a freshly generated UUID string (36 chars with dashes). The ClientType enum:

ValueMeaning
1Viewer
2Client (Operator)

ClientType = 1 (Viewer) was rejected with state = 0. The deployment had no viewer licenses configured. ClientType = 2 went straight through.

Server → Client: License Ack (cmd 58)

[Int32 state]    — 1 = approved
[String version] — e.g. "2.3.0"

On approval, the server immediately begins the initialization burst.

4. The Initialize Packet

After the license ack, the server emits a burst of packets beginning with cmd 66 (BeginInitialize) and ending with cmd 67 (EndInitialize). The centrepiece is cmd 1 — Initialize: a single serialised payload containing nine database tables.

Structure

[Int32 = 9]              ← legacy table count header, ignored by clients
For each of 9 tables:
  [String tableName]
  [Int32 rowCount]
  For each row:
    [VirtualTable metadata header]  ← 6 fields, same across ALL tables
    [table-specific fields]

The table order is fixed: Conference, DelegateData, Delegates, Seats, ConferenceDelegateSeatMap, AgendaItem, DelAgendaItem, DocAgendaItem, Documents.

The VirtualTable Header

Every row — regardless of table — begins with a fixed 6-field header emitted by VirtualTable.WriteData:

String tableName
String internalID
String primaryIDName
String condition
Int32  cmd
String fieldlist

Missing this header was the most expensive bug. My first parser skipped it, so every table’s field reads started at the wrong offset. The fields parsed as valid data — just completely wrong data. Only when I hex-dumped a known row and manually walked the bytes did the header become visible.

Name Resolution Chain

The ConferenceDelegateSeatMap table is the key linking table. It maps a physical seat number to a delegate:

Long MapID
Long ConferenceID
Long DelegateID
Long SeatNumber      ← plain integer (e.g. 97), not an FK to the Seats table

Resolution for any live event:

seatNumber → seatMap[seatNumber]   → DelegateID
           → delMap[DelegateID]    → DelegateDataID
           → ddMap[DelegateDataID] → { firstName, lastName, constituency }

The DelegateData table stores the delegate name in the LastName field and constituency in the Organisation field. A bit janky, but it works.

5. Speaker and Request Events

Dual-Format Discovery

This is the part that surprised me most. The server reuses command numbers for two semantically different formats depending on whether EndInitialize has been sent:

During init — bulk state dump (e.g. cmd 3, initial speaker list):

[Int32 count]
For each entry:
  [Int64 seatNumber]  ← plain integer (e.g. 97)
  [Int64 dcenUnit]    ← linearised hardware unit index
  [Int32 pos]         ← always -1
  [Int32 state]
  [Int32 flags]

After EndInitialize — live incremental updates:

[Int64 seatId]        ← compound: (seatNumber << 32) | lineNumber
[Int64 dcenUnit]      ← compound: (unitIndex << 32) | 0
[Int32 pos]           ← actual 0-based queue position
[Int32 state]
[Int32 flags]

I discovered the compound encoding empirically. A live add event produced seatId = 416611827713. I stared at that for a while, then tried shifting right by 32: 416611827713 >> 32 = 97. Known seat number. The low 32 bits (416611827713 & 0xFFFFFFFF) = 1 was the DCEN line number.

Remove events (cmd 4, 6, 47) always use the compound format regardless of init state.

Complete Command Reference

CmdDescription
1Initialize — full DB snapshot
2ConferenceState
3Speaker added / initial speaker list
4Speaker removed
5Request added / initial request list
6Request removed
11DelegateLogin
46Intervention added / initial list
47Intervention removed
50Previous speaker added (history)
51Previous speaker removed (history)
58ClientIdentification / ack
66BeginInitialize
67EndInitialize

6. Implementation Challenges

.NET Framework DLL in PowerShell 7

My first approach: load APATools.dll at runtime via reflection and compile a thin wrapper with Add-Type. PowerShell 7 uses Roslyn targeting .NET Core. APATools.dll targets .NET Framework 4.x. Resolution of framework assembly references (mscorlib, System.Collections.NonGeneric) failed across multiple attempts with explicit assembly path injection.

The root cause is a fundamental incompatibility between Roslyn/.NET Core and .NET Framework 4.x runtime assembly resolution. No workaround exists that doesn’t involve hosting the .NET Framework runtime explicitly.

Resolution: abandoned DLL loading entirely and reimplemented the wire protocol from scratch in pure PowerShell. No Add-Type, no reflection, no external assemblies. Painful but clean.

Nested Class Resolution

Reflection lookup APATools.Message returned null. .NET uses + to denote nested classes: the correct name was APATools.Queue+Message. Without this, the message processing pipeline silently dropped all received packets — no error, just nothing.

NOTE: If you're reflecting on .NET assemblies and getting null for a type that clearly exists in the decompiled source, check whether it's a nested class. OuterClass+InnerClass is the reflection name.

Variable Interpolation Collision in PowerShell

# Broken — PowerShell parses "${tbl}:" as a scoped variable reference
Write-Host "${tbl}: $count rows"

# Fixed
Write-Host "$($tbl): $count rows"

PowerShell’s ${name} syntax resolves a variable with that exact name, including the colon as a scope qualifier. So ${tbl}: was interpreted as “variable tbl in scope named by what follows the colon” — which is nothing, producing an empty string.

TcpClient Constructor

TcpClient(IPEndPoint localEP, IPEndPoint remoteEP) throws ArgumentNullException if you pass $null for the local endpoint. Fixed by using [System.Net.IPAddress]::Any with port 0.

7. Results

9
DB tables decoded from wire
17
command types mapped
0
proprietary SDKs used

The full reverse-engineering effort produced two working artefacts:

poc-speakerlist.ps1 — a pure PowerShell wire-protocol monitor. Connects to a live server, parses the full initialization snapshot, and streams annotated speaker/request/intervention events with delegate names and constituency in real time. Zero dependencies; runs on any Windows machine with PowerShell 7.

node-braehleros (npm) — a production-quality Node.js client library with zero dependencies. Implements the full protocol with AbortSignal lifecycle control, exponential backoff reconnection, send queuing, and a clean event-driven API surface. Built for real-time parliamentary dashboards.

Both were validated against a live production deployment serving a national parliament in session.

DISCLOSURE: This research was conducted on a system I had authorised access to. No disruption was caused to parliamentary proceedings. The findings are published to document protocol behaviour for legitimate integration purposes.

8. Takeaways

A few things worth noting for anyone doing similar work:

  • Decompile the client, not just the server. The operator Unity client contained the handshake logic and packet parsing code that made the server-side code interpretable. The server code alone doesn’t tell you what a client is supposed to send first.

  • Find the encoding mismatch early. Two incompatible string formats in the same codebase, each used by different services, is a silent failure mode. Confirm which one your target path uses before writing a single byte of parser.

  • The VirtualTable header is invisible until it isn’t. No comment in the decompiled code says “every row starts with a 6-field metadata block.” You find it only by following the call graph through VirtualTable.WriteData and realising the caller never skips it.

  • Hex dump everything during init. The bulk/compound dual-format for speaker events would have taken far longer without manually hex-dumping the init payload and comparing field values against known seat numbers.

References

  • braehlerOS.exe — decompiled via ilspycmd
  • ConfOperator_Data/Managed/Assembly-CSharp.dll — decompiled via ilspycmd
  • APATools.dll, SMic.Basics.dll, SMic.Database.dll — inspected via reflection and decompilation
  • PostgreSQL dump of BDB1_1 — used for schema validation
  • node-braehleros on npm
  • PROTOCOL.md — wire protocol reference
  • Full research repository