Retro Reverse Engineering · Amiga · VB.NET
Als ich neulich wieder einmal Amberstar auf dem Amiga-Emulator gespielt habe, ergab sich für mich die Notwendigkeit die Werte meiner Spielercharaktere frei verändern zu können. Daraus wurde ein kleines Projekt, das ich euch heute vorstellen möchte.
Am Ende stand ein Windows-Tool in VB.NET, das sowohl klassische Savegame-Dateien als auch Amiga-Disk-Images (ADF) direkt lesen und schreiben kann — ohne Hex-Editor, ohne Emulator-Tricks.
Das ist der fertige Editor - Der interessante Teil ist aber nicht das fertige Tool, sondern der Weg dorthin.
Das Ziel: Charakterwerte in Amberstar (Amiga) editieren — direkt in den originalen Savegame-Dateien, ohne Hex-Editor. Der Weg: Dateiformat durch Reverse Engineering verstehen, dann ein komfortables UI bauen.
Die Datei verstehen: CHARDATA.AMB
Amberstar speichert Charakterdaten in einer Datei namens CHARDATA.AMB. Die Endung .AMB ist kein Zufall: Amberstar verwendet ein eigenes Container-Format — nennen wir es AMBR — das mehrere Unter-Dateien in einer einzigen Datei bündelt. Das ist vergleichbar mit einem ZIP-Archiv, nur viel simpler.
Der AMBR-Header
Mit einem Hex-Editor wird die Struktur schnell klar. Die Datei beginnt mit einem kurzen Header:
Offset Bytes Bedeutung 0×0000 41 4D 42 52 Magic ”AMBR” 0×0004 00 14 Anzahl Einträge (hier: 20 Unter-Dateien) 0×0006 00 00 01 44 Größe Eintrag 0 (= 324 Bytes) 0×000A 00 00 01 44 Größe Eintrag 1 … … (für alle n Einträge)
Nach der Tabelle folgen die Unter-Dateien direkt hintereinander — keine Lücken, kein Alignment. Wichtig: alle Zahlen sind Big-Endian (Amiga = Motorola 68000). Ein 16-Bit-Wert wie 0×00 0×14 bedeutet also Dezimal 20, nicht 5120.
‘ Anzahl Unter-Dateien aus Bytes 4-5 Dim numSubFiles As Integer = (CInt(fileBytes(4)) << 8) Or CInt(fileBytes(5)) ‘ Größen-Tabelle auslesen (ab Byte 6, je 4 Bytes pro Eintrag) Dim pos As Integer = 6 + numSubFiles * 4 For i As Integer = 0 To numSubFiles - 1 Dim e As Integer = 6 + i * 4 subFileSizes(i) = (CInt(fileBytes(e + 2)) << 8) Or CInt(fileBytes(e + 3)) subFileOffsets(i) = pos pos += subFileSizes(i) Next
Ein typischer Fehler beim Big-Endian-Lesen in VB.NET: CInt(byte) << 24 kann einen Integer-Überlauf erzeugen, wenn das höchste Bit gesetzt ist. Die Lösung ist CLng() für Zwischenrechnungen — erst am Ende wieder nach Integer konvertieren.
Charaktere finden: Wer steckt wo?
CHARDATA.AMB enthält 20 Unter-Dateien. Aber welche gehören zu meinen sechs Partycharakteren? Mein erster Instinkt — die ersten sechs nehmen — war falsch. Die Datei enthält offensichtlich auch Daten für NPCs oder andere Spielobjekte.
Der Name als Anker
Der entscheidende Hinweis: Jeder Charakter kennt seinen eigenen Namen. Wenn ich weiß, dass mein erster Charakter “Tom” heißt, muss dieser Name irgendwo in dessen Unter-Datei stehen — als druckbare ASCII-Bytes.
Die Methodik: Für jede Unter-Datei ab einem bestimmten Offset die Bytes als Zeichen interpretieren. Sind alle Bytes im druckbaren Bereich (0×20–0×7E)? Dann ist es ein gültiger Name. Ist irgendein Byte außerhalb dieses Bereichs — sofort abbrechen und als “kein Name” markieren.
Private Function ReadName(offset As Integer) As String Dim result As String = ”" Dim i As Integer = offset Do While i < fileBytes.Length Dim b As Byte = fileBytes(i) If b = 0 Then Exit Do ’ Ungültiges Zeichen → kein Name hier If b < 32 OrElse b > 126 Then Return ”" result &= Chr(b) i += 1 Loop Return result End Function
Mit etwas Probieren zeigte sich: Bei Offset 0xF0 innerhalb jeder Unter-Datei steht der Charaktername (null-terminiert). Alle sechs Partymitglieder waren so eindeutig identifizierbar — und die “falschen” Einträge enthielten dort binäre Daten, die sofort herausgefiltert wurden.
Die Werte kartieren: Vom bekannten Wert zum Offset
Jetzt kommt der eigentlich spannende Teil: Wo genau steckt z.B. Toms Paradewert 30? Die klassische Methode beim Reverse Engineering von Savegames heißt Known-Value-Search: Man sucht nach einem Wert, den man kennt.
Schritt 1: Wert suchen
Tom hat einen Paradewert von 30, also dezimal. Als Hex ist das 0×1E. Als 16-Bit-Big-Endian-Wert: 0×00 0×1E. Diese Zwei-Byte-Sequenz suche ich in Toms Sub-File-Daten — und finde sie an einer Handvoll Stellen.
Schritt 2: Kreuzvergleich
Jetzt kommt der entscheidende Trick: Ich schaue mir die Werte meiner anderen Charaktere an und prüfe, ob an den gleichen Offsets deren Werte stehen. Silk hat ATT 50 und PAR 07 — passt das zu dem Offset, den ich bei Tom gefunden habe?
Bei Offset 0×07 (relativ zum Start der Sub-Datei) steht Toms Parade-Wert 30 = 0×1E. Bei Silk steht dort der Wert 7 = 0×07. Treffer! Die Fähigkeits-Bytes beginnen bei 0×06.
Durch dieses systematische Kreuzprüfen aller Charaktere ergab sich die vollständige Offset-Tabelle:
Offset Größe Inhalt 0×06 10 × 1 Byte Fähigkeiten aktuell (ATT, PAR, SWM, …) 0×10 10 × 1 Byte Fähigkeiten Maximum 0×1B 1 Byte Level 0×48 7 × 1 Byte Attribute aktuell (STR, INT, GES, …) 0×5C 7 × 1 Byte Attribute Maximum 0×86 2 Bytes LP aktuell* (inkl. Ausrüstungsbonus, read-only) 0×88 2 Bytes LP Basis (editierbar) 0×8A 2 Bytes SP aktuell* (read-only) 0×8C 2 Bytes SP Basis 0×8E 2 Bytes SLP (Spruchlernpunkte) 0×90 4 Bytes Gold (32-Bit) 0xCE 4 Bytes Erfahrungspunkte (32-Bit) 0xF0 max. 15 Charaktername (null-terminiert)
Eine interessante Falle lauerte bei den Lebenspunkten: Toms “LP aktuell” war höher als seine “LP Max”. Des Rätsels Lösung: Die “LP Max” im Savegame ist der Basis-Wert ohne Ausrüstung. Toms Rüstung gibt +10 LP — daher ist der angezeigte aktuelle Wert größer. Konsequenz für den Editor: LP und SP aktuell sind read-only, nur die Basis-Werte werden editiert.
Der ADF-Reader: Amiga-Filesystem in 200 Zeilen VB.NET
Das war das Sahnehäubchen des Projekts. Amberstar-Savegames liegen auf einer virtuellen Amiga-Diskette im ADF-Format — einem rohen Sektor-Dump der Diskette. Das Amiga Old File System (OFS) ist durchaus komplex, aber für unsere Zwecke reicht ein lesend/schreibender Zugriff auf eine bekannte Datei.
Wie ein ADF aufgebaut ist
Ein ADF ist eine 880 KB große Datei (80 Spuren × 2 Seiten × 11 Sektoren × 512 Bytes). Sektor 880 ist der Root-Block — das Verzeichnis der Diskette. Er enthält eine Hash-Tabelle mit 72 Einträgen, über die Dateinamen gefunden werden.
Datei finden: Die AmigaOS Hash-Funktion
Um eine Datei im Root-Block zu finden, berechnet man einen Hash über den Dateinamen. AmigaOS verwendet dafür eine simple, aber spezifische Funktion:
Private Function HashName(name As String) As Integer name = name.ToUpper() Dim h As Integer = name.Length For Each c As Char In name h = ((h * 13) + Asc(c)) And &H7FF Next Return h Mod 72 ’ 72 Slots in der Hash-Tabelle End Function
Der Hash gibt den Slot in der 72-Einträge-Tabelle des Root-Blocks an. Da mehrere Dateien denselben Hash haben können, ist an jeder Slot-Position eine einfach verkettete Liste: Offset 496 jedes Datei-Headers zeigt auf den nächsten Eintrag mit gleichem Hash.
Checksum: Der knifflige Teil
Wenn ich in einem OFS-Datenblock Daten verändere, muss ich die Prüfsumme neu berechnen — sonst lehnt der Amiga (oder Emulator) den Block als korrupt ab. Die AmigaOS-Checksumme ist die bitweise Negierung der Summe aller 32-Bit-Wörter im Block, wobei das Checksum-Feld selbst als null gezählt wird:
Private Sub FixChecksum(block() As Byte, checksumOffset As Integer) ’ Schritt 1: Checksum-Feld auf null setzen block(checksumOffset) = 0 block(checksumOffset + 1) = 0 block(checksumOffset + 2) = 0 block(checksumOffset + 3) = 0 ’ Schritt 2: Alle 32-Bit-Wörter summieren (modulo 2³²) Dim s As Long = 0 For i As Integer = 0 To 508 Step 4 s = (s + ReadU32Unsigned(block, i)) And &HFFFFFFFFL Next ’ Schritt 3: Negieren (= 2³² - Summe) und eintragen Dim chk As Long = (&H100000000L - s) And &HFFFFFFFFL block(checksumOffset) = CByte((chk >> 24) And &HFF) block(checksumOffset + 1) = CByte((chk >> 16) And &HFF) block(checksumOffset + 2) = CByte((chk >> 8) And &HFF) block(checksumOffset + 3) = CByte(chk And &HFF) End Sub
Der Test: Ich habe Toms Paradewert in einem realen ADF auf 60 geändert, das Image gespeichert, im Emulator geladen — und der Wert war korrekt übernommen. Checksumme stimmte, keine Fehlermeldung. Das war der schönste Moment des ganzen Projekts.
Fazit: Was ich gelernt habe
- Reverse Engineering braucht Anker. Bekannte Werte (Toms Parade = 30) sind der Einstiegspunkt. Von dort aus kartiert man durch Kreuzvergleich systematisch alle Offsets.
- Big-Endian ist überall. Jeder Amiga-Wert ist Motorola-Big-Endian. In .NET (Little-Endian) muss man das konsequent im Kopf behalten — und für Intermediate-Rechnungen auf CLng() ausweichen.
- OFS ist erstaunlich transparent. Das Amiga Old File System ist vergleichsweise simpel und gut dokumentiert. Wer einmal Sektor-Kette, Hash-Lookup und Checksum verstanden hat, kann in ein paar hundert Zeilen einen vollständigen Reader/Writer bauen.
- Backup zuerst. Das Tool legt automatisch eine .bak-Datei an, bevor es schreibt. Bei Experimenten mit binären Formaten unverzichtbar.
Das Projekt hat mir wieder einmal gezeigt, wie zugänglich Retro-Reverse-Engineering ist — man braucht keinen Disassembler und keine Kernel-Kenntnisse. Ein Hex-Editor, etwas Geduld und ein paar bekannte Ausgangswerte reichen, um ein unbekanntes Binärformat zu entschlüsseln.
Die Entwicklung dieses Editors war aber kein rein akademisches Projekt. Wer wissen will, wofür ich den Editor dann tatsächlich eingesetzt habe, kann das hier in meinem Video über die Analyse des Kampfsystems in Amberstar ansehen.
