
Autor des Artikels https://github.com/Nalen98
Guten Tag!
Das Thema meiner Forschung im Rahmen des Sommerpraktikums Summer of Hack 2019 bei Digital Security war „Dekompilieren von eBPF in Ghidra“. In PCode Ghidra in Sleigh musste ein eBPF-Bytecode-Übersetzungssystem entwickelt werden, damit eBPF-Programme zerlegt und dekompiliert werden konnten. Das Ergebnis der Studie ist eine entwickelte Erweiterung für Ghidra, die den eBPF-Prozessor unterstützt. Die Studie kann wie die anderer Praktikanten zu Recht als „wegweisend“ angesehen werden, da es früher nicht möglich war, eBPF in anderen Reverse Engineering-Tools zu dekompilieren.
Hintergrund
Dieses Thema ging mir in einer großen Ironie des Schicksals zu, weil ich eBPF vorher nicht gekannt hatte und Ghidr es vorher nicht benutzt hatte, weil es ein Dogma gab, dass "IDA Pro besser ist". Wie sich herausstellte, ist dies nicht ganz richtig.
Die Bekanntschaft mit Ghidra erwies sich als sehr schnell, da die Entwickler sehr kompetente und zugängliche Dokumentationen erstellten. Außerdem musste ich die Spezifikationssprache des Schlittenprozessors beherrschen, auf der die Entwicklung durchgeführt wurde. Die Entwickler haben ihr Bestes gegeben und eine sehr detaillierte Dokumentation sowohl für das Tool selbst als auch für Sleigh erstellt , wofür wir uns sehr bedanken .
Auf der anderen Seite der Barrikade befand sich ein erweiterter Berkeley Packet Filter. eBPF ist eine virtuelle Maschine im Linux-Kernel, mit der Sie beliebigen Benutzercode laden können, mit dem Prozesse verfolgt und Pakete im Kernelraum gefiltert werden können. Die Architektur ist eine RISC-Registermaschine mit 11 64-Bit-Registern, einem Softwarezähler und einem 512-Byte-Stapel. Es gibt eine Reihe von Einschränkungen für eBPF:
- Zyklen sind verboten;
- Der Zugriff auf den Speicher ist nur über den Stapel möglich (es wird eine separate Geschichte darüber geben).
- Kernelfunktionen sind nur über spezielle Wrapper-Funktionen (eBPF-Helfer) verfügbar.

Die Struktur der eBPF-Technologie. Bildquelle: http://www.brendangregg.com/ebpf.html .
Grundsätzlich wird diese Technologie für Netzwerkaufgaben verwendet - Debugging, Paketfilterung usw. auf Kernelebene. Die Unterstützung für eBPF wurde seit der Version 3.15 des Kernels hinzugefügt. Auf der Linux-Installationskonferenz 2019 wurden dieser Technologie einige Berichte gewidmet. Im Gegensatz zu Ghidra ist die Dokumentation bei eBPF jedoch unvollständig und enthält nicht viel. Daher mussten im Internet nach Klarstellungen und fehlenden Informationen gesucht werden. Es hat eine ganze Weile gedauert, um die Antworten zu finden, und es bleibt nur zu hoffen, dass die Technologie fertiggestellt und eine normale Dokumentation erstellt wird.
Schlechte Dokumentation
Um eine Spezifikation für Sleigh zu entwickeln, müssen Sie zunächst die Funktionsweise der Architektur des Zielprozessors verstehen. Und hier wenden wir uns der offiziellen Dokumentation zu .
Es enthält eine Reihe von Mängeln:
Die Struktur der eBPF-Anweisungen ist nicht vollständig beschrieben.
Die meisten Spezifikationen, wie z. B. Intel x86, geben normalerweise an, zu welchem Befehlsbit gehört und zu welchem Block es gehört. Leider sind diese Details in der eBPF-Spezifikation entweder im gesamten Dokument verteilt oder fehlen vollständig, weshalb wir die fehlenden Körner aus den Implementierungsdetails im Linux-Kernel ziehen müssen.
Zum Beispiel wird in der Befehlsstruktur op:8, dst_reg:4, src_reg:4, off:16, imm:32
kein Wort gesagt, dass Offset (aus) und sofort (imm) signed
, und dies ist äußerst wichtig, weil es sich auswirkt von arithmetischen Anweisungen zu Sprüngen zu arbeiten. Der Quellcode für den Linux-Kernel hat geholfen.
Es gibt kein vollständiges Bild aller möglichen Mnemoniken der Architektur.
In einigen Dokumentationen werden nicht nur alle Anweisungen, ihre Operanden angegeben, sondern auch ihre Semantik in C, Anwendungsfällen, Operandenfunktionen usw. Die eBPF-Dokumentation enthält Anweisungsklassen, dies reicht dem Entwickler jedoch nicht aus. Betrachten wir sie genauer.
Alle Anweisungen von eBPF sind 64-Bit, mit Ausnahme von LDDW
(Load Double Word). Es hat eine Größe von 128 Bit und verkettet zwei imm mit jeweils 32 Bit. eBPF-Anweisungen haben die folgende Struktur.

eBPF-Anweisungscodierung
Die Struktur des OPAQUE
Felds hängt von der OPAQUE
ab (ALU / JMP, Load / Store).
Zum Beispiel die ALU
Anweisungsklasse:

Codierung von ALU-Anweisungen
und die JMP
Klasse haben ihre eigene Feldstruktur:

Codierung von Verzweigungsanweisungen
Bei Anweisungen zum Laden / Speichern ist die Struktur unterschiedlich:

Codierung der Lade- / Speicheranweisungen
Inoffizielle eBPF-Dokumentation half dabei, dies zu klären .
Es gibt keine Informationen zu Call-Helfern, auf denen der größte Teil der Logik von eBPF-Programmen für den Linux-Kernel basiert.
Und das ist äußerst seltsam, da Helfer in eBPF-Programmen das Wichtigste sind und nur die Aufgaben ausführen, auf die sich die Technologie konzentriert.

EBPF-Interoperabilität mit Kernfunktionen
Das Programm ruft diese Funktionen aus dem Kernel ab und sie arbeiten nur mit Prozessen, bearbeiten Netzwerkpakete, arbeiten mit eBPF-Maps, greifen auf Sockets zu und interagieren mit dem Userspace. Trotz der Tatsache, dass die Funktionen immer noch nuklear sind, lohnt es sich, in der offiziellen Dokumentation ausführlicher darüber zu schreiben. Ausführliche Informationen finden Sie in der Linux- Quelle .
- Kein Wort über Tail Calls.

EBPF-Tail-Calls. Bildquelle: https://cilium.readthedocs.io/en/latest/bpf/#tail-calls .
Tail Calls sind ein Mechanismus, mit dem ein eBPF-Programm ein anderes aufrufen kann, ohne zum vorherigen zurückzukehren, dh zwischen verschiedenen eBPF-Programmen zu wechseln. Sie sind in der entwickelten Erweiterung nicht implementiert, detaillierte Informationen finden Sie in der Cilium-Dokumentation .
Schlechte Dokumentation und eine Reihe von Architekturmerkmalen von eBPF waren die wichtigsten "Splitter" in der Entwicklung, da sie andere Probleme verursachten. Glücklicherweise wurden die meisten von ihnen erfolgreich gelöst.
Über die Entwicklungsumgebung

Nicht alle Entwickler wissen, dass es zum Erstellen und Bearbeiten von Schlittencode und im Allgemeinen allen Erweiterungs- / Plugin-Dateien für Ghidra ein recht praktisches Tool gibt - Eclipse IDE mit Unterstützung für GhidraDev- und GhidraSleighEditor-Plugins . Beim Erstellen der Erweiterung wird sie sofort als Arbeitsentwurf eingerahmt, es gibt ein recht praktisches Highlight für den Schlittencode sowie eine Überprüfung der Hauptfehler in der Sprachsyntax.
In Eclipse können Sie Ghidra (bereits mit aktivierter Erweiterung) debuggen, was äußerst praktisch ist. Aber vielleicht ist die coolste Möglichkeit, den "Ghidra Headless" -Modus zu unterstützen. Sie müssen Ghidr nicht 100500 Mal über die GUI neu starten, um einen Fehler im Code zu finden. Alle Prozesse werden im Hintergrund ausgeführt.
Notizblock kann geschlossen werden! Und Sie können Eclipse von der offiziellen Website herunterladen. Um das Plugin zu installieren, wählen Sie in Ecplise Hilfe → Neue Software installieren ... , klicken Sie auf Hinzufügen und wählen Sie das Plugin-Zip-Archiv aus.
Erweiterungsentwicklung
Für die Erweiterung wurden Prozessorspezifikationsdateien entwickelt, ein Loader, der vom Haupt-ELF-Loader erbt und seine Funktionen zur Erkennung von eBPF-Programmen erweitert, ein Relocation-Prozessor zur Implementierung von eBPF-Maps im Ghidra- Disassembler und -Decompiler sowie ein Analysator zur Bestimmung von eBPF-Helfer-Signaturen.


Erweiterungsdateien als Projekt in der Eclipse-IDE
Nun zu den Hauptdateien:
.cspec
- .cspec
an, welche Datentypen verwendet werden, wie viel Speicher ihnen in eBPF zugewiesen ist, die Stapelgröße festgelegt ist, das Label „Stapelzeiger“ auf das Register R10
ist und die Anrufvereinbarung unterzeichnet ist. Die Vereinbarung wurde (wie der Rest) gemäß der Dokumentation umgesetzt:
Daher ist die eBPF-Aufrufkonvention wie folgt definiert:
- R0 - Rückgabewert von der Kernel-Funktion und Exit-Wert für das eBPF-Programm
- R1 - R5 - Argumente vom eBPF-Programm zur Kernel-Funktion
- R6 - R9 - Gespeicherte Register, die von der Kernel-Funktion beibehalten werden
- R10 - Nur-Lese-Frame-Zeiger für den Zugriff auf den Stack
eBPF.cspec <?xml version="1.0" encoding="UTF-8"?> <compiler_spec> <data_organization> <absolute_max_alignment value="0" /> <machine_alignment value="2" /> <default_alignment value="1" /> <default_pointer_alignment value="4" /> <pointer_size value="4" /> <wchar_size value="4" /> <short_size value="2" /> <integer_size value="4" /> <long_size value="4" /> <long_long_size value="8" /> <float_size value="4" /> <double_size value="8" /> <long_double_size value="8" /> <size_alignment_map> <entry size="1" alignment="1" /> <entry size="2" alignment="2" /> <entry size="4" alignment="4" /> <entry size="8" alignment="8" /> </size_alignment_map> </data_organization> <global> <range space="ram"/> <range space="syscall"/> </global> <stackpointer register="R10" space="ram"/> <default_proto> <prototype name="__fastcall" extrapop="0" stackshift="0"> <input> <pentry minsize="1" maxsize="8"> <register name="R1"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R2"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R3"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R4"/> </pentry> <pentry minsize="1" maxsize="8"> <register name="R5"/> </pentry> </input> <output killedbycall="true"> <pentry minsize="1" maxsize="8"> <register name="R0"/> </pentry> </output> <unaffected> <varnode space="ram" offset="8" size="8"/> <register name="R6"/> <register name="R7"/> <register name="R8"/> <register name="R9"/> <register name="R10"/> </unaffected> </prototype> </default_proto> </compiler_spec>
Bevor ich mit der Beschreibung der Entwicklungsdateien fortfahre, werde ich auf eine kleine Zeile der .cspec
Datei .cspec
.
<stackpointer register="R10" space="ram"/>
Es ist die Hauptquelle des Bösen beim Dekompilieren von eBPF in Ghidra und es begann eine aufregende Reise in den eBPF-Stapel, der eine Reihe unangenehmer Momente aufweist und die Entwicklung am meisten schmerzte.
Alles was wir brauchen ist ... Stapel
Schauen wir uns die offizielle Kernel- Dokumentation an :
F: Können BPF-Programme auf den Anweisungszeiger oder die Rücksprungadresse zugreifen?
A: NEIN.
F: Können BPF-Programme auf den Stapelzeiger zugreifen?
A: NEIN. Nur der Rahmenzeiger (Register R10) ist zugänglich. Aus Compilersicht ist ein Stapelzeiger erforderlich. Beispielsweise definiert LLVM das Register R11 als Stapelzeiger in seinem BPF-Backend, stellt jedoch sicher, dass der generierte Code es niemals verwendet.
Der Prozessor hat weder einen Befehlszeiger (IP) noch einen Stapelzeiger (SP), und letzterer ist für Ghidra äußerst wichtig, und die Qualität der Dekompilierung hängt davon ab. In der cspec
Datei müssen Sie angeben, welches Register der Stapelzeiger ist (wie oben gezeigt). R10
ist das einzige eBPF-Register, das den Zugriff auf den Programmstapel ermöglicht. Es ist ein Framepointer, statisch und immer Null. Das Aufhängen des Labels „Stackpointer“ auf R10
in der cspec
Datei ist grundsätzlich falsch, es gibt jedoch keine anderen Optionen, da Ghidra dann nicht mit dem Programmstapel funktioniert. Dementsprechend fehlt der ursprüngliche SP, und nichts ersetzt ihn in der eBPF-Architektur.
Daraus ergeben sich mehrere Probleme:
Das Feld "Stapeltiefe" in Ghidra wird garantiert Null sein, da wir R10
diesen architektonischen Bedingungen einfach R10
Stapler bezeichnen müssen und es im Wesentlichen immer Null ist, was zuvor argumentiert wurde. "Stapeltiefe" spiegelt das Register mit der Bezeichnung "Stapelzeiger" wider.
Und man muss sich damit abfinden, das sind die Merkmale der Architektur.
Anweisungen, die mit R10
( R10
diejenigen, die den Stapel verarbeiten), werden häufig nicht dekompiliert. Ghidra dekompiliert im Allgemeinen nicht, was es als toten Code betrachtet (dh Snippets, die niemals ausgeführt werden). Und da R10
unveränderlich ist, werden viele Speicher- / Ladeanweisungen von Ghidr als Deadcode erkannt und verschwinden aus dem Dekompiler.
Glücklicherweise wurde dieses Problem gelöst, indem ein benutzerdefinierter Analysator geschrieben und ein zusätzlicher Adressraum mit eBPF-Helfern in einer pspec
Datei pspec
, was von einem der Ghidra-Entwickler im Issue-Projekt veranlasst wurde .
Erweiterungsentwicklung (Fortsetzung)
.ldefs
beschreibt die Funktionen des Prozessors und definiert Spezifikationsdateien.
eBPF.ldefs <?xml version="1.0" encoding="UTF-8"?> <language_definitions> <language processor="eBPF" endian="little" size="64" variant="default" version="1.0" slafile="eBPF.sla" processorspec="eBPF.pspec" id="eBPF:LE:64:default"> <description>eBPF processor 64-bit little-endian</description> <compiler name="default" spec="eBPF.cspec" id="default"/> <external_name tool="DWARF.register.mapping.file" name="eBPF.dwarf"/> </language> </language_definitions>
Die .opinion
Datei .opinion
Loader dem Prozessor zu.
eBPF.opinion <opinions> <constraint loader="Executable and Linking Format (ELF)" compilerSpecID="default"> <constraint primary="247" processor="eBPF" endian="little" size="64" /> </constraint> </opinions>
Ein Programmzähler ist in .pspec deklariert, aber mit eBPF ist er implizit und wird in keiner Weise in der Spezifikation verwendet, daher nur für Pro-forma-Zwecke. Übrigens ist der PC
von eBPF arithmetisch, nicht adressiert (er zeigt die Anweisung an, nicht das spezifische Byte des Programms). Denken Sie beim Springen daran.
Die Datei enthält auch einen zusätzlichen Adressraum für eBPF-Helfer, hier werden sie als Zeichen deklariert.
eBPF.pspec <?xml version="1.0" encoding="UTF-8"?> <processor_spec> <programcounter register="PC"/> <default_symbols> <symbol name="bpf_unspec" address="syscall:0x0"/> <symbol name="bpf_map_lookup_elem" address="syscall:0x1"/> <symbol name="bpf_map_update_elem" address="syscall:0x2"/> <symbol name="bpf_map_delete_elem" address="syscall:0x3"/> <symbol name="bpf_probe_read" address="syscall:0x4"/> <symbol name="bpf_ktime_get_ns" address="syscall:0x5"/> <symbol name="bpf_trace_printk" address="syscall:0x6"/> <symbol name="bpf_get_prandom_u32" address="syscall:0x7"/> <symbol name="bpf_get_smp_processor_id" address="syscall:0x8"/> <symbol name="bpf_skb_store_bytes" address="syscall:0x9"/> <symbol name="bpf_l3_csum_replace" address="syscall:0xa"/> <symbol name="bpf_l4_csum_replace" address="syscall:0xb"/> <symbol name="bpf_tail_call" address="syscall:0xc"/> <symbol name="bpf_clone_redirect" address="syscall:0xd"/> <symbol name="bpf_get_current_pid_tgid" address="syscall:0xe"/> <symbol name="bpf_get_current_uid_gid" address="syscall:0xf"/> <symbol name="bpf_get_current_comm" address="syscall:0x10"/> <symbol name="bpf_get_cgroup_classid" address="syscall:0x11"/> <symbol name="bpf_skb_vlan_push" address="syscall:0x12"/> <symbol name="bpf_skb_vlan_pop" address="syscall:0x13"/> <symbol name="bpf_skb_get_tunnel_key" address="syscall:0x14"/> <symbol name="bpf_skb_set_tunnel_key" address="syscall:0x15"/> <symbol name="bpf_perf_event_read" address="syscall:0x16"/> <symbol name="bpf_redirect" address="syscall:0x17"/> <symbol name="bpf_get_route_realm" address="syscall:0x18"/> <symbol name="bpf_perf_event_output" address="syscall:0x19"/> <symbol name="bpf_skb_load_bytes" address="syscall:0x1a"/> <symbol name="bpf_get_stackid" address="syscall:0x1b"/> <symbol name="bpf_csum_diff" address="syscall:0x1c"/> <symbol name="bpf_skb_get_tunnel_opt" address="syscall:0x1d"/> <symbol name="bpf_skb_set_tunnel_opt" address="syscall:0x1e"/> <symbol name="bpf_skb_change_proto" address="syscall:0x1f"/> <symbol name="bpf_skb_change_type" address="syscall:0x20"/> <symbol name="bpf_skb_under_cgroup" address="syscall:0x21"/> <symbol name="bpf_get_hash_recalc" address="syscall:0x22"/> <symbol name="bpf_get_current_task" address="syscall:0x23"/> <symbol name="bpf_probe_write_user" address="syscall:0x24"/> </default_symbols> <default_memory_blocks> <memory_block name="eBPFHelper_functions" start_address="syscall:0" length="0x200" initialized="true"/> </default_memory_blocks> </processor_spec>
.sinc
Datei ist die umfangreichste Erweiterungsdatei. Hier werden alle Register, die Struktur der eBPF-Anweisung, Token, Mnemonics und die Semantik der Anweisungen in Sleigh definiert.
EBPF.sinc kleiner Ausschnitt define space ram type=ram_space size=8 default; define space register type=register_space size=4; define space syscall type=ram_space size=2; define register offset=0 size=8 [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 PC ]; define token instr(64) imm=(32, 63) signed off=(16, 31) signed src=(12, 15) dst=(8, 11) op_alu_jmp_opcode=(4, 7) op_alu_jmp_source=(3, 3) op_ld_st_mode=(5, 7) op_ld_st_size=(3, 4) op_insn_class=(0, 2) ; #We'll need this token to operate with LDDW instruction, which has 64 bit imm value define token immtoken(64) imm2=(32, 63) ; #To operate with registers attach variables [ src dst ] [ R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 _ _ _ _ _ ]; … :ADD dst, src is src & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=1 & op_insn_class=0x7 { dst=dst + src; } :ADD dst, imm is imm & dst & op_alu_jmp_opcode=0x0 & op_alu_jmp_source=0 & op_insn_class=0x7 { dst=dst + imm; } …
Der eBPF-Loader erweitert die grundlegenden Funktionen des ELF-Loaders, sodass er erkennt, dass das auf Ghidra heruntergeladene Programm über einen eBPF-Prozessor verfügt. Für ihn wird in ElfConstants
Ghidra eine BPF-Konstante zugewiesen, aus der der Loader den eBPF-Prozessor ermittelt.
eBPF_ElfExtension.java package ghidra.app.util.bin.format.elf.extend; import ghidra.app.util.bin.format.elf.*; import ghidra.program.model.lang.*; import ghidra.util.exception.*; import ghidra.util.task.TaskMonitor; public class eBPF_ElfExtension extends ElfExtension { @Override public boolean canHandle(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF && elf.is64Bit(); } @Override public boolean canHandle(ElfLoadHelper elfLoadHelper) { Language language = elfLoadHelper.getProgram().getLanguage(); return canHandle(elfLoadHelper.getElfHeader()) && "eBPF".equals(language.getProcessor().toString()) && language.getLanguageDescription().getSize() == 64; } @Override public String getDataTypeSuffix() { return "eBPF"; } @Override public void processGotPlt(ElfLoadHelper elfLoadHelper, TaskMonitor monitor) throws CancelledException { if (!canHandle(elfLoadHelper)) { return; } super.processGotPlt(elfLoadHelper, monitor); } }
Der Relocation-Handler ist erforderlich, um eBPF-Maps im Disassembler und Decompiler zu implementieren. Die Interaktion mit ihnen erfolgt über eine Reihe von Helfern. Die Funktionen verwenden einen Dateideskriptor, um Karten anzuzeigen. Anhand der Verschiebungstabelle ist ersichtlich, dass der Loader den LDDW-Befehl patcht, der für diese Helfer Rn
generiert (z. B. bpf_map_lookup_elem(…)
).
Daher analysiert der Handler die Programmverschiebungstabelle, findet die Verschiebungsadressen (Anweisungen) und sammelt auch Zeichenfolgeninformationen über den Kartennamen. Unter Bezugnahme auf die Symboltabelle werden die tatsächlichen Adressen dieser Karten berechnet und die Anweisungen gepatcht.
eBPF_ElfRelocationHandler.java public class eBPF_ElfRelocationHandler extends ElfRelocationHandler { @Override public boolean canRelocate(ElfHeader elf) { return elf.e_machine() == ElfConstants.EM_BPF; } @Override public void relocate(ElfRelocationContext elfRelocationContext, ElfRelocation relocation, Address relocationAddress) throws MemoryAccessException, NotFoundException { ElfHeader elf = elfRelocationContext.getElfHeader(); if (elf.e_machine() != ElfConstants.EM_BPF) { return; } Program program = elfRelocationContext.getProgram(); Memory memory = program.getMemory(); int type = relocation.getType(); int symbolIndex = relocation.getSymbolIndex(); long value; boolean appliedSymbol = true;

Das Ergebnis der Demontage und Dekompilierung von eBPF
Und am Ende bekommen wir den eBPF Disassembler und Decompiler! Verwenden Sie für die Gesundheit!
Erweiterung auf GitHub: eBPF für Ghidra .
Veröffentlichungen hier: hier .
PS
Vielen Dank an Digital Security für ein interessantes Praktikum, insbesondere an die Mentoren der Forschungsabteilung (Alexander und Nikolai). Ich verneige mich vor dir!