Precaución: contiene la programación del sistema. Sí, en esencia, no contiene nada más.
Imaginemos que te dieron la tarea de escribir un juego de fantasía y fantasía. Bueno, hay sobre los elfos. Y sobre la realidad virtual. Desde la infancia, soñaste con escribir algo así y, sin dudarlo, estás de acuerdo. Pronto te das cuenta de que conoces la mayor parte del mundo de los elfos por las bromas del viejo bashorgh y otras fuentes dispares. Vaya, un problema. Bueno, donde el nuestro no desapareció ... Enseñado por una rica experiencia en programación, vas a Google, ingresas la "especificación Elf" y sigues los enlaces. Oh! Este lleva a algún tipo de PDF ... así que lo que tenemos aquí ... algún tipo de Elf32_Sword
- espadas élficas - parece lo que necesitas. 32 es aparentemente el nivel del personaje, y las dos cuatro patas en las siguientes columnas son probablemente daños. ¡Exactamente lo que necesita, y además de cómo está sistematizado! ..
Como se indicó en una tarea de programación de la Olimpiada, después de un par de párrafos de un texto detallado sobre el tema de Japón, samurai y geisha: "Como ya entendieron, la tarea no será sobre eso en absoluto". Oh sí, el concurso fue, por supuesto, por un tiempo. En general, declaro cerrados cinco minutos de tenacidad.
Hoy intentaré hablar sobre el análisis de un archivo en formato ELF de 64 bits. En principio, lo que simplemente no almacenan en él son programas nativos, bibliotecas estáticas, bibliotecas dinámicas, cada implementación específica, como crashdumps ... Se usa, por ejemplo, en Linux y muchos otros sistemas similares a Unix, sí, dicen, incluso en teléfonos su soporte se rellenó activamente en firmware parcheado antes. Parece que apoyar el formato para almacenar programas de sistemas operativos serios debería ser difícil. Eso pensé. Sí, probablemente lo sea. Pero admitiremos un caso de uso muy específico: cargar el bytecode eBPF desde archivos .o
. Por qué Solo para más experimentos, necesitaré un código de bytes multiplataforma serio (es decir, no hasta la rodilla ), que se puede obtener de C y no escribir manualmente, por lo que eBPF es simple y hay un back-end LLVM para ello. Y solo necesito analizar ELF como un contenedor en el que el compilador coloca este bytecode.
Por si acaso, aclararé: el artículo es programación exploratoria y no pretende ser una guía exhaustiva. El objetivo final es crear un gestor de arranque que le permita leer programas C compilados en eBPF usando Clang, los que tengo, en un volumen suficiente para continuar los experimentos.
Titular
Comenzando en cero desplazamiento en el ELF se encuentra el encabezado. Contiene las mismas letras E, L, F, que se pueden ver si intenta abrirlo con un editor de texto y algunas variables globales. En realidad, el encabezado es la única estructura en el archivo ubicada en un desplazamiento fijo, y contiene información para encontrar el resto de la estructura. (En lo sucesivo, me guía la documentación para el formato de 32 bits y elf.h
, quién sabe acerca de 64 bits. Entonces, si observa errores, no dude en corregirlos)
Lo primero que nos encuentra en el archivo es el unsigned char e_ident[16]
. ¿Recuerdas estos divertidos artículos de la serie "todas las siguientes afirmaciones son falsas"? Aquí es casi lo mismo: ELF puede contener código de 32 o 64 bits, Little o Big Endian e incluso una docena de arquitecturas de procesador. Lo leerá como Elf64 en Little endian, bueno, buena suerte ... Esta matriz de bytes es una especie de firma de lo que hay dentro y cómo analizarlo.
Con los primeros cuatro bytes, todo es simple: es [0x7f, 'E', 'L', 'F']
. Si no coinciden, entonces hay razones para creer que son algún tipo de abejas equivocadas. El siguiente byte contiene la clase. personaje Archivo: ELFCLASS32
o ELFCLASS64
- profundidad de bits. Para simplificar, solo trabajaremos con archivos de 64 bits (¿hay un eBPF de 32 bits?). Si la clase resultó ser ELFCLASS32
, simplemente salimos con un error: de todos ELFCLASS32
, las estructuras "flotarán" y el control de cordura no le hará daño. El último byte que nos interesa en esta estructura indica la resistencia del archivo: solo trabajaremos con el orden de bytes nativo para nuestro procesador.
Por si acaso, aclararé: cuando trabaje con el formato ELF en C, no debe restar cada int por el desplazamiento hábilmente calculado: elf.h
contiene las estructuras necesarias e incluso los números de bytes en e_ident
: EI_MAG0
, EI_MAG1
, EI_MAG2
, EI_MAG3
, EI_CLASS
, EI_DATA
... Simplemente necesita traer ... puntero a los datos leídos o mapeados en la memoria desde el archivo al puntero a la estructura y lectura.
Además de e_ident
encabezado contiene otros campos, algunos solo los comprobaremos y otros se usarán para un análisis posterior, pero más adelante. Es decir, verificamos que e_machine == EM_BPF
(es decir, está "bajo la arquitectura del procesador eBPF"), e_type == ET_REL
, e_shoff != 0
. La última comprobación tiene el siguiente significado: un archivo puede contener información para vincular (tabla de secciones y secciones), para iniciar (tabla de programas y segmentos), o ambos. Con las dos últimas comprobaciones, verificamos que la información que necesitamos (como para vincular) esté en el archivo. Compruebe también que la versión del formato es EV_CURRENT
.
Inmediatamente haga una reserva, no comprobaré la validez del archivo, suponiendo que si lo cargamos en nuestro proceso, entonces confiamos en él. En el código del núcleo u otros programas que trabajan con archivos no confiables, es naturalmente imposible hacer esto en cualquier caso .
Mesa de sección
Como dije, estamos interesados en la vista de enlace del archivo, es decir, la tabla de secciones y las secciones mismas. La información sobre dónde buscar la tabla de sección se encuentra en el encabezado. Su tamaño también se indica allí, así como el tamaño de un elemento : puede ser mayor que sizeof(Elf64_Shdr)
(sinceramente, no lo sé). Algunos números de secciones principales están reservados, y en realidad no están presentes en la tabla. Hacer referencia a ellos tiene un significado especial. Aparentemente, solo SHN_UNDEF
interesados en SHN_UNDEF
(el cero también está reservado: la sección que falta; por cierto, como saben, su título en la tabla sigue ahí) SHN_ABS
. El carácter "definido en la sección SHN_UNDEF
" en realidad no está definido, y en SHN_ABS
tiene un valor absoluto y no se reubica. Sin embargo, SHN_ABS
tampoco parece ser SHN_ABS
mí.
Mesa de la fila
Aquí nos encontramos por primera vez con tablas de cadenas: tablas de cadenas utilizadas en un archivo. De hecho, si const char *strtab
es una tabla de cadenas, entonces el nombre sh_name
es simplemente strtab + sh_name
. Sí, es solo una línea que comienza con un índice determinado y continúa hasta el byte cero. Las líneas pueden cruzarse (más precisamente, una puede ser el sufijo de la otra). Las secciones pueden tener nombres, luego, en el encabezado ELF, el campo e_shstrndx
apuntará a una sección de la tabla de filas (la de los nombres de sección, si hay varias), y el campo sh_name
en el encabezado de sección a una línea específica.
Los primeros (cero) y los últimos bytes de la tabla de filas contienen caracteres nulos. El último es comprensible por qué: valor-hora, termina la última línea. Pero el desplazamiento cero especifica un nombre ausente o vacío , según el contexto.
Cargando secciones
Hay dos direcciones en el encabezado de cada sección: una, sh_addr
es la dirección de carga (donde la sección se colocará en la memoria), la otra, sh_offset
es el desplazamiento en el archivo en el que se encuentra esta sección. No sé cómo son ambos, pero cada uno de estos valores individualmente puede ser 0: en un caso, la sección "permanece en el disco", porque hay algún tipo de información de servicio. En otra, la sección no se carga desde el disco , por ejemplo, solo tiene que seleccionarla y .bss
con ceros ( .bss
). Honestamente, aunque no tuve que procesar la dirección de descarga, donde se cargó, allí se cargó :) Sin embargo, francamente también tenemos programas específicos.
Reubicación
Y ahora lo interesante: según las medidas de seguridad, como saben, no van a Matrix sin un operador que permanezca en la base. Y como todavía tenemos fantasía aquí, la conexión con el operador será telepática. Oh sí, anuncié cinco minutos de tenacidad completada. En general, discutiremos brevemente el proceso de vinculación.
Para mi experimento, necesito un código compilado en un arranque normal, cargado con libdl
regular. Aquí ni siquiera lo describiré en detalle: solo abra dlopen
, extraiga los caracteres a través de dlsym
, ciérrelo con dlclose
cuando dlclose
el programa. Sin embargo, incluso estos son detalles de implementación que no están relacionados con nuestro cargador de archivos ELF. Simplemente hay algo de contexto : la capacidad de obtener un puntero por nombre.
En general, el conjunto de instrucciones eBPF es un triunfo del código de máquina alineado: una instrucción siempre toma 8 bytes y tiene una estructura
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
Además, es posible que no se utilicen muchos campos en cada instrucción específica: ahorrar espacio para un código de "máquina" no se trata de nosotros.
De hecho, la primera instrucción puede seguir inmediatamente a la segunda, que no contiene ningún código de operación, sino que simplemente extiende el campo inmediato de 32 a 64 bits. Aquí hay un parche para dicha instrucción compuesta llamada R_BPF_64_64
.
Para realizar la reubicación, una vez más, veremos en la tabla de sección sh_type == SHT_REL
. El campo sh_info
del encabezado indicará qué sección estamos parcheando y sh_link
, de qué tabla tomar una descripción de los caracteres.
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
En realidad, hay dos tipos de secciones de reubicación: REL
y RELA
: la segunda contiene explícitamente un término adicional, pero aún no lo he visto, así que solo agregamos una afirmación al hecho de que no se cumple y lo procesaremos. A continuación, agregaré al valor que está escrito en las instrucciones, la dirección del símbolo. ¿Y dónde conseguirlo? Aquí, como ya sabemos, las opciones son posibles:
- El símbolo se refiere a la sección
SHN_ABS
. Entonces solo toma st_value
- El carácter se refiere a la sección `SHN_UNDEF. Luego tira del símbolo exterior
- En otros casos, simplemente parchee el enlace a otra sección del mismo archivo`
Cómo probarlo tú mismo
Primero, ¿qué leer? Además de la especificación ya especificada , tiene sentido leer este archivo , en el que el equipo de iovisor recopila información extraída del núcleo de Linux a través de eBPF.
En segundo lugar, ¿cómo, en realidad, deberían todos trabajar con esto? Primero necesitas obtener el archivo ELF de alguna parte. Como se indicó en StackOverfow , el equipo nos ayudará.
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
En segundo lugar, debe obtener de alguna manera un análisis de referencia del archivo en pedazos. En una situación normal, el comando objdump
nos ayudaría:
$ objdump : objdump <> <()> <()>. : -a, --archive-headers Display archive header information -f, --file-headers Display the contents of the overall file header -p, --private-headers Display object format specific file header contents -P, --private=OPT,OPT... Display object format specific contents -h, --[section-]headers Display the contents of the section headers -x, --all-headers Display the contents of all headers -d, --disassemble Display assembler contents of executable sections -D, --disassemble-all Display assembler contents of all sections --disassemble=<sym> Display assembler contents from <sym> -S, --source Intermix source code with disassembly -s, --full-contents Display the full contents of all sections requested -g, --debugging Display debug information in object file -e, --debugging-tags Display debug information using ctags style -G, --stabs Display (in raw form) any STABS info in the file -W[lLiaprmfFsoRtUuTgAckK] or --dwarf[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes, =gdb_index,=trace_info,=trace_abbrev,=trace_aranges, =addr,=cu_index,=links,=follow-links] Display DWARF info in the file -t, --syms Display the contents of the symbol table(s) -T, --dynamic-syms Display the contents of the dynamic symbol table -r, --reloc Display the relocation entries in the file -R, --dynamic-reloc Display the dynamic relocation entries in the file @<file> Read options from <file> -v, --version Display this program's version number -i, --info List object formats and architectures supported -H, --help Display this information
Pero en este caso, es impotente:
$ objdump -d test-bpf.o test-bpf.o: elf64-little objdump: UNKNOWN!
Más precisamente, mostrará secciones, pero el desmontaje es un problema. Aquí recordamos lo que recolectamos usando LLVM. LLVM tiene sus propios análogos extendidos de utilidades de binutils, con nombres de la forma llvm-< >
. Ellos, por ejemplo, entienden el código de bits LLVM. Y también entienden eBPF: seguro que depende de las opciones de compilación, pero dado que se compiló, probablemente siempre debería analizarse. Por lo tanto, por conveniencia, recomiendo crear un script:
vim test-bpf.c
Entonces para tal fuente:
#include <stdint.h> extern uint64_t z; uint64_t func(uint64_t x, uint64_t y) { return x + y + z; }
Habrá tal resultado:
$ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 func: 0: bf 20 00 00 00 00 00 00 r0 = r2 1: 0f 10 00 00 00 00 00 00 r0 += r1 2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 0000000000000010: R_BPF_64_64 z 4: 79 11 00 00 00 00 00 00 r1 = *(u64 *)(r1 + 0) 5: 0f 10 00 00 00 00 00 00 r0 += r1 6: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 g F .text 00000038 func 0000000000000000 *UND* 00000000 z
Código
Parte 1. QInst: es mejor perder un día, luego volar en cinco minutos (escribir instrumentos es trivial)