Eine kurze Einführung in Reverse Engineering

In den vergangenen Wochen habe ich x86_64-Assembler, AArch64-Assembler und verschiedene Grundlagen gelernt. Vor Kurzem hatte ich endlich meine allererste Erfahrung mit Reverse Engineering. Ich freue mich, mehr Einzelheiten über meine Erfahrung zu teilen.

Was ist Reverse Engineering

Ein Computer ist in Wirklichkeit eine ziemlich dumme Maschine. Er löst kein Problem von selbst; er führt nur die Lösung aus — genauer gesagt führt er nur die Anweisungen von Menschen nacheinander und in der Reihenfolge aus. Um mit so einer Maschine ein Problem zu lösen, gehen wir typischerweise so vor:

  • ein Problem analysieren;
  • eine Lösung für dieses Problem finden;
  • die Lösung durch das Entwerfen eines Algorithmus umsetzen;
  • den Algorithmus durch das Schreiben von Code umsetzen;
  • aus dem Code durch Kompilieren das ausführbare Programm erzeugen;
  • dieses ausführbare Programm der dummen Maschine übergeben, damit sie das Problem löst.

Das ist ein Verfahren, bei dem die Lösung in Code und dann in Anweisungen umgewandelt wird.

Gewöhnlich können wir leicht verstehen, was ein Programm tut, indem wir den Code lesen. Das ist der Mittelweg zwischen der Lösung und den Anweisungen. In den 1970er Jahren wurde Code meist unter Menschen geteilt, damit jede Person Lösungen innerhalb der Gemeinschaft studieren, verbessern und weitergeben konnte.

Seit dem Auftauchen dieser äusserst bösen Monster der Entwicklung proprietärer Software hat sich das jedoch geändert. Sie verweigern dem Volk den Zugang zum Code, nehmen den Nutzerinnen und Nutzern die Kontrolle über ihre Rechner ab, bauen allerlei bösartige Funktionen ein, um ihre Nutzer auszubeuten, und nutzen alle möglichen Methoden, um zu verbergen, was ihre Programme tun — die Lösung. Sie errichten Barrieren. Sie zerstören das Teilen. Sie schaffen Katastrophen und Verzweiflung. Sie tun alles gegen das Volk, die Gemeinschaft und die Menschheit. All das tun sie für ihre unethischen und ungerechten Profite.

Software sollte dem Volk dienen, nicht den unethischen und ungerechten Profiten der Kapitalisten. Doch gerade diese unethischen und ungerechten Entwickler proprietärer Software, die niemals existieren sollten und aus der Welt ausgemerzt werden müssten, machen die Welt schlechter. Aber wir können proprietäre Software nicht auf einmal beseitigen. Wir entwickeln dafür freie Ersatzlösungen und nehmen die Kontrolle über das Rechnen schrittweise zurück.

Deshalb ist es an der Zeit, das oben beschriebene Verfahren in umgekehrter Reihenfolge durchzuführen. Mit anderen Worten:

  • wir analysieren das ausführbare Programm;
  • wir versuchen, den Code wiederherzustellen, den das ausführbare Programm ausführt;
  • wir finden den Algorithmus, den der Code umsetzt;
  • wir kehren die Lösung um, die der Algorithmus umsetzt;
  • wir implementieren die Lösung neu, um das Problem zu lösen.

Das ist die Umwandlung von Anweisungen in Code und dann in Lösung.

Die Praxis, so etwas in umgekehrter Reihenfolge zu tun, heisst Reverse Engineering, auch RE genannt.

Warum Reverse Engineering wichtig ist

Der wichtigste Grund, warum Reverse Engineering wichtig ist, besteht darin, dass es uns hilft, freie Betriebssysteme wie postmarketOS und LineageOS auf verschiedene Geräte zu portieren. Das ist auch der Hauptgrund, warum ich Reverse Engineering lerne.

Auf Desktop-PCs ist Softwarefreiheit viel leichter zu erreichen, da wir normalerweise alle möglichen freien GNU/Linux-Distributionen ohne ernsthafte Probleme ausführen können. Auf Mobilgeräten wie Smartphones oder Tablets ist das jedoch nicht der Fall.

Diese Geräte haben oft viele herstellerspezifische und gerätespezifische Anpassungen, und es ist fast unmöglich, eine Einheitslösung zu finden, um ein freies Betriebssystem universell auf solchen Geräten zum Laufen zu bringen. Noch schlimmer ist, dass die Hersteller diese technischen Details zu verbergen wählen, und selbst Google hat sich entschieden, die Gerätetrees und Hersteller-Binärblobs nicht mehr zu veröffentlichen. Daher müssen wir jetzt die binären Firmware-Blobs analysieren und ihre Logik ableiten, um postmarketOS, LineageOS oder andere freie Betriebssysteme auf diesen Geräten zum Laufen zu bringen.

Tatsächlich arbeitet auch die Free Software Foundation unter dem Projekt Librephone an diesem Problem. Dieses Projekt kann ohne Reverse Engineering nicht fortgeführt werden.

Die Anwendung von Reverse Engineering ist allerdings keineswegs auf das Portieren freier Betriebssysteme beschränkt. Es ist auch von grosser Bedeutung für:

  • das Herausfinden der Einzelheiten proprietärer Protokolle,
  • das Finden von Beweisen für bösartige Funktionen in proprietärer Software,
  • das Umgehen von Digital Restrictions Management,
  • und vieles mehr.

Wie man Reverse Engineering macht

Typisches Reverse Engineering besteht aus diesen zwei Schritten:

  1. Das ausführbare Programm in Assembler umwandeln; dieser Prozess heisst Disassemblieren.
  2. Durch das Lesen des Assemblercodes versuchen, die Ideen wiederherzustellen oder gleichwertigen Code neu zu schreiben.

Wir können dies direkt am ausführbaren Programm tun, ohne es auszuführen; das nennt man statische Analyse, oder während es ausgeführt wird; das nennt man dynamische Analyse.

Beispiel für statische Analyse

Nehmen wir dieses Beispiel:

#include <stdio.h>
#include <string.h>

const char *PASS = "weakpasswd";

int main()
{
    char buf[64];
    for (int i = 0; i < 3; i++)
    {
        printf("%d verbleibende Versuche.\nPasswort: ", 3 - i);
        fgets(buf, sizeof buf, stdin);
        if (strncmp(PASS, buf, strlen(PASS)))
            puts("Falsches Passwort!\n");
        else
        {
            puts("Richtig!\nWillkommen in der Welt "
                 "des Reverse Engineering!\n");
            break;
        }
    }
    return 0;
}

Kompilieren Sie dies mit GCC:

$ gcc -o ./demo ./demo.c

Öffnen Sie das ausführbare Programm in radare2:

$ r2 ./demo

Sie werden dies sehen:

[0x00000800]> 

Analysieren wir das ausführbare Programm:

[0x00000800]> aaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Analyze all functions arguments/locals (afva@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Finding function preludes (aap)
INFO: Emulate functions to find computed references (aaef)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (afft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis
INFO: Finding xrefs in noncode sections (e anal.in=io.maps.x; aav)
WARN: Skipping aav because base address is zero. Use -B 0x800000 or aav0

Listen wir die Funktionen auf:

[0x00000800]> afl
0x00000750    1     16 sym.imp.strlen
0x00000760    1     16 sym.imp.__libc_start_main
0x00000770    1     16 sym.imp.__cxa_finalize
0x00000780    1     32 sym.imp.strncmp
0x000007a0    1     16 sym.imp.abort
0x000007b0    1     16 sym.imp.puts
0x000007c0    1     16 sym.imp.printf
0x000007d0    1     20 sym.imp.fgets
0x00000800    1     48 entry0
0x00000834    3     20 sym.call_weak_fn
0x00000860    4     48 sym.deregister_tm_clones
0x00000890    4     60 sym.register_tm_clones
0x000008cc    5     80 entry.fini0
0x00000920    1      8 entry.init0
0x000009f8    1     24 sym._fini
0x00000928    7    208 main
0x00000708    1     28 sym._init
0x00000730    1     32 fcn.00000730

Jetzt können wir in die Funktion main springen und den Disassemblierungscode ausgeben:

[0x00000800]> s main
[0x00000928]> pdf
            ; DATA XREF from entry0 @ 0x820(r)
            ; DATA XREF from entry.fini0 @ 0x8e0(r)
┌ 208: int main (int argc);
│ `- args(x0) vars(5:sp[0x4..0x70])
│           0x00000928      fd7bb9a9       stp x29, x30, [sp, -0x70]!
│           0x0000092c      fd030091       mov x29, sp
│           0x00000930      f30b00f9       str x19, [var_10h]
│           0x00000934      ff6f00b9       str wzr, [var_6ch]          ; argc
│       ┌─< 0x00000938      29000014       b 0x9dc
│       │   ; CODE XREF from main @ 0x9e4(x)
│      ┌──> 0x0000093c      61008052       mov w1, 3
│      ╎│   0x00000940      e06f40b9       ldr w0, [var_6ch]
│      ╎│   0x00000944      2000004b       sub w0, w1, w0
│      ╎│   0x00000948      e103002a       mov w1, w0
│      ╎│   0x0000094c      00000090       adrp x0, 0
│      ╎│   0x00000950      00a02891       add x0, x0, str._d_retries_remaining._nPassword: ; 0xa28 ; "%d verbleibende Versuche.\nPasswort: " ; const char *format
│      ╎│   0x00000954      9bffff97       bl sym.imp.printf           ; int printf(const char *format)
│      ╎│   0x00000958      e00000f0       adrp x0, 0x1f000
│      ╎│   0x0000095c      00e447f9       ldr x0, [x0, 0xfc8]         ; [0x1ffc8:4]=0
│      ╎│                                                              ; reloc.stdin
│      ╎│   0x00000960      010040f9       ldr x1, [x0]                ; int size
│      ╎│   0x00000964      e0a30091       add x0, sp, 0x28            ; char *s
│      ╎│   0x00000968      e20301aa       mov x2, x1                  ; FILE *stream
│      ╎│   0x0000096c      01088052       mov w1, 0x40
│      ╎│   0x00000970      98ffff97       bl sym.imp.fgets            ; char *fgets(char *s, int size, FILE *stream)
│      ╎│   0x00000974      00010090       adrp x0, reloc.strlen       ; 0x20000
│      ╎│   0x00000978      00600191       add x0, x0, 0x58
│      ╎│   0x0000097c      130040f9       ldr x19, [x0]               ; [0xa18:4]=0x6b616577 ; "weakpasswd"
│      ╎│   0x00000980      00010090       adrp x0, reloc.strlen       ; 0x20000
│      ╎│   0x00000984      00600191       add x0, x0, 0x58
│      ╎│   0x00000988      000040f9       ldr x0, [x0]                ; [0xa18:4]=0x6b616577 ; "weakpasswd" ; const char *s
│      ╎│   0x0000098c      71ffff97       bl sym.imp.strlen           ; size_t strlen(const char *s)
│      ╎│   0x00000990      e10300aa       mov x1, x0
│      ╎│   0x00000994      e0a30091       add x0, sp, 0x28
│      ╎│   0x00000998      e20301aa       mov x2, x1                  ; size_t n
│      ╎│   0x0000099c      e10300aa       mov x1, x0                  ; const char *s2
│      ╎│   0x000009a0      e00313aa       mov x0, x19                 ; const char *s1
│      ╎│   0x000009a4      77ffff97       bl sym.imp.strncmp          ; int strncmp(const char *s1, const char *s2, size_t n)
│      ╎│   0x000009a8      1f000071       cmp w0, 0
│     ┌───< 0x000009ac      a0000054       b.eq 0x9c0
│     │╎│   0x000009b0      00000090       adrp x0, 0
│     │╎│   0x000009b4      00402991       add x0, x0, str.Wrong_password__n ; 0xa50 ; "Falsches Passwort!\n" ; const char *s
│     │╎│   0x000009b8      7effff97       bl sym.imp.puts             ; int puts(const char *s)
│    ┌────< 0x000009bc      05000014       b 0x9d0
│    ││╎│   ; CODE XREF from main @ 0x9ac(x)
│    │└───> 0x000009c0      00000090       adrp x0, 0
│    │ ╎│   0x000009c4      00a02991       add x0, x0, str.Correct__nWelcome_to_the_world_of_reverse_engineering__n ; 0xa68 ; "Richtig!\nWillkommen in der Welt des Reverse Engineering!\n" ; const char *s
│    │ ╎│   0x000009c8      7affff97       bl sym.imp.puts             ; int puts(const char *s)
│    │┌───< 0x000009cc      07000014       b 0x9e8
│    ││╎│   ; CODE XREF from main @ 0x9bc(x)
│    └────> 0x000009d0      e06f40b9       ldr w0, [var_6ch]
│     │╎│   0x000009d4      00040011       add w0, w0, 1
│     │╎│   0x000009d8      e06f00b9       str w0, [var_6ch]
│     │╎│   ; CODE XREF from main @ 0x938(x)
│     │╎└─> 0x000009dc      e06f40b9       ldr w0, [var_6ch]
│     │╎    0x000009e0      1f080071       cmp w0, 2
│     │└──< 0x000009e4      cdfaff54       b.le 0x93c
│     │     ; CODE XREF from main @ 0x9cc(x)
│     └───> 0x000009e8      00008052       mov w0, 0
│           0x000009ec      f30b40f9       ldr x19, [var_10h]
│           0x000009f0      fd7bc7a8       ldp x29, x30, [sp], 0x70
└           0x000009f4      c0035fd6       ret

Radare2 liefert uns nicht nur den Assemblercode, sondern auch die jeweilige Adresse jeder Anweisung und die Aufrufbeziehungen zwischen ihnen.

Es ist jedoch noch nicht besonders klar. Zeigen wir also die Disassemblierung als Graph an:

[0x00000928]> VV

Nun werden Sie dies sehen:

Graphansicht der radare2-Disassemblierung

Im Graphen bedeutet t, dass bei wahrer Bedingung gesprungen wird. f bedeutet, dass bei falscher Bedingung gesprungen wird. v bedeutet einen unbedingten Sprung.

Jetzt wird das Leben viel einfacher. Radare2 hat die logischen Beziehungen zwischen den Assemblercode-Blöcken bereits herausgefunden, und Sie müssen nur noch die Assemblercode-Blöcke selbst verstehen.

Im Graphen können wir die Logik der Passwortprüfung am Ende des [0x93c]-Blocks leicht finden:

; int strncmp(const char *s1, const char *s2, size_t n)
bl sym.imp.strncmp
cmp w0, 0
b.eq 0x9c0

Hier wird sym.imp.strncmp aufgerufen, und dann wird der von dieser Funktion in w0 zurückgegebene Wert überprüft. Wenn er gleich 0 ist, springt das Programm zum [0x9c0]-Block und signalisiert Erfolg. Andernfalls geht es zum [0x9b0]-Block weiter, was einen Fehler bedeutet.

Gemäss der AArch64-Aufrufkonvention befinden sich die Zeiger auf die beiden zu vergleichenden Zeichenketten in den Registern x0 und x1; dann rufen wir sym.imp.strncmp auf. Das Ergebnis wird anschliessend im Register x0 gespeichert (w0 ist einfach die untere Hälfte von x0), und wenn die beiden Zeichenketten identisch sind, erhalten wir 0.

Nun schauen wir zurück und sehen:

mov x0, x19

Dann schauen wir weiter zurück, um die Anweisung zu finden, die x19 bearbeitet:

; [0xa18:4]=0x6b616577
; "weakpasswd"
ldr x19, [x0]

Radare2 hat uns das Top-Secret bereits verraten. Probieren wir es aus:

$ ./demo
3 verbleibende Versuche.
Passwort: weakpasswd
Richtig!
Willkommen in der Welt des Reverse Engineering!

Das ist alles.

Wenn wir den Graphen genauer betrachten, können wir mehr als nur das Passwort lernen. Zum Beispiel sehen wir im Graphen einen Zyklus: [0x9dc] -> [0x93c] -> [0x9b0] -> [0x9d0] -> [0x9dc]. In einem Graphen deutet ein Zyklus oft auf Schleifensteuerfluss hin. Das ist eine sehr wichtige Denkweise beim Reverse Engineering.

Im [0x9dc]-Block:

ldr w0, [var_6ch]
cmp w0, 2
b.le 0x93c

Er lädt die Variable var_6ch in ein Register und vergleicht sie mit 2. Wenn sie grösser ist, geht es zum [0x9e8]-Block und das Programm endet. Andernfalls geht es zum [0x93c]-Block.

Schauen Sie sich das Ende des [0x93c]-Blocks an. Wir wissen bereits, dass das Programm bei erfolgreicher Passwortprüfung zum [0x9c0]-Block springt, die Erfolgsnachricht ausgibt und dann zum [0x9e8]-Block springt, der das Programm beendet.

Aber was geschieht, wenn die Prüfung fehlschlägt? Dann geht es zum [0x9b0]-Block. Dort wird einfach die Fehlermeldung ausgegeben, also ist hier nichts Interessantes; gehen wir also zum [0x9d0]-Block weiter. Dort finden wir etwas Interessantes:

ldr w0, [var_6ch]
add w0, w0, 1
str w0, [var_6ch]

Er lädt die Variable var_6ch in ein Register, erhöht den Wert um eins und speichert den Wert wieder zurück. Dann springt er zurück zum [0x9dc]-Block, der den Wert von var_6ch überprüft.

Drücken Sie zweimal q, um zur Eingabeaufforderung zurückzukehren. Führen Sie afv und afvd aus, um die Variablen und ihre Informationen aufzulisten:

[0x000009b0]> afv
arg int argc @ x0
var int64_t var_70h @ sp+0x0
var int64_t var_70h_2 @ sp+0x8
var int64_t var_10h @ sp+0x10
var char * s2 @ sp+0x28
var int64_t var_6ch @ sp+0x6c
[0x000009b0]> afvd
arg argc = 0x00000000 0x00010102464c457f   .ELF.... @ pstate
var var_6ch = 0x0017806c = (qword)0x0000000000000000
var s2 = 0x00178028 = ""
var var_10h = 0x00178010 = (qword)0x0000000000000000
var var_70h = 0x00178000 = (qword)0x0000000000000000
var var_70h_2 = 0x00178008 = (qword)0x0000000000000000

Wir sehen, dass der Wert var_6ch ein int64_t ist und sein Anfangswert 0 beträgt.

Nun können wir schliessen, dass var_6ch ein Zähler ist. Sein Anfangswert ist 0. Wenn die Passwortprüfung fehlschlägt, wird der Zähler um eins erhöht. Sobald er grösser als 2 ist, fragt das Programm nicht mehr nach dem Passwort und beendet sich. Nun können wir sagen, dass dies unserem C-Code for (int i = 0; i < 3; i++) entspricht!

Das ist jedoch ein sehr einfaches Beispiel. In der realen Welt wird es schwieriger, da Entwickler proprietärer Software oft Anti-Analyse- und Anti-Debugging-Techniken einsetzen.

Compileroptimierungen verstehen

Kompilieren wir nun diesen noch einfacheren Code:

#include <stdio.h>

int main()
{
    int a;
    scanf("%d", &a);
    printf("%d", a % 65536);
    return 0;
}

Er liest eine ganze Zahl aus der Eingabe und gibt ihr Modulo 65536 aus.

Disassemblieren wir seine main-Funktion:

[0x00000700]> aaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
......
WARN: Skipping aav because base address is zero. Use -B 0x800000 or aav0
[0x00000700]> s main
[0x00000828]> pdf
            ; DATA XREF from entry0 @ 0x720(r)
            ; DATA XREF from entry.fini0 @ 0x7e0(r)
┌ 76: int main (int argc, char **argv, char **envp);
│ afv: vars(3:sp[0x4..0x20])
│           0x00000828      fd7bbea9       stp x29, x30, [sp, -0x20]!
│           0x0000082c      fd030091       mov x29, sp
│           0x00000830      e0730091       add x0, sp, 0x1c
│           0x00000834      e10300aa       mov x1, x0
│           0x00000838      00000090       adrp x0, 0
│           0x0000083c      00602291       add x0, x0, 0x898
│           0x00000840      94ffff97       bl sym.imp.__isoc23_scanf
│           0x00000844      e01f40b9       ldr w0, [var_1ch]
│           0x00000848      e103006b       negs w1, w0
│           0x0000084c      003c0012       and w0, w0, 0xffff
│           0x00000850      213c0012       and w1, w1, 0xffff
│           0x00000854      0044815a       csneg w0, w0, w1, mi
│           0x00000858      e103002a       mov w1, w0
│           0x0000085c      00000090       adrp x0, 0
│           0x00000860      00602291       add x0, x0, 0x898           ; const char *format
│           0x00000864      97ffff97       bl sym.imp.printf           ; int printf(const char *format)
│           0x00000868      00008052       mov w0, 0
│           0x0000086c      fd7bc2a8       ldp x29, x30, [sp], 0x20
└           0x00000870      c0035fd6       ret

Sie könnten verwirrt sein, weil Sie hier nichts über eine Modulo-Operation sehen (sdiv, mul, sub usw.). Stattdessen sehen Sie:

and w0, w0, 0xffff
and w1, w1, 0xffff

Das liegt daran, dass moderne Compiler klug genug sind, gewisse spezielle Berechnungsmuster zu erkennen und sie in einfachere Anweisungen zu optimieren. 65536 ist 2^16, also ist die binäre Zahl modulo 65536 einfach ihre niederwertigsten 16 Bits. Eine bitweise Operation reicht aus; es sind keine Addierer oder Multiplikatoren nötig.

Wenn wir 65536 jedoch durch etwas anderes ersetzen, zum Beispiel 50000, ist das nicht mehr der Fall:

            ; DATA XREF from entry0 @ 0x720(r)
            ; DATA XREF from entry.fini0 @ 0x7e0(r)
┌ 80: int main (int argc, char **argv, char **envp);
│ afv: vars(3:sp[0x4..0x20])
│           0x00000828      fd7bbea9       stp x29, x30, [sp, -0x20]!
│           0x0000082c      fd030091       mov x29, sp
│           0x00000830      e0730091       add x0, sp, 0x1c
│           0x00000834      e10300aa       mov x1, x0
│           0x00000838      00000090       adrp x0, 0
│           0x0000083c      00602291       add x0, x0, 0x898
│           0x00000840      94ffff97       bl sym.imp.__isoc23_scanf
│           0x00000844      e01f40b9       ldr w0, [var_1ch]
│           0x00000848      016a9852       mov w1, 0xc350
│           0x0000084c      020cc11a       sdiv w2, w0, w1
│           0x00000850      016a9852       mov w1, 0xc350
│           0x00000854      417c011b       mul w1, w2, w1
│           0x00000858      0000014b       sub w0, w0, w1
│           0x0000085c      e103002a       mov w1, w0
│           0x00000860      00000090       adrp x0, 0
│           0x00000864      00602291       add x0, x0, 0x898           ; const char *format
│           0x00000868      96ffff97       bl sym.imp.printf           ; int printf(const char *format)
│           0x0000086c      00008052       mov w0, 0
│           0x00000870      fd7bc2a8       ldp x29, x30, [sp], 0x20
└           0x00000874      c0035fd6       ret

Nun sehen wir die Anweisungen sdiv, mul und sub, die zusammen die Modulo-Operation ausführen.

Und was ist mit dynamischer Analyse?

Für die dynamische Analyse benötigen Sie GDB. Mit GDB können Sie Disassemblierung anzeigen, Haltepunkte setzen, Speicher und Register untersuchen und sogar die Werte von Registern und Variablen ändern, während das Programm läuft.

Ich habe die dynamische Analyse bisher noch nicht im Detail untersucht. Vielleicht schreibe ich in Zukunft einen neuen Blogbeitrag, um die dynamische Analyse zu erklären.

Empfohlene Ressourcen

Was ich hier erklärt habe, ist nur die Spitze des Eisbergs. Um Reverse Engineering zu lernen, müssen Sie tiefer gehen, mehr üben und durch Tun lernen. Nachfolgend eine Liste empfohlener Ressourcen:

  1. Pwn.college ist ein guter Ort für den Einstieg. Sie können dort im Einführungs-Dojo x86_64-Assembler lernen und anschliessend im Modul Intro to Cybersecurity zum Bereich Reverse Engineering übergehen.
  2. Reverse Engineering for Beginners ist ein sehr gutes Buch über Reverse Engineering. Es ist frei (im Sinne von Freiheit), unter der Lizenz CC BY-SA 4.0 veröffentlicht und hier zum Herunterladen verfügbar.
  3. ARM bietet offizielle Dokumentation für seine AArch64-Architektur an.
  4. Hier können Sie Crackmes herunterladen, um Ihre Fähigkeiten im Reverse Engineering zu üben.
  5. Hier ist ein GitHub-Repository mit vielen Ressourcen zum Erlernen von Reverse Engineering.

Haftungsausschluss

Dieser Artikel dient nur zu Bildungszwecken. Bitte konsultieren Sie Ihre örtlichen Gesetze, um zu erfahren, was Sie nicht tun dürfen. Ich bin nicht für irgendwelche Ihrer Handlungen verantwortlich.

Erstellt mit Hugo
Theme Stack gestaltet von Jimmy