Ich versuche mich am "Hacken" einer Firmware (ARM Cortex-M3) und habe
mir IDA (7.0 Free kann kein ARM), radare2/Cutter (komplex, muss erstmal
richtig konfiguiert werden), Binary Ninja Demo (schon ganz gut), Ghidra
(naja) mal angesehen.
Es schwankt zwischen "erkennt Funktionen nicht richtig" und
"[Oberfläche] unbedienbar".
Ich hatte gehofft, man könne den Programm- und Datenfluss (erfordert
Simulation?) einigermaßen einfach verfolgen um die für einen
interessanten Stellen zu finden und ggf. zu "hacken".
Mit komplettem Call-Graph und erklärten Instruktionen, sowie eine Kern-
und Peripherie-Register-Map (gibt es eigentlich standardisierte Daten
vom Hersteller oder köchelt da jeder seine eigene Suppe für die eigene
IDE?). Ein Simulator wäre auch nett..
Kurzum: der Einstieg erfordert viel low-level Wissen (Memory Map im
Kopf, Assembler fließend lesen, ggf. Ergebnis der automatischen Analyse
prüfen) zu erfordern, die Tools nehmen mehr oder weniger gut einen
kleinen Teil der Arbeit ab, und die Analyse erfordert sehr, sehr viel
Handarbeit.
Es ist aber wohl auch ein eher spezielles Beschäftigungsfeld.
Wie sind eure Erfahrungen?
Noob schrieb:> die Analyse erfordert sehr, sehr viel> Handarbeit.
So ist es. Manchmal erkennt man, welcher Compiler verwendet wurde und
welche Libs. Das hilft mächtig. Aber ansonsten ist Hirn angesagt (und
Geduld).
Mit einem Assembler/Disassembler hackt man nichts, sondern man wandelt
Code nur in eine andere Darstellungsform um, also Binärsprache in
Assenmbersprache. Seit wann sind das denn Code-Analysatoren?
Um einen Algorytmus zu "hacken" wäre ein Debugger das passendere
Werkzeug, denke ich. Die haben aber ihre Grenzen, weil größere
Programme damit praktisch nicht mehr zu handeln wären.
Dafür gibts ausgefeiltere Software die hier erst vor ein paar Wochen
mal erwähnt wurden. Leider habe ich mir da nichts markiert und weiß auch
nicht nach welchem Begriff ich da suchen müsste.
Da bleibt immer sehr viel Handarbeit über.
Schaut man sich beliebigen Code "von der Seite" an, so ist oft noch
nicht mal ersichtlich, ob überhaupt etwas ausführbares vorliegt, oder ob
es sich um Daten handelt.
Die Daten eines Textstrings oder einer Konstantenliste lassen sich oft
auch als ausführbarer Code darstellen und umgekehrt. Ob eine (oft
verwendet) Sprungtabelle vorliegt ist bis zu dem Zeitpunkt unklar, indem
Du die Registerbelegung kennst.
Ein schöner Gruß an den guten, alten Sisyphos.
bei alten 8-bittern war man ja noch mit fast jedem Byte per DU.
Ich habe nach 20 Jahren auch meinen alten PC1500 Code wieder
disassembliert zu Fuss, mein Vorteil es war MEIN Programm und so
ungefähr wusste ich noch wie und warum ich das so geschrieben hatte, ich
wollte den Code relokatibel machen und direkten Zugriff auf den PC hatte
ich nicht nur indirekt.
Ich habe mich aber auch nie in die vielbenutzten ROM Routinen begeben,
das wäre dann komplizierter geworden. Ich denke auch manche ROM Routinen
sind deswegen so unübersichtlich wegen Mehrfachnutzung (durch
optimnierende Assembler/Compiler)und um Platz zu sparen, da wird mit
Sicherheit nichts linear gerade runter programmiert sein.
Noob schrieb:> Es ist aber wohl auch ein eher spezielles Beschäftigungsfeld.
Es kommt drauf an, was man erreichen will.
Um die Funktionen einer Steuerung zu erfassen, muss man u.U. garnicht
disassemblieren oder hacken, da kann man phänomenologisch vorgehen -
drückt man Button X, bewegt sich die X-Achse usw. Das setzt
funktionierende Hard- und Software voraus. So entsteht praktisch ein
Pflichtenheft.
Das identische Programm wieder übersetzen zu können ist auch trivial,
dafür reichen Define Bytes mit dem ROM-Inhalt. Ist nur ziemlich sinnlos.
Programmideen zu klauen ist für jemanden, der die Qualifikation zur
Code-Analyse hat, auch uninteressant, da schreibt man den nötigen Code
schneller selber.
Sinnvoll ist es nur, einen Sourcecode zu erzeugen und zu verstehen, um
das vorliegende Programm an entscheidenden Stellen ändern und neu
übersetzen zu können, das ist die Königsdisziplin. Leider setzt das viel
Erfahrung voraus und artet trotzdem in harte Arbeit aus.
Georg
Sebastian S. schrieb:> Schaut man sich beliebigen Code "von der Seite" an, so ist oft noch> nicht mal ersichtlich, ob überhaupt etwas ausführbares vorliegt
Also bei ARM sieht man das ziemlich schnell. Wenn man irgendwelche Daten
disassembliert, stellt man schnell fest dass das ziemlicher Blödsinn
ist, z.B. wenn da 10x hintereinander die gleiche Instruktion steht.
Vielleicht ist es ja sogar typisch RISC, dass man öfters bestimmte
Muster hat, die man schnell erkennt. Der Klassiker sind ja
Funktions-Anfang und -Ende.
Bei ARM A32 Code sieht man eine Menge "E" in der hexadezimalen
Darstellung - der Condition Code "Always".
Wie schon öfter erwähnt, muss man sowieso noch einen Plan des
Portmappings erstellen - am besten ausgedruckt mit den Alternate
Functions des MC, um den es geht.
Das hat mir hier sehr gut geholfen, obwohl es nie in meiner Absicht lag,
die Firmware zu disassemblieren, sondern neue Firmware für die
vorhandene Hardware zu entwickeln. Auf dem Ausdruck kann man dann die
Funktionen abhaken, die schon erkannt sind und sich dann gezielt um die
noch fehlenden kümmern. - bis alle 176 Pins zugeordnet sind...
Zusätzlich braucht man natürlich die Datenblätter etwaiger
Peripheriechips.
Rote T. schrieb:> So gut, das manche Leute damit schon alte N64 Spiele soweit> zerlegt haben, damit man sie hinterher wieder binärkompatibel> compilieren konnte....
Da wusste man aber auch, welcher Compiler benutzt wurde, welche
Bibliotheken verwendet wurden und was für Code da hinten rausgefallen
ist.
Für eine unbekannte Cortex M3-Firmware gilt das nicht.
Noob schrieb:> Ich hatte gehofft, man könne den Programm- und Datenfluss> (erfordert Simulation?) einigermaßen einfach verfolgen um> die für einen interessanten Stellen zu finden und ggf. zu "hacken".
Das geht einigermaßen gut, je nachdem, was den Code ursprünglich erzeugt
hat (Compiler oder handgeklöppeltes Assembler), wie der Decompiler das
versucht zu verarbeiten (welche Plugins für welche
Compiler-Entry/Exit-Sequenzen er kennt), und welche Register bekannt
sind.
Der Cortex M3-Kern ist gut standardisiert, daher kennst du den
Einstiegspunkt und kannst du dich vom Reset_Handler aus durch den Code
hangeln. Der Anfang ist normalerweise auch handgeschriebener
Assemblercode, d.h. der ist auch im Disassembler recht einfach lesbar.
Was danach kommt, hängt vom Inhalt ab.
Danke für's Feedback, da lag ich ja nicht so falsch mit meiner
Einschätzung.
Zum Resethandler bin ich schon gekommen, da wird der SP explizit nochmal
per Software gesetzt (=schlechter Compiler bzw. Startup Code?),
ansonsten geht es dann los mit der fehlerhaften disassembly und Analyse
- und vielen undurchschaubaren Verzweigungen.
Mein erstes Ziel wäre, zumindest die Zugriffe auf Peripherieregister zu
finden und Schleifen aufzuspüren.
Den Befehlssatz werde ich mir also mal durchlesen müssen...
Tipp: Cutter kann offenbar nur lokale Funktionen graphisch darstellen,
für die Funktionszusammenhänge muss es dann radare2 sein (Download für
Windows von appveyor, die offiziellen Downloads sind veraltet). Ebenso
sollte man Cutter gleich in der Kommandozeile starten, sonst ist die
Konsole nicht wirklich nutzbar.
Und zum Vergleich lässt sich Binary Ninja immer mal wieder für 15
Minuten oder so nutzen (zeitbeschränkt).
georg schrieb:> Sinnvoll ist es nur, einen Sourcecode zu erzeugen und zu verstehen, um> das vorliegende Programm an entscheidenden Stellen ändern und neu> übersetzen zu können, das ist die Königsdisziplin. Leider setzt das viel> Erfahrung voraus und artet trotzdem in harte Arbeit aus.
Genau, "ein bisschen anpassen" wäre schön.
Noob schrieb:> Zum Resethandler bin ich schon gekommen, da wird der SP explizit> nochmal per Software gesetzt (=schlechter Compiler bzw. Startup Code?),
Das machen einige Startup-Codes so, weil man dort mit #ifdef arbeiten
und so ein gemeinsames Linkerscript für mehrere Chips nutzen kann.
> ansonsten geht es dann los mit der fehlerhaften disassembly> und Analyse - und vielen undurchschaubaren Verzweigungen.
Um was für einen Chip geht es denn? Wenn es was halbwegs bekanntes ist,
dann schau dir einfach mal den Startup-Code vom Hersteller als Vergleich
an.
Bist du dir sicher, dass dein Decompiler die korrekte Architektur
decompiliert? Weil "Cortex-M3" ist nicht dasselbe wie "ARM", und wenn da
eine unbekannte Instruktion steht, dann fliegt der Decompiler aus der
Bahn.
Bedenke, dass der Startup-Code in aller Regel handgeklöppelter
Assemblercode ist. Da kann man wunderschöne Tricks benutzen, wie z.B.
mehrere sich überlappende Schleifen und sowas. Ein linearer Decompiler
kommt mit sowas absolut nicht klar. ;-)
Achso, viele Sprünge sind relativ, auch zu PC oder SP. Wenn du die
Basisadresse der Firmware nicht korrekt angegeben hast, dann kann der
Disassembler die Zieladressen nicht korrekt berechnen und du bekommst
Müll. Üblicherweise kannst du dem Disassembler bestimmte Registerwerte
zu bestimmten Zeiten vorgeben, damit das funktioniert.
Auf einem Cortex-M3 gilt allgemein: Flash@0x08000000, SRAM@0x20000000
und oft irgendein Alias oder Boot-ROM oder sowas bei 0x00000000.
> Mein erstes Ziel wäre, zumindest die Zugriffe auf> Peripherieregister zu finden und Schleifen aufzuspüren.
Ich würde an deiner Stelle erstmal den Befehlsverlauf vom Startup-Code
verfolgen. Der muss logisch aufgebaut sein. Ich hänge mal einen von mir
(in C für LM3S811 im Qemu) geklöppelten Startup-Code an. Mindestens
diese Funktionalität solltest du finden, wenn die Firmware in C oder C++
geschrieben wurde.
Noob schrieb:> Den Befehlssatz werde ich mir also mal durchlesen müssen...
Das sowieso. Du musst den Assemblercode schon einigermaßen lesen können,
sonst wird das nix. Datenblatt vom Controller ist immer gut bei der Hand
zu haben, und wenn du das nicht hast, zumindest das Datenblatt vom
Cortex-M3.
Nachtrag: Praktische Erfahrung hab ich maximal mit IDA Free, da hab ich
mal angefangen, ein 80186-BIOS zu zerlegen. Also nicht unbedingt ARM
Cortex-M3, wobei ich da auch öfter mit objdump mal Hex-Dateien zerlegt
habe.