Taming Gorynych, or Decompiling eBPF in Ghidra







Author of the article https://github.com/Nalen98







Good afternoon!







The topic of my research as part of the Summer of Hack 2019 summer internship at Digital Security was “Decompiling eBPF in Ghidra.” It was necessary to develop an eBPF bytecode translation system in PCode Ghidra in Sleigh for the ability to disassemble and decompile eBPF programs. The result of the study is a developed extension for Ghidra that adds support for the eBPF processor. The study, like that of other interns, can rightfully be considered “first-time”, since earlier it was not possible to decompile eBPF in other reverse engineering tools.







Background



This topic went to me in a great irony of fate, because I was not familiar with eBPF before, and Ghidr was not used before, because there was a certain dogma that "IDA Pro is better." As it turned out, this is not entirely true.







Acquaintance with Ghidra turned out to be very rapid, since its developers drew up very competent and accessible documentation. Also, I had to master the Sleigh processor specification language, on which the development was carried out. The developers did their best and created very detailed documentation for both the tool itself and for Sleigh , for which many thanks to them.







On the other side of the barricade was an extended Berkeley Packet Filter. eBPF is a virtual machine in the Linux kernel that allows you to load arbitrary user code that can be used to trace processes and filter packets in kernel space. The architecture is a RISC register machine with 11 64-bit registers, a software counter and a 512-byte stack. There are a number of limitations to eBPF:











The structure of eBPF technology. Image source: http://www.brendangregg.com/ebpf.html .







Basically, this technology is used for network tasks - debugging, packet filtering, and so on at the kernel level. EBPF support has been added since version 3.15 of the kernel; quite a few reports were devoted to this technology at Linux plumbers conference 2019. But at eBPF, unlike Ghidra, the documentation is incomplete and does not contain much. Therefore, clarifications and missing information had to be searched on the Internet. It took quite a while to find the answers, and all that remains is to hope that the technology will be finalized and normal documentation will be created.







Bad documentation



In order to develop a specification for Sleigh, you first need to understand how the architecture of the target processor works. And here we turn to the official documentation .







It contains a number of flaws:











EBPF Interoperability with Nuclear Functions







The program pulls these functions from the kernel, and they just work with processes, manipulate network packets, work with eBPF maps, access sockets, interact with userspace. Despite the fact that the functions are still nuclear, in the official documentation it would be worth writing in more detail about them. Full details are found in the Linux source .











EBPF tail calls. Image source: https://cilium.readthedocs.io/en/latest/bpf/#tail-calls .







Tail calls are a mechanism that allows one eBPF program to call another without returning to the previous one, that is, jumping between different eBPF programs. They are not implemented in the developed extension; detailed information can be found in the Cilium documentation .







Poor documentation and a number of architectural features of eBPF were the main "splinters" in development, as they created other problems. Fortunately, most of them were resolved successfully.







About the development environment









Not all developers know that for creating and editing Sleigh code and generally all extension / plugin files for Ghidra there is a rather convenient tool - Eclipse IDE with support for GhidraDev and GhidraSleighEditor plugins . When creating the extension, it will be immediately framed as a working draft, there is a rather convenient highlight for the Sleigh-code, as well as a checker of the main errors in the language syntax.







In Eclipse, you can run Ghidra (already with the extension turned on), debug, which is extremely convenient. But perhaps the coolest opportunity is to support the "Ghidra Headless" mode, you do not need to restart Ghidr from the GUI 100500 times to find an error in the code, all processes are carried out in the background.







Notepad can be closed! And you can download Eclipse from the official site . To install the plugin, in Ecplise, select Help → Install New Software ... , click Add and select the plugin zip archive.







Extension development



For the extension, processor specification files were developed, a loader that inherits from the main ELF loader and expands its capabilities in terms of recognizing eBPF programs, a relocation processor for implementing eBPF Maps in the Ghidra disassembler and decompiler, as well as an analyzer for determining eBPF helper signatures.















Extension files as a project in the Eclipse IDE







Now about the main files:







.cspec



- it indicates which data types are used, how much memory is allocated to them in eBPF, the stack size is set, the “stackpointer” label is set to register R10



, and the call agreement is signed. The agreement (like the rest) was implemented according to the documentation:







Therefore, eBPF calling convention is defined as:

  • R0 - return value from in-kernel function, and exit value for eBPF program
  • R1 - R5 - arguments from eBPF program to in-kernel function
  • R6 - R9 - callee saved registers that in-kernel function will preserve
  • R10 - read-only frame pointer to access 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>
      
      





Before continuing to describe development files, I will .cspec



on a small line of the .cspec



file.







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





It is the main source of evil when decompiling eBPF in Ghidra, and it began an exciting journey into the eBPF stack, which has a number of unpleasant moments, and which brought the most pain to the development.







All we need is ... Stack



Let's look at the official kernel documentation :







Q: Can BPF programs access instruction pointer or return address?



A: NO.



Q: Can BPF programs access stack pointer?



A: NO. Only frame pointer (register R10) is accessible. From compiler point of view it's necessary to have stack pointer. For example, LLVM defines register R11 as stack pointer in its BPF backend, but it makes sure that generated code never uses it.

The processor has neither an instruction pointer (IP) nor a stack pointer (SP), and the latter is extremely important for Ghidra, and the quality of decompilation depends on it. In the cspec



file, you need to specify which register is the stackpointer (as demonstrated above). R10



is the only eBPF register that allows accessing the program stack, it is framepointer, it is static and always zero. Hanging the “stackpointer” label on R10



in the cspec



file is fundamentally wrong, but there are no other options, since then Ghidra will not work with the program stack. Accordingly, the original SP is absent, and nothing replaces it in the eBPF architecture.







Several issues arise from this:







  1. The "Stack Depth" field in Ghidra will be guaranteed to be zero, since we simply have to designate R10



    stacker in these architectural conditions, and in essence it is always zero, which was argued earlier. "Stack Depth" will reflect the register with the label "stackpointer".







    And you have to put up with it, these are the features of architecture.







  2. Instructions that operate on R10



    (that is, working with the stack) are often not decompiled. Ghidra generally does not decompile what it considers dead code (that is, snippets that never execute). And since R10



    immutable, many store / load instructions are recognized by Ghidr as deadcode and disappear from the decompiler.







    Fortunately, this problem was solved by writing a custom analyzer, as well as declaring an additional address space with eBPF helpers in a pspec



    file, which was prompted by one of the Ghidra developers in the Issue project .









Extension development (continued)



.ldefs



describes the features of the processor, defines specification files.







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>
      
      





The .opinion



file .opinion



loader to the processor.







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





A program counter is declared in .pspec, but with eBPF it is implicit and is not used in the specification in any way, therefore it is only for pro forma purposes. By the way, the PC



at eBPF is arithmetic, not address (it indicates an instruction, not a specific byte of the program), keep this in mind when jumping.







The file also contains an additional address space for eBPF helpers, here they are declared as characters.







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



file is the most voluminous extension file, all registers, the structure of the eBPF instruction, tokens, mnemonics and semantics of instructions in Sleigh are defined here.







EBPF.sinc small snippet
 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; } …
      
      





The eBPF loader extends the basic capabilities of the ELF loader so that it can recognize that the program you loaded into Ghidra has an eBPF processor. For him, a BPF constant is allocated in ElfConstants



Ghidra, and the loader determines the eBPF processor from it.







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





The relocation handler is required to implement eBPF maps in the disassembler and decompiler. Interaction with them is carried out through a number of helpers, functions use a file descriptor to indicate maps. Based on the relocation table, it can be seen that the loader patches the LDDW instruction, which generates Rn



for these helpers (for example, bpf_map_lookup_elem(…)



).







Therefore, the handler parses the program relocation table, finds the relocation addresses (instructions), and also collects string information about the map name. Further, referring to the symbol table, it calculates the real addresses of these maps and patches the 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()); } } }
      
      







The result of disassembling and decompiling eBPF







And in the end, we get the eBPF disassembler and decompiler! Use for health!







Extension on GitHub: eBPF for Ghidra .







Releases here: here .







PS



Many thanks to Digital Security for an interesting internship, especially to the mentors from the research department (Alexander and Nikolai). I bow to you!








All Articles