De Zyxel NSA310 bevat in zijn (haar?) naam een verwijzing naar een organisatie die een opensourcetool heeft ontwikkeld. Dankzij deze tool kon ik de logica in de firmware checksum achterhalen. Vroeger was het ontleden van machinetaal zeer moeilijk, maar dat is nu veranderd...
Henk van de Kamer
In de vorige aflevering ontdekten we met het binwalk-commando dat de eerste 317 bytes van een Zyxel NSA310 firmware-bestand een header moet zijn. Tijdens het bestuderen, zag ik in de eerste 48 bytes geen duidelijk patroon. De rest kon ik opdelen [https://www.hetlab.tk/embedded/zyxel-nsa310-firmware-header] in steeds drie stukken: twee bytes voor vermoedelijk een ID, dan vier bytes voor de lengte van de daarop volgende data. Ofwel:
00 00 04 00 00 00 31 2e 30 30 1.00
01 00 0c 00 00 00 56 34 2e 37 30 28 41 46 4b 2e 34 29 V4.70(AFK.4)
02 00 05 00 00 00 35 30 39 39 38 50998
01 01 04 00 00 00 41 32 30 33 A203
bin2ram
In de bovenstaande link laat ik zien waar de MD5-hashes vandaan komen. En wie verder leest in mijn labjournaal, ziet dat ik ook de meeste andere kan verklaren. Tot slot vond ik een bericht van iemand anders die dezelfde indeling ontdekte in een oudere firmware. Daarmee kon ik nog een aantal elementen toewijzen.
De eerste 48 bytes bevat in tweevoud de grootte van de firmware. Deze en een tweetal andere bytes zijn verschillend tussen mijn versie en die van de hierboven genoemde onderzoeker. De overige 36 bytes zijn identiek. Ik besloot dat ik genoeg informatie had om het pre-upgrade-script te vervangen door mijn eigen versie en de nodige velden aan te passen voor de kleinere lengte. Deze via de webinterface uploaden gaf een foutmelding. Omdat ik via de seriële verbinding kon zien welke processen werden opgestart, ontdekte ik het volgende:
/ # ps
...
3696 root 3544 R < /sbin/bin2ram little /mnt/ram1/ras.bin /mnt/ram2/tlv
Dat proces is vrij lang bezig, waarna mijn firmware wordt afgekeurd. Nu kunnen we de bin2ram opdracht ook handmatig starten en dat geeft uiteindelijk:
Checksum incorrect. It should be E56B instead of 344A.
De eerste is de inhoud – in omgekeerde volgorde – van de twee bytes die in beide onderzoeken verschillen. Ofwel dit is een zestien bits checksum. Mogelijk dat jij net als ik aan een CRC16 variant denkt, maar geen van de vele varianten leverde een correcte versie op.
Machinetaal
De code die de checksum berekent, is natuurlijk aanwezig in het bin2ram-programma. Deze is ruim tien kilobyte groot en dat via een disassembler uitzoeken, is een flinke klus. Waarschijnlijk moet ik dat gaan uitleggen? Veel programma’s worden in C – of andere hogere programmeertalen – geschreven. Daar kan een computer niets mee. Ofwel het geheel wordt gecompileerd naar machinetaal: een serie enen en nullen. Uiteraard kunnen we die op een scherm tonen, maar dat is een rijstebrij.
Beter leesbaar is elke byte weergeven als hexadecimale waarde. Een deel daarvan zijn opcodes, ofwel instructies die de processor begrijpt. De rest is data die deze opcodes verwachten. Het lezen en begrijpen van hexadecimale waardes is nog steeds niet gemakkelijk. In een ver verleden ontstond daarom assembly: ofwel een vertaling van elke opcode naar een leesbare afkorting. Een assembler vertaalt deze afkortingen naar machinetaal en een disassembler doet het omgekeerde.
Ghidra
Zoals gezegd, is het ontrafelen van code via een disassembler nog steeds een moeizame klus. Omdat computers tegenwoordig veel krachtiger zijn, is een vertaalslag van machinetaal naar een hogere programmeertaal mogelijk geworden. Van elke compiler kunnen we uitvogelen hoe deze een regel code vertaald naar machinetaalinstructies. Met een database van deze vertalingen, kunnen we ook het omgekeerde doen. Niet exact, want namen van variabelen gaan bijvoorbeeld verloren. In plaats daarvan zien we automatisch gegeneerde variabelen in de terugvertaling. Waarmee de machinetaal opeens zeer leesbaar wordt!
Er zijn meerdere partijen die een programma met deze truc kunnen leveren. Uiteindelijk ontdekte ik Ghidra dat door de NSA – National Security Agency – is ontwikkeld. Ooit voor intern gebruik, maar na de opkomst van de vele onveilige IoT-apparaten is het geheel nu open source. In eerste instantie twijfelde ik of een programma van de NSA kan vertrouwen. Maar een YouTubefilmpje met de belangrijkste ontwikkelaar maakte duidelijk dat het veilig houden van Amerika een hogere prioriteit heeft dan het spioneren. Dat, gecombineerd met het installeren op een virtuele Linux-installatie, is afdoende beveiliging aan mijn kant. Ik zag in ieder geval geen verdachte verbindingen naar buiten…
Stripped
Na het openen van het bin2ram in Ghidra zien we het adres (regel) waar de rest van de code zich in de binary bevind. Vervolgens de machinetaal in hexadecimale vorm en het equivalent in assembly (zie afbeelding).
Een deel van het bin2ram-programma in Ghidra |
De volgende stap is uitvogelen waar de processor start met het uitvoeren van de machinetaalinstructies. Alle hogere programmeertalen hebben een duidelijk startpunt en in C is dat de main functie. In een tweede venster vertaalt Ghidra de gevonden machinetaal naar C, maar ik kon de genoemde functie niet vinden. Via een ander YouTubefilmpje ontdekte ik dat dit het geval is in een stripped binary. In deze is een tabel met gebruikte functienamen verwijderd om een kleiner programma te krijgen. De oplossing is op zoek gaan naar een entry-functie, ofwel de code die wordt uitgevoerd voordat de main-functie kan worden aangeroepen. Die is te zien in de eerder getoonde afbeelding en de param_X zijn de parameters die we via de commandline doorgeven aan bin2ram. Deze kunnen we nu duidelijker namen geven. Nogmaals, namen van variabelen zijn niet aanwezig in de gecompileerde code. Op het einde zien we de aanroep van main via de libc-bibliotheken.
Python
Uiteindelijk vond ik de code waar de checksum wordt bepaald. Met behulp van de C-code kon ik een Python-versie schrijven die de juiste checksum berekent.
file = open("test_hk1.bin", "rb")
data = file.read(48)
checksum = 0
while data:
data = file.read(2 * 1024 * 1024)
for i in range(0, len(data) - 1, 2):
checksum += int.from_bytes(data[i:i+2], byteorder="little", signed=False)
if checksum > 65535: checksum = (checksum + 1) & 0xFFFF
if len(data) % 2 == 1:
checksum += int.from_bytes(data[i+2:], byteorder="little", signed=False)
if checksum > 65535: checksum = (checksum + 1) & 0xFFFF
file.close()
print("Zyxel checksum in header =", hex(checksum)[2:])
Het is logisch dat we de eerste 48 bytes overslaan, hierin zit immers de checksum. Dit deel van de header bevat de informatie voor de bin2ram-opdracht, de overige 269 bytes worden door een ander programma gebruikt om het geheel te splitsen in maximaal vier onderdelen. De checksum is domweg het optellen van words – twee bytes – met een speciale truc indien het resultaat groter is dan in een word past. Indien een bestand een oneven aantal bytes groot is, moet op het einde een word gemaakt worden, vandaar de laatste if-constructie.
Hacken
Als we bovenstaande Python-code gebruiken na het in elkaar sleutelen van een eigen firmware, kunnen we de berekende checksum op de juiste positie in de eerste 48 bytes doorvoeren. Waarmee we in theorie de Zyxel webinterface kunnen misbruiken om te upgraden naar bijvoorbeeld OpenWrt of Debian, zonder dat we de behuizing hoeven te openen voor het aansluiten van een seriële verbinding.
In theorie? Nadat bin2ram de firmware de checksum goedkeurt, worden de eerste 48 bytes verwijderd. Het resultaat wordt door het fw_unpack-programma uitgepakt [https://www.hetlab.tk/embedded/zyxel-nsa310-firmware-uitpakken]. Tot slot wordt het resultaat weer verwerkt door een shell-script. Ik vermoed dat ik deze kan onderbreken en zo de hele upgrade kan regelen via een pre-upgrade shell-script in de firmware zelf. Op dit punt ben ik helaas met mijn onderzoek gestopt vanwege dringende familiezaken.
Tot slot
Hopelijk heb ik je in deze twee afleveringen overtuigd om zelf een oude NAS van de schroothoop te redden. Indien je in een mail of nog beter een eigen labjournaal jouw onderzoek beschrijft, wil ik graag meedenken als je vastloopt. Wie echter te lui is om zelf het werk te doen, zal van mij geen antwoord krijgen.