Analyse von Legacy-Code bei Verlust des Quellcodes: zu tun oder nicht zu tun?

Die Analyse von Binärcode, dh Code, der direkt von der Maschine ausgeführt wird, ist eine nicht triviale Aufgabe. In den meisten Fällen wird der Binärcode, wenn er analysiert werden muss, zuerst durch Zerlegen und anschließendes Zerlegen in eine übergeordnete Darstellung wiederhergestellt. Anschließend wird analysiert, was passiert ist.

Hier muss gesagt werden, dass der Code, der gemäß der Textdarstellung wiederhergestellt wurde, wenig mit dem Code gemein hat, der ursprünglich vom Programmierer geschrieben und in eine ausführbare Datei kompiliert wurde. Es ist unmöglich, genau die Binärdatei wiederherzustellen, die von kompilierten Programmiersprachen wie C / C ++, Fortran empfangen wurde, da dies eine algorithmisch nicht formalisierte Aufgabe ist. Während der Konvertierung des vom Programmierer geschriebenen Quellcodes in das vom Computer ausgeführte Programm führt der Compiler irreversible Konvertierungen durch.

In den 90er Jahren des letzten Jahrhunderts wurde allgemein angenommen, dass der Compiler wie ein Fleischwolf das ursprüngliche Programm mahlt, und die Aufgabe, es wiederherzustellen, ähnelt der Aufgabe, einen Widder aus einer Wurst wiederherzustellen.


Allerdings nicht so schlimm. Bei der Gewinnung von Würstchen verliert das Schaf seine Funktionalität, während das Binärprogramm es speichert. Wenn die resultierende Wurst laufen und springen könnte, wären die Aufgaben ähnlich.

Sobald das Binärprogramm seine Funktionalität beibehalten hat, können wir sagen, dass es möglich ist, den ausführbaren Code in einer Darstellung auf hoher Ebene wiederherzustellen, so dass die Funktionalität des Binärprogramms, das keine anfängliche Darstellung hat, und des Programms, dessen Textdarstellung wir erhalten haben, äquivalent sind.

Per Definition sind zwei Programme funktional äquivalent, wenn bei denselben Eingabedaten beide ihre Ausführung abschließen oder nicht abschließen und wenn die Ausführung abgeschlossen ist, dasselbe Ergebnis erzielt wird.

Die Aufgabe der Demontage wird normalerweise in einem halbautomatischen Modus gelöst, dh der Spezialist führt die manuelle Wiederherstellung mit interaktiven Tools durch, z. B. dem interaktiven IdaPro- Disassembler, Radare oder einem anderen Tool. Ferner wird auch im halbautomatischen Modus eine Dekompilierung durchgeführt. HexRays , SmartDecompiler oder ein anderer Dekompiler, der zur Lösung dieser Dekompilierungsaufgabe geeignet ist, wird als Dekompilierungswerkzeug verwendet, um einem Spezialisten zu helfen.

Die Wiederherstellung der ursprünglichen Textdarstellung des Programms aus dem Bytecode kann ziemlich genau erfolgen. Für interpretierte Sprachen wie Java oder Sprachen der .NET-Familie, deren Übersetzung in Bytecode erfolgt, wird die Dekompilierungsaufgabe anders gelöst. Wir betrachten dieses Problem in diesem Artikel nicht.

Sie können also Binärprogramme durch Dekompilierung analysieren. In der Regel wird eine solche Analyse durchgeführt, um das Verhalten eines Programms zu verstehen und es zu ersetzen oder zu ändern.

Aus der Praxis der Arbeit mit Legacy-Programmen


Einige Software, die vor 40 Jahren in der Low-Level-Sprachfamilie C und Fortran geschrieben wurde, steuert die Ölförderanlagen. Der Ausfall dieser Ausrüstung kann für die Produktion kritisch sein, daher ist eine Änderung der Software höchst unerwünscht. In den letzten Jahren gingen jedoch die Quellcodes verloren.

Der neue Mitarbeiter der Abteilung für Informationssicherheit, dessen Aufgabe es war, die Funktionsweise zu verstehen, stellte fest, dass das Sensorüberwachungsprogramm regelmäßig etwas auf die Festplatte schreibt und dass es schreibt und wie diese Informationen verwendet werden können, ist nicht klar. Er hatte auch die Idee, dass die Überwachung des Betriebs von Geräten auf einem großen Bildschirm angezeigt werden kann. Dazu musste man verstehen, wie das Programm funktioniert, was und in welchem ​​Format es auf die Festplatte schreibt und wie diese Informationen interpretiert werden können.

Um das Problem zu lösen, wurde die Dekompilierungstechnologie angewendet, gefolgt von der Analyse des wiederhergestellten Codes. Wir haben zuerst die Softwarekomponenten einzeln zerlegt, dann den Code lokalisiert, der für die Eingabe / Ausgabe von Informationen verantwortlich war, und haben angesichts der Abhängigkeiten allmählich begonnen, diesen Code wiederherzustellen. Anschließend wurde die Logik des Programms wiederhergestellt, wodurch alle Fragen des Sicherheitsdienstes zur analysierten Software beantwortet werden konnten.

Wenn Sie ein Binärprogramm analysieren müssen, um die Logik seines Betriebs wiederherzustellen, die Logik der Konvertierung von Eingabedaten in Ausgabedaten usw. teilweise oder vollständig wiederherzustellen, ist es zweckmäßig, dies mit einem Dekompiler zu tun.

Zusätzlich zu solchen Aufgaben gibt es in der Praxis Probleme bei der Analyse von Binärprogrammen auf Anforderungen an die Informationssicherheit. Darüber hinaus hat der Kunde nicht immer das Verständnis, dass diese Analyse sehr zeitaufwändig ist. Es scheint, führen Sie die Dekompilierung durch und führen Sie den resultierenden Code mit einem statischen Analysator aus. Aber als Ergebnis einer qualitativen Analyse gelingt es fast nie.

Erstens müssen gefundene Schwachstellen nicht nur finden, sondern auch erklären können. Wenn die Sicherheitsanfälligkeit in einem Programm in einer höheren Sprache gefunden wurde, zeigt der Analyst oder das Code-Analyse-Tool darin an, welche Fragmente des Codes bestimmte Fehler enthalten, deren Vorhandensein die Sicherheitsanfälligkeit verursacht hat. Was ist, wenn kein Quellcode vorhanden ist? Wie kann gezeigt werden, welcher Code die Sicherheitsanfälligkeit verursacht hat?

Der Dekompiler stellt Code wieder her, der mit Wiederherstellungsartefakten „übersät“ ist, und es ist sinnlos, die aufgedeckte Sicherheitsanfälligkeit einem solchen Code zuzuordnen, aber nichts ist klar. Darüber hinaus ist der wiederhergestellte Code schlecht strukturiert und eignet sich daher schlecht für Code-Analyse-Tools. Es ist auch schwierig, die Schwachstelle in Bezug auf ein Binärprogramm zu erklären, da die Person, für die die Erklärung abgegeben wird, mit der Binärdarstellung von Programmen vertraut sein sollte.

Zweitens muss eine Binäranalyse gemäß den Anforderungen an die Informationssicherheit durchgeführt werden, um zu verstehen, was mit dem resultierenden Ergebnis zu tun ist, da es sehr schwierig ist, eine Sicherheitsanfälligkeit in einem Binärcode zu beheben, aber es gibt keinen Quellcode.

Trotz aller Merkmale und Schwierigkeiten bei der Durchführung einer statischen Analyse von Binärprogrammen gemäß den Anforderungen der Informationssicherheit gibt es viele Situationen, in denen eine solche Analyse erforderlich ist. Wenn aus irgendeinem Grund kein Quellcode vorhanden ist und das Binärprogramm Funktionen ausführt, die für die Anforderungen an die Informationssicherheit von entscheidender Bedeutung sind, muss dies überprüft werden. Wenn Schwachstellen gefunden werden, sollte eine solche Anwendung nach Möglichkeit zur Überarbeitung gesendet werden, oder es sollte eine zusätzliche „Shell“ dafür erstellt werden, mit der die Bewegung vertraulicher Informationen gesteuert werden kann.

Wenn die Sicherheitsanfälligkeit in einer Binärdatei versteckt war


Wenn der Code, den das Programm ausführt, ein hohes Maß an Kritikalität aufweist, selbst wenn der Quellcode des Programms in einer höheren Sprache vorliegt, ist es hilfreich, die Binärdatei zu prüfen. Dies hilft dabei, die vom Compiler eingeführten Funktionen zu eliminieren, indem Optimierungen durchgeführt werden. Daher wurde im September 2017 die vom Clang-Compiler durchgeführte Optimierungstransformation ausführlich diskutiert. Das Ergebnis war ein Aufruf einer Funktion , die niemals aufgerufen werden sollte.

#include <stdlib.h> typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); } 

Als Ergebnis von Optimierungstransformationen erhält der Compiler einen solchen Assembler-Code. Das Beispiel wurde unter Linux X86 mit dem Flag -O2 kompiliert.

  .text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9 

Der Quellcode weist ein undefiniertes Verhalten auf. Die NeverCalled () -Funktion wird aufgrund der vom Compiler durchgeführten Optimierungstransformationen aufgerufen. Während des Optimierungsprozesses führt er höchstwahrscheinlich eine Analyse der Alliasen durch , und als Ergebnis erhält die Do () - Funktion die Adresse der NeverCalled () - Funktion. Und da die main () -Methode die nicht definierte Do () -Funktion aufruft, die undefiniertes Verhalten (undefiniertes Verhalten) ist, lautet das Ergebnis wie folgt: Die EraseAll () -Funktion wird aufgerufen, die den Befehl rm -rf / ausführt.

Das folgende Beispiel: Aufgrund von Optimierungstransformationen des Compilers haben wir die Prüfung auf einen Zeiger auf NULL verloren, bevor wir ihn dereferenziert haben.

 #include <cstdlib> void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; } 

Da Zeile 3 den Zeiger dereferenziert, geht der Compiler davon aus, dass der Zeiger nicht Null ist. Als nächstes wurde Zeile 4 als Ergebnis der Optimierung "Entfernen von nicht erreichbarem Code" gelöscht, da der Vergleich als redundant angesehen wird, und danach wurde Zeile 3 vom Compiler als Ergebnis der Optimierung " Eliminierung toten Codes" gelöscht. Es bleibt nur Zeile 5 übrig. Der Assembler-Code, der sich aus dem Kompilieren von gcc 7.3 unter Linux x86 mit dem Flag -O2 ergibt, wird unten angezeigt.

  .text .p2align 4,,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret 

Die obigen Beispiele für Compileroptimierungsarbeiten sind das Ergebnis eines undefinierten Verhaltens von UB im Code. Dies ist jedoch ganz normaler Code, den die meisten Programmierer als sicher betrachten würden. Heute verbringen Programmierer Zeit damit, undefiniertes Verhalten im Programm zu eliminieren, während sie es vor 10 Jahren nicht beachteten. Infolgedessen kann Legacy-Code UB-Schwachstellen enthalten.

Die meisten modernen statischen Quellcode-Analysatoren erkennen keine Fehler im Zusammenhang mit UB. Wenn der Code eine für die Anforderungen der Informationssicherheit kritische Funktion ausführt, müssen daher sowohl sein Quellcode als auch der Code, der ausgeführt wird, überprüft werden.

Source: https://habr.com/ru/post/de415475/


All Articles