Domar a Gorynych o descompilar eBPF en Ghidra


Autor del artículo https://github.com/Nalen98


Buenas tardes


El tema de mi investigación como parte de la pasantía de verano Summer of Hack 2019 en Digital Security fue "Descompilación de eBPF en Ghidra". Fue necesario desarrollar en lenguaje Sleigh un sistema de traducción de bytecode eBPF en PCode Ghidra para poder desmontar y descompilar los programas eBPF. El resultado del estudio es una extensión desarrollada para Ghidra que agrega soporte para el procesador eBPF. El estudio, como el de otros pasantes, puede considerarse legítimamente "pionero", ya que anteriormente no era posible descompilar eBPF en otras herramientas de ingeniería inversa.


Antecedentes


Este tema me llegó en una gran ironía del destino, porque no había estado familiarizado con eBPF antes, y Ghidr no lo había usado antes, porque había un dogma de que "IDA Pro es mejor". Al final resultó que, esto no es del todo cierto.


El conocimiento de Ghidra resultó ser muy rápido, ya que sus desarrolladores elaboraron documentación muy competente y accesible. Además, tuve que dominar el lenguaje de especificación del procesador Sleigh, en el que se llevó a cabo el desarrollo. Los desarrolladores hicieron todo lo posible y crearon documentación muy detallada tanto para la herramienta en sí como para Sleigh , por lo que muchas gracias a ellos.


Al otro lado de la barricada había un filtro de paquetes Berkeley extendido. eBPF es una máquina virtual en el kernel de Linux que le permite cargar código de usuario arbitrario que puede usarse para rastrear procesos y filtrar paquetes en el espacio del kernel. La arquitectura es una máquina de registro RISC con 11 registros de 64 bits, un contador de software y una pila de 512 bytes. Existen varias limitaciones para eBPF:


  • los ciclos están prohibidos;
  • el acceso a la memoria solo es posible a través de la pila (habrá una historia separada al respecto);
  • las funciones del núcleo están disponibles solo a través de funciones especiales de envoltura (eBPF-helpers).


La estructura de la tecnología eBPF. Fuente de la imagen: http://www.brendangregg.com/ebpf.html .


Básicamente, esta tecnología se utiliza para tareas de red: depuración, filtrado de paquetes, etc. a nivel del núcleo. El soporte EBPF se ha agregado desde la versión 3.15 del kernel; se dedicaron bastantes informes a esta tecnología en la conferencia de fontaneros de Linux 2019. Pero en eBPF, a diferencia de Ghidra, la documentación está incompleta y no contiene mucho. Por lo tanto, las aclaraciones y la información faltante tuvieron que buscarse en Internet. Tomó bastante tiempo encontrar las respuestas, y todo lo que queda es esperar que la tecnología se finalice y se cree la documentación normal.


Mala documentación


Para desarrollar una especificación para Sleigh, primero debe comprender cómo funciona la arquitectura del procesador de destino. Y aquí pasamos a la documentación oficial.


Contiene una serie de fallas:


  • La estructura de las instrucciones eBPF no se describe completamente.


    La mayoría de las especificaciones, como Intel x86, generalmente indican a qué va cada bit de instrucción, a qué bloque pertenece. Desafortunadamente, en la especificación eBPF, estos detalles están dispersos en todo el documento o completamente ausentes, como resultado de lo cual tenemos que extraer los granos faltantes de los detalles de implementación en el kernel de Linux.


    Por ejemplo, en la estructura de instrucciones op:8, dst_reg:4, src_reg:4, off:16, imm:32 no se dice una palabra que signifique offset (off) e inmediato (imm), y esto es extremadamente importante, porque afecta para trabajar desde instrucciones aritméticas hasta saltos. El código fuente para el kernel de Linux ayudó.


  • No hay una imagen completa de todas las posibles mnemotecnias de la arquitectura.


    En alguna documentación, no solo se indican todas las instrucciones, se indican sus operandos, sino también su semántica en C, casos de aplicación, características de operandos, etc. La documentación de eBPF contiene clases de instrucción, pero esto no es suficiente para el desarrollador. Consideremos con más detalle.


    Todas las instrucciones de eBPF son de 64 bits, excepto LDDW (Load double word), tiene un tamaño de 128 bits, concatena dos imm con 32 bits cada una. Las instrucciones de eBPF tienen la siguiente estructura.

    codificación de instrucciones eBPF


    La estructura del campo OPAQUE depende de la clase de instrucciones (ALU / JMP, Load / Store).


    Por ejemplo, la clase de instrucción ALU :

    Codificación de instrucciones ALU


    y la clase JMP tiene su propia estructura de campo:

    Codificación de instrucciones de rama


    Para las instrucciones de carga / almacenamiento, la estructura es diferente:

    Codificación de instrucciones de carga / almacenamiento


    La documentación no oficial de eBPF ayudó a resolver esto .


  • No hay información sobre call helpers, sobre la cual se basa la mayor parte de la lógica de los programas eBPF para el kernel de Linux.


    Y esto es extremadamente extraño, ya que los ayudantes son lo más importante en los programas eBPF, simplemente realizan las tareas en las que se enfoca la tecnología.




Interoperabilidad EBPF con funciones nucleares


El programa extrae estas funciones del núcleo, y solo trabajan con procesos, manipulan paquetes de red, trabajan con mapas eBPF, acceden a sockets, interactúan con el espacio de usuario. A pesar de que las funciones siguen siendo nucleares, en la documentación oficial valdría la pena escribir con más detalle sobre ellas. Los detalles completos se encuentran en la fuente de Linux.


  • Ni una palabra sobre las llamadas de cola.


EBPF llama a la cola. Fuente de la imagen: https://cilium.readthedocs.io/en/latest/bpf/#tail-calls .


Las llamadas de cola son un mecanismo que permite que un programa eBPF llame a otro sin volver al anterior, es decir, saltar entre diferentes programas eBPF. No se implementan en la extensión desarrollada, se puede encontrar información detallada en la documentación de Cilium .


La mala documentación y una serie de características arquitectónicas de eBPF fueron las principales "astillas" en el desarrollo, ya que crearon otros problemas. Afortunadamente, la mayoría de ellos se resolvieron con éxito.


Sobre el entorno de desarrollo



No todos los desarrolladores saben que para crear y editar código Sleigh y, en general, todos los archivos de extensión / plugin para Ghidra, existe una herramienta bastante conveniente: Eclipse IDE con soporte para los complementos GhidraDev y GhidraSleighEditor . Al crear la extensión, se enmarcará inmediatamente en forma de borrador de trabajo, hay un resaltado bastante conveniente para el código de trineo, así como un verificador de los principales errores en la sintaxis del idioma.


En Eclipse, puede ejecutar Ghidra (ya con la extensión activada), depurar, lo cual es extremadamente conveniente. Pero quizás la mejor oportunidad es admitir el modo "Ghidra Headless", no es necesario reiniciar Ghidr desde la GUI 100500 veces para encontrar un error en el código, todos los procesos se llevan a cabo en segundo plano.


¡El bloc de notas se puede cerrar! Y puede descargar Eclipse desde el sitio oficial . Para instalar el complemento, en Ecplise, seleccione Ayuda → Instalar nuevo software ... , haga clic en Agregar y seleccione el archivo zip del complemento.


Desarrollo de extensión


Para la extensión, se desarrollaron archivos de especificación de procesador, un cargador que hereda del cargador principal de ELF y expande sus capacidades en términos de reconocimiento de programas eBPF, un procesador de reubicación para implementar Mapas eBPF en el desensamblador y descompilador Ghidra , así como un analizador para determinar las firmas de ayuda eBPF.




Archivos de extensión como proyecto en el IDE de Eclipse


Ahora sobre los archivos principales:


.cspec : indica qué tipos de datos se utilizan, cuánta memoria se les asigna en eBPF, se establece el tamaño de la pila, se configura la etiqueta "stackpointer" para registrar R10 y se firma el acuerdo de llamada. El acuerdo (como el resto) se implementó de acuerdo con la documentación:


Por lo tanto, la convención de llamadas eBPF se define como:
  • R0: valor de retorno de la función en el núcleo y valor de salida para el programa eBPF
  • R1 - R5 - argumentos del programa eBPF a la función en el núcleo
  • R6 - R9 - registros guardados por el destinatario que la función en el núcleo preservará
  • R10: puntero de marco de solo lectura para acceder a la pila


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> 

Antes de continuar describiendo los archivos de desarrollo, me .cspec en una pequeña línea del archivo .cspec .


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

Es la principal fuente de maldad al descompilar eBPF en Ghidra, y comenzó un emocionante viaje hacia la pila de eBPF, que tiene varios momentos desagradables y que trajo el mayor dolor al desarrollo.


Todo lo que necesitamos es ... Pila


Veamos la documentación oficial del kernel:


P: ¿Pueden los programas BPF acceder al puntero de instrucción o la dirección de retorno?

A: NO

P: ¿Pueden los programas BPF acceder al puntero de la pila?

A: NO Solo se puede acceder al puntero de cuadro (registro R10). Desde el punto de vista del compilador es necesario tener un puntero de pila. Por ejemplo, LLVM define el registro R11 como un puntero de pila en su backend BPF, pero se asegura de que el código generado nunca lo use.

El procesador no tiene un puntero de instrucción (IP) ni un puntero de pila (SP), y este último es extremadamente importante para Ghidra, y la calidad de la descompilación depende de ello. En el archivo cspec , debe especificar qué registro es el stackpointer (como se demostró anteriormente). R10 es el único registro eBPF que permite acceder a la pila de programas, es framepointer, estático y siempre cero. Colgar la etiqueta "stackpointer" en R10 en el archivo cspec es fundamentalmente incorrecto, pero no hay otras opciones, porque Ghidra no funcionará con la pila de programas. En consecuencia, el SP original está ausente, y nada lo reemplaza en la arquitectura eBPF.


Varios problemas surgen de esto:


  1. Se garantizará que el campo "Profundidad de apilamiento" en Ghidra será cero, ya que simplemente tenemos que designar a R10 apilador en estas condiciones arquitectónicas, y en esencia siempre es cero, como se argumentó anteriormente. "Stack Depth" reflejará el registro con la etiqueta "stackpointer".


    Y tienes que soportarlo, estas son las características de la arquitectura.


  2. Las instrucciones que operan en R10 (es decir, aquellos que manejan la pila) a menudo no se descompilan. Ghidra generalmente no descompila lo que considera código muerto (es decir, fragmentos que nunca se ejecutan). Y dado que R10 inmutable, Ghidr reconoce muchas instrucciones de almacenamiento / carga como código muerto y desaparece del descompilador.


    Afortunadamente, este problema se resolvió escribiendo un analizador personalizado, así como declarando un espacio de direcciones adicional con ayudantes eBPF en un archivo pspec , que fue impulsado por uno de los desarrolladores de Ghidra en el proyecto Issue .



Desarrollo de extensión (continuación)


.ldefs describe las características del procesador, define los archivos de especificación.


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> 

El archivo .opinion cargador al procesador.


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

Un contador de programa se declara en .pspec, pero con eBPF es implícito y no se utiliza en la especificación de ninguna manera, por lo tanto, es solo para fines pro forma. Por cierto, la PC de eBPF es aritmética, no de dirección (indica la instrucción, no el byte específico del programa), tenga esto en cuenta al saltar.


El archivo también contiene un espacio de dirección adicional para los ayudantes de eBPF, aquí se declaran como caracteres.


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 archivo .sinc es el archivo de extensión más voluminoso, todos los registros, la estructura de la instrucción eBPF, tokens, mnemónicos y la semántica de instrucciones en Sleigh se definen aquí.


Fragmento pequeño EBPF.sinc
 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; } … 

El cargador eBPF amplía las capacidades básicas del cargador ELF para que pueda reconocer que el programa que descargó a Ghidra tiene un procesador eBPF. Para él, se asigna una constante BPF en ElfConstants Ghidra, y el cargador determina el procesador eBPF a partir de ella.


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

Se requiere el controlador de reubicación para implementar mapas eBPF en el desensamblador y el descompilador. La interacción con ellos se lleva a cabo a través de varios ayudantes, las funciones utilizan un descriptor de archivo para indicar mapas. Según la tabla de reubicación, se puede ver que el cargador parchea la instrucción LDDW, que genera Rn para estos ayudantes (por ejemplo, bpf_map_lookup_elem(…) ).


Por lo tanto, el controlador analiza la tabla de reubicación del programa, encuentra las direcciones de reubicación (instrucciones) y también recopila información de cadena sobre el nombre del mapa. Además, refiriéndose a la tabla de símbolos, calcula las direcciones reales de estos mapas y aplica parches a las instrucciones.


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


El resultado de desmontar y descompilar eBPF


¡Y al final, obtenemos el desensamblador y descompilador eBPF! Uso para la salud!


Extensión en GitHub: eBPF para Ghidra .


Lanzamientos aquí: aquí .


PS


Muchas gracias a Digital Security por una interesante pasantía, especialmente a los mentores del departamento de investigación (Alexander y Nikolai). Me inclino ante ti!

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


All Articles