Apprivoiser Gorynych ou décompiler eBPF dans Ghidra


Auteur de l'article https://github.com/Nalen98


Bon après-midi


Le sujet de ma recherche dans le cadre du stage d'été Summer of Hack 2019 chez Digital Security était «Décompilation de l'eBPF dans Ghidra». Il était nécessaire de développer en langage traîneau un système de traduction de bytecode eBPF dans PCode Ghidra afin de pouvoir démonter et décompiler les programmes eBPF. Le résultat de l'étude est une extension développée pour Ghidra qui ajoute la prise en charge du processeur eBPF. L'étude, comme celle d'autres stagiaires, peut à juste titre être considérée comme «pionnière», car auparavant il n'était pas possible de décompiler eBPF dans d'autres outils de rétro-ingénierie.


Contexte


Ce sujet m'est venu dans une grande ironie du destin, parce que je ne connaissais pas eBPF auparavant, et Ghidr ne l'avait pas utilisé auparavant, car il y avait un dogme selon lequel "IDA Pro est mieux." Il s'est avéré que ce n'est pas entièrement vrai.


La connaissance de Ghidra s'est avérée très rapide, puisque ses développeurs ont élaboré une documentation très compétente et accessible. J'ai également dû maîtriser le langage de spécification du processeur Sleigh, sur lequel le développement a été effectué. Les développeurs ont fait de leur mieux et ont créé une documentation très détaillée à la fois pour l' outil lui-même et pour Sleigh , dont beaucoup de remerciements.


De l'autre côté de la barricade se trouvait un filtre à paquets Berkeley étendu. eBPF est une machine virtuelle dans le noyau Linux qui vous permet de charger du code utilisateur arbitraire qui peut être utilisé pour tracer des processus et filtrer des paquets dans l'espace du noyau. L'architecture est une machine à registres RISC avec 11 registres 64 bits, un compteur logiciel et une pile de 512 octets. EBPF comporte un certain nombre de limitations:


  • les cycles sont interdits;
  • l'accès à la mémoire n'est possible que via la pile (il y aura une histoire distincte à ce sujet);
  • les fonctions du noyau ne sont disponibles que par le biais de fonctions d'encapsuleur spéciales (assistants eBPF).


La structure de la technologie eBPF. Source de l'image: http://www.brendangregg.com/ebpf.html .


Fondamentalement, cette technologie est utilisée pour les tâches réseau - débogage, filtrage de paquets, etc. au niveau du noyau. Le support EBPF a été ajouté depuis la version 3.15 du noyau; plusieurs rapports ont été consacrés à cette technologie lors de la conférence des plombiers Linux 2019. Mais chez eBPF, contrairement à Ghidra, la documentation est incomplète et ne contient pas grand chose. Par conséquent, les clarifications et les informations manquantes ont dû être recherchées sur Internet. Il a fallu un certain temps pour trouver les réponses, et il ne reste plus qu'à espérer que la technologie sera finalisée et qu'une documentation normale sera créée.


Mauvaise documentation


Afin de développer une spécification pour Sleigh, vous devez d'abord comprendre comment fonctionne l'architecture du processeur cible. Et ici, nous nous tournons vers la documentation officielle.


Il contient un certain nombre de défauts:


  • La structure des instructions eBPF n'est pas entièrement décrite.


    La plupart des spécifications, comme Intel x86, indiquent généralement à quoi va chaque bit d'instruction, à quel bloc il appartient. Malheureusement, dans la spécification eBPF, ces détails sont soit dispersés dans le document, soit complètement absents, ce qui nous oblige à tirer les grains manquants des détails d'implémentation dans le noyau Linux.


    Par exemple, dans la structure d'instructions op:8, dst_reg:4, src_reg:4, off:16, imm:32 aucun mot n'est dit que offset (off) et immédiat (imm) sont signed , ce qui est extrêmement important, car cela affecte pour passer des instructions arithmétiques aux sauts. Le code source du noyau Linux a aidé.


  • Il n'y a pas d'image complète de toutes les mnémoniques possibles de l'architecture.


    Dans certaines documentations, non seulement toutes les instructions, leurs opérandes sont indiqués, mais aussi leur sémantique en C, les cas d'application, les fonctionnalités des opérandes, etc. La documentation eBPF contient des classes d'instructions, mais cela ne suffit pas pour le développeur. Examinons-les plus en détail.


    Toutes les instructions eBPF sont en 64 bits, à l'exception de LDDW (Load double word), il a une taille de 128 bits, il concatène deux imm avec 32 bits chacun. Les instructions eBPF ont la structure suivante.

    Encodage des instructions eBPF


    La structure du champ OPAQUE dépend de la classe d'instructions (ALU / JMP, Load / Store).


    Par exemple, la classe d'instructions ALU :

    Encodage des instructions ALU


    et la classe JMP ont leur propre structure de champ:

    Encodage des instructions de branchement


    Pour les instructions de chargement / stockage, la structure est différente:

    Encodage des instructions de chargement / stockage


    La documentation non officielle de eBPF a aidé à résoudre ce problème .


  • Il n'y a aucune information sur les assistants d'appel, sur lesquels la plupart de la logique des programmes eBPF pour le noyau Linux est construite.


    Et cela est extrêmement étrange, car les assistants sont la chose la plus importante des programmes eBPF, ils effectuent simplement les tâches sur lesquelles la technologie se concentre.




Interopérabilité EBPF avec les fonctions nucléaires


Le programme tire ces fonctions du noyau, et ils travaillent simplement avec des processus, manipulent des paquets réseau, travaillent avec des cartes eBPF, accèdent à des sockets, interagissent avec l'espace utilisateur. Malgré le fait que les fonctions soient encore nucléaires, dans la documentation officielle, il vaudrait la peine de les décrire plus en détail. Tous les détails se trouvent dans la source Linux.


  • Pas un mot sur les appels de queue.


Appels de queue EBPF. Source de l'image: https://cilium.readthedocs.io/en/latest/bpf/#tail-calls .


Les appels de queue sont un mécanisme qui permet à un programme eBPF d'en appeler un autre sans revenir au précédent, c'est-à-dire en sautant entre différents programmes eBPF. Ils ne sont pas implémentés dans l'extension développée, des informations détaillées peuvent être trouvées dans la documentation Cilium .


La mauvaise documentation et un certain nombre de caractéristiques architecturales de l'eBPF ont été les principaux «éclats» du développement, car ils ont créé d'autres problèmes. Heureusement, la plupart d'entre eux ont été résolus avec succès.


À propos de l'environnement de développement



Tous les développeurs ne savent pas que pour créer et modifier du code Sleigh et généralement tous les fichiers d'extension / plug-in pour Ghidra, il existe un outil plutôt pratique - Eclipse IDE avec prise en charge des plug - ins GhidraDev et GhidraSleighEditor . Lors de la création de l'extension, elle sera immédiatement encadrée sous la forme d'un brouillon de travail, il y a un point fort plutôt pratique pour le code Sleigh, ainsi qu'un vérificateur des principales erreurs dans la syntaxe du langage.


Dans Eclipse, vous pouvez exécuter Ghidra (avec l'extension déjà activée), debug, ce qui est extrêmement pratique. Mais peut-être que l'opportunité la plus intéressante est de prendre en charge le mode "Ghidra Headless", vous n'avez pas besoin de redémarrer Ghidr à partir de l'interface graphique 100500 fois pour trouver une erreur dans le code, tous les processus sont effectués en arrière-plan.


Le bloc-notes peut être fermé! Et vous pouvez télécharger Eclipse sur le site officiel . Pour installer le plugin, dans Ecplise, sélectionnez Aide → Installer un nouveau logiciel ... , cliquez sur Ajouter et sélectionnez l'archive zip du plugin.


Développement d'extension


Pour l'extension, des fichiers de spécifications de processeur ont été développés, un chargeur qui hérite du chargeur ELF principal et étend ses capacités en termes de reconnaissance des programmes eBPF, un processeur de relocalisation pour implémenter les cartes eBPF dans le désassembleur et décompilateur Ghidra , ainsi qu'un analyseur pour déterminer les signatures d'assistance eBPF.




Fichiers d'extension en tant que projet dans l'IDE Eclipse


Maintenant sur les fichiers principaux:


.cspec - il indique quels types de données sont utilisés, combien de mémoire leur est allouée dans eBPF, la taille de la pile est définie, l'étiquette «stackpointer» est définie pour enregistrer R10 et l'accord d'appel est signé. L'accord (comme le reste) a été mis en œuvre selon la documentation:


Par conséquent, la convention d'appel eBPF est définie comme suit:
  • R0 - renvoie la valeur de la fonction dans le noyau et la valeur de sortie pour le programme eBPF
  • R1 - R5 - arguments du programme eBPF à la fonction dans le noyau
  • R6 - R9 - registres enregistrés sauvegardés que la fonction dans le noyau préservera
  • R10 - pointeur de trame en lecture seule pour accéder à la pile


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> 

Avant de continuer à décrire les fichiers de développement, je .cspec sur une petite ligne du fichier .cspec .


 <stackpointer register="R10" space="ram"/> 

C'est la principale source de mal lors de la décompilation de eBPF dans Ghidra, et il a commencé un voyage passionnant dans la pile eBPF, qui a un certain nombre de moments désagréables, et qui a causé le plus de douleur au développement.


Tout ce dont nous avons besoin, c'est ...


Regardons la documentation officielle du noyau:


Q: Les programmes BPF peuvent-ils accéder au pointeur d'instruction ou à l'adresse de retour?

R: NON.

Q: Les programmes BPF peuvent-ils accéder au pointeur de pile?

R: NON. Seul le pointeur de trame (registre R10) est accessible. Du point de vue du compilateur, il est nécessaire d'avoir un pointeur de pile. Par exemple, LLVM définit le registre R11 comme pointeur de pile dans son backend BPF, mais il s'assure que le code généré ne l'utilise jamais.

Le processeur n'a ni pointeur d'instruction (IP) ni pointeur de pile (SP), et ce dernier est extrêmement important pour Ghidra, et la qualité de la décompilation en dépend. Dans le fichier cspec , vous devez spécifier quel registre est le stackpointer (comme illustré ci-dessus). R10 est le seul registre eBPF qui permet d'accéder à la pile de programmes, il est framepointer, il est statique et toujours nul. Accrocher l'étiquette «stackpointer» sur R10 dans le fichier cspec est fondamentalement faux, mais il n'y a pas d'autres options, car Ghidra ne fonctionnera pas avec la pile du programme. En conséquence, le SP d'origine est absent et rien ne le remplace dans l'architecture eBPF.


Plusieurs problèmes en découlent:


  1. Le champ "Profondeur de la pile" dans Ghidra sera garanti comme étant nul, car nous devons simplement désigner R10 empileur dans ces conditions architecturales, et en substance, il est toujours nul, ce qui a été avancé plus tôt. "Stack Depth" reflétera le registre avec l'étiquette "stackpointer".


    Et vous devez le supporter, ce sont les caractéristiques de l'architecture.


  2. Les instructions qui fonctionnent sur R10 (c'est-à-dire celles qui manipulent la pile) ne sont souvent pas décompilées. Ghidra ne décompile généralement pas ce qu'il considère comme du code mort (c'est-à-dire des extraits qui ne s'exécutent jamais). Et puisque R10 immuable, de nombreuses instructions de stockage / chargement sont reconnues par Ghidr comme un code mort et disparaissent du décompilateur.


    Heureusement, ce problème a été résolu en écrivant un analyseur personnalisé, ainsi qu'en déclarant un espace d'adressage supplémentaire avec les assistants eBPF dans un fichier pspec , ce qui a été demandé par l'un des développeurs Ghidra dans le projet Issue .



Développement d'extension (suite)


.ldefs décrit les fonctionnalités du processeur, définit les fichiers de spécifications.


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> 

Le fichier .opinion chargeur sur le processeur.


eBPF.opinion
 <opinions> <constraint loader="Executable and Linking Format (ELF)" compilerSpecID="default"> <constraint primary="247" processor="eBPF" endian="little" size="64" /> </constraint> </opinions> 

Un compteur de programme est déclaré en .pspec, mais avec eBPF, il est implicite et n'est utilisé en aucune façon dans la spécification, il est donc uniquement à des fins pro forma. Soit dit en passant, le PC à eBPF est arithmétique, pas d'adresse (il indique l'instruction, pas un octet spécifique du programme), gardez cela à l'esprit lorsque vous sautez.


Le fichier contient également un espace d'adressage supplémentaire pour les assistants eBPF, ici ils sont déclarés en tant que caractères.


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 fichier .sinc est le fichier d'extension le plus volumineux, tous les registres, la structure de l'instruction eBPF, les jetons, les mnémoniques et la sémantique des instructions dans Sleigh sont définis ici.


EBPF.sinc petit extrait
 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; } … 

Le chargeur eBPF étend les capacités de base du chargeur ELF afin qu'il puisse reconnaître que le programme que vous avez téléchargé sur Ghidra possède un processeur eBPF. Pour lui, une constante BPF est allouée dans ElfConstants Ghidra, et le chargeur en détermine le processeur eBPF.


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); } } 

Le gestionnaire de relocalisation est requis pour implémenter les cartes eBPF dans le désassembleur et le décompilateur. L'interaction avec eux est effectuée par un certain nombre d'aides, les fonctions utilisent un descripteur de fichier pour indiquer les cartes. Sur la base de la table de relocalisation, on peut voir que le chargeur corrige l'instruction LDDW, qui génère Rn pour ces assistants (par exemple, bpf_map_lookup_elem(…) ).


Par conséquent, le gestionnaire analyse la table de relocalisation du programme, recherche les adresses de relocalisation (instructions) et collecte également des informations de chaîne sur le nom de la carte. De plus, se référant à la table des symboles, il calcule les adresses réelles de ces cartes et corrige les instructions.


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; //Relocations with maps always have type 0x1. Since eBPF hasn't names of constants (types) of relocations, it was decided to use magic //number 1. if (type == 1) { try { int SymbolIndex= relocation.getSymbolIndex(); ElfSymbol Symbol = elfRelocationContext.getSymbol(SymbolIndex); String map = Symbol.getNameAsString(); SymbolTable table = program.getSymbolTable(); Address mapAddr = table.getSymbol(map).getAddress(); String sec_name = elfRelocationContext.relocationTable.getSectionToBeRelocated().getNameAsString(); if (sec_name.toString().contains("debug")) { return; } value = mapAddr.getAddressableWordOffset(); Byte dst = memory.getByte(relocationAddress.add(0x1)); memory.setLong(relocationAddress.add(0x4), value); memory.setByte(relocationAddress.add(0x1), (byte) (dst + 0x10)); } catch(NullPointerException e) {} } if (appliedSymbol && symbolIndex == 0) { markAsWarning(program, relocationAddress, Long.toString(type), "applied relocation with symbol-index of 0", elfRelocationContext.getLog()); } } } 


Le résultat du démontage et de la décompilation de eBPF


Et à la fin, nous obtenons le désassembleur et décompilateur eBPF! Utilisez pour la santé!


Extension sur GitHub: eBPF pour Ghidra .


Sorties ici: ici .


PS


Un grand merci à Digital Security pour un stage intéressant, en particulier aux mentors du département de recherche (Alexander et Nikolai). Je vous salue!

Source: https://habr.com/ru/post/fr468529/


All Articles