x86-64-Debugging und Disassembly in der Praxis
Funktions-, Stack-, ABI- und Speicherverträge aus einer Absturzadresse rekonstruieren
x86-64-Debugging und Disassembly in der Praxis
Dieser Artikel ist als Lehrmaterial gedacht. Disassembly beobachtet Binärverhalten, beweist aber weder Quellabsicht noch Analyseberechtigung. Analysieren Sie nur Programme und Abstürze, für die Sie berechtigt sind.
In optimierten Builds verschwinden Variablen, Funktionen werden inline und der Instruction Pointer landet in einem einzelnen mov. Wichtiger als Opcodewissen ist ein Ablauf, der Register, Stack, Speicher und ABI nach Beweisstärke verbindet.
Beweise zuerst sichern
Sichern Sie exakte Executables/Module mit Build ID oder Hash, passende PDB/DWARF-Symbole, Exception/Signal, Fault-Adresse und Moduloffset, Register/Flags, Stacktrace und Stackbytes, Read/Write/Execute-Art, Eingabe, Thread, OS und CPU. Symbole eines ähnlichen Builds können denselben Offset falsch zuordnen.
ASLR-Adressen normalisieren:
module_offset = faulting_address - loaded_module_base
Beim Faulting Instruction beginnen
mov eax, dword ptr [rcx + rdx*4]
effective_address = RCX + RDX * 4
access_width = 4 bytes
Prüfen: Ist rcx null, freigegeben oder das falsche Objekt? Ist rdx Index oder Byteoffset? Entspricht die Rechnung der Fault-Adresse? Sind alle vier Bytes gültig? Woher stammen beide Register? Der Faultort ist nicht zwingend der Ursprungsort.
Provenance und Pfadbedingungen
mov rcx, [rbx + 20h]
test rcx, rcx
je error
mov edx, [rsi + 8]
mov eax, [rcx + rdx*4]
Elternobjekte, realen Branchpfad und mögliche Lifetime-Änderungen anderer Threads prüfen. Ein Race lässt sich oft nicht aus einem Threadsnapshot beweisen.
cmp rdx, r8
jae fail
mov eax, [rcx + rdx*4]
Die normale Kante beweist unsigned rdx < r8. Solche Constraints notieren und jl nicht mit jb verwechseln.
Stacktrace und ABI auditieren
Stacktraces hängen von Symbolen und Unwind-Metadaten ab. Buffer Overflow, falsches ASM-Unwind, falsche Symbole, Tail Calls, Inlining oder besondere Context Switches können sie beschädigen. Jede Rücksprungadresse sollte in ausführbarem Modulspeicher an einer plausiblen Call Site liegen.
Windows-Argumente starten meist in rcx,rdx,r8,r9, System V in rdi,rsi,rdx,rcx,r8,r9, werden aber im Funktionskörper wiederverwendet. Erst bis zur Caller-Call-Site zurückverfolgen.
push rbx
sub rsp, 40h
mov rbx, rcx
Mit Eintritt rsp=S Positionen von Saved Registers und Rücksprungadresse ableiten und alle Exits prüfen. Unter Windows .pdata/.xdata gegen den echten Prolog verifizieren.
Indirekte Calls und Speicher
call qword ptr [rax + 18h]
Möglich sind Funktionspointertabelle, Vtable, Import-Thunks oder Callback. Herkunft von rax, Tabellenzugriff, ausführbares Ziel, Calling Convention, Argumente, Objektlebensdauer und CFG/CET prüfen. Eine ausführbare Adresse beweist keine korrekte Signatur.
Speicher als Raw Bytes mit Zugriffsbreite und Little Endian betrachten. Use-after-free kann noch lesbar sein; Guard Pages und AddressSanitizer finden die Ursache oft früher.
Disassembler dekodieren Bytes, Debugger beobachten eine Ausführung, Decompiler schätzen Hochsprache, Profiler sampeln Zeit und Sanitizer instrumentieren bestimmte Vertragsverletzungen. Decompiler-Namen und if bleiben Hypothesen.
Zusammenfassung
x86-64-Crashanalyse erweitert vom Faulting Instruction über effektive Adresse, Registerprovenance, Pfadbedingungen und ABI bis zu Stack und Objektlebensdauer. Exakte Binary/Symbole sind Voraussetzung; Stacktrace und Decompiler müssen verifiziert werden. Abschluss durch Source-Fix und Regressionstests.