Crear el paquete de archivos x86_64 ELF para Linux

Introduccion


Esta publicación describirá la creación de un simple paquete de archivos ejecutables para linux x86_64. Se supone que el lector está familiarizado con el lenguaje de programación C, el lenguaje ensamblador para la arquitectura x86_64 y con los archivos ELF del dispositivo. Para garantizar la claridad, el manejo de errores se eliminó del código en el artículo y no se mostraron las implementaciones de algunas funciones, el código completo se puede encontrar haciendo clic en los enlaces a github ( cargador , empacador ).

La idea es esta: transferimos el archivo ELF al empaquetador y obtenemos uno nuevo con la siguiente estructura en la salida:
Encabezado ELF
Título del programa
Segmento de códigoDescargador de archivos ELF empaquetado
Archivo ELF empaquetado
256 bytes de datos aleatorios

Para la compresión, se decidió utilizar el algoritmo Huffman, para el cifrado: AES-CTR con una clave de 256 bits, es decir, la implementación de kokke tiny-AES-c . Se utilizan 256 bytes de datos aleatorios para inicializar la clave AES y el vector de inicialización utilizando un generador de números pseudoaleatorios, como se muestra a continuación:

for(int i = 0; i < 32; i++) { seed = (1103515245*seed + 12345) % 256; key[i] = buf[seed]; } 

Esta decisión fue causada por el deseo de complicar la ingeniería inversa. Hasta la fecha, me di cuenta de que la complicación es insignificante, pero no comencé a eliminarla, ya que no quería gastar tiempo y energía en ella.

Cargador de arranque


Primero, se considerará el trabajo del gestor de arranque. El cargador no debe tener ninguna dependencia, por lo que todas las funciones necesarias de la biblioteca C estándar deberán escribirse de forma independiente (la implementación de estas funciones está disponible por referencia ). También debe ser posicionalmente independiente.

_Función de inicio


El gestor de arranque comienza desde la función _start, que simplemente pasa argc y argv a main:

 .extern main .globl _start .text _start: movq (%rsp), %rdi movq %rsp, %rsi addq $8, %rsi call main 

Función principal


El archivo main.c comienza definiendo varias variables externas:

 extern void* loader_end; //    , .   //  ELF . extern size_t payload_size; //   ELF  extern size_t key_seed; //     // -   . extern size_t iv_seed; //     // -     

Todos ellos se declaran como externos para encontrar la posición de los caracteres correspondientes a las variables (Elf64_Sym) en el empaquetador y cambiar sus valores.

La función principal en sí es bastante simple. El primer paso es inicializar punteros en un archivo ELF empaquetado, un búfer de 256 bytes y en la parte superior de la pila. Luego, el archivo ELF se descifra y se expande, luego se coloca en el lugar correcto de la memoria utilizando la función load_elf, y finalmente, el valor del registro rsp vuelve a su estado original y se produce un salto al punto de entrada del programa:

 #define SET_STACK(sp) __asm__ __volatile__ ("movq %0, %%rsp"::"r"(sp)) #define JMP(addr) __asm__ __volatile__ ("jmp *%0"::"r"(addr)) int main(int argc, char **argv) { uint8_t *payload = (uint8_t*)&loader_end; //    // ELF  uint8_t *entropy_buf = payload + payload_size; //   256- //  void *rsp = argv-1; //     struct AES_ctx ctx; AES_init_ctx_iv(&ctx, entropy_buf, key_seed, iv_seed); //  AES AES_CTR_xcrypt_buffer(&ctx, payload, payload_size); //  ELF memset(&ctx, 0, sizeof(ctx)); //   AES size_t decoded_payload_size; //  ELF char *decoded_payload = huffman_decode((char*)payload, payload_size, &decoded_payload_size); //     ELF  , //   ET_EXEC  NULL. void *load_addr = elf_load_addr(rsp, decoded_payload, decoded_payload_size); load_addr = load_elf(load_addr, decoded_payload); //  ELF  , //    //  . memset(decoded_payload, 0, decoded_payload_size); //   ELF munmap(decoded_payload, decoded_payload_size); //   //    //  ELF     AES AES_init_ctx_iv(&ctx, entropy_buf, key_seed, iv_seed); AES_CTR_xcrypt_buffer(&ctx, payload, payload_size); memset(&ctx, 0, sizeof(ctx)); SET_STACK(rsp); //    JMP(load_addr); //       } 

El restablecimiento del estado de AES y el archivo ELF descomprimido se realiza por motivos de seguridad, de modo que la clave y los datos descifrados se almacenan en la memoria solo durante el tiempo de uso.

A continuación se considerará la implementación de algunas funciones.

cargar


Tomé esta función del usuario de github con el apodo bediger de su repositorio userlandexec y la finalicé, ya que la función original se bloqueó en archivos como ET_DYN. La falla se produjo debido al hecho de que el valor del primer argumento de la llamada al sistema mmap se estableció en NULL, y la dirección se devolvió bastante cerca del programa principal, durante las llamadas posteriores a mmap y se copiaron segmentos a las direcciones devueltas por ellos, se sobrescribió el código del programa principal y se produjo un defecto. Por lo tanto, se decidió agregar la dirección inicial como parámetro a la función load_elf. La función en sí pasa por todos los encabezados del programa, asigna memoria (su número debe ser un múltiplo del tamaño de página) para los segmentos PT_LOAD del archivo ELF, copia su contenido a las áreas de memoria asignadas y establece los derechos de lectura, escritura y ejecución correspondientes a estas áreas:

 //      #define PAGEUP(x) (((unsigned long)x + 4095)&(~4095)) //      #define PAGEDOWN(x) ((unsigned long)x&(~4095)) void* load_elf(void *load_addr, void *mapped) { Elf64_Ehdr *ehdr = mapped; Elf64_Phdr *phdr = mapped + ehdr->e_phoff; void *text_segment = NULL; unsigned long initial_vaddr = 0; unsigned long brk_addr = 0; for(size_t i = 0; i < ehdr->e_phnum; i++, phdr++) { unsigned long rounded_len, k; void *segment; //   PT_LOAD,    if(phdr->p_type != PT_LOAD) continue; if(text_segment != 0 && ehdr->e_type == ET_DYN) { //  ET_DYN phdr->p_vaddr    , //        //    ,      //     load_addr = text_segment + phdr->p_vaddr - initial_vaddr; load_addr = (void*)PAGEDOWN(load_addr); } else if(ehdr->e_type == ET_EXEC) { //  ET_EXEC phdr->p_vaddr     load_addr = (void*)PAGEDOWN(phdr->p_vaddr); } //        rounded_len = phdr->p_memsz + (phdr->p_vaddr % 4096); rounded_len = PAGEUP(rounded_len); //        segment = mmap(load_addr, rounded_len, PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); if(ehdr->e_type == ET_EXEC) load_addr = (void*)phdr->p_vaddr; else load_addr = segment + (phdr->p_vaddr % 4096); //         memcpy(load_addr, mapped + phdr->p_offset, phdr->p_filesz); if(!text_segment) { text_segment = segment; initial_vaddr = phdr->p_vaddr; } unsigned int protflags = 0; if(phdr->p_flags & PF_R) protflags |= PROT_READ; if(phdr->p_flags & PF_W) protflags |= PROT_WRITE; if(phdr->p_flags & PF_X) protflags |= PROT_EXEC; mprotect(segment, rounded_len, protflags); //   // , ,  k = phdr->p_vaddr + phdr->p_memsz; if(k > brk_addr) brk_addr = k; } if (ehdr->e_type == ET_EXEC) { brk(PAGEUP(brk_addr)); //  ET_EXEC ehdr->e_entry     load_addr = (void*)ehdr->e_entry; } else { //  ET_DYN ehdr->e_entry    , //           load_addr = (void*)ehdr + ehdr->e_entry; } return load_addr; //       } 

elf_load_addr


Esta función para archivos ET_EXEC ELF devuelve NULL, ya que los archivos de este tipo deben ubicarse en las direcciones especificadas en ellos. Para los archivos ET_DYN, primero se calcula la dirección igual a la diferencia entre la dirección base del programa principal (es decir, el gestor de arranque), la cantidad de memoria requerida para colocar el ELF en la memoria y 4096, 4096: el espacio necesario para no colocar el archivo ELF justo al lado del programa principal. Después de calcular esta dirección, se verifica si el área de memoria se cruza, desde la dirección dada a la dirección base del programa principal, con el área desde el comienzo del archivo ELF desempaquetado hasta su final. En caso de intersección, la dirección se devuelve igual a la diferencia entre la dirección de inicio del ELF desempaquetado y la cantidad de memoria requerida para colocarlo, de lo contrario se devuelve la dirección calculada previamente.

La dirección base del programa se encuentra extrayendo la dirección de los encabezados del programa del vector auxiliar (vector auxiliar ELF), que se encuentra después de los punteros a las variables de entorno en la pila, y restando el tamaño del encabezado ELF:

       ---------------------------------------------------------------------------    -> [ argc ] 8 [ argv[0] ] 8 [ argv[1] ] 8 [ argv[..] ] 8 * x [ argv[n – 1] ] 8 [ argv[n] ] 8 (= NULL) [ envp[0] ] 8 [ envp[1] ] 8 [ envp[..] ] 8 [ envp[term] ] 8 (= NULL) [ auxv[0] (Elf64_auxv_t) ] 16 [ auxv[1] (Elf64_auxv_t) ] 16 [ auxv[..] (Elf64_auxv_t) ] 16 [ auxv[term] (Elf64_auxv_t) ] 16 (= AT_NULL) [  ] 0 - 16 [    ] >= 0 [   ] >= 0 [   ] 8 (= NULL) <    > 0 --------------------------------------------------------------------------- 

La estructura por la cual se describe cada elemento del vector auxiliar tiene la forma:

 typedef struct { uint64_t a_type; //   union { uint64_t a_val; //  } a_un; } Elf64_auxv_t; 

Uno de los valores válidos de a_type es AT_PHDR, a_val apuntará a los encabezados del programa. El siguiente es el código para la función elf_load_addr:

 void* elf_base_addr(void *rsp) { void *base_addr = NULL; unsigned long argc = *(unsigned long*)rsp; char **envp = rsp + (argc+2)*sizeof(unsigned long); //    //   while(*envp++); //        Elf64_auxv_t *aux = (Elf64_auxv_t*)envp; //    //  for(; aux->a_type != AT_NULL; aux++) { //        if(aux->a_type == AT_PHDR) { //   ELF ,     //      base_addr = (void*)(aux->a_un.a_val – sizeof(Elf64_Ehdr)); break; } } return base_addr; } size_t elf_memory_size(void *mapped) { Elf64_Ehdr *ehdr = mapped; Elf64_Phdr *phdr = mapped + ehdr->e_phoff; size_t mem_size = 0, segment_len; for(size_t i = 0; i < ehdr->e_phnum; i++, phdr++) { if(phdr->p_type != PT_LOAD) continue; segment_len = phdr->p_memsz + (phdr->p_vaddr % 4096); mem_size += PAGEUP(segment_len); } return mem_size; } void* elf_load_addr(void *rsp, void *mapped, size_t mapped_size) { Elf64_Ehdr *ehdr = mapped; if(ehdr->e_type == ET_EXEC) return NULL; size_t mem_size = elf_memory_size(mapped) + 0x1000; void *load_addr = elf_base_addr(rsp); if(mapped < load_addr && mapped + mapped_size > load_addr - mem_size) load_addr = mapped; return load_addr - mem_size; } 

Descripción del script de vinculador


Es necesario definir los caracteres para las variables externas descritas anteriormente, y también asegurarse de que el código y los datos del cargador después de la compilación estén en la misma sección .text. Esto es necesario para extraer convenientemente el código de máquina del cargador simplemente cortando el contenido de esta sección del archivo. Para lograr estos objetivos, se escribió el siguiente script de enlazador:

 ENTRY(_start) SECTIONS { . = 0; .text :{ *(.text) *(.text.startup) *(.data) *(.rodata) payload_size = .; QUAD(0) key_seed = .; QUAD(0) iv_seed = .; QUAD(0) loader_end = .; } } 

Vale la pena explicar que QUAD (0) coloca 8 bytes de ceros, en lugar de los cuales el empaquetador sustituirá valores específicos. Para recortar el código de la máquina, se escribió una pequeña utilidad que también escribe al comienzo del código de la máquina el desplazamiento del punto de entrada al gestor de arranque desde el inicio del gestor de arranque, el desplazamiento de los valores de los caracteres payload_size, key_seed y iv_seed desde el inicio del gestor de arranque. El código para esta utilidad está disponible aquí . Esto finaliza la descripción del gestor de arranque.

Empacador directo


Considere la función principal del empacador. Utiliza dos argumentos de línea de comando: el nombre del archivo de entrada es argv [1] y el nombre del archivo de salida es argv [2]. Primero, el archivo de entrada se muestra en la memoria y se verifica la compatibilidad con el empaquetador. El empaquetador funciona con solo dos tipos de archivos ELF: ET_EXEC y ET_DYN, y solo con archivos compilados estáticamente. La razón para introducir esta restricción fue el hecho de que diferentes sistemas Linux tienen diferentes versiones de bibliotecas compartidas, es decir. La probabilidad de que un programa compilado dinámicamente no se inicie en un sistema que no sea el sistema principal es bastante alta. El código correspondiente en la función principal:

 size_t mapped_size; void *mapped = map_file(argv[1], &mapped_size); if(check_elf(mapped) < 0) return 1; 

Después de eso, si el archivo de entrada pasa la verificación de compatibilidad, se comprime:

 size_t comp_size; uint8_t *comp_buf = huffman_encode(mapped, &comp_size); 

A continuación, se genera el estado AES y el archivo ELF comprimido se cifra. El estado de AES está determinado por la siguiente estructura:

 #define AES_ENTROPY_BUFSIZE 256 typedef struct { uint8_t entropy_buf[AES_ENTROPY_BUFSIZE]; // 256-  size_t key_seed; //      size_t iv_seed; //       struct AES_ctx ctx; //  AES-CTR } AES_state_t; 

Código correspondiente en main:

 AES_state_t aes_st; for(int i = 0; i < AES_ENTROPY_BUFSIZE; i++) state.entropy_buf[i] = rand() % 256; state.key_seed = rand(); state.iv_seed = rand(); AES_init_ctx_iv(&state.ctx, state.entropy_buf, state.key_seed, state.iv_seed); AES_CTR_xcrypt_buffer(&aes_st.ctx, comp_buf, comp_size); 

Después de eso, la estructura que almacena información sobre el gestor de arranque se inicializa, los valores de payload_size, key_seed y iv_seed en el gestor de arranque se cambian a los generados en el paso anterior, después de lo cual se restablece el estado AES. La información sobre el gestor de arranque se almacena en la siguiente estructura:

 typedef struct { char *loader_begin; //      size_t entry_offset; //       size_t *payload_size_patch_offset; //     // ELF    size_t *key_seed_pacth_offset; //     //       size_t *iv_seed_patch_offset; //     //     //    size_t loader_size; //     } loader_t; 

Código correspondiente en main:

 loader_t loader; init_loader(&loader); *loader.payload_size_patch_offset = comp_size; *loader.key_seed_pacth_offset = aes_st.key_seed; *loader.iv_seed_patch_offset = aes_st.iv_seed; memset(&aes_st.ctx, 0, sizeof(aes_st.ctx)); 

En la parte final, creamos un archivo de salida, escribimos un encabezado ELF, un encabezado de programa, un código de cargador, un archivo ELF comprimido y encriptado, y un búfer de 256 bytes en él:

 int out_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0755); //  //   write_elf_ehdr(out_fd, &loader); //  ELF  write_elf_phdr(out_fd, &loader, comp_size); //    write(out_fd, loader.loader_begin, loader.loader_size); //   write(out_fd, comp_buf, comp_size); //     ELF write(out_fd, aes_st.entropy_buf, AES_ENTROPY_BUFSIZE); //  // 256-  

El código principal del empaquetador termina aquí, luego se considerarán las siguientes funciones: la función de inicializar información sobre el cargador de arranque, la función de escribir el encabezado ELF y la función de escribir el encabezado del programa.

Inicializando la información del gestor de arranque


El código de máquina del cargador está incrustado en el ejecutable del empaquetador utilizando el código simple a continuación:

 .data .globl _loader_begin .globl _loader_end _loader_begin: .incbin "loader" _loader_end: 

Para determinar su dirección en la memoria, se declaran las siguientes variables en el archivo main.c:

 extern void* _loader_begin; extern void* _loader_end; 

A continuación, considere la función init_loader. Primero, los siguientes valores se leen secuencialmente en él: el desplazamiento del punto de entrada desde el inicio del cargador de arranque (entry_offset), el cambio del tamaño del archivo ELF empaquetado desde el inicio del cargador de arranque (payload_size_patch_offset), el cambio del valor inicial del generador para la clave desde el inicio del cargador de arranque (key_seed_inicial_patch_ el generador del valor del vector del parámetro inicialización desde el inicio del gestor de arranque (iv_seed_patch_offset). Luego, la dirección del cargador se agrega a los últimos tres valores, por lo que al desreferenciar los punteros y asignarles valores, reemplazaremos los ceros asignados en la etapa de diseño (QUAD (0)) con los valores que necesitamos.

 void init_loader(loader_t *l) { void *loader_begin = (void*)&_loader_begin; l->entry_offset = *(size_t*)loader_begin; loader_begin += sizeof(size_t); l->payload_size_patch_offset = *(void**)loader_begin; loader_begin += sizeof(void*); l->key_seed_pacth_offset = *(void**)loader_begin; loader_begin += sizeof(void*); l->iv_seed_patch_offset = *(void**)loader_begin; loader_begin += sizeof(void*); l->payload_size_patch_offset = (size_t)l->payload_size_patch_offset + loader_begin; l->key_seed_pacth_offset = (size_t)l->key_seed_pacth_offset + loader_begin; l->iv_seed_patch_offset = (size_t)l->iv_seed_patch_offset + loader_begin; l->loader_begin = loader_begin; l->loader_size = (void*)&_loader_end - loader_begin; } 


write_elf_ehdr


 void write_elf_ehdr(int fd, loader_t *loader) { //  ELF  Elf64_Ehdr ehdr; memset(ehdr.e_ident, 0, sizeof(ehdr.e_ident)); memcpy(ehdr.e_ident, ELFMAG, SELFMAG); ehdr.e_ident[EI_CLASS] = ELFCLASS64; ehdr.e_ident[EI_DATA] = ELFDATA2LSB; ehdr.e_ident[EI_VERSION] = EV_CURRENT; ehdr.e_ident[EI_OSABI] = ELFOSABI_NONE; ehdr.e_type = ET_DYN; ehdr.e_machine = EM_X86_64; ehdr.e_version = EV_CURRENT; ehdr.e_entry = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + loader->entry_offset; ehdr.e_phoff = sizeof(Elf64_Ehdr); ehdr.e_shoff = 0; ehdr.e_flags = 0; ehdr.e_ehsize = sizeof(Elf64_Ehdr); ehdr.e_phentsize = sizeof(Elf64_Phdr); ehdr.e_phnum = 1; ehdr.e_shentsize = sizeof(Elf64_Shdr); ehdr.e_shnum = 0; ehdr.e_shstrndx = 0; write(fd, &ehdr, sizeof(ehdr)); //     return 0; } 

Aquí se produce la inicialización estándar del encabezado ELF y su posterior escritura en un archivo, lo único a lo que hay que prestar atención es al hecho de que en los archivos ET_DYN ELF el segmento descrito por el primer encabezado del programa incluye no solo el código ejecutable, sino también el encabezado ELF y todos los encabezados programas Por lo tanto, su desplazamiento desde el principio debe ser igual a cero, el tamaño debe ser la suma del tamaño del encabezado ELF, todos los encabezados del programa y el código ejecutable, y el punto de entrada se determina como la suma del tamaño del encabezado ELF, el tamaño de todos los encabezados del programa y el desplazamiento desde el comienzo del código ejecutable.

write_elf_phdr


 void write_elf_phdr(int fd, loader_t *loader, size_t payload_size) { //    Elf64_Phdr phdr; phdr.p_type = PT_LOAD; phdr.p_offset = 0; phdr.p_vaddr = 0; phdr.p_paddr = 0; phdr.p_filesz = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + loader->loader_size + payload_size + AES_ENTROPY_BUFSIZE; phdr.p_memsz = phdr.p_filesz; phdr.p_flags = PF_R | PF_W | PF_X; phdr.p_align = 0x1000; write(fd, &phdr, sizeof(phdr)); //      } 

Aquí, el encabezado del programa se inicializa y luego se escribe en un archivo. Debe prestar atención al desplazamiento relativo al comienzo del archivo y al tamaño del segmento descrito por el encabezado del programa. Como se describe en el párrafo anterior, el segmento descrito por este encabezado incluye no solo el código ejecutable, sino también el encabezado ELF y el encabezado del programa. También hacemos que el segmento con código ejecutable sea accesible para la escritura, esto se debe al hecho de que la implementación de AES utilizada en el gestor de arranque cifra y descifra los datos "en su lugar".

Algunos hechos sobre el trabajo del empacador


Durante las pruebas, se notó que los programas compilados estáticamente con glibc pasan a segfault cuando se inician, en esta instrucción:

  movq% fs: 0x28,% rax 

No pude averiguar por qué sucede esto, me alegrará si comparte información sobre este tema. En lugar de glibc, puede usar musl-libc, todo funciona sin fallas. Además, el empacador se probó con programas de golang compilados estáticamente, por ejemplo, un servidor http. Para bloqueos estáticos completos de los programas de golang, se deben utilizar los siguientes indicadores:

  CGO_ENABLED = 0 vaya a construir -a -ldflags '-extldflags "-static"'. 

Lo último con lo que se probó el empaquetador fue con los archivos ET_DYN ELF sin un vinculador dinámico. Es cierto que al trabajar con estos archivos, la función elf_load_addr puede fallar. En la práctica, se puede cortar del gestor de arranque y usar una dirección fija, por ejemplo 0x10000.

Conclusión


Este empaquetador, obviamente, no tiene sentido usarlo para el propósito previsto, ya que los archivos protegidos por él se descifran con bastante facilidad. El objetivo de este proyecto era dominar mejor el trabajo con archivos ELF, la práctica de generarlos, así como la preparación para la creación de un empaquetador más completo.

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


All Articles