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:
- Das ausführbare Programm in Assembler umwandeln; dieser Prozess heisst Disassemblieren.
- 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:

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:
- 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.
- 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.
- ARM bietet offizielle Dokumentation für seine AArch64-Architektur an.
- Hier können Sie Crackmes herunterladen, um Ihre Fähigkeiten im Reverse Engineering zu üben.
- 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.