Frage:
Warum sind Maschinencode-Dekompilierer weniger leistungsfähig als beispielsweise die für CLR und JVM?
Rolf Rolles
2013-03-27 15:12:24 UTC
view on stackexchange narkive permalink

Java- und .NET-Dekompilierer können (normalerweise) einen nahezu perfekten Quellcode erzeugen, der oft sehr nahe am Original liegt.

Warum kann dies nicht für den nativen Code durchgeführt werden? Ich habe ein paar ausprobiert, aber sie funktionieren entweder nicht oder erzeugen ein Durcheinander von Gotos und Casts mit Zeigern.

Es ist großartig, dass Sie diesen Beitrag geschrieben haben, aber er muss immer noch in Form von Fragen und Antworten vorliegen. Wenn du daraus eine Reihe von Fragen machen könntest, wäre es sogar noch besser :)
Ist das besser?
Erweitern Sie wirklich, wie Sie die Wiederherstellung von Code auf hoher Ebene erschweren können? Ich würde diesen Teil der Frage überspringen und dies einfach über die Dekompilierung machen. Ihre Antwort ist sehr gut, obwohl imo.
@IgorSkochinsky Haben Sie gerade Ihren Hex-Rays-Dekompiler mit dieser Bearbeitung als beschissen bezeichnet? : P.
Nun, ich ging mit dem allgemeinen Gefühl um, das man in vielen solchen Fragen lesen kann :)
Ich habe versucht, es schöner zu machen. Nicht sicher, ob es noch den Geist der Frage Rolf erfasst?
Ja, es funktioniert. Grundsätzlich habe ich es so geschrieben, dass in Zukunft darauf Bezug genommen werden kann, daher ist es mir eigentlich egal, wie der Titel lautet. Ihr Titel fängt jedoch den Geist des Fragens und der Antwort perfekt ein, sodass er für mich großartig aussieht.
Zwei antworten:
#1
+40
Rolf Rolles
2013-03-27 15:12:24 UTC
view on stackexchange narkive permalink

TL; DR: Maschinencode-Dekompilierer sind sehr nützlich, erwarten jedoch nicht dieselben Wunder, die sie für verwaltete Sprachen bieten. Um einige Einschränkungen zu nennen: Das Ergebnis kann im Allgemeinen nicht neu kompiliert werden, es fehlen Namen, Typen und andere wichtige Informationen aus dem ursprünglichen Quellcode, es ist wahrscheinlich viel schwieriger zu lesen als der ursprüngliche Quellcode ohne Kommentare und kann seltsam hinterlassen prozessorspezifische Artefakte in der Dekompilierungsliste.

  1. Warum sind Dekompilierer so beliebt?

    Dekompilierer sind sehr attraktive Reverse Engineering-Tools, weil Sie haben das Potenzial, viel Arbeit zu sparen. Tatsächlich sind sie für verwaltete Sprachen wie Java und .NET so unangemessen effektiv, dass "Java- und .NET-Reverse Engineering" als Thema praktisch nicht vorhanden ist. Diese Situation führt dazu, dass sich viele Anfänger fragen, ob dies auch für Maschinencode gilt. Dies ist leider nicht der Fall. Maschinencode-Dekompilierer existieren und sind nützlich, um dem Analysten Zeit zu sparen. Sie sind jedoch nur eine Hilfe für einen sehr manuellen Prozess. Der Grund dafür ist, dass Bytecode-Sprache und Maschinencode-Dekompilierer vor unterschiedlichen Herausforderungen stehen.

  2. Werden die ursprünglichen Variablennamen in der dekompilierten Quelle angezeigt? Code?

    Einige Herausforderungen ergeben sich aus dem Verlust semantischer Informationen während des gesamten Kompilierungsprozesses. Verwaltete Sprachen behalten häufig die Namen von Variablen bei, z. B. die Namen von Feldern innerhalb eines Objekts. Daher ist es einfach, dem menschlichen Analytiker Namen zu präsentieren, die der Programmierer erstellt hat und die hoffentlich von Bedeutung sind. Dies verbessert die Geschwindigkeit des Verstehens von dekompiliertem Maschinencode.

    Auf der anderen Seite zerstören Compiler für Maschinencode-Programme normalerweise die meisten dieser Informationen, während sie das Programm kompilieren (möglicherweise lassen sie einige davon in Form von Debug-Informationen zurück). Selbst wenn ein Maschinencode-Dekompiler in jeder anderen Hinsicht perfekt wäre, würde er dennoch nicht informative Variablennamen (wie "v11", "a0", "esi0" usw.) rendern, die das menschliche Verständnis verlangsamen würden .

  3. Kann ich das dekompilierte Programm neu kompilieren?

    Einige Herausforderungen betreffen das Zerlegen des Programms. In Bytecode-Sprachen wie Java und .NET beschreiben die dem kompilierten Objekt zugeordneten Metadaten im Allgemeinen die Positionen aller Code-Bytes innerhalb des Objekts. Das heißt, alle Funktionen haben einen Eintrag in einer Tabelle in einer Kopfzeile des Objekts.

    In der Maschinensprache hingegen, um beispielsweise die x86-Windows-Demontage ohne die Hilfe umfangreicher Debug-Informationen wie z Bei einem PDB weiß der Disassembler nicht, wo sich der Code in der Binärdatei befindet. Es werden einige Hinweise wie der Einstiegspunkt des Programms gegeben. Infolgedessen müssen Maschinencode-Disassembler ihre eigenen Algorithmen implementieren, um die Codepositionen innerhalb der Binärdatei zu ermitteln. Sie verwenden im Allgemeinen zwei Algorithmen: lineares Sweep (Durchsuchen des Textabschnitts nach bekannten Byte-Sequenzen, die normalerweise den Beginn einer Funktion bezeichnen) und rekursives Durchlaufen (wenn eine Aufrufanweisung an einen festen Ort auftritt, wird dieser Ort als Code enthaltend betrachtet ).

    Diese Algorithmen erkennen jedoch im Allgemeinen nicht den gesamten Code innerhalb der Binärdatei, da Compiler-Optimierungen wie die Zuweisung von Interprocedural-Registern, die Funktionsprologe modifizieren, die zum Ausfall der linearen Sweep-Komponente führen, und der natürlich vorkommende indirekte Steuerungsfluss ( dh Aufruf über Funktionszeiger), wodurch die rekursive Durchquerung fehlschlägt. Selbst wenn ein Maschinencode-Dekompiler keine anderen Probleme als dieses auftreten würde, könnte er daher im Allgemeinen keine Dekompilierung für ein gesamtes Programm erzeugen, und daher könnte das Ergebnis nicht neu kompiliert werden.

    Der Code / Das oben beschriebene Problem der Datentrennung fällt in eine spezielle Kategorie theoretischer Probleme, die als "unentscheidbare" Probleme bezeichnet werden und die es mit anderen unmöglichen Problemen wie dem Halteproblem teilt. Geben Sie daher die Hoffnung auf, einen automatisierten Maschinencode-Dekompiler zu finden, der eine Ausgabe erzeugt, die neu kompiliert werden kann, um einen Klon der ursprünglichen Binärdatei zu erhalten.

  4. Habe ich Informationen zu die vom dekompilierten Programm verwendeten Objekte?

    Es gibt auch Herausforderungen in Bezug auf die Art und Weise, wie Sprachen wie C und C ++ im Vergleich zu den verwalteten Sprachen kompiliert werden. Ich werde hier Typinformationen besprechen. Im Java-Bytecode gibt es eine dedizierte Anweisung namens 'new' zum Zuweisen von Objekten. Es wird ein ganzzahliges Argument verwendet, das als Referenz in die Metadaten der .class-Datei interpretiert wird, die das zuzuweisende Objekt beschreiben. Diese Metadaten beschreiben wiederum das Layout der Klasse, die Namen und Typen der Mitglieder usw. Dies macht es sehr einfach, Verweise auf die Klasse auf eine Weise zu dekompilieren, die dem menschlichen Inspektor gefällt.

    Wenn ein C ++ - Programm kompiliert wird und keine Debug-Informationen wie RTTI vorhanden sind, wird die Objekterstellung nicht ordnungsgemäß durchgeführt. Es ruft einen benutzerdefinierbaren Speicherzuweiser auf und übergibt den resultierenden Zeiger als Argument an die Konstruktorfunktion (die möglicherweise auch inline und daher keine Funktion ist). Die Anweisungen, die auf Klassenmitglieder zugreifen, sind syntaktisch nicht von lokalen Variablenreferenzen, Arrayreferenzen usw. zu unterscheiden. Außerdem wird das Layout der Klasse an keiner Stelle in der Binärdatei gespeichert. Tatsächlich besteht die einzige Möglichkeit, die Datenstrukturen in einer abgespeckten Binärdatei zu ermitteln, in der Datenflussanalyse. Daher muss ein Dekompiler eine eigene Typrekonstruktion implementieren, um mit der Situation fertig zu werden. Tatsächlich überlässt der beliebte Dekompilierer Hex-Rays diese Aufgabe meistens dem menschlichen Analytiker (obwohl er auch die nützliche Unterstützung des Menschen bietet).

  5. Wird die Dekompilierung im Grunde genommen ähneln dem ursprünglichen Quellcode in Bezug auf seine Kontrollflussstruktur?

    Einige Herausforderungen ergeben sich aus Compiler-Optimierungen, die auf die kompilierte Binärdatei angewendet wurden. Die beliebte Optimierung, die als "Tail Merging" bekannt ist, führt dazu, dass der Kontrollfluss des Programms im Vergleich zu weniger aggressiven Compilern verstümmelt wird, was sich normalerweise in vielen goto-Anweisungen innerhalb der Dekompilierung äußert. Das Kompilieren von spärlichen switch-Anweisungen kann ähnliche Probleme verursachen. Auf der anderen Seite verfügen verwaltete Sprachen häufig über Anweisungen für switch-Anweisungen.

  6. Gibt der Dekompiler eine aussagekräftige Ausgabe, wenn dunkle Facetten des Prozessors betroffen sind?

    Einige Herausforderungen ergeben sich aus den Architekturmerkmalen des betreffenden Prozessors. Zum Beispiel ist die in x86 integrierte Gleitkommaeinheit ein Albtraum einer Tortur. Es gibt keine Gleitkomma- "Register", es gibt einen Gleitkomma- "Stapel" und er muss genau verfolgt werden, damit das Programm ordnungsgemäß dekompiliert wird. Im Gegensatz dazu verfügen verwaltete Sprachen häufig über spezielle Anweisungen für den Umgang mit Gleitkommawerten, die selbst Variablen sind. (Hex-Rays verarbeitet Gleitkomma-Arithmetik einwandfrei.) Oder bedenken Sie, dass es auf x86 viele hundert Arten von Rechtsanweisungen gibt, von denen die meisten niemals von einem regulären Compiler erstellt werden, ohne dass der Benutzer ausdrücklich angibt, dass dies über eine intrinsisch. Ein Dekompiler muss eine spezielle Verarbeitung für die Anweisungen enthalten, die er nativ unterstützt. Daher enthalten die meisten Dekompiler einfach Unterstützung für diejenigen Anweisungen, die am häufigsten von Compilern generiert werden, und verwenden Inline-Assemblys oder (bestenfalls) Intrinsics für diejenigen, die er nicht unterstützt. P. >

  7. ol>

    Dies sind nur einige der zugänglichen Beispiele für Herausforderungen, die Maschinencode-Dekompilierer plagen. Wir können davon ausgehen, dass auf absehbare Zeit Einschränkungen bestehen bleiben. Suchen Sie daher nicht nach einem Wundermittel, das so effektiv ist wie Dekompilierer für verwaltete Sprachen.

Bevorzugen Sie eine neue Antwort für zusätzliche Aspekte oder bearbeiten Sie diese in Ihrer Antwort? Im Allgemeinen fühle ich mich beim Bearbeiten auf dieser Wiederholungsebene unwohl (vielleicht ist es bei privaten Betas anders?), Weil es in einer Warteschlange und so weiter endet. Aber was auch immer. Also was ist es? :) :)
Sie können es jederzeit bearbeiten oder neue Themen vorschlagen, und ich werde es bearbeiten.
Am 6. Wenn der Code die * Pipeline-Optimierung * durchlaufen hat, kann eine logische Folge einzelner Operationen mit dem vorherigen und / oder nächsten logischen Operationsblock gemischt werden.
#2
+7
Ed McMan
2013-03-27 22:48:57 UTC
view on stackexchange narkive permalink

Die Dekompilierung ist schwierig, da Dekompilierer Quellcode-Abstraktionen wiederherstellen müssen, die im Binär- / Bytecode-Ziel fehlen.

Es gibt verschiedene Arten von Abstraktionen:

  • Funktionen: Die Identifizierung von Code, der einer hohen Funktion entspricht, mit Eingang, Argumenten, Rückgabewert (en) und Ausgang.
  • Variablen: Die lokalen Variablen in jeder Funktion sowie alle globalen oder statischen Variablen.
  • Typen: Der Typ jeder Variablen sowie die Argumente und der Rückgabewert jeder Funktion.
  • Kontrollfluss auf hoher Ebene: Das Kontrollflussschema eines Programms, z. B. while (. ..) {if (...) {...} else {...}}

Das Dekompilieren von nativem Code ist schwierig, da keine dieser Abstraktionen explizit dargestellt wird im nativen Code. Um netten dekompilierten Code zu erzeugen (d. H. Nicht überall goto s zu verwenden), müssen Dekompilierer diese Abstraktionen basierend auf dem Verhalten des nativen Codes neu definieren. Dies ist ein schwieriger Prozess, und es wurden viele Artikel darüber verfasst, wie man auf diese Abstraktionen schließen kann. Siehe Balakrishnan und Lee für den Anfang.

Im Gegensatz dazu ist Bytecode einfacher zu dekompilieren, da er normalerweise genügend Informationen enthält, um die Typprüfung . Infolgedessen enthält der Bytecode normalerweise explizite Abstraktionen für Funktionen (oder Methoden), Variablen und den Typ jeder Variablen. Die primäre Abstraktion, die im Bytecode fehlt, ist der Kontrollfluss auf hoher Ebene.



Diese Fragen und Antworten wurden automatisch aus der englischen Sprache übersetzt.Der ursprüngliche Inhalt ist auf stackexchange verfügbar. Wir danken ihm für die cc by-sa 3.0-Lizenz, unter der er vertrieben wird.
Loading...