 Ist euer On-Air-Knopf? Also wenn es funktioniert... ...that analysis are not easily able to create signatures for those and the malware lives longer without being picked up by an AV. And the third, I think, most interesting case is digital rights management where software, especially AAA games, larger games are just officiated in order to prevent cracking attempts and illegal distribution of the game itself. And especially in the context of digital rights management there's a very fitting quote which settles the scene as to what we can expect from code of investigation. It's from Martin Slater of 2K Australia and was issued regarding the release of Bioshock back in 2007 and he told us that they achieved their goals because they were uncracked for 13 whole days. So apparently we cannot really prevent obfuscation, we can just make sure to make the software withstand cracking attempts at least 13 days, which is about the time in which the game distributor makes most of his revenue. So, how do we protect software? So, there are different approaches to protecting software, some better, some worse. For example, we can just take a look at the software that's been used to analyze the programs itself, like all the debug, like disassemblers and so on. And one idea that comes to our mind is just to abuse shortcomings of these tools. So, if we know all the debug crashes due to some code analysis of the specific FPU sequence, we can just issue that and make all the debug crash. Or similarly, if we got some process tamper, which is relying on fields of the PE header memory, we can easily confuse them and just render these tools unusable. Another approach that's very popular is just to detect the environment the program runs in and check if we are running an application in an environment that gives us information about the presence of a debugger. So, there's some capability by the operating system. For example, the being-debugged bit in the PEB, which is being said if an debugger is attached. Similarly, there are more low-level tricks we can abuse or some operating system internals, which allows us to somehow detect the presence of a debugger and then just devote execution to just prevent any reverse engineering attempts. However, both techniques have the drawback that first they are, once you know how they work, they are easily fixed. You can just patch your tool. Du kannst einfach deinen Tool patchen oder die anti-debugged Tricks mit ein bisschen vernünftigen Werte einsetzen, fixen. Man kann auch einfach bei Google eingeben, das Spiel läuft nicht. Und man kriegt sofort ein Haufen Resultate, wo Leute sich beschweren, dass ihr Spiel grad nicht geht. Weil ihr normal, ihr Crack, der auf Windows XP ging, jetzt nicht mehr ging. Und jetzt ist es so, dass normale, gutmütige Käufer ihr Spiel nicht mehr benutzen können. Wir haben ein paar Voraussetzungen, die wir brauchen für binary code Deobfusskation. Jede Technik, die man anwählen sollte, die das beobachtbare Verhalten nicht ändern, ist, wir wollen nur den Schutz davon übermachen. Es gibt viele Versuche, Daten und sonstige Sachen auf Externe Medien und Internet auszulagern. Aber am Ende ist es dann auch nur ein Whitebox-Szenario, wo man immer noch Erfolge erzielen kann. Zugeht er das, geht es darum, dass wir Techniken einsetzen wollen, die für uns deutlich weniger Aufwand bedeuten, als für den Angreifer. Der Aufwand ist ungefähr 1 zu 1 bei Antidebargien Tricks. Wir wollen aber eigentlich Techniken finden, die für uns einfach sind, aber für die anderen relativ schwer. Code Verschleidungsverfahren, es gibt verschiedene Execution Engines, die das in den Code umschreiben. Zum Beispiel schaut euch diesen Kontrollflussgrafen an, auf der linken Seite, das ist relativ einfach. Linear und alles. Wenn wir jetzt Prädikate einführen, die nichts weiter tun, wir haben keinen Ton mehr. Wir haben keinen weiteren Ton aus der Hand, wir können gerade ins Streit übersetzen. Wir haben ein bisschen Code, wir haben eine falsche Branche, beide branche zu einem anderen Basic Block, also wenn wir uns das anschauen, können wir in diesem Fall denken, dass wir nicht wissen, welche Branche genommen wird. Aber wir können es nicht wissen, dass wir nicht wissen, welche Branche genommen wird. Aber tatsächlich ist es ein Prädikat, was immer wahr ist, das heißt, der rechte Teil ist tot. Nur der linke Teil wird jemals ausgeführt. Was hier passiert ist, wir haben ein undurchsichtiges Statement ganz oben, aber es hängt von der return value von der API ab. Aber ohne weitere Informationen darüber wissen wir nicht, dass es eine konstante Wert zurückgeht. So dass auf diese Art und Weise versichern wir, dass wir immer die linke Branche nehmen. Das sorgt dafür, dass der reverse engineer weiter verunsichert wird. Genauso gibt es auch undurchsichtige falsche Bedingungen, die immer den falsch... Es gibt auch zufällige undurchsichtige Blöcke, wo es nicht weiter wichtig ist, welcher der beiden Blöcke ausgeführt wird, weil beide einen sementisch gleichen Code ausführen werden. Jedenfalls ist es aber auch eine... eine Herausforderung, dass der Angreifer nicht herausfindet, dass die beiden Codeblöcke das Gleiche tun. Es kann natürlich auch auf harten Problemen basieren, aber am Ende zwingt es den Angreifer dazu, zusätzliches Wissen mit einzusetzen. Man muss zum Beispiel Hintergrundwissen haben über die API, über harte Probleme, die er alle in seinem Tool mit einbauen muss, um Erfolg zu haben. Die sind statisch sehr schwer zu lösen. Es gibt aber die Möglichkeit, dass man sich auf echte Ausführungs-Traces konzentriert, dann fällt das relativ kostenfrei heraus. Eine andere sehr interessante Technik ist, der Einsatz von virtuellen Maschinen, besonders im Moment bei AAA-Games benutzt. Wenn wir sich das jetzt hier anschauen, dann hat man hier so ein LuL und X86-Assembler-Code. Und jetzt nehmen wir mal an, dass dieser LuL hier der Teil ist, den wir geschützten wollen. Natürlich können wir hier ein Disassembler benutzen, ein Eider, uns das damit anschauen, uns das genauer analysieren und versuchen, rauszukriegen, was es macht. Aber die Idee ist hier, dass wir das ersetzen durch etwas, was unser Tool versteht. Wir werden einfach kreativ und machen einen komplett neuen Instructions-Set auf. Also einen, den wir komplett frei erfinden mit Instruktionen, die bei keiner Architektur irgendwie verstanden werden. Neue Register, neue Encodings für die Befehle, neue Native-Codes. Aber Semantisch sollten sie schon exakt identisch sein. Und wir setzen jetzt diesen nativen Code mit einem Einsprung in eine virtuelle Maschine, die ja nichts anderes ist, als eine virtuelle CPU, die in Software läuft und es emuliert. Die führt dann unseren eigenen Instructions-Set aus. Und dann können wir jetzt die gängigen Tools benutzen, wie Oli Debak oder Ida oder... Und das bringt natürlich nichts, weil die haben keine Ahnung von unserer Architektur. Aber müssen immer noch sicherstellen, dass dieser Übergang von unserem Code in die virtuelle Maschine relativ nahtlos funktioniert. Was wir brauchen, die Grundbestandteile für eine virtuelle Maschine ist VMEntry und VMExis. Das macht den Übergang, den Kontextübergang von dem normalen Code in den virtuellen Kontext und zurück. Normal ist das Mapping zwischen den echten Registern in der Maschine und der emulierten 1 zu 1. Was das dann macht, ist, es sucht sich die Instruktion raus, schaltet den virtuellen Instruktions-Pointe eins weiter und schaut danach in einem Table, der sich die Behandler für jeden Opcode anschauen. Und wenn ich... Also ich mach quasi ein Dictionary auf für jeden Opcode in einen Handler und der wird dann ausgeführt, wenn der Opcode aufdrückt. Das heißt, man hat dann einen Behandler oder eine Routine, die aufgerufen wird für jeden Opcode. Und man kann dann jeder dieser Routine, die das ausführt, updated dann halt den Kontext der virtuellen Maschine. Und hier sieht man jetzt so einen Grafischen Trace quasi. Das ist im Prinzip ein Haufen Blackboxes für uns, die schwierigste erst mal zu verstehen sind. Das ist ein Einsprungpunkt in die virtuellen Maschine. Und da unten wird der Kontext umgeschaltet oder ausgetauscht. Wenn man hier den Dispatcher anschaut, der sucht sich dann halt jeweils die richtige Routine aus für jeden Opcode und führt sie dann aus. Jeder dieser... diese Routine updated dann halt den Kontext und geht dann wieder zurück in den Dispatcher. Und ganz am Ende rufen wir dann halt den VM-Exit-Händler auf. Das ist der, der dann wieder den Kontext zurückschaltet in den nativen Bereich. Wenn wir uns das mal genauer anschauen, dann haben wir in RSI den virtuellen Instruktionsporten da und im RbP den Kontext der virtuellen Maschine. Hier sehen wir, nehmen wir einen Byte. Hier wird ein Byte vom Speicher genommen und die entsprechende Instruktion ausgeführt. Der Händler, hier sieht man, wird von dem virtuellen Kontext gelesen und dann wird tatsächlich das Nohr ausgeführt und schließlich wird das Ergebnis zurückgeschrieben und zurück zum Dispatcher gesprungen. So, wie machen wir jetzt jetzt schwerer, diese virtuellen Maschinen zu verstehen? Was können wir denn hier machen? Wir können zum Beispiel die einzelnen virtuellen Maschinen-Instruktionen schwieriger zu verstehen, zu machen. Zum Beispiel können wir, so wie vorher, undurchsichtige Prädikate machen. Wir können Befehle durch equivalente Austauschen und so weiter. Weiterhin können wir, sobald wir feststellen, dass wir bloß sehr wenige Instruktionen haben, ist, wir könnten einfach mehrere Instruktionen haben, die das Gleiche tun, sodass der Angreifer mehr analysieren müssen. Wir haben durch das einzelne Instruktionsbyte bis zu 256 verschiedene Instruktionen, so können wir dann 256 Möglichkeiten für die Instruktionen, sodass der Angreifer so viele Instruktionen miteinander vergleichen müssen. Hierbei hat der Angreifer 256 verschiedene Instruktionen zu analysieren, was mehr Aufwand für ihn ist als für uns, weil wir Brust duplikieren müssen. Hier sehen wir, dass nach jeder Instruktion einfach direkt zum Dispatcher gegangen wird. Hier können wir anstatt dessen auch den Dispatcher an das Ende jeder Instruktion tun, sodass jeder Händler zum nächsten Händler springt. So haben wir keinen zentralen Dispatcher, sondern wir haben für jeden Instruktionen so, dass der Angreifer nicht so gut die vor allem analysieren können. Ohne einen expliziten Händler-Table machen wir es dem Angreifer noch schwerer, weil jeder einzelne Instruktion in dem Händler drin steht. Das heißt, anstatt den nächsten Instruktionen zu encodieren, können wir einfach den nächsten Händler reinschreiben, die Adresse des nächsten Händlers. Das hat den Effekt, dass es die Anfangspunkte von jeder Routine, die das behandelt, versteckt, weil man nicht mehr reinspringen muss. Wenn man so einen Händler-Table hätte, dann wäre das einfach, die zu finden und man könnte da direkt drauf zuspringen und die analysieren. Aber so kann man nur die sehen, die wirklich durchlaufen werden in einem Programm. Wir haben jetzt noch eine weitere Technik, um Code zu verschleiern. Das sind gemischte Bullion Arithmetik. Wenn man sich jetzt das hier anschaut, es sieht ein bisschen kompliziert aus und ihr werdet wahrscheinlich überrascht, wenn ich sage, dass es auch einfacher geht. Das ist einfach nur eine Adition. Und dieser doch etwas kompliziertere Ausdruck, auch dafür gibt es eine einfache Variante. Das ist einfach nur die Adition von drei Zahlen. Ihr könnt mir jetzt, wenn ihr mir jetzt glaubt, dass man das in euren Lieblings-SMP-Solver reinhaut, dass dann die Äquivalenz sofort bewiesen ist. Aber wir können natürlich versuchen, bulgische Identitäten zu benutzen oder arithmetische, oder sogar Kanu-Wedge-Dialgramme zu malen, die man dann benutzt. Aber das hilft uns hier nicht wirklich. Was wir hier benutzen, ist die bulgische arithmetische Algebra, die uns einen Satz primitiver Operationen gibt, die wir verwenden können. Das sind bulgische Operatoren, Vergleiche und ein paar arithmetische Operatoren. Was sagt es? Und sollte festhalten, dass die gemischte bulgische arithmetik auch bulgische Algebra enthält. Und es gibt keine Techniken, um solche Ausdrücke zu vereinfachen, die beide enthalten. Wir wissen, wie man Ausdrücke in der bulgischen Algebra oder im Ring der ganzen Zahlen zu vereinfachen. Aber es gibt keinen Solver, der gemischte Ausdrücke aus beiden vereinfachen kann. Jetzt möchte ich mich noch der Entschleierung widmen, also dem Rückgängigmachen von Verschleierung. Es gibt immer so einen Punkt, wo wir Techniken einsetzen, die symbolische Ausführung heißen. Das ist jetzt wieder so ein Händer, den wir vorhin schon mal gesehen haben, der führt Noah aus. Wir führen das jetzt aus mit konkreten Werten, aber nur symbolisch. Wir weisen RCX den Base-Pointer-Register zu. Und jetzt machen wir noch weitere Assignments, also Zuweisungen zu den verschiedenen Registern. Hier haben wir jetzt die Negation von RCX. Also die Negation des Base-Pointers, der referenziert. Hier wird es interessant. Hier machen wir ein logisches Und vom Base-Pointer. Das ist jetzt dasselbe wie der Negation des Base-Pointers und der Negation von dem Base-Pointer und 4. Und wenn wir jetzt bekannte Identitäten einsetzt aus der Buschenalgebra, dann kommt man hier auf diesen Ausdruck. Das ist ein Noah der beiden Adressen. Dann können wir die Ausführung einfach weiterlaufen lassen und der symbolische Ausführer erkennt dann, dass hier der Ausdruck ist, der wirklich ausgeführt wird. Und wir sehen, dass es nur Noah ist und dass wir das in eine Speicherzelle schreiben. Das ist jetzt das Ergebnis, wenn wir dieses ganze Konzept auf einen deutlich komplizierteren Händler werfen. Das ist jetzt mal ein einfaches Beispiel eines verschleierten Behandlers oder einer Routine. Das ist jetzt eine Tronne an Information, die dabei rauskommt. Wir wollen keine Tronne. Wir wollen einfach nur die Semantik und die ist wirklich einfach in dem Fall. Wir wollen einfach nur die Noah-Operation von zwei Speicherzellen und also den Adressen der Speicherzellen und das dann wiederzuweisen. Jetzt benutzen wir halt wieder gemischte bulsch-arhythmetische Ausdrücke und das kompalen wir in ein Programm und darauf lassen wir dann die symbolische Exclusion Engine, also die Ausführungs-Engine laufen lassen. Leider passt es hier schon gar nicht mehr auf die Folie drauf. Kann man ihn nicht lesen, aber... Es ist eine super komplizierte Ausdruck und alles was es am Schluss ist, ist das relativ simpel, einfach nur eine relativ einfache Ausdruck, die wir natürlich ganz klar den komplizierten vorziehen. Das heißt, diese symbolische Ausführung hilft uns, den kompletten Semantik zu behalten und manchmal kann man relativ viel vereinfachen von den Zwischenausdrücken. Aber es ist trotzdem so, dass die Benutzbarkeit relativ runtergeht, je größer die syntaktische Komplexität des zugrunde liegenden Codes ist. Also wir können künstliche Komplexität inzufügen durch Ersetzungen oder einfach durch undurchsichtige Prädikate oder wir können die Algebraische Komplexität hochdrehen mit nix gemischter Bulscher Arithmetik. Es ist offensichtlich, dass wir ein Problem mit so einer kalktischer Komplexität haben und wir müssen es irgendwie machen. Was könnten wir machen, wenn wir bloß diese Semantik des Codes behandeln könnten? Das führt uns zu einem Thema der Programmsynthese. Wir haben ein paar Schwächen der syntaktischen Komplexität gefunden, aber wenn wir das Programm undurchsichtig machen wollen, dann wird das Input und Output gleich bleiben. Warum nehmen wir nicht einfach ein Programm als Blackbox und schauen, wie Input und Output und schauen, was dabei rauskommt? Jetzt könnten wir z.B. 1, 1, 1 reingeben und 3 rauskriegen, 2, 3, 1, zu 6 und dann machen wir das immer und immer wieder und dann schauen wir nicht auf den Code, sondern wir schauen bloß auf das Input und das Output und was wir dann herausbekommen ist, ist eine Funktion, die das Gleiche verhalten haben, z.B. x plus y plus z und die Aufgabe von Programmsynthese ist, genau solche Funktionen zu lernen gegeben, Input und Output. Was können wir da machen? Wir machen einen probabilistischen Ansatz, ein Optimisierungsproblem und wir haben quasi eine Oberfläche und wir haben ein globales Maximum und wir suchen nach diesem globalen Maximum was das Gleiche Input und Output verhalten hat für unser Programm. Genauer wir unsere Funktion approximately, dass du höher ist, unsere Punktestand. Was wir gerade machen, ist, wir nehmen uns irgendeine Funktion und versuchen dann, die Punkte zu verbessern. Wie wir das machen, ist ein Algorithmus, der auf Monte Carlo Algorithmus basiert. Das ist auch der gleiche Algorithmus, der auf dem AlphaGo basiert, der menschliche Spieler mit Computer-Go schlagen konnte. Zum Beispiel wollen wir A plus B Modulo 8 synthetisieren. Zuerst müssen wir definieren, wie wir Programme generieren. Zum Beispiel haben wir ein Nicht-Terminal-Symbol U, d.h. wir können es nur mit anderen Dingen ersetzen. Zum Beispiel können wir U durch U plus U ersetzen oder durch U mal U oder durch A oder durch B. A und B sind unsere Inputvariablen. D.h. wir können sie nicht weiter ableiten oder ersetzen. D.h. wir haben verschiedene Kandidaten für unser Programm, z.B. A, B oder A mal B oder A plus B oder so weiter. Alternativ haben wir auch Zwischenprogramme, die mindestens ein U enthalten. Die sind noch nicht fertig, weil wir die noch weiter ableiten müssten. Was wir jetzt machen, ist Monte Carlo Baumsuche und fangen da an mit dem U als Anfangssymbol. Und jetzt nutzen wir einfach unsere Grammatik, um weitere Programme zu generieren. Zum Beispiel als allererstes wir können auf das Programm was nur A enthalten. Und dann machen wir einen properblistischen Punktestand für dieses Programm. Und dann kriegen wir z.B. 0,64 raus. Nächstes Programm wäre z.B. B. Kriegen wir auch wieder einen Punkt raus. Und jetzt haben wir U mal U als synthesisiert. Dieses Programm kann keinen Punktestand haben, da ist ein abstrakter Term der noch nicht alle Us ersetzt hat. Das heißt, wir ersetzen jetzt einfach mal U durch zufällige Terme, bis wir ein Programm haben, was wir ausführen können. Und das Gleiche machen wir dann auch mit U plus U. Genau. Und dann haben wir jetzt haben wir alle Regeln gemacht. Wir wählen die beste Kinderknoten aus und machen dasselbe nochmal. Wir haben hier einen Ausdruck hergeleitet und gehen dann wieder zurück. Jeder, jeder Wert, der das Programm ist, den wir einem Knoten zuweisen. Da präsentiert den Wert, den wir auch den Kindern zuweisen. Wir wählen wieder den besten Knoten aus und leiten wieder her, gehen wir zurück, updaten den Wert des Vaters und dann wieder zurück und so weiter. Hier bei U plus A haben wir einen sehr hohen Wert. Und bei U plus B haben wir einen eher niedrigeren Wert. Und wir wollen A plus B herleiten. Das heißt, U plus B hat einen schlechteren Lauf als U plus A, weil einfach der Score bei U plus A besser ist. Also updaten wir das, gehen wir zurück. Wir gehen wieder mit diesem Knoten rein. Wir sehen schon, wir haben hier immer wieder den gleichen Fahrt ausgewertet. Das heißt, es ist aber ein Problem, weil wir halt nur ein wahrscheinlich getriebenes Verhalten haben. Das heißt, wir müssen manchmal auch einfach runterlaufen, nur um zu sehen, dass wir nichts verpassen. Normalerweise würden wir jetzt hier nochmal U plus A auswerten. Aber um zu sehen, ob man da nicht irgendwas verpasst, müssen wir auch mal andere Seiten auswerten. Wir stellen fest, es war nicht wirklich gut, also gehen wir zurück. Und das nächste Mal landen wir wieder bei U plus A. Jetzt werden wir B plus A aus und dann haben wir unser finales Programm gefunden, dass wir nicht weiter auswerten können. Dem geben wir dann auch den Score 1. Weil wir wissen, dass B plus A, A plus B equivalent ist, deswegen ist das auch unser Endprogramm. Wie bewerten wir solche Werte jetzt? Am Ende, was wir machen, ist, wir generieren so zufälligen Input, wir stöcken die in unsere Blackbox rein, über die wir nichts wissen und schauen, was rauskommt. Und hier sehen wir halt 2 plus 2 ist 4 und mit dem selben Input stecken wir jetzt in unser Zwischenprogramm und schauen uns den Output ein. Dann wehrten wir aus, wie ähnlich die Werte sind, die bei rauskommen und bewerten das. Wir müssen hier irgendwie einfach nur die Ähnlichkeit bewerten. Ich werde dann noch mal darauf zurückkommen. Wir machen das jetzt für viele Werte und müssen uns jetzt anschauen. Hier sehen wir sogar, die Werte sind gleich, also ist die Ähnlichkeit wohl 1 und das machen wir jetzt noch ein paar Mal. Am Ende ist dann der Wert des Knotens, der Durchschnittswert von diesen Ähnlichkeiten, die wir bewertet haben. Wie bewerten wir jetzt die Ähnlichkeit? Wir schauen uns jetzt im Bitvektor Raum an. Beide Werte jeweils und vergleichen die dann. Sind sie in dem gleichen Bereich überhaupt, in der gleichen Größenordnung? Wir schauen uns halt die Führenden und die Nullen und Einsen am Ende an und am Anfang. Dann können wir uns anschauen, wie viele Bits sind wirklich unterschiedlich. Das ist dann der Hemmingabstand. Das benutzen wir für Erkennung von Überfluss, also von dem Integerüberfluss bei Integerarithmetik. Und dann können wir mal noch gucken, wie nah sind sie wirklich numerisch, ohne Überfluss. Bei der Artition können wir schauen, wie dicht die beiden Zahlen tatsächlich sind und dann nehmen wir diese Matrix um die Entfernung von den beiden Vektoren von der anderen Entfernung sind. Am Ende nehmen wir den Durchschnitt der drei Matratzen. Wie können wir das jetzt benutzen, um verschleiten Codes zu generieren? Was wir machen können, ist, wir nehmen ein Instruktionsfahrt und emulieren den irgendwie und so weiter und kriegen so Inputs und Outputs heraus. Wir sagen einfach, alles, was wir lesen, bevor wir es schreiben, ist ein Input. Also das, was wir am Schluss beschreiben, ist ein Output. Dann nehmen wir das wieder als eine Blackbox, nehmen zufälligen Inputs, packen den in unsere Blackbox und schauen uns den Output aus an. Dann machen wir das ganz, ganz oft, 20 oder 30 mal und synthesisieren das Output. In diesem Fall kriegen wir heraus, dass RBX am Ende nicht M0 ist. Das Gleiche machen wir dann für RCX und kriegen heraus, dass RCX M0 NOR M1 ist. Und schließlich finden wir heraus, dass M0 ein NOR ist. Um zusammenzufassen, kriegen wir heraus, dass 2 NORs sind und eine Negation. Was wir nicht herausbekommen, ist die Flag-Operation Push-F. Warum kriegen wir das nicht heraus? Weil wir, weil uns das nicht interessiert. Die Grammatik ist so abstrakt, dass Bit-Level-Semantik nicht beachtet wird. Das heißt, Inputs sind keine Form von Input für uns. Wir machen also zufällige Beispiele von einem Code. Was passiert, wenn wir einen Branch haben, also es von den Inputs abhängt, um zu sehen, welcher Code ausgeführt wird. Wir können verschiedene Verhalten ... Wir können verschiedene Verhalten ... Was wir auf jeden Fall machen, ist, wir ignorieren die Bedingungen und führen immer die eine der beiden Branches aus. So dass wir dann zwei verschiedene Funktionen haben ausführen. Wie können wir jetzt Code ausführen? Zum Beispiel irgendeine Form von Emulationen, wie in Box oder Unicorn oder mit dynamischen Instrumentationen auf den Banneries. Was man auch tun kann, ist, es in eine Intermediate-Langage und dann symbolisch auszuführen. Und dann es mit konkreten Inputs zu füttern, aber dann ist es meist viel, viel zu langsam. Im Allgemeinen ist es allerdings möglich. Was wir gemacht haben, ist, wir haben alles implementiert in unserer Framework Cynthia. Cynthia ist eine Programmsynthese Framework für Code Deification. Es ist geschrieben in Python und es implementiert Monte Carlo Research den Monte Carlo Research Aggression. Dies ist der Link, auf dem ihr das bekommen könnt. Nun werden wir euch das kurz demonstrieren. Am Anfang werde ich euch ein bisschen was über die Programmsynthese zeigen. Wir definieren ein Oracle, das wir als Blackbox simulieren. Was wir wollen, ist x plus y plus y zu synthesisieren. Was wir jetzt machen, ist einfach, es auszuführen und was ihr jetzt sehen könnt, ist, dass wir mit verschiedenen Grammatikknoten anfangen, versuchen verschiedene Dinge und kommen zu immer größeren Punkten. Hier sehen wir, dass es gelernt hat, dass es viele Additionen geben muss und am Schluss bekommen wir so was in dieser Richtung. Das ist jetzt vielleicht nicht die einfachste Form, aber wir können es einfach vereinfachen. So dass wir am Ende rauskriegen, dass es zweimal das eine Input plus zweimal das andere Input ist. Wir können das nochmal tun und wir könnten jetzt einen komplett anderen Pfad herausbekommen, weil das alles probabilistisch ist, also auch Wahrscheinlichkeiten. Manchmal dauert es länger, manchmal dauert es weniger lang, es kommt drauf an, wie gut die Qualität uns Inputs und so weiter ist. Aber meistens erreichen wir irgendwann unser Ziel. Beim ersten Mal hat das 20 Sekunden gedauert, beim zweiten Mal hat das 24 Sekunden gedauert. Und jetzt haben wir eine deutlich bessere Form in unserem Output. Wie können wir das für verschleiertem Code benutzen? Lasst uns einen Blick in den Source Code werfen. Das ist die unverschleierte Funktion oben. Das ist der reine Code. Es wird eine Edition und eine Multiplikation ausgeführt und wird schließlich in IAX gespeichert. Das ist die verschleierte Version mit Tigris. Diese Version ist deutlich länger. Das kompellieren wir jetzt und kriegen ein Assembly Output. Das sind ungefähr 760 Zeilen. Wir können das nicht symbolisch ausführen, weil das ewig dauern würde. Ansonsten, anstatt dessen lernen wir einfach, was die Blackbox tut. Wir nehmen in unser Code übergeben die Architektur unseres Prozesses. Wir nehmen 20 Input-Output-Samples und wir schreiben noch die Output-Datei. Das seht ihr, das war weniger als fünf Sekunden. Wenn wir uns das anschauen, sehen wir, dass wir verschiedene Outputs haben. Das ist noch ein Output. Wir haben quasi 18 Outputs, weil wir bei 0 anfangen zu zählen. Wir sind nur interessiert in IAX. Das sind unsere fünf Speicher-Inputs, unsere Parameter der Funktion und auch ein paar weitere Register. Was wir jetzt tun, ist, wir definieren unsere Synthese. Wir lesen das Input-File von vorher. Und hier sehen wir, dass wir bloß ein Output synthesisieren wollen. Wir starten das jetzt. Es nimmt ein Sampling-File und ein Output-File, in dem die Ergebnisse stehen werden. Das könnte Sekunden oder Minuten dauern. Wir sind fertig. Schauen wir uns das Ergebnis an. Das ist das Output. Das beste Nicht-Terminal ist Ganzzahl mal Ganzzahl plus eine Speicher-Location. Das beste Programm, das nicht weiter abgeleitet werden kann, ist Speicher mal Speicher plus Speicher. Das ist dann auch unsere endgültige Das ist diese Expression. Es ist also einfach bloß Speicher-Zelle mal Speicher-Zelle plus Speicher-Zelle. Gehen wir zurück zu unseren Vortragsfolien. Wie können wir das jetzt benutzen, um Verschleierung bei virtuellen Maschinen zu verwenden? Das Ziel bei virtuellen Maschinenverschleinungen ist, dass man ein neues CPU-Architekturschema benutzt, dass man zumindest alle Routinen für jeden Opcode entschlüsseln muss, um es zu verstehen. Aber für uns ist es einfacher, wenn man einfach nur die einfachen Operationen rausfinden muss. Die Frage ist jetzt, wie gehen wir damit um und knacken sie? Die erste Technik war jeden einzelnen Händler, jede einzelne Routine, die ein Opcode benutzt, individuell komplizierter machen. Das dann alles duplicieren, den zentralen VM-Dispatcher abschaffen und am Schluss sogar die Tabelle abschaffen, die die Adressen für die Routinen hält. Was du jetzt hier siehst, ist ein Tabelle mit den Handlern und Optimizer. Es ist ziemlich unklar, was hier eigentlich passiert. Wenn wir Sintia-Fragen sagt, okay, die Semantik ist, dass es am Ende nur zwei Speicher-Adressen addiert und das in einen 64-Bit-Adresse speichert. Das wäre schon mal einfach. Okay, jetzt kommen wir zu den duplizierten Routinen für dieses Opcode-Handling. Wir können jetzt so ein Trace durchgehen und schauen, wo jeder Handler abgelegt ist. Wenn wir wissen, wo jeder ist, wie kriegen wir dann raus, welche davon Duplikate voneinander sind? Wenn wir jetzt die Semantik kennen und dadurch Duplikate finden können, wenn die einfach gleich sind in der einfachen Form, dann sind die wohl ja selber Handler gewesen. Also wenn man jetzt diesen Handler wieder anguckt, ohne zentralen Dispatcher, dann schauen wir uns das Ende von dem Handler an. Also das und der Code drüber ist der Code, den man braucht, um für den nächsten Handler zu finden, damit man weiterspringen kann. Das ist jetzt ein bisschen zu kompliziert für unser Vereinfacher. Aber wir verstehen immer noch die Semantik von dem Handler. Und wenn uns die zu kompliziert ist, dann lernen wir sie halt einfach nicht. Und hier sieht man die Sprungadresse im RWX. Der Nachteil davon ist, dass es eine relativ harte Heuristik gibt, mit der wir den Ende von VM-Handlern finden können. Das Ende ist immer ein Return- oder ein Jump. Das heißt, wir können mit diesen indirekten Übergaben, also für den Kontrollfluss, das heißt, wenn der Kontrollfluss nicht mehr linear läuft, kann man benutzen, um die Grenzen von Code-Stückchen zu finden und damit kann man das dann segmentieren in einzelne Code-Teile. Das heißt, wir haben schon mal eine relativ gute Idee, wo jeder Einzelne ist. Und was wir jetzt machen ist, wir lassen es einfach laufen und schauen uns den Trace an. Und dann sehen wir ungefähr das Folgende auf der Folie. Wir splitten jetzt einfach alles, wo wir einen Übergabe vom Kontrollfluss sehen und dann sehen wir jetzt das und jede Farbe, die wir da sehen, ist ein anderer Handler. Was wir jetzt machen ist, wir nehmen diese einfachen Handler und vereinfachen die alle. Und dadurch haben wir schon sehr viele oder sehr große Teile von dem Coach schon verstanden. Und wir haben schon ein paar Sachen Semantik vereinfacher gefunden. Jetzt können wir euch natürlich viel erzählen, aber am Ende probiert es doch einfach mal aus. Wir haben unser gesamtes Code veröffentlicht und auch unsere Samples dazu, damit ihr damit rumspielen könnt. Probiert es einfach mal aus, um es zusammenzufassen. Wir haben uns ein paar Verschleitungstechniken angeschaut, wie Karte, Viertelmaschinen und so weiter. Man kann sie natürlich alle gleichzeitig benutzen. Wenn wir das auf dem Instruktionsfahrt-Level anschauen, sehen die alle eher gleich aus. Das heißt, wir können symbolische Ausführungen nutzen, um es syntaktisch zu entschleiern. Auf der anderen Seite haben wir uns gefragt, was könnten wir tun, wenn wir uns einfach bloß die Semantik des Codes anschauen und so den Code als Blackbox betrachten. Das ist Programmsynthese. Und... Endgültige Erinnerung. Hier könnt ihr unseren Code finden. Vielen Dank. Vielen, vielen Dank für die Lösungen für eines Webcom-Problems. Wir haben jetzt eine Q&A-Session für 10 Minuten. Geht bitte an die Mikrofone. Gibt es schon Fragen aus dem Internet-Signalinge? And other questions from the audience here. One, okay. In the back, please. All right. Yes, I was wondering... And please try to go out silent. What? Okay, do you still hear me? Ich frage mich, wenn ich mir die Verschleinungstechniken anschaue, wäre es nicht möglich, für ein propreteres Betriebssystem Debugging zu verhindern, was man natürlich machen kann, ist, wenn du das ausführst in eine virtuelle Maschine. Also, es geht natürlich, aber wenn du das kannst, dann kannst du halt einfach Backpoints setzen und den Speicher dann dampen mit dem Moment. Und wenn du jetzt noch Reverse-Engineering dazu benutzt und du weißt, dass es einer bestimmten Speicher hat, dass es irgendwas ist, also der entschüsselte Code liegt, weil es gerade benutzt wurde und deswegen entschüsselt wurde, dann kannst du einfach einen Schnappschuss nehmen von dem gesamten Speicher und das dann in den Opfoskey da rein hauen. Und da gibt es auch noch ein nicht technisches Argument, das man hier machen kann. Dann haben wir auf ein paar Slides, also ein paar Folien davor gehabt, du packst ja deinen Code raus mit Outsourcing im Prinzip und in manchen Umgebungen ist das einfach auch nicht funktioniert, wie zum Beispiel OSB-Dongel in der Firma, wo du oft irgendwie Policies verletzt, das funktioniert in einem System, das funktioniert in einem Widebox-Szenario, funktioniert das natürlich, aber wir sind in einem sehr nicht restriktiven Szenario unterwegs. Als nächstes Mikrofon 1. Ich würde gerne wissen, diese Techniken könnten auch für Code-Optimierung verwenden werden. Gab es da Forschung in die Richtung? Klar, es gibt einen Riesenhaufen Forschung in der Richtung. Zum Beispiel ein Projekt, das heißt SOCT, von den Stanford-Leuten, die machen stochastische Programmanalyse für Optimierung auf einer großen Skala. Das war auch eine unserer Inspirationen, um Stochastik für die Synthese zu benutzen. Danke schön. Wenn ich euch richtig verstanden habe, dann habt ihr euch hauptsächlich darauf spezialisiert aratmetische Funktionen zu benutzen. Gab es da die Möglichkeit, numerisch die Kurven zu approximieren? Wäre natürlich möglich oder wahrscheinlich möglich, haben wir jetzt noch nicht wirklich angeschaut, aber wir haben im Moment einfach nur hingeguckt, weil wir irgendwas brauchten, um unseren Ansatz zu bewerten und wir haben unsere Grammatik in dem auf eine Weise gemacht, um das möglichst einfach zu machen, aber du kannst natürlich auch eine viel komplizitere Grammatik nehmen oder mit anderen ausdrücken, dass wir wollten, war relativ einfach aber der andere Ansatz klingt mächtiger. Das Internet hat auch eine Frage. Habt ihr versucht, Intermediate Representations zu synthesisieren? Nee, das ist im Allgemeinen auch nicht möglich. Wolf Reus hat da eine Arbeit drüber gemacht und es gibt Gründe, warum das nicht funktioniert, aber es kommt hauptsächlich auf die Grammatik an, die man synthesisieren will und die Input-Output-Paare. Also wenn es dir darum geht, Intermediate Representation Language kurz zu synthesisieren, dann geht das schon. Eine Frage da hinten. Ihr habt komplizierte Wörter genutzt, aber hauptsächlich habt ihr doch eine Suche nach einem Ausdruck gemacht, die eurer Blackbox passen. Warum habt ihr geschaut, ob das einfach besser ist als zufällig Thema auszuwählen? Am Ende hat man einen riesigen Raum, den man durchsuchen muss. Wenn man nur A plus B rechnen muss, dann ist das einfach. Aber normal haben wir 20 bis 50 Inputs und ähnlich viele Outputs von der Funktion. Wir haben nicht nur Multiplikation, sondern wir haben einen viel größeren Raum, den wir durchsuchen müssen. Wir haben die Extensions, die komplette Arithmetik-Operationen, die der X86-Iser her gibt, XOR, Modulo, alles. Viele von diesen Bausteinen, die alle möglicherweise vorkommen und die wir deswegen auch unterstützen wissen. Für A plus B bist du mit Sicherheit schneller, als wir. Wenn du einen großen Sucherum hast, dann hilft es, wenn du gelenktes Lernen einsetzt. Letzte Frage, Mikrofon 1. Das sieht sehr interessant aus eurer Arbeit. Aber was passiert, wenn die Funktionen, die ihr herausbekommen wollt, was passiert, wenn die Ähnlichkeitsfunktionen nicht möglich sind? Was passiert, wenn die Distanzfunktion nicht in der Nähe ist, wenn sie eigentlich in der Nähe ist? Ja, natürlich. Also, wenn du es demantisch härter machst, dann funktioniert es halt irgendwann einfach nicht mehr. Das ist halt so. Aber was du probierst, ist, wenn du es in der Nähe hast, dann geht es sicher nicht mehr. Eine der Beobachtungen, die wir gemacht haben, ist, das macht noch keiner. Also, das war für uns auch relativ interessant. Also, das ist auch relativ interessant, wo du die Grenzen setzt, also wo du anfängst und wo du aufhörst. Also, das Fenster, in dem man seine Analyse macht. Also, das Fenster, in dem man seine Analyse macht. Und in unserem Fall haben wir uns auf die Ebene des Handlers begeben haben. Aber es spricht nichts dagegen, auch andere Sachen zu machen, zum Beispiel den halben Händler oder Fenster-Grenzen anders setzen. An der Stelle, die Leute, die keine Frage stellen konnten, ihr könnt sicherlich einen Vortrag nachfragen. Eine Runde Applaus, bitte.