Terugkijken op een DVR die daar niets over losliet

Voor een klant beheer ik een setje IP-camera’s dat hangt op een oudere Samsung SRD-850D-DVR: acht kanalen, vijf camera’s actief, firmware uit 2012. De bijbehorende fabrikantssoftware is al jaren niet meer actief onderhouden: verlopen dependencies, en zelfs een werkende installer ervan vinden op het internet kostte behoorlijk wat moeite. Precies dát was de aanleiding voor Watchtower, mijn eigen Windows-app om meerdere RTSP-camerastreams tegelijk te bekijken (Electron, FFmpeg zet elke stream lokaal om naar HLS, de UI speelt dat terug met hls.js) — in plaats van te blijven vechten met verouderde software wilde ik een simpelere, modernere interface voor streamen én terugkijken. Ik ben ‘m gaan bouwen via vibe coding (snel itereren met AI-assistentie), en live meekijken stond binnen een uur overeind: gewoon RTSP, direct bruikbaar.

Terugkijken naar opgenomen beelden was een ander verhaal. Wat ik niet had ingecalculeerd: hoeveel maanden het zou kosten — en hoeveel keer ik een veelbelovend spoor weer moest afschrijven.

Dit artikel gaat niet over het eindresultaat (dat staat, iets compacter, straks bij mijn projecten). Het gaat over het proces: hoe je een protocol reverse-engineert waarvoor geen documentatie bestaat, hoe je jezelf meerdere keren voor de gek laat houden door een server die gewoon “200 OK” blijft zeggen, en wat je daarvan leert over hoe je aannames moet toetsen in plaats van erop te vertrouwen.

Het probleem: alles lijkt te werken, maar niets werkt

Live streamen ging via gewone RTSP: `rtsp://…/0` tot `/4`, één per camera, direct bruikbaar in VLC, ffmpeg en Watchtower. Voor terugkijken ligt het voor de hand om dezelfde RTSP-sessie een tijdsbereik te geven — het RTSP-protocol kent daar een standaardmechanisme voor (`Range: clock=<start>-<eind>`). De DVR accepteerde dat verzoek zonder te klagen: `200 OK`, en hij echode zelfs het opgegeven tijdsbereik terug.

Het beeld dat erna kwam, was alleen steeds… van nu.

Dat is precies het soort resultaat waar je jezelf makkelijk mee voor de gek houdt, want er komt geen foutmelding — er komt gewoon beeld. Dus heb ik het niet op het oog beoordeeld, maar getest: twee keer hetzelfde tijdstip van vandáág opvragen gaf twee verschillende beelden (logisch, want het was allebei “nu”); twee keer een tijdstip van gisteren opvragen gaf ook twee verschillende beelden terwijl een echte opname identiek had moeten zijn. Ook een tijdstip middenin de nacht gaf gewoon daglicht. Conclusie: de DVR leest het tijdsbereik, bevestigt het netjes, en negeert het vervolgens volledig. Dat was dus geen bug om te fixen — het was gewoon niet de juiste ingang.

Dode sporen opruimen

Voor je ergens dieper induikt, is het de moeite waard om te checken of iemand het probleem al heeft opgelost. Dat had ik hier niet: het enige bestaande open-source project tegen dit type DVR ondersteunde uitsluitend live beeld. De fabrikant heeft wel een eigen Windows-kijker (“SmartViewer”), maar die ondersteunt dit specifieke, oudere model niet — en een alternatieve viewer bleek na een paar uur installeren en configureren eveneens een ander, incompatibel protocol te spreken. Beide sporen leverden niets op buiten de tijd die ik erin stak, maar wel een belangrijk gegeven: er bestond geen kant-en-klare oplossing, dus zelf uitzoeken was de enige weg.

De ingebouwde webinterface van de DVR zelf werkt niet meer in een moderne browser — hij leunt op een ActiveX-plugin die alleen Internet Explorer begrijpt. Maar die plugin, een losse DLL die de DVR zelf aanbiedt voor download, bleek de sleutel: hij bevat de volledige clientlogica voor het protocol van dít model, inclusief het zoeken en afspelen van opnames.

Statische analyse: een protocol uit een DLL lezen

Met reverse-engineeringtools (ik gebruik hiervoor REA, een MCP-toolset bovenop onder andere Ghidra) kun je een gecompileerde DLL doorzoeken op strings, geëxporteerde functienamen en de manier waarop die functies met elkaar samenhangen — zonder dat er ergens broncode of een protocolspecificatie beschikbaar is. Zo kwam ik erachter dat de DVR naast de live-RTSP-poort nog een eigen, binair controleprotocol spreekt op een andere poort: een klein framingformaat met een vast “magic number”, een berichttype en een tekstuele payload van `sleutel:waarde`- paren. Daarmee kon ik inloggen, apparaatinformatie opvragen, en — belangrijker — een zoeklaag aanspreken die teruggeeft welke dagen er opnames op de schijf staan en in welke tijdvakken.

Die zoeklaag werkte meteen overtuigend: een opgevraagde maand gaf een bitmap terug van exact de dagen waarop echt was opgenomen, en een dag zonder opname gaf expliciet niets terug. Dat is precies het soort “harde” bevestiging die het eerdere RTSP-tijdsbereik miste — hier wist de DVR duidelijk wél waar zijn eigen opnames stonden.

Het probleem was de volgende stap: de media zelf ophalen. Het protocol kende daarvoor een duidelijk commando, met een nette payload (afspeeltijd, kanaal, snelheid). Op locatie bleek dat commando echter stil te worden geslikt: geen foutmelding, maar ook geen enkele byte mediadata — niet op de controleverbinding, niet op de poorten die de DVR er zelf bij aankondigde. Een DVR die onbekende of verkeerd uitgevoerde commando’s gewoon negeert in plaats van een fout te geven, geeft je geen enkel houvast om te zien of je dichtbij zit of er faliekant naast.

Terug naar de bron: meekijken met een werkende sessie

Als statische analyse en educated guessing vastlopen, is er nog één harde waarheid beschikbaar: een netwerkcapture van een client die het probleem wél oplost. Inmiddels had ik de fabrikantssoftware zelf aan de praat gekregen — dezelfde, bijna onvindbare installer die ooit de aanleiding was om Watchtower te gaan bouwen — en die bleek dit model wél te ondersteunen. Terugkijken werkte daar meteen. Een pakketcapture tijdens die sessie legde in één keer bloot wat er werkelijk over de lijn ging: een apart afspeelcommando op de bekende controleverbinding, gevolgd door een losse mediaverbinding per kanaal waarover de DVR de beelden actief naar de client duwt — een stuk dat ik niet had kunnen raden vanuit de DLL alleen, omdat die mediaverbinding in de statische analyse niet als zodanig herkenbaar was.

De beelden zelf kwamen niet als gewone RTP binnen, maar in een eigen, simpel blokformaat: vaste blokken met een kleine header (een teller, een frame-markering, een “laatste stuk van dit frame”-vlag) die, aan elkaar geplakt en van hun headers ontdaan, gewoon H.264 opleverden — dezelfde codec als het livebeeld. Dat laatste was goed nieuws: de decodeerketen die ik al voor live gebruikte, kon direct hergebruikt worden.

Ik bouwde een kleine ontleder die deze blokken herkent via de oplopende teller (robuust tegen een los pakketje verlies) en zette die om naar een schone videostroom. Op de opgenomen capture werkte dat foutloos. Het leek klaar.

De aanname die bij de echte DVR niet standhield

Een eerste test op locatie, rechtstreeks tegen de echte DVR, bevestigde opnieuw de zoeklaag (dag- en tijdvak-informatie klopte precies), maar het afspeelcommando leverde weer nul bytes op — ook na het systematisch uitsluiten van de meest logische verklaringen: verkeerde kanaalnummering, verkeerde volgorde van verbinding-openen-en-commando-versturen, een afgerond versus een exact tijdstip, een conflicterende tweede sessie. Allemaal getest, allemaal zonder verschil.

Dat was het moment om de aanname zelf in twijfel te trekken in plaats van de parameters ervan. De eerdere pakketcapture had ik ná het starten van een sessie beginnen op te nemen — dus het was nooit zeker dat ik het eigenlijke startcommando had gezien, alleen het resultaat ervan. Precies het verschil tussen “ik zag een werkende sessie” en “ik zag hoe een sessie wordt gestart”.

Een nieuwe decompilatieronde, en de uiteindelijke verklaring

Verdere analyse — deze keer tegen de daadwerkelijk geïnstalleerde desktopclient in plaats van de oudere browserplugin — bracht de echte verklaring. Het eigen binaire protocol dat ik al had gereverse-engineerd, speelt bij terugkijken geen rol in het starten van de media. Terugkijken loopt via doodgewone RTSP, op precies dezelfde verbinding als live, met twee kleine toevoegingen: een ander URL-pad specifiek voor opgenomen beelden, en één extra RTSP-header die aangeeft dat het om een afspeelverzoek gaat in plaats van live beeld. Het eigen controleprotocol blijft wel nodig — maar uitsluitend voor de zoeklaag (welke opnames bestaan er), niet voor het daadwerkelijk ophalen van de beelden.

Die ontdekking verklaarde in één keer waarom zowel mijn allereerste RTSP-poging als de latere blokformaat-aanpak faalden: de eerste miste de specifieke header en het juiste pad, de tweede loste een probleem op dat er — voor dit toestel, in deze volgorde — niet was. Het echte mechanisme zat ergens tussenin: standaard-RTSP, met één klein, niet voor de hand liggend extraatje.

Op het moment van schrijven staat dit spoor klaar om op locatie definitief bevestigd te worden. Of het klopt, weet ik dus, enigszins toepasselijk voor dit artikel, nog niet helemaal zeker — maar het is wel de eerste hypothese die geen van de eerdere, onafhankelijk bevestigde feiten tegenspreekt.

Wat dit me oplevert

Los van de techniek is dit voor mij een goed voorbeeld van hoe reverse engineering in de praktijk voelt: niet één moment van “aha, gevonden”, maar een lange reeks hypothesen die je zo hard mogelijk moet proberen te breken voordat je ze gelooft. De DVR die altijd “200 OK” zegt, ongeacht of hij je verzoek serieus neemt, dwong me om elke keer zelf een manier te vinden om een resultaat te verifiëren — met een tijdstip-vergelijking, met een capture, met een systematische uitsluiting van alternatieven — in plaats van op het eerste succesvolle antwoord te vertrouwen. Dat is precies de instelling die ik ook meeneem naar mijn gewone werk en mijn andere projecten zoals netwerken en infrastructuur: een systeem dat zegt dat iets gelukt is, heeft het nog niet per se ook echt gedaan.

Meer van wat ik bouw en onderhoud staat op de Projecten-pagina.

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *