Sie befinden sich hier: Termine » Prüfungsfragen und Altklausuren » Hauptstudiumsprüfungen » Lehrstuhl 1 » Software Reverse Engineering 2014-09-18   (Übersicht)

Software Reverse Engineering 2014-09-18

Fach: Software Reverse Engineering

Prüfer: Tilo Müller

Beisitzer: Johannes Stüttgen

Angenehme Atmosphäre, man kennt sich. Wie in allen Reversing-Prüfungen gab es ein A4-Blatt mit Assembler-Code, Platz für C-Code und leeren Stack-Darstellungen (bei mir nicht benutzt); später bei Exploitation noch ein weiteres Blatt zum Zeichnen des Exploits im Stack.

Fragen

Die Prüfung beginnt wie immer mit einem kleinen x86-Assembler-Programm. (Fakultät, rekursiv, unoptimiert, 64 Bit.)

F: 32 oder 64 Bit?

A: 64 wegen Registernamen mit 'r'.

F: Was ist der erste Parameter und wie groß ist er?

A: (Gezeigt und) 32 Bit wegen DWORD.

F: Wie viele Parameter sind es insgesamt?

A: Nur der eine.

F: Kann es damit ein Pointer sein?

A: Nein, muss ein int sein.

F: Das er unsigned ist, sag ich dir mal dazu. Schreib das doch schonmal als C-Code hin. Wo sind Epilog und Prolog?

A: (Markiert.)

F: Was passiert am Schluss?

A: Ergebnis wird in eax geschrieben für den Rückgabewert. Hier eigtl. unnötig, weil es da schon steht, also kein optimierter Code.

F: Welchen Typ hat der Rückgabewert?

A: Auch int.

F: Und auch der ist unsigned. Schauen wir uns mal die erste Fallunterscheidung an, was passiert da unter welcher Bedingung?

A: Sprung zum Ende und Rückgabe von 1, falls der Parameter 0 ist.

F: Hier steht ja cmp <reg>, 0. Was führt das intern aus und welche Möglichkeit gäbe es stattdessen?

A: Eine Subtraktion. Alternativ könnte man test benutzen, was einer Verundung entspricht – dann ist der zweite Operand aber nicht 0, sondern auch das Register.

F: Gut, dann schauen wir uns noch den rekursiven Teil an.

A: Funktion selbst wird aufgerufen mit Parameter (Eigener Parameter - 1). Das Ergebnis wird mit dem eigenen Parameter multipliziert.

F: Das wird dann ja auch zurückgegeben. Hier steht imul. Wie unterscheidet es sich denn von mul?

A: Weiß ich nicht.

F: Schau dir mal die Operanden an und vergleich es mit div?

A: Nee, komm ich nicht drauf.

F: Welche Calling-Convention haben wir hier denn? Das Programm wurde auf Linux erstellt.

A: Dann wird es wohl die Standard-AMD64-ABI sein. Sieht man auch daran, dass der einzige Parameter in rdi steht.

F: Welche Calling-Convention gibt es noch auf 64 Bit und wie unterscheidet sie sich?

A: Die von Microsoft. (Hier kam ich dann durcheinander mit Registernamen und ihrer Reihenfolge und habe erzählt, es würden nur zwei Parameter über den Stack übergeben.)

F: Also werden bei der AMD64 vier Parameter über den Stack übergeben und bei Microsoft zwei?

A: Ja, irgendwie so.

F: Nein, es sind sechs bzw. vier.

A Stimmt, r8 und r9 gibts ja auch noch.

F: Wo sind es denn zwei Parameter über den Stack?

A: Bei Fastcall auf 32 Bit.

F: Und wie funktionieren Linux-Syscalls?

A: Alles über Register, weil es über den Stack nicht geht. Ich weiß aber nicht, welche Register.

F: Was macht man da bei mehr als sechs Parametern?

A: Geht wohl nicht.

F: Genau. Auf x86 gibt es ja Carry- und Overflow-Flag. Warum diese Unterscheidung?

A: Hmm, das eine ist irgendwie für signed und das andere für unsigned.

F: Was ist was?

A: Weiß ich nicht.

F: Zeichne doch mal das x86-Befehlsformat auf Byte-Ebene bitte.

A: (Prefix, Opcode und Mod R/M gezeichnet.)

F: (Immer zwischendurch:) Was steht z.B. im Prefix? Wie lang sind die einzelnen Teile? Wie viele Byte jeweils für Mod, R und M?

A: Wiederholungsanweisungen oder Operandengröße. Ein, zwei oder drei Byte. Zwei Byte Mod, je zwei und drei R und M.

F: Was tut etwa repz ret?

A: Würde eigtl. ret wiederholen, bis Zero-Flag gesetzt. ret kann aber nicht wiederholt werden, in diesem Fall nur Optimierung für einige AMD-CPUs.

F:: Macht mal mit SIB weiter.

A: (Mir war in dem Moment komplett entfallen, was das ist. Mit einiger Hilfe kam ich dann auf Scale/Index/Base und hab es hingezeichnet.)

F: Wie viele Byte sind das jeweils?

A: Scale zwei, Index und Base je drei.

F: Was kommt dann noch?

A: Immediate und Displacement, aber in welcher Reihenfolge? Zuerst Displacement, dann Immediate.

F: Welche Möglichkeiten gibt es denn, ein switch-Statement in Assembler auszudrücken?

A: Im einfachsten Fall als eine Kette von Fallunterscheidungen, aber halt lang und aufwändig. Deshalb Switch-Tabellen; diese kommen aber nur in Frage, wenn die einzelnen Fälle nah beieinander liegen. Der jeweilige Wert gibt dann mit einem Faktor die Position in der Switch-Tabelle an, wo die Adresse des entsprechenden Codes steht.

F: Wann ist eine Switch-Tabelle ungeeignet?

A: Wenn die einzelnen Fälle weit auseinander liegen, sind Switch-Tabellen nicht mehr effizient. Bis zu eine gewissen Grad kann man „Lücken“ mit der Adresse der Default-Implementierung auffüllen, aber irgendwann wird die Switch-Tabelle einfach zu groß.

F: Was benutzt man dann?

A: Einen binären Suchbaum.

F: Reden wir mal über den Real-Mode. Mit wie vielen Bit läuft er?

A: 16.

F: Und wie sieht es mit dem Speicher aus?

A: Einfach nur einen linearer, physischer Adressraum. In 16 Segmente unterteilt, die alle gleich groß sind. Adressiert werden sie über 16 Bit Segment-Register und 16 Bit Offset, die allerdings so „ineinander verschoben“ sind, dass sich effektiv eine 20-Bit-Adresse ergibt.

F: Was ist im Protected-Mode neu?

A: Linearer, logischer und physischer Adressraum werden unterschieden durch Segmentation bzw. Paging.

F: Was durch was?

A: Segmentation für logischen Adressraum und Paging für linearen.

F: Was ist bei 64 Bit im Long-Mode anders?

A: Segmentation ist faktisch abgeschafft.

F: Das war vorher bei 32 Bit ja praktisch auch schon so. Wie wird es bewerkstelligt?

A: Alle Segmente maximal groß und überlappen sich quasi vollständig.

F: Für was benutzt man dann die Register fs und gs?

A: Unter Windows kann man über fs auf Process-Environment-Block und Thread-Environment-Block zugreifen.

F: Und gs?

A: Soweit ich weiß, benutzt man für beide fs und einen unterschiedlichen Offset.

F: Auf 64 Bit wird aus irgendwelchen Gründen stattdessen gs verwendet. Mal doch mal den PE-Header auf.

A: Am Anfang steht der DOS-Stub. Sein Code gibt eigtl. immer nur eine Fehlermeldung aus, aber er enthält die Startadresse des PE-Headers.

F: Weißt du, mit welchen Byte er beginnt?

A: Hmm, vielleicht mit „MZ“?

F: Genau. Wofür steht das?

A: Gerüchten zufolge für die Initialen des Erfinders; offiziell aber natürlich nicht.

F: Weißt du auch, wie der Mann mit vollem Namen heißt?

A: Mark, der Nachname ist irgendwas mit 'Z', 'i' und 'w'.

F: Zbikowski – das hat bisher noch niemand gewusst. Und womit fängt dann der PE-Header an?

A: Mit „PE“. Dann kommt der eigentliche PE-Header, der Optional-Header, das Data-Directory und die Section-Table. (Gezeichnet.) Optional-Header und Data-Directory gehören noch zum PE-Header dazu, Section-Table ist schon außerhalb.

F: Das, was du als „eigentlichen Header“ bezeichnest, ist der File-Header und zusammen mit Optional-Header und Data-Directory bildet er den PE-Header, das passt soweit. Was steht denn im File-Header?

A: Zeitstempel, Anzahl der Sektionen, CPu-Typ und ein paar Flags, z.B. für Kernel-Module oder DLLs.

F:: Und im Optional-Header?

A: Start und Größe der Segmente, und damit v.a. die Einsprungadresse. Außerdem z.B. eine Checksumme.

F: Was steht im Data-Directory?

A: Verweise auf Import- und Export-Table sowie Import-Address-Table.

F: Und in der Section-Table?

A: Informationen zu den einzelnen Sektionen.

F: Und was kommt danach?

A: Der eigentliche Code.

F: Ja, die Sektionen. Jetzt haben wir nicht mehr allzu viel Zeit, überspringen wir mal Softwareschutz und kommen zur Exploitation.

A: Von mir aus können wir auch überziehen und Softwareschutz auch noch machen.

F: Na gut, machen wir ganz kurz Softwareschutz: Was ist ein Quine?

A: Ein Programm, das seinen eigenen Code ausgibt.

F: Wie lautet die Definition eines Virtual-Black-Box-Obfuscators und ist ein Quine gemäß dieser Definition obfuskierbar?

A: Man darf aus dem obfuskierten Programm nicht mehr erfahren, als wenn man das unobfuskierte Programm als Black-Box betrachtet, also nur Input und Output. Entsprechend dieser Definition ist ein Quine perfekt obfuskierbar. Eigentlich gilt es aber ja als prinzipiell nicht obfuskierbar.

F: Ja, das zeigt die Unsinnigkeit mancher dieser Definitionen. Jetzt aber zur Exploitation – warum ist denn eine Nullpointer-Dereferenzierung im Kernel besonders gefährlich?

A: Adresse 0x0 gehört zum User-Space, daher kann der User dort beliebige Daten schreiben. Wenn der Kernel dann einen Nullpointer dereferenziert, greift er auf diese Daten zu.

F: 0x0 gehört zum User-Space?

A: Ja?!

F: Und was tut man, um diese Sicherheitslücke abzuschwächen?

A: Man lässt den nutzbaren Bereich des Speichers erst „weiter oben“ beginnen.

F: Wird dieser Schutz auch tatsächlich verwendet?

A: Ja, in aktuellen Betriebssystemen fängt der User-Space bspw. erst bei 4096 an, ist aber konfigurierbar.

F: Ihr habt ja eine Vielzahl von Exploitation-Techniken kennengelernt und durchgeführt. Wie heißt denn eine Methode, die mit aktiviertem ASLR und NX-Bit funktioniert?

A: Return-Oriented-Programming.

F: Was macht ROP auf 64 Bit schwerer?

A: Hmm, geht doch genauso?!

F: Denk mal daran, wie 64-Bit-Adressen aussehen. Das Code-Segment ist ja normalerweise „unten“, hat also was für Adressen?

A: Niedrige. Ahh, deshalb enthalten sie viele Nullbytes und das ist schwierig mit String-Funktionen.

F: Richtig, d.h. man braucht eigtl. einen Exploit ohne Strings. Eine andere Technik ist jmp2esp. Kannst du mal das mal illustrieren? (Kein NX-Bit oder Canaries.)

A: Man schreibt Shellcode in den Stack und überschreibt den RIP mit er Adresse eines jmp esp. (Gezeichnet, allerdings zunächst mit Shellcode am Beginn des Strings.)

F: Mach mal bitte weiter, was jetzt Schritt für Schritt passiert.

A: (Stack nach jeder einzelnen Instruktion gezeichnet.) Ahh, wenn man bei jmp esp landet, ist der esp schon wiederhergestellt und zeigt auf unseren RIP. D.h., der Shellcode musst unter diesem stehen. Nein, darüber!

Bewertung

Zwischen 1,7 und 2,7 nach langer Bedenkzeit, geht in Ordnung. Ärgerlich waren die Blackouts bei den Calling-Conventions und beim x86-Befehlsformat, die mir sehr negativ ausgelegt wurden, da beides eigtl. „Standard-Repertoire“ ist.

Vorbereitung

Aufgrund vorhergehender Reise effektiv nur eineinhalb Tage – mit einem Tag mehr hätte ich den Stoff, den ich ansich konnte, sicher besser festigen können und die Blackouts vermeiden.

Man sollte auf jeden Fall alle Folien durchgehen, da auch Details gefragt werden. Das praktische Reversing geht sehr langsam voran und ist eher der leichtere Teil, dafür ist man durch die Übungsaufgaben bereits gut vorbereitet.