Digitales Schwarzes Brett

02.03.2020 Jonas L. App-Kritik

Das digitale schwarze Brett ist eine Lösung von “heineking media”, die u.a. an Schulen für die Darstellung von Vertretungsplänen genutzt wird. Das umfasst Monitore, eine Webseite und Client-Apps, aber nicht das Erstellen von Vertretungsplänen.

Diese Lösung ist wie auch viele andere Produkte eine Cloud-Lösung. Das heißt, dass man die Daten an den Anbieter sendet, nur damit diese von dort aus zu den Endgeräten kommen. Was bei einer Schul-Webseite aufgrund der Internetverbindungen der Schulen normal ist, ist bei Inhalten für Monitore, die im gleichen Gebäude hängen, irgendwie viel zu umständlich.

Im Fall von Schulen werden die Vertretungspläne mehr oder weniger automatisiert (per Webbrowser oder per Tool des Anbieters) an den Anbieter gesendet. Dieser baut daraus eine statische HTML-Datei und Benachrichtigungen, die dann ausgeliefert werden. Die Preise gibt es nur auf Anfrage und ich hatte keine Lust auf einen Identitätsdiebstahl (nach einer Schule suchen und mich per E-Mail als Vertreter dieser ausgeben, um nach den Preisen zu fragen), daher kenne ich diese nicht. Ich gehe aber davon aus, dass diese deutlich über dem Wert der Dienstleistung liegen, weil die Zielgruppe Schulen und damit Lehrer und keine IT-Fachkräfte sind.

Ich habe DSB-Zugangsdaten und kenne auch die zugehörige Schule. Bei dem Ansatz, dort hinzufahren und zu fragen, ob ich mir die Technik der Monitore genauer ansehen darf, habe ich nicht erwartet, dass ich weit komme. Daher habe ich es nicht gemacht.

Dann bleibt noch die Android-App und der Webclient für Schüler. Beides kann ich bequem analysieren.

Zuerst die Webversion (dsbmobile.de).

Amüsant ist der Footer ©2008-2016 heinekingmedia GmbH. Man kann ja per JavaScript/ PHP/ was auch immer einfach die aktuelle Jahreszahl bei so etwas einsetzen lassen. Man kann es auch jedes Jahr manuell aktualisieren. Wenn dort aber so eine alte Jahreszahl steht, dann könnte man meinen, dass es ungepflegt ist.

Positiv ist anzumerken, dass es keine Drittquellen gibt. Negativ ist, dass man dort weder HSTS (HTTP strict transport security = HTTPS bei zukünftigen Aufrufen erzwingen) noch einen CSP-Header (CSP = Content Security Policy = Whitelist, von wo Skripte, Bilder, Schriften u.s.w. geladen werden dürfen) hat.

Nicht besonders einladend ist der Header “Server: Microsoft-IIS/7.5”. Das liegt aber nur daran, dass ich Microsoft nicht mag. Anders sieht es aus mit “X-AspNet-Version: 4.0.30319”. Ein kurzer Blick auf Wikipedia zeigt, dass ASP.Net 4.0 am 12. April 2010 eingeführt wurde und nicht mehr gepflegt wird.

Dann melde ich mich mal an. Um Nutzer mit geöffneten Entwicklertools im Browser zu nerven gibt es ein wichtiges Skript, das unleserlich gemacht wurde und sehr oft das debugger Statement enthält, wodurch man immer wieder “Fortsetzen” wählen darf und der Browser irgendwann keine Lust mehr hat, die Seite weiter zu laden. Daher wiederhole ich den Spaß und öffne die Entwicklerwerkzeuge erst, nachdem die Seite geladen wurde. Um etwas zu sehen gehe ich von den Plänen zu den Aushängen. Dann werden Vorschaubilder geladen. Dann wähle ich einen Aushang. Vom Aussehen her sieht es aus wie ein Word-Dokument. Durch die Leitung kam allerdings ein png-Bild. Entsprechend interaktiv sind auch die Links im Dokument - man kann Links anklicken, aber das löst nur die Zoomfunktion aus.

Bis hierhin also Methoden, um bei Analysen zu nerven. Dann muss jetzt mitmproxy herhalten, um eine Analyse der Anfragen zu ermöglichen. Dabei gibt es eine interessante Anfrage.

Anfrage der Webseite vom digitalen schwarzen Brett

Antwort auf die Anfrage für Inhalte beim digitalen schwarzen Brett

Data und d sehen unlesbar aus (d ist natürlich nicht wirklich verpixelt gewesen, aber es sah dem Data ähnlich). Im Fall von data stehen am Ende zwei “=”. Das sieht nach einer Base64-“Verschlüsselung” aus. (Da man bei Base64 nur das Verfahren kennen muss und man keinen Schlüssel hat ist es keine Verschlüsselung, sondern eine Kodierung.)

An die Anfrage (Data) kommt man mittels

echo "H4sIAAAAAAAAA4VP0UoDMRD8lZAnBZvbpN5xpk+KKELFPtgqikjSrHeh1+S4NO2h+O/mToqPwj7Mzgwzu190GbC7M1RSejbixeEXX2ofqHx9S6htV9gF611SBJsmca5cFVWFiTCY9ofw57j3n7ZpVJYzICfPnM/I3LrYk74s3ovzGen2sigZnJJbXG98JoBDGk5ubIcfvs8GkY6tx7OucW/XQ9cT6kQnZhFDfVSvojMNjptBVqN1uLGu2qKxillX+xiQmaC3XtsG2QH1EKl2Q6AAARMQE1E+cpB5LqcXDCB/GV8Mu2Vr/vF9/wBaVTH1QQEAAA==" | base64 --decode

Das Resultat ist aber alles andere als lesbar. Wenn man die Ausgabe in eine Datei schreibt, dann wird es als Archivdatei eingestuft. Wenn man es öffnet, dann ist darin eine Textdatei, bei deren Öffnen es einen Fehler gibt. Das liegt daran, dass es nur per gzip komprimiert ist, aber darin kein Tar-Archiv ist sondern direkt ein Text. Also muss man an den Befehl ein | gunzip anhängen. Die Ausgabe ergibt dann mehr Sinn:

{"UserId":"","UserPw":"","Abos":[],"AppVersion":"2.3","Language":"de","OsVersion":"Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0","AppId":"","Device":"WebApp","PushId":"","BundleId":"de.heinekingmedia.inhouse.dsbmobile.web","Date":"2020-02-28T10:55:39.005Z","LastUpdate":"2020-02-28T10:55:39.005Z"}

Was der Browser im Rahmen der Header sendet steht hier also noch einmal.

Dann noch der Inhalt von d nach dem lesbar machen (gekürzt und mit ersetzten Angaben):

{
  "Resultcode": 0,
  "ResultStatusInfo": "",
  "StartIndex": -1,
  "ResultMenuItems": [
    {
      "Index": 0,
      "IconLink": "",
      "Title": "Inhalte",
      "Childs": [
        {
          "Index": 1,
          "IconLink": "https://app.dsbcontrol.de/static/androidicons/Tiles.png",
          "Title": "Aushänge",
          "Root": {
            "Id": "",
            "Date": "",
            "Title": "",
            "Detail": "",
            "Tags": "",
            "ConType": 0,
            "Prio": 0,
            "Index": 0,
            "Childs": [
              {
                "Id": "0000001-0002-0003-0004-00000000005",
                "Date": "01.01.2020 12:00",
                "Title": "Aushang 1",
                "Detail": "",
                "Tags": "",
                "ConType": 2,
                "Prio": 0,
                "Index": 0,
                "Childs": [
                  {
                    "Id": "0000001-0002-0003-0004-00000000005",
                    "Date": "01.01.2020 12:00",
                    "Title": "Aushang 1",
                    "Detail": "https://app.dsbcontrol.de/data/0000006-0007-0008-0009-00000000010/0000001-0002-0003-0004-00000000005/0000001-0002-0003-0004-00000000005_000.png",
                    "Tags": "",
                    "ConType": 4,
                    "Prio": 0,
                    "Index": 0,
                    "Childs": [],
                    "Preview": "0000006-0007-0008-0009-00000000010/0000001-0002-0003-0004-00000000005/0000001-0002-0003-0004-00000000005_000.png"
                  },
                  {
                    "Id": "0000001-0002-0003-0004-00000000005",
                    "Date": "01.01.2020 12:00",
                    "Title": "Aushang 1",
                    "Detail": "https://app.dsbcontrol.de/data/0000006-0007-0008-0009-00000000010/0000001-0002-0003-0004-00000000005/0000001-0002-0003-0004-00000000005_001.png",
                    "Tags": "",
                    "ConType": 4,
                    "Prio": 0,
                    "Index": 1,
                    "Childs": [],
                    "Preview": "0000006-0007-0008-0009-00000000010/0000001-0002-0003-0004-00000000005/0000001-0002-0003-0004-00000000005_001.png"
                  }
                ],
                "Preview": ""
              }
            ],
            "Preview": ""
          },
          "Childs": [],
          "MethodName": "tiles",
          "NewCount": 0,
          "SaveLastState": true
        },
        {
          "Index": 0,
          "IconLink": "https://app.dsbcontrol.de/static/androidicons/Timetable.png",
          "Title": "Pläne",
          "Root": {
            "Id": "",
            "Date": "",
            "Title": "",
            "Detail": "",
            "Tags": "",
            "ConType": 0,
            "Prio": 0,
            "Index": 0,
            "Childs": [
              {
                "Id": "00000011-0012-0013-0014-00000000015",
                "Date": "01.01.2020 12:00",
                "Title": "HTMLVplan",
                "Detail": "",
                "Tags": "",
                "ConType": 2,
                "Prio": 0,
                "Index": 0,
                "Childs": [
                  {
                    "Id": "00000011-0012-0013-0014-00000000015",
                    "Date": "01.01.2020 12:00",
                    "Title": "Vertretungsplan 2020-01-01.html",
                    "Detail": "https://app.dsbcontrol.de/data/0000006-0007-0008-0009-00000000010/00000011-0012-0013-0014-00000000015/00000011-0012-0013-0014-00000000015.htm",
                    "Tags": "",
                    "ConType": 6,
                    "Prio": 0,
                    "Index": 0,
                    "Childs": [],
                    "Preview": "0000006-0007-0008-0009-00000000010/00000011-0012-0013-0014-00000000015/preview.png"
                  }
                ],
                "Preview": ""
              }
            ],
            "Preview": ""
          },
          "Childs": [],
          "MethodName": "timetable",
          "NewCount": 0,
          "SaveLastState": true
        }
      ],
      "MethodName": "",
      "NewCount": 0,
      "SaveLastState": true
    },
    {
      "Index": 1,
      "IconLink": "",
      "Title": "Sonstiges",
      "Childs": [
        {
          "Index": 0,
          "IconLink": "https://app.dsbcontrol.de/static/androidicons/Feedback.png",
          "Title": "Feedback",
          "Childs": [],
          "MethodName": "feedback",
          "NewCount": 0,
          "SaveLastState": false
        },
        {
          "Index": 1,
          "IconLink": "https://app.dsbcontrol.de/static/androidicons/About.png",
          "Title": "Info",
          "Childs": [],
          "MethodName": "about",
          "NewCount": 0,
          "SaveLastState": false
        },
        {
          "Index": 2,
          "IconLink": "https://app.dsbcontrol.de/static/androidicons/Logout.png",
          "Title": "Logout",
          "Childs": [],
          "MethodName": "logout",
          "NewCount": 0,
          "SaveLastState": false
        }
      ],
      "MethodName": "",
      "NewCount": 0,
      "SaveLastState": true
    }
  ],
  "ChannelType": 0,
  "MandantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Was hier die gleiche Zeichenfolge hat, hatte auch im Original die gleiche Zeichenfolge. Unter dem Aspekt ist es interessant, dass sich manche IDs wiederholen und einige IDs mehrfach in URLs vorkommen. Allgemein haben die URLs immer die gleiche Struktur. Dazu kommt die relativ umständliche Verschachtelung der Informationen.

Und all diesen Unsinn versucht man mit mehreren Verschleierungstechniken zu verstecken. (Im Normalfall ist in der Anfrage und Antwort der eigentliche Inhalt direkt kodiert … und das verschleierte Anti-Debugging-Skript ist auch eine Verschleierung) Scheinbar war die Verschleierung auch so erfolgreich, dass schon eine andere Person, die mit der offiziellen App unzufrieden war, eine eigene gebaut hat und wahrscheinlich ähnlich vorgegangen ist bei der Analyse.

Daher noch ein paar Ideen, mit denen die Verschleierung erfolgreicher gewesen wäre:

  • der GZ-Header/ Teile des GZ-Headers sind immer gleich, man hätte ihn für die Übertragung entfernen können (dann hätte der Dateimanager es nicht als Archiv eingestuft)
  • anstelle einer Kodierung (Base64) hätte man eine richtige Verschlüsselung mit hart kodierten Schlüssel verwenden können; Da hätte man XOR und eine Umsortierung der Bytes oder auch etwas richtiges wie AES verwenden können

Diese Techniken hätten dann die Nutzung eines Decompilers/ eine Analyse des Skriptes erforderlich gemacht. Es wäre natürlich immer noch Unsinn gewesen.

Um es kurz zu machen: Die offizielle Android-App verwendet fast die gleiche Schnittstelle. Dort ist die URL https://app.dsbcontrol.de/JsonHandler.ashx/GetData, die Nutzernamen und Passwörter werden über die Felder “UserId” und “UserPw” übermittelt und einige der Felder haben andere Belegungen.

Viel interessanter ist die App aus Nutzersicht. Die App hat ihr letztes Update (Version 2.5.9) am 18. Februar 2016 bekommen.

Nach dem Starten will die App den Zugriff auf den externen Speicher bekommen. Wenn man das ablehnt wird man wieder darauf hingewiesen. Nun kann man unter Android Berechtigungen dauerhaft ablehnen. Dann gibt es ein Dialog, das - wenn man es bestätigt - direkt wieder geöffnet wird.

Beim digitalen schwarzen Brett kennt man sich mit dem Android-Berechtigungssystem aus - und auch die Qualitätssicherung war bei dem Thema gründlich

Dazu muss man sagen, dass jede App ihren eigenen Ordner bekommt, auf den sie ohne irgendeine Berechtigung zugreifen kann. Mit der Speicherzugriffsberechtigung kann man auch auf andere Ordner zugreifen. Wenn man aber keine anderen Daten lesen will, dann kann man den eigenen Ordner nehmen und dann braucht man diese Berechtigung überhaupt nicht.

Da es nur ein Emulator ist erteile ich die Berechtigung über die Systemeinstellungen. Weil es so viel Spaß macht trenne ich die Internetverbindung und wähle danach den Demo-Button in der App.

Positiv: Beim digitalen schwarzen Brett kann man eine passende Fehlermeldung anzeigen, wenn es keine Verbindung gibt

Da war man sogar lernfähig. In einer früheren Version stand da etwas von einem IO-Fehler. (IO = Input Output = Eingabe und Ausgabe, in dem Fall auf das Netzwerk bezogen)

Dann habe ich einen sehr üblen Fehler gefunden: Man kann sich durch das Scannen eines Barcodes anmelden. Da die “Nutzernamen” zum Anmelden große Zahlen sind kann das sogar praktisch sein. (Das Passwort kann man sich in meinem Fall besser merken als den Benutzernamen …) Zum Scannen eines Barcodes braucht man die Kamera-Berechtigung. Die muss man anfordern, bevor man die Kamera benutzt. Aber das macht die App nicht. Wenn man den Barcode-Button betätigt gibt es ein Scan-Fenster und einen schwarzen Hintergrund. Erst wenn man über die Systemeinstellungen die Berechtigung gewährt funktioniert es. Dabei kann man bei der Kamera-Berechtigung (wie auch bei vielen anderen Berechtigungen) ein Berechtigungsanfragedialog anzeigen.

Die App hat eine geniale Funktion: Wenn man sie neu startet muss man sich nicht erneut anmelden. Dafür gibt es etwas Schönes beim Beenden mittels Zurück-Taste:

Beim digitalen schwarzen Brett will man die Benutzer in der App halten, aber warum eigentlich?

Den Titel “Hinweis” hätte man besser weglassen sollen. Und was will ich wohl, wenn ich die Zurück-Taste drücke? Man hätte das Menü öffnen können und beim nächsten Betätigen die App schließen können (das soll man nicht so machen) oder man hätte ein Toast (Hinweis am unteren Bildschirmrand) anzeigen können, dass man zum Verlassen der App die Zurück-Taste erneut drücken soll. In der Form ist das aber unpassend.

In meiner Testumgebung, die sogar auf Deutsch gestellt war, gab es Probleme beim Zeichensatz. In den Vertretungsplänen war anstelle von Umlauten ein Platzhaltersymbol. Ich kann nicht eindeutig sagen, ob das Problem vor dem Hochladen zum DSB oder erst dort entstanden ist. Einen wertigen Eindruck hinterlässt das zumindest nicht,

Wenn man einen Vertretungsplan öffnet, die App schließt, die Verbindung trennt und die App wieder öffnet könnte man erwarten, dass man den Vertretungsplan immer noch öffnen kann. Stattdessen hängt die App für eine Weile und danach kommt eine Fehlermeldung, wie ich sie schon gezeigt habe. Danach kann man die App aber normal bedienen und den zuvor geöffneten Plan kann man wieder öffnen. Das Hängen spricht dafür, dass die Netzwerkkommunikation im UI-Thread und nicht in einem Hintergrundthread gemacht wurde. Das sollte man aber absolut nie so machen, damit die Oberfläche responsiv bleibt.

Wenn ich Offline bleibe und etwas öffne, was nicht davor geöffnet wurde, gibt es sogar eine andere Fehlermeldung:

Immer noch vorhanden: zu technische Fehlermeldungen beim digitalen schwarzen Brett

Wenn man “Nein” wählt kann man sich endlos den Ladebildschirm ansehen oder die Zurück-Taste drücken:

sieht aus wie ein Ladebildschirm, aber man kann beim digitalen schwarzen Brett endlos warten und es passiert Nichts weiteres

Was man aber wieder kann sind Animationen, wenn es mehrere Dateien in einer Ansicht gibt. Dann fliegen die vom rechten Bildschirmrand aus ein.

Man hat also viel gemacht, aber anstelle einer guten Umsetzung der Kernfunktion gibt es viele Details, die genau dann dran wären, wenn die Kernfunktionen gut umgesetzt wären.


Was man beim DSB kann ist Marketing, ich “empfehle” dazu digitales-schwarzes-brett.de. Die Technik dahinter finde ich nicht besonders überzeugend.