Эта статья вторая в цикле по созданию crackme под linux amd64. В этой части мы создадим исполняемый файл, в котором каждая функция будет зашифрована собственным ключом, и будет расшифровываться только на время исполнения. Процесс создания будет полностью автоматизирован, то есть при добавлении нового кода или изменении старого никаких дополнительных действий делать будет не нужно. Код всего проекта находится в репозитории на github.

Алгоритм

Алгоритм создания такого бинарника достаточно прост, распишем его по шагам:

  1. Скомпилировать код в исполняемый файл так, чтобы каждая функция имела размер кратный 16. Это условие исходит из того, что в режиме CBC можно работать только с буферами, размер которых кратен размеру блока, то есть 16.

  2. Сгенерировать ключ и вектор инициализации для каждой функции, затем зашифровать их. После этого передать информацию о функциях (адрес и размер), а также ключи и векторы инициализации программе для генерации функций AddRoundKey и get_iv, которую я описал в предыдущей части. После данного шага исполняемый файл изменится следующим образом:

    img
    Шаг 2
  3. Скомпилировать код движка, отвечающего за шифрование и расшифровывание функций. Он как раз использует код, сгенерированный на предыдущем шаге. Чтобы скомпилированный код движка можно было встроить в наш исполняемый файл, нужно передать компилятору флаг -fPIC, чтобы код был позиционно-независимым.

  4. Встроить код движка в исполняемый файл, вставить в начало каждой функции инструкцию call для вызова кода движка. Здесь появляется проблема, связанная с тем, что мы не можем менять размер функции, потому что тогда у других функций поменяются адреса, что может привести к некорректной работе инструкций, которые обращаются к памяти. Поэтому мы будем заменять первые 5 байт каждой функции на инструкцию call, оригинальные же 5 байт положим в хэш-таблицу и при расшифровывании будем копировать их из неё обратно в начало функции. После данного шага наш исполняемый файл будет выглядеть так:

    img
    Шаг 4

Реализация

Создаём исполняемый файл

Как я писал выше, нам нужно скомпилировать код так, чтобы размеры всех функций были кратны 16. Для начала напишем простой код на си, который и будем компилировать. Мы не будем использовать стандартную библиотеку, так как иначе у нас будет слишком много функций, а как мы выяснили в прошлой главе, чем больше функций, тем больше размер AddRoundKey, тем медленнее исполнение и компиляция. Сам код представлен ниже:

start.s:

.text
.globl _start
.type _start,@function
.section .text._start
_start:
    # Call main
    movq (%rsp), %rdi
    leaq 8(%rsp), %rsi
    callq main

    # Exit
    movq %rax, %rdi
    movq $60, %rax
    syscall
.size _start,.-_start

crackme.c:

#define write(fd, buf, count) syscall3(1, fd, (long)buf, count)

__attribute__((always_inline))
static inline long syscall3(long n, long a1, long a2, long a3) {
  unsigned long ret;
  __asm__ __volatile__ ("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2),
                                            "d"(a3) : "rcx", "r11", "memory");
  return ret;
}

static int calc_hash(unsigned char *pass) {
  int hash = 0;
  while(*pass) {
    hash += (*pass + 11) % 23;
    pass++;
  }
  return hash;
}

// Пароль - Hello Habr!?
int main(int argc, char **argv) {
  if(argc != 2)
    return 1;

  int hash = calc_hash((unsigned char*)argv[1]);
  if(hash != 152) {
    char failure[] = "Password is wrong\n";
    write(1, failure, sizeof(failure));
    return 1;
  }

  char success[] = "Password is right!\n";
  write(1, success, sizeof(success));
}

Есть один простой способ, как сделать размеры всех функций кратными 16 — поместить каждую функцию в отдельную секцию и задать в скрипте компоновщика выравнивание для каждой такой секции. Первая задача решается передачей флага -ffunction-sections компилятору gcc, вторая — генерацией скрипта компоновщика. Для генерации скрипта я решил использовать стандартные консольные утилиты — readelf и awk. Сначала с помощью readelf получаем из объектных файлов имена секций, затем с помощью awk на их основе генерируем скрипт компоновщика. Текст программы на awk приведён ниже.

create_linker_script.awk:

BEGIN {
  print "ENTRY(_start)\nSECTIONS {\n    . = 0x400000;\n" \
        "    .text : {\n        * (.data)\n        * (.rodata)"
}

{
  if($2 ~ /^\.text\..*/)
      matched=$2;
  else if($3 ~ /^\.text\..*/)
      matched=$3;
  else
      next;

  print "\n        . = ALIGN(16);\n        * ("matched")\n        . = ALIGN(16);"
}

END {
  print "    }\n}"
}

Мы проверяем, начинается ли второй или третий столбец с .text, если нет, то пропускаем строку, иначе печатаем название секции вместе с ALIGN(16). Проверка двух столбцов связана с тем, что readelf выравнивает номера секций, для секций с однозначными номерами имя будет в третьем столбце, а для секций с двухзначными номерами - во втором. Пример вывода readelf для лучшего понимания:

There are 32 section headers, starting at offset 0x22258:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000318 000318 00001c 00   A  0   0  1
  [ 2] .note.gnu.property NOTE            0000000000000338 000338 000050 00   A  0   0  8
  [ 3] .note.gnu.build-id NOTE            0000000000000388 000388 000024 00   A  0   0  4
  [ 4] .note.ABI-tag     NOTE            00000000000003ac 0003ac 000020 00   A  0   0  4
  [ 5] .note.package     NOTE            00000000000003cc 0003cc 00008c 00   A  0   0  4
  [ 6] .gnu.hash         GNU_HASH        0000000000000458 000458 000040 00   A  7   0  8
  [ 7] .dynsym           DYNSYM          0000000000000498 000498 000c48 18   A  8   1  8
  [ 8] .dynstr           STRTAB          00000000000010e0 0010e0 000625 00   A  0   0  1
  [ 9] .gnu.version      VERSYM          0000000000001706 001706 000106 02   A  7   0  2
  [10] .gnu.version_r    VERNEED         0000000000001810 001810 0000d0 00   A  8   2  8

Почему мы проверяем, что название секции начинается с .text? Потому что при передаче флага -ffunction-sections gcc кладёт каждую функцию в свою секцию с именем, соответствующим шаблону .text.имя_функции, именно поэтому ранее мы явно указали .section .text._start для функции _start. Теперь напишем Makefile для сборки исполняемого файла:

CC=gcc
CFLAGS=-Wall -std=c11 -fno-stack-protector -Wno-builtin-declaration-mismatch \
       -fno-stack-protector -ffunction-sections -O0
LDFLAGS=-nostdlib -nostartfiles -nodefaultlibs -static -z noexecstack \
        -Wl,-z,noseparate-code
AS=as

.PHONY: all clean
all: crackme-unencrypted
crackme-unencrypted: linker.ld start.o crackme.o
        $(CC) $(LDFLAGS) -T linker.ld start.o crackme.o -o crackme-unencrypted
linker.ld: start.o crackme.o
        readelf --wide -S start.o crackme.o | awk -f create_linker_script.awk > linker.ld
crackme.o: crackme.c
        $(CC) $(CFLAGS) -c crackme.c -o crackme.o
start.o: start.s
        $(AS) -c start.s -o start.o
clean:
        rm -f start.o crackme.o linker.ld crackme-unencrypted

Как можно видеть по флагам компиляции - мы собираем статический исполняемый файл без линковки со стандартной библиотекой. Стоит также обратить внимание на то, что readelf вызывается с флагом --wide, потому что иначе имена секций могут выводится не полностью. Для наглядности далее приведён текст сгенерированного скрипта компоновщика:

ENTRY(_start)
SECTIONS {
    . = 0x400000;
    .text : {
        * (.data)
        * (.rodata)

        . = ALIGN(16);
        * (.text._start)
        . = ALIGN(16);

        . = ALIGN(16);
        * (.text.calc_hash)
        . = ALIGN(16);

        . = ALIGN(16);
        * (.text.main)
        . = ALIGN(16);
    }
}

Шифруем функции

Создадим директорию ./utils/patcher, в которой будет хранится код программы, которая будет модифицировать наш бинарник, и положим туда файлы aes.h, aes.c, являющиеся модифицированными файлами из проекта tiny-AES-c. Для лучшего понимания кода, далее приведена сигнатура функции, которою мы будем использовать для шифрования:

struct AES_ctx
{
  uint8_t *RoundKey;
  uint8_t *Iv;
};

void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, size_t length);

Теперь приступим к созданию шифровальщика. Начнём с написания небольшой структуры для хранения информации об ELF файле и функции, которая считывает ELF файл в данную структуру:

typedef struct {
  const char *filepath;
  uint8_t    *mem;
  size_t     size;
  Elf64_Ehdr *ehdr;
  Elf64_Phdr *phdrs;
  Elf64_Shdr *shdrs;
  Elf64_Sym  *symbols;
  size_t     num_symbols;
  char       *sh_names;
  char       *sym_names;
} elf_t;

// Эта функция находит секцию с соответствующим типом.
static Elf64_Shdr* section_by_type(elf_t *elf, uint32_t sh_type) {
  for(size_t i = 0; i < elf->ehdr->e_shnum; i++) {
    if(elf->shdrs[i].sh_type == sh_type)
      return &elf->shdrs[i];
  }
  return NULL;
}

static bool open_elf(const char *filepath, elf_t *elf) {
  elf->mem = MAP_FAILED;

  int fd = open(filepath, O_RDONLY);
  if(fd < 0) {
    perror("open_elf (open)");
    goto error;
  }

  struct stat statbuf;
  if(fstat(fd, &statbuf) < 0) {
    perror("open_elf (fstat)");
    goto error;
  }

  elf->filepath = filepath;
  elf->size = statbuf.st_size;
  elf->mem = mmap(NULL, elf->size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
  if(elf->mem == MAP_FAILED) {
    perror("open_elf (mmap)");
    goto error;
  }

  elf->ehdr = (Elf64_Ehdr*)elf->mem;
  elf->phdrs = (Elf64_Phdr*)(elf->mem + elf->ehdr->e_phoff);
  elf->shdrs = (Elf64_Shdr*)(elf->mem + elf->ehdr->e_shoff);

  if(elf->ehdr->e_shnum != 0) {
    // Получаем указатель на область памяти с именами секций.
    elf->sh_names = (char*)elf->mem + elf->shdrs[elf->ehdr->e_shstrndx].sh_offset;

    // Получаем заголовок секции, в которой хранятся
    // имена символов.
    Elf64_Shdr *sh_strtab = section_by_name(elf, ".strtab");
    if(!sh_strtab) {
      fputs("Failed to find strtab section\n", stderr);
      goto error;
    }
    // Получаем указатель на область памяти с именами символов.
    elf->sym_names = (char*)elf->mem + sh_strtab->sh_offset;

    // Находим заголовок секции, в которой хранятся символы.
    Elf64_Shdr *sym_shdr = section_by_type(elf, SHT_SYMTAB);
    if(!sym_shdr) {
      fputs("Executable does not have a symtab\n", stderr);
      goto error;
    }
    // Получаем указатель на область памяти, в которой
    // лежат символы.
    elf->symbols = (void*)elf->mem + sym_shdr->sh_offset;
    // Вычисляем количество символов, разделив размер секции
    // с символами на размер одного символа.
    elf->num_symbols = sym_shdr->sh_size / sizeof(Elf64_Sym);
  }

  close(fd);
  return true;
error:
  if(fd >= 0) close(fd);
  if(elf->mem != MAP_FAILED) munmap(elf->mem, elf->size);
  return false;
}

Теперь напишем процедуру для шифрования одной функции:

typedef struct {
  uint64_t addr;
  uint32_t size;
  uint8_t  iv[16];
  uint8_t  key[240];
} func_data;

// Данная функция округляет x вверх до align, при условии, что
// align является степенью двойки.
static inline uint32_t roundup(uint32_t x, uint32_t align) {
  return (x + align - 1) & (~(align - 1));
}

// elf - ELF файл, в котором шифруются функции.
// func - символ, в котором хранится информация о функции.
// data - указатель на структуру, в которую мы будем заносить нужную нам
// информацию о функции вместе с ключом шифрования и вектором инициализации.
// Ключ и вектор инициализации понадобятся нам позже для генерации кода
// AddRoundKey, поэтому мы и сохраняем их в структуру.
static void encrypt_function(elf_t *elf, Elf64_Sym *func, func_data *data) {
  // Инициализируем ключ и вектор инициализации случайными данными
  // из /dev/urandom.
  getrandom(data->key, sizeof(data->key), 0);
  getrandom(data->iv, sizeof(data->iv), 0);

  // Получаем адрес функции.
  data->addr = func->st_value;

  // Получаем размер функции, округлённый в большую сторону до
  // размеров блока. Ранее мы скомпилировали исполняемый файл так,
  // чтобы размер функций был кратен 16. Если размер функции не кратен
  // 16, компоновщик заполнит нужное пространство нулями, однако информация
  // о размере функции в ELF файле не поменяется, поэтому размер приходится
  // округлять.
  data->size = roundup(func->st_size, AES_BLOCKLEN);

  // Заголовок секции, в которой находится шифруемая функция.
  Elf64_Shdr *func_shdr = &elf->shdrs[func->st_shndx];

  // Указатель на тело функции.
  // st_value - адрес функции в памяти, sh_addr - адрес секции в памяти,
  // таким образом st_value - sh_addr - смещение начала функции от начала
  // секции. sh_offset - смещение секции от начала файла, поэтому
  // sh_offset + st_value - sh_adrr - смещение функции от начала файла.
  uint8_t *func_start = elf->mem + func_shdr->sh_offset +
                        func->st_value - func_shdr->sh_addr;

  // Шифруем функцию.
  struct AES_ctx ctx = (struct AES_ctx) { .RoundKey = data->key, .Iv = data->iv };
  AES_CBC_encrypt_buffer(&ctx, func_start, data->size);
}

Далее создадим процедуру, которая будет шифровать все функции:

// Процедура, которая возвращает количество функций.
static size_t count_functions(elf_t *elf) {
  size_t num_functions = 0;
  for(size_t i = 0; i < elf->num_symbols; i++) {
    if(ELF64_ST_TYPE(elf->symbols[i].st_info) != STT_FUNC)
      continue;

    num_functions++;
  }

  return num_functions;
}

static bool encrypt_and_generate_add_round_key(
  // ELF файл.
  elf_t *elf,
  // Количество функций в нём.
  size_t num_functions,
  // Путь до файла, в который мы поместим код AddRoundKey.
  const char* addr_round_key
) {
  bool result = false;
  func_data *funcs_data = malloc(sizeof(func_data) * num_functions);
  if(!funcs_data) {
      perror("encrypt_and_generate_add_round_key (malloc)");
      goto exit;
  }

  size_t func_idx = 0;
  // Проходимся по всем символам.
  for(size_t i = 0; i < elf->num_symbols; i++) {
      // Если символ не является функцией, ничего не делаем.
      if(ELF64_ST_TYPE(elf->symbols[i].st_info) != STT_FUNC)
          continue;
      // Иначе шифруем функцию.
      encrypt_function(elf, &elf->symbols[i], &funcs_data[func_idx++]);
  }

  // Эту процедуру рассмотрим далее, в ней вызывается программа
  // gen-add-round-key, созданная в предыдущей статье.
  if(!generate_add_round_key(funcs_data, num_functions, addr_round_key))
      goto exit;

  result = true;
exit:
  if(funcs_data) free(funcs_data);
  return result;
}

Приступим к процедуре generate_add_round_key. Программа gen-add-round-key принимает следующие аргументы командной строки: -a, в котором передаётся адрес функции, и -s, в котором передаётся размер функции. Вектор инициализации и ключ программа считывает из стандартного ввода. Код процедуры generate_add_round_key имеет вид:

// Максимальный размер строки, содержащей 64-битное число.
#define UINT64_MAX_STRING_SIZE 20
// Максимальный размер строки, содержащей 32-битное число.
#define UINT32_MAX_STRING_SIZE 10
// Размер строки, содержащей аргумент -a или -s.
#define OPT_SIZE 2

// Данная функция подготавливает массив argv для execvp.
static char** prepare_generate_add_round_key_argv(
  func_data *funcs_data,
  size_t num_functions
) {
  char **argv = NULL, *strings = NULL;
  // Размер массива argv. На каждую функцию
  // нужен следующий набор аргументов: -a addr -s size,
  // отсюда берётся 4 * num_functions. Слагаемое 1
  // нужно так как в начале argv должны быть следующие строки:
  // cabal exec gen-add-round-key --
  size_t argc = 4 * (num_functions + 1);

  argv = malloc(sizeof(char*) * (argc + 1));
  if(!argv)
    goto error;

  // Выделяем участок памяти, в котором будут храниться все строки,
  // используемые в argv. На каждую функцию приходится две строки
  // с именами аргументов 2*(OPT_SIZE + 1), одна строка с адресом -
  // UINT64_MAX_STRING_SIZE + 1 и одна строка с размером -
  // UINT32_MAX_STRING_SIZE + 1.
  size_t strings_size = num_functions * (UINT64_MAX_STRING_SIZE + 1 +
                                         UINT32_MAX_STRING_SIZE + 1 +
                                         2*(OPT_SIZE + 1));
  strings = malloc(strings_size);
  if(!strings)
      goto error;

  argv[0] = "cabal";
  argv[1] = "exec";
  argv[2] = "gen-add-round-key";
  argv[3] = "--";
  argv[argc] = NULL;

  // Заполняем массив argv.
  size_t strings_offset = 0, argv_offset = 4;
  for(size_t i = 0; i < num_functions; i++) {
    argv[argv_offset++] = &strings[strings_offset];
    memcpy(&strings[strings_offset], "-a", 3);
    strings_offset += 3;
    argv[argv_offset++] = &strings[strings_offset];
    strings_offset += sprintf(&strings[strings_offset], "%lu", funcs_data[i].addr);
    strings_offset++;
    argv[argv_offset++] = &strings[strings_offset];
    memcpy(&strings[strings_offset], "-s", 3);
    strings_offset += 3;
    argv[argv_offset++] = &strings[strings_offset];
    strings_offset += sprintf(&strings[strings_offset], "%u", funcs_data[i].size);
    strings_offset++;
  }

  return argv;
error:
  perror("prepare_generate_add_round_key_argv (malloc)");
  if(argv) free(argv);
  if(strings) free(strings);
  return NULL;
}

static bool generate_add_round_key(
  func_data *funcs_data,
  size_t num_functions,
  const char *addr_round_key
) {
  // Создаём канал.
  int pipefd[2];
  if(pipe(pipefd) < 0) {
    perror("generate_add_round_key (pipe)");
    return false;
  }

  // Создаём дочерний процесс.
  pid_t pid = fork();
  if(pid == 0) {
    // В дочернем процессе подготавливаем argv для вызова gen-add-round-key.
    char **argv = NULL;
    argv = prepare_generate_add_round_key_argv(funcs_data, num_functions);
    if(!argv)
        exit(EXIT_FAILURE);

    // Перенаправляем pipefd[0] в стандартный ввод дочернего процесса.
    if(dup2(pipefd[0], 0) < 0) {
        perror("generate_add_round_key (dup2)");
        exit(EXIT_FAILURE);
    }
    close(pipefd[0]);
    close(pipefd[1]);

    // Открываем файл, в который поместим код AddRoundKey.
    int fd = open(addr_round_key, O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if(fd < 0) {
      perror("generate_add_round_key (open)");
      exit(EXIT_FAILURE);
    }
    // Перенаправляем стандартный вывод в этот файл.
    if(dup2(fd, 1) < 0) {
      perror("generate_add_round_key (dup2)");
      exit(EXIT_FAILURE);
    }

    // Запускаем gen-add-round-key.
    if(execvp("cabal", argv) < 0) {
      perror("generate_add_round_key (execvp)");
      exit(EXIT_FAILURE);
    }
  } else if(pid < 0) {
    perror("generate_add_round_key (fork)");
    return false;
  }

  close(pipefd[0]);

  // Пишем в канал вектор инициализации и ключ для каждой функции.
  for(size_t i = 0; i < num_functions; i++) {
    if(!write_all(pipefd[1], funcs_data[i].iv, sizeof(funcs_data[i].iv)))
      return false;
    if(!write_all(pipefd[1], funcs_data[i].key, sizeof(funcs_data[i].key)))
      return false;
  }
  close(pipefd[1]);

  // Ждём пока gen-add-round-key не завершиться.
  int wstatus;
  if(waitpid(pid, &wstatus, 0) < 0) {
    perror("generate_add_round_key (waitpid)");
    return false;
  }
  // Если gen-add-round-key завершилась с ошибкой, возвращаем false.
  if(WEXITSTATUS(wstatus) != 0) {
    return false;
  }
  return true;
}

После того, как мы зашифровали все функции, нам надо сохранить новый ELF файл. За это отвечает процедура write_encrypted_exec:

static bool write_encrypted_exec(elf_t *elf, const char *patched_executable) {
  int fd = open(patched_executable, O_WRONLY | O_CREAT | O_TRUNC, 0755);
  bool result = true;

  if(fd < 0) {
    perror("write_encrypted_exec (open)");
    return false;
  }

  if(!write_all(fd, elf->mem, elf->size)) {
    result = false;
  }

  close(fd);
  return result;
}

Кроме генерации AddRoundKey нам нужно записать в файл table.inc размер хэш-таблицы, в которой будут храниться первые пять байт функций, которые мы будем заменять на инструкцию call. В директории utils/patcher лежат файлы funcs_table.h и funcs_table.c, в которых имплементирована простая хэш-таблица с открытой адресацией, интерфейс которой выглядит так:

#pragma once

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

typedef struct {
  // адрес функции является ключом в хэш-таблице.
  uint64_t addr;
  // Не помню, почему сделал 8 байт, а не 5. В любом случае,
  // всё выровняется до 8.
  uint8_t  data[8];
} func_node;

typedef struct {
  func_node *nodes;
  size_t    num_nodes;
  size_t    max_nodes;
  int       size_ind;
} funcs_table;

funcs_table* funcs_create(size_t max_nodes);
// Размер таблицы - простое число, эта функция возвращает
// простое число >= max_nodes.
size_t get_func_table_size(size_t max_nodes);
bool funcs_insert(funcs_table *funcs, uint64_t addr, void *data);
void funcs_free(funcs_table *funcs);

Процедура для записи размера хэш-таблицы выглядит следующим образом:

static bool write_table_inc(const char *table, size_t hash_table_size) {
  FILE *f = fopen(table, "w");
  if(!f) {
    perror("write_table_inc (fopen)");
    return false;
  }
  fprintf(f, "#define FUNCS_TABLE_SIZE %lu\n", hash_table_size);
  fclose(f);
  return true;
}

Наконец рассмотрим функцию main:

typedef enum {
  ENCRYPT,
  HELP
} action_t;

int main(int argc, char **argv) {
  int result = EXIT_FAILURE;
  elf_t elf;
  action_t action = HELP;
  char *executable, *table, *addr_round_key, *patched_executable;

  if(argc == 6 && !strcmp(argv[1], "-e")) {
    // ELF файл, в котором мы будем шифровать функции.
    executable = argv[2];
    // Файл table.inc, в который мы положим размер хэш-таблицы.
    table = argv[3];
    // Файл, в который мы положим код AddRoundKey.
    addr_round_key = argv[4];
    // Путь до нового бинарника с зашифрованными функциями.
    patched_executable = argv[5];
    action = ENCRYPT;
  }

  if(action == HELP) {
    fprintf(
      stderr,
      "Usage:\n\t%s -e <executable> <table.inc> <add_round_key.c> "
      "<patched_executable>\n",
      argv[0]
    );
    return EXIT_FAILURE;
  }

  if(!open_elf(executable, &elf))
    goto exit;

  if(action == ENCRYPT) {
    // Получаем количество функций в ELF файле.
    size_t num_functions = count_functions(&elf);
    // Шифруем их и генерируем код AddRoundKey.
    if(!encrypt_and_generate_add_round_key(&elf, num_functions, addr_round_key))
      goto exit;
    // Получаем размер хэш-таблицы.
    num_functions = get_func_table_size(num_functions);
    // Пишем его в файл.
    if(!write_table_inc(table, num_functions))
      goto exit;
    // Пишем в файл бинарник с зашифрованными функциями.
    if(!write_encrypted_exec(&elf, patched_executable))
      goto exit;
  }

  result = EXIT_SUCCESS;
exit:
  if(elf.mem != MAP_FAILED) close_elf(&elf);
  return result;
}

Makefile для сборки patcher довольно простой, поэтому здесь не приведён. Сейчас нам нужно интегрировать patcher в процесс сборки, для начала положим в utils код программы gen-add-round-key и создадим директорию engine, в которой будет лежать код движка расшифровки функций. Именно в неё мы будем сохранять код функции AddRoundKey и файл table.inc с размером хэш-таблицы. Структура проекта на данном этапе выглядит так:

.
├── engine
├── utils
│   ├── gen-add-round-key
│   └── patcher
├── crackme.c
├── create_linker_script.awk
├── Makefile
└── start.s

Теперь добавим в наш Makefile вызов patcher:

CC=gcc
CFLAGS=-Wall -std=c11 -fno-stack-protector -Wno-builtin-declaration-mismatch \
       -fno-stack-protector -ffunction-sections -O0
LDFLAGS=-nostdlib -nostartfiles -nodefaultlibs -static -z noexecstack \
        -Wl,-z,noseparate-code
AS=as

.PHONY: all clean
all: crackme-encrypted
crackme-encrypted: crackme-unencrypted utils/patcher/patcher \
        utils/gen-add-round-key/build
        # Так как patcher вызывает cabal в своей рабочей директории,
        # сначала меняем её на utils/gen-add-round-key. После своей работы
        # в корневой директории проекта будет создан файл crackme-encrypted, а в
        # директории engine будут созданы файлы AddRoundKey.c и table.inc
        cd utils/gen-add-round-key && ../patcher/patcher -e ../../crackme-unencrypted \
                ../../engine/table.inc ../../engine/AddRoundKey.c ../../crackme-encrypted
crackme-unencrypted: linker.ld start.o crackme.o
        $(CC) $(LDFLAGS) -T linker.ld start.o crackme.o -o crackme-unencrypted
linker.ld: start.o crackme.o
        readelf --wide -S start.o crackme.o | awk -f create_linker_script.awk > linker.ld
crackme.o: crackme.c
        $(CC) $(CFLAGS) -c crackme.c -o crackme.o
start.o: start.s
        $(AS) -c start.s -o start.o
utils/patcher/patcher: FORCE
        # Компилируем patcher
        $(MAKE) $(MFLAGS) -C utils/patcher
utils/gen-add-round-key/build: FORCE
        # Компилируем gen-add-round-key. Если ничего не поменялось,
        # cabal выведет в стандартный вывод "Up to date", в таком случае
        # ничего не делаем, иначе обновляем время модификации файла utils/gen-add-round-key/build.
        @test "$(shell cd utils/gen-add-round-key && cabal build | tee /dev/fd/2)" = "Up to date" \
                || { touch utils/gen-add-round-key/build; } \
                && { test -f utils/gen-add-round-key/build || \
                touch utils/gen-add-round-key/build; }
FORCE:

clean:
        $(MAKE) -C utils/patcher clean
        rm -f start.o crackme.o linker.ld utils/gen-add-round-key/build \
                crackme-unencrypted crackme-encrypted

Создаём движок

Теперь приступим к созданию движка. Алгоритм его работы достаточно прост:

  1. Если функция, которую мы расшифровываем (далее caller), была вызвана из другой функции (далее prev_caller), шифруем prev_caller, иначе сразу переходим к шагу 2. Это условие нужно, потому что мы также шифруем функцию _start, которая ни откуда не вызывается.

  2. Сохраняем адрес возврата caller, т.е. адрес, с которого продолжится исполнение после выхода из caller.

  3. Расшифровываем caller.

  4. Вызываем caller и запоминаем возвращаемое значение.

  5. Шифруем caller.

  6. Если на шаге 1 мы зашифровали prev_caller, расшифровываем его обратно.

  7. Кладём адрес возврата в стек, так чтобы после инструкции ret исполнение продолжилось с него.

  8. Кладём в регистр rax значение, которое мы запомнили на 4 шаге и выходим из движка.

Переходим к реализации. Создаём в директории engine файл engine.c со следующим содержимым:

// Так как код движка будет встраиваться в наш бинарник
// мы не можем зависеть от libc.
#define NULL ((void*)0)
typedef unsigned long size_t;
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef long int64_t;
typedef unsigned long uint64_t;

// Этот макрос нужен для отладки с разными
// уровнями оптимизации. Если на -O0 определить его
// как always_inline, то все inline функции будут заинлайнены,
// иначе они будут вызываться как обычно.
#define INLINE __attribute__((always_inline))
//#define INLINE

// #define FUNCS_TABLE_SIZE размер_хэш_таблицы
#include "table.inc"

#define Nb 4
#define Nk 8
#define Nr 14
#define AES_BLOCKLEN 16
#define CHAR_BIT 8

typedef uint8_t state_t[4][4];

// Функции AddRoundKey и get_iv
#include "AddRoundKey.c"

Далее идут все функции из tiny-AES-c нужные для работы AES_CBC_decrypt_buffer и AES_CBC_encrypt_buffer у большей части из них поменялось лишь определение: я заменил static на INLINE static inline, это нужно, чтобы в результате движок был одной большой функцией, в которой чёрт ногу сломит. Далее разберём некоторые существенные изменения в функциях шифрования. У функций InvCipher и Cipher исчез за ненадобностью параметр const uint8_t* RoundKey:

INLINE
static inline void InvCipher(state_t* state)
{
  uint8_t round = 0;

  AddRoundKey(state, Nr);

  for (round = (Nr - 1); ; --round)
  {
    InvShiftRows(state);
    InvSubBytes(state);
    AddRoundKey(state, round);
    if (round == 0) {
      break;
    }
    InvMixColumns(state);
  }

}

INLINE
static inline void Cipher(state_t* state)
{
  uint8_t round = 0;

  AddRoundKey(state, 0);

  for (round = 1; ; ++round)
  {
    SubBytes(state);
    ShiftRows(state);
    if (round == Nr) {
      break;
    }
    MixColumns(state);
    AddRoundKey(state, round);
  }
  AddRoundKey(state, Nr);
}

У функций AES_CBC_encrypt_buffer и AES_CBC_decrypt_buffer ушли за ненадобностью параметры struct AES_ctx *ctx и size_t length:

INLINE
static inline void AES_CBC_encrypt_buffer(uint8_t* buf)
{
  size_t i;
  uint8_t initIv[AES_BLOCKLEN];
  // Получаем размер функции и вектор инициализации.
  // адрес буфера используется в их вычислении.
  size_t length = get_iv(initIv, (uint64_t)buf);
  uint8_t *Iv = initIv;
  for (i = 0; i < length; i += AES_BLOCKLEN)
  {
    XorWithIv(buf, Iv);
    Cipher((state_t*)buf);
    Iv = buf;
    buf += AES_BLOCKLEN;
  }
}

// Так как мы не можем использовать memcpy,
// пишем свою реализацию.
INLINE
static inline void ivcpy(uint8_t *dst, uint8_t *src) {
  *(uint64_t*)dst = *(uint64_t*)src;
  *(uint64_t*)(dst + 8) = *(uint64_t*)(src + 8);
}

INLINE
static inline void AES_CBC_decrypt_buffer(uint8_t *buf)
{
  size_t i;
  uint8_t storeNextIv[AES_BLOCKLEN];
  uint8_t curIv[AES_BLOCKLEN];
  // Получаем размер функции и вектор инициализации.
  // адрес буфера используется в их вычислении.
  size_t length = get_iv(curIv, (uint64_t)buf);

  for (i = 0; i < length; i += AES_BLOCKLEN)
  {
    ivcpy(storeNextIv, buf);
    InvCipher((state_t*)buf);
    XorWithIv(buf, curIv);
    ivcpy(curIv, storeNextIv);
    buf += AES_BLOCKLEN;
  }
}

Теперь перейдём к реализации самого движка. Для начала рассмотрим вспомогательные функции для работы с хэш-таблицей:

typedef struct {
  uint64_t addr;
  uint8_t  data[8];
} func_node;

// Помещаем хэш-таблицу в секцию .data, чтобы компоновщик
// не поместил её в .bss, это нужно, чтобы в нашем исполняемом файле
// была область нужного размера, заполненная нулями
// (нужные данные в неё мы будем помещать на следующем шаге алгоритма).
__attribute__((section(".data")))
static func_node funcs_table[FUNCS_TABLE_SIZE];

// Мы поддерживаем только функции с аргументами в регистрах,
// т.е. те, которые используют до 6 параметров.
typedef void* (generic_func)(void*,void*,void*,void*,void*,void*);

// Глобальная переменная, в которой будет храниться адрес предыдущей функции.
__attribute__((section(".data")))
static generic_func *prev_caller = NULL;

// Уже знакомые нам функции для работы с хэш-таблицей.
INLINE
static inline uint32_t jenkins_hash_func(uint64_t addr) {
  size_t i = 0;
  uint32_t hash = 0;
  uint8_t *key = (uint8_t*)&addr;
  while (i != sizeof(addr)) {
    hash += key[i++];
    hash += hash << 10;
    hash ^= hash >> 6;
  }
  hash += hash << 3;
  hash ^= hash >> 11;
  hash += hash << 15;
  return hash;
}

INLINE
static inline func_node* func_lookup(uint64_t addr) {
  uint32_t hash = jenkins_hash_func(addr);
  int cur = hash % FUNCS_TABLE_SIZE;
  int step = hash % (FUNCS_TABLE_SIZE - 2) + 1;
  int nstep = step;

  for(size_t i = 0; i < FUNCS_TABLE_SIZE; i++) {
    func_node *cur_node = &funcs_table[cur];
    if(!cur_node->addr)
      break;
    if(cur_node->addr == addr)
      return cur_node;
    cur = (hash + nstep) % FUNCS_TABLE_SIZE;
    nstep += step;
  }

  return NULL;
}

Наконец, рассмотрим код самого движка:

// Процедура для шифрования функции.
INLINE
static inline void encrypt_function(func_node *func) {
  // Шифруем тело функции.
  uint8_t *data = (uint8_t*)func->addr;
  AES_CBC_encrypt_buffer(data);

  // Обмениваем содержимое первых пяти байт функции и буфера
  // из хэш-таблицы. В хэш-таблице на данный момент лежит инструкция call,
  // для вызова движка.
  uint64_t tmp;
  tmp = *(uint64_t*)data;
  *(uint32_t*)data = *(uint32_t*)func->data;
  data[4] = func->data[4];
  *(uint64_t*)func->data = tmp;
}

// Процедура для расшифровывания функции.
INLINE
static inline void decrypt_function(func_node *func) {
  // Обмениваем содержимое первых пяти байт функции и буфера
  // из хэш-таблицы. В хэш-таблице на данный момент лежат пять
  // зашифрованных байт из тела функции, а первые пять байт функции
  // содержат инструкцию call для вызова движка.
  uint8_t *data = (uint8_t*)func->addr;
  uint64_t tmp;
  tmp = *(uint64_t*)data;
  *(uint32_t*)data = *(uint32_t*)func->data;
  data[4] = func->data[4];
  *(uint64_t*)func->data = tmp;

  // Расшифровываем тело функции.
  AES_CBC_decrypt_buffer(data);
}

// Так как мы вызываем движок в первой же инструкции каждой функции,
// аргументы этой функции передаются в него.
void* engine(void *a1, void *a2, void *a3, void *a4, void *a5, void *a6) {
  void *ret_addr;
  // Получаем адрес возврата.
  __asm__ __volatile__ ("mov 16(%%rbp), %%rax" : "=a"(ret_addr) : : "memory");

  // Адрес начала функции, которую мы будем расшифровывать
  // равен адресу возврата - минус пять байт, так как инструкция
  // call занимает ровно пять байт.
  generic_func *caller = __builtin_return_address(0) - 5;
  func_node *prev_caller_info = NULL;
  if(prev_caller != NULL) {
    // Если caller был вызван из другой функции (prev_caller),
    // шифруем prev_caller.
    prev_caller_info = func_lookup((uint64_t)prev_caller);
    encrypt_function(prev_caller_info);
  }

  // Расшифровываем caller.
  func_node *caller_info = func_lookup((uint64_t)caller);
  decrypt_function(caller_info);

  // Сохраняем текущее значение prev_caller и заменяем его на caller.
  void *prev_caller_bak = prev_caller;
  prev_caller = caller;

  // Вызываем только что расшифрованную функцию.
  void *res = caller(a1, a2, a3, a4, a5, a6);

  // Зашифровываем её обратно.
  encrypt_function(caller_info);

  // Восстанавливаем prev_caller.
  prev_caller = prev_caller_bak;
  if(prev_caller_info != NULL)
    // Если мы зашифровали prev_caller ранее,
    // расшифровываем обратно.
    decrypt_function(prev_caller_info);

  // Кладём адрес возврата в стек.
  __asm__ __volatile__ ("mov %%rax, 8(%%rbp)" : : "a"(ret_addr) : "memory");
  // Возвращаем результат.
  return res;
}

Рассмотрим строчку, в которой мы получаем адрес возврата:

__asm__ __volatile__ ("mov 16(%%rbp), %%rax" : "=a"(ret_addr) : : "memory");

На иллюстрации показано состояние стека для каждой из вызываемых функций, как можно видеть нужный нам адрес возврата находится именно по адресу rbp + 16.

img
Стек при вызове движка

Однако, чтобы эта инструкция работала так, как нам нужно, необходимо компилировать движок с флагом -fno-omit-frame-pointer, чтобы gcc оставлял ниже приведённые инструкции:

pushq %rbp
movq %rsp, %rbp

Код движка на этом завершён, теперь добавим в Makefile цели для его компиляции. Но для начала напишем простой скрипт компоновщика, чтобы код и данные лежали в одной секции, и мы могли легко их вырезать и вставить в наш исполняемый файл. Создадим файл engine/linker.ld со следующим содержимым:

ENTRY(engine)
SECTIONS {
    .text : {
        * (.text)
        * (.data)
        * (.rodata)
    }
}

Теперь допишем пару инструкций в наш Makefile:

CC=gcc
CFLAGS=-Wall -std=c11 -fno-stack-protector -Wno-builtin-declaration-mismatch \
       -fno-stack-protector -ffunction-sections -O0
LDFLAGS=-nostdlib -nostartfiles -nodefaultlibs -static -z noexecstack \
        -Wl,-z,noseparate-code
AS=as

.PHONY: all clean
all: crackme-unencrypted engine/engine
engine/engine: engine/engine.o
        $(CC) $(LDFLAGS) -T engine/linker.ld -pie -Xlinker --no-dynamic-linker \
                engine/engine.o -o engine/engine
engine/engine.o: crackme-encrypted engine/engine.c
        # Обязательно добавляем -fno-omit-frame-pointer и -fPIC (чтобы код был позиционно-независимым).
        # Также я добавил -Wno-overflow, так как в сгенерированном коде AddRoundKey часто встречаются переполнения,
        # так и было задумано, поэтому предупреждения об этом нам не нужны. Флаг -fno-function-sections
        # я добавил, потому что в данном случае нам не нужно класть функции в раздельные секции. И последний
        # флаг -mno-sse я добавил, потому что иначе происходит ошибка segmentation fault как раз
        # на sse инструкции.
        $(CC) $(CFLAGS) -fno-function-sections -O3 -mno-sse -fno-omit-frame-pointer -Wno-overflow -fPIC -c \
                engine/engine.c -o engine/engine.o
crackme-encrypted: crackme-unencrypted utils/patcher/patcher \
        utils/gen-add-round-key/build
        cd utils/gen-add-round-key && ../patcher/patcher -e ../../crackme-unencrypted \
                ../../engine/table.inc ../../engine/AddRoundKey.c ../../crackme-encrypted
crackme-unencrypted: linker.ld start.o crackme.o
        $(CC) $(LDFLAGS) -T linker.ld start.o crackme.o -o crackme-unencrypted
linker.ld: start.o crackme.o
        readelf --wide -S start.o crackme.o | awk -f create_linker_script.awk > linker.ld
crackme.o: crackme.c
        $(CC) $(CFLAGS) -c crackme.c -o crackme.o
start.o: start.s
        $(AS) -c start.s -o start.o
utils/patcher/patcher: FORCE
        $(MAKE) $(MFLAGS) -C utils/patcher
utils/gen-add-round-key/build: FORCE
        @test "$(shell cd utils/gen-add-round-key && cabal build | tee /dev/fd/2)" = "Up to date" \
                || { touch utils/gen-add-round-key/build; } \
                && { test -f utils/gen-add-round-key/build || \
                touch utils/gen-add-round-key/build; }
FORCE:

clean:
        $(MAKE) -C utils/patcher clean
        rm -f start.o crackme.o linker.ld utils/gen-add-round-key/build \
                engine/table.inc engine/AddRoundKey.c engine/engine.o engine/engine \
                crackme-unencrypted crackme-encrypted

На этом рассмотрение работы движка окончено, переходим к четвёртому шагу алгоритма.

Встраиваем движок в исполняемый файл

Reverse text infection

Здесь будет описана данная техника встраивания собственного кода в уже готовый бинарник, если вы с ней знакомы, смело пропускайте это описание. Эта техника так называется, потому что встраиваемый код будет располагаться до основного кода исполняемого файла, таким образом мы увеличиваем сегмент с кодом (text segment) в обратную сторону:

img
Reverse text infection

Как видно из иллюстрации, при таком встраивании адреса всех функций и данных остаются неизменными. Также можно заменить, что мы увеличиваем размер сегмента на размер нашего встраиваемого кода, округлённый до размера страницы. Дело в том, что адрес начала сегмента должен быть кратен размеру страницы (4096 байт), если наш код не кратен ему, мы вынуждены заполнить оставшееся пространство нулями (padding на иллюстрации).

Теперь можно описать алгоритм:

  1. Вычисляем округлённую до размера страницы длину встраиваемого кода, далее rounded_size.

  2. Проходимся по всем заголовкам секций, если секция расположена после сегмента с кодом (shdr->sh_offset >= text_phdr.p_offset + text_phdr.p_filesz), увеличиваем её смещение на rounded_size: shdr->sh_offset += round_size. Находим заголовок .text секции, уменьшаем адрес на rounded_size и увеличиваем размер на rounded_size.

  3. Находим заголовок исполняемого сегмента, уменьшаем p_vaddr и p_paddr на rounded_size, увеличиваем p_filesz и p_memsz на rounded_size.

  4. Проходимся по всем заголовкам программы, если сегмент расположен после исполняемого, увеличиваем его смещение относительно начала файла (p_offset) на rounded_size.

  5. Сохраняем на диск новый ELF файл:

    1. сначала пишем все данные до заголовков программы включительно,

    2. потом пишем встраиваемый код,

    3. далее нули, чтобы сделать размер кратным размеру страницы,

    4. наконец пишем все данные после заголовков программы из оригинального файла.

Алгоритм встраивания движка

Мы подошли к финальному шагу, для начала рассмотрим алгоритм встраивания движка:

  1. Вычисляем адрес функции движка в нашем исполняемом файле.

  2. Создаём пустую хэш-таблицу, в которой будут лежать первые пять байт функций.

  3. Проходимся по всем функциям, добавляем первые пять байт в хэш-таблицу, заменяем их на инструкцию call с вызовом функции движка, для этого используем адрес из первого шага.

  4. Находим символ funcs_table в движке, копируем в него нашу хэш-таблицу.

  5. Встраиваем код движка в наш файл с помощью reverse text infection.

Встраиванием будет заниматься patcher, поэтому пишем код в utils/patcher/patcher.c. Для начала рассмотрим две вспомогательные функции:

// Эта функция находит секцию в ELF файле по имени.
static Elf64_Shdr* section_by_name(elf_t *elf, const char *name) {
  for(size_t i = 0; i < elf->ehdr->e_shnum; i++) {
    char *sh_name = &elf->sh_names[elf->shdrs[i].sh_name];
    if(!strcmp(sh_name, name))
      return &elf->shdrs[i];
  }
  return NULL;
}

// Эта функция находит символ в ELF файле по имени.
static Elf64_Sym* symbol_by_name(elf_t *elf, const char *name) {
  for(size_t i = 0; i < elf->num_symbols; i++) {
    char *sym_name = &elf->sym_names[elf->symbols[i].st_name];
    if(!strcmp(sym_name, name))
      return &elf->symbols[i];
  }
  return NULL;
}

А теперь приступим к основному коду:

#define PAGE_ALIGN 4096

static const uint8_t call_opcode[] = {0xe8,0x00,0x00,0x00,0x00};
static const uint8_t zeros[PAGE_ALIGN] = {0};

// Данная процедура вставляет в хэш-таблицу первые пять байт функции,
// и заменяет их на инструкцию call.
static bool jump_to_engine(
  elf_t *elf,
  funcs_table *funcs,
  Elf64_Sym *func,
  uint64_t engine_func_addr
) {
  Elf64_Shdr *func_shdr = &elf->shdrs[func->st_shndx];
  uint8_t *func_start = elf->mem + func_shdr->sh_offset +
                        func->st_value - func_shdr->sh_addr;

  if(!funcs_insert(funcs, func->st_value, func_start))
    return false;

  memcpy(func_start, call_opcode, sizeof(call_opcode));
  int32_t jump_diff = engine_func_addr - func->st_value - sizeof(call_opcode);
  memcpy(&func_start[1], &jump_diff, sizeof(jump_diff));

  return true;
}

// Процедура для встраивания кода движка и сохранения получившегося ELF файла.
// elf - наш исполняемый файл.
// engine - ELF файл с движком.
// patched_executable - путь до нового ELF файла.
static bool infect_engine(elf_t *elf, elf_t *engine, const char *patched_executable) {
  bool result = false;
  funcs_table *funcs = NULL;
  int fd = -1;

  // Находим .text секцию движка, именно её мы и будем встраивать
  // в наш бинарник.
  Elf64_Shdr *engine_text = section_by_name(engine, ".text");
  if(!engine_text) {
    fputs("engine has not .text section\n", stderr);
    goto exit;
  }

  // Находим функцию engine.
  Elf64_Sym *engine_func = symbol_by_name(engine, "engine");
  if(!engine_func) {
    fputs("engine has not engine function\n", stderr);
    goto exit;
  }

  // Находим хэш-таблицу funcs_table.
  Elf64_Sym *engine_funcs_table = symbol_by_name(engine, "funcs_table");
  if(!engine_funcs_table) {
    fputs("engine has not funcs_table\n", stderr);
    goto exit;
  }

  // Создаём хэш-таблицу необходимого размера.
  size_t num_functions = engine_funcs_table->st_size / sizeof(func_node);
  funcs = funcs_create(num_functions);
  if(!funcs)
    goto exit;

  // Вычисляем адрес функции engine. Так как код движка встраивается
  // в самое начало исполняемого сегмента, адрес функции engine равен
  // новому начальному адресу сегмента (orig_p_vaddr - rounded_size)
  // плюс смещению функции engine относительно её .text секции.
  Elf64_Addr orig_p_vaddr = elf->phdrs[0].p_vaddr;
  size_t rounded_size = roundup(engine_text->sh_size, PAGE_ALIGN);
  uint64_t new_base_addr = orig_p_vaddr - rounded_size;
  uint64_t engine_func_addr = new_base_addr +
                              engine_func->st_value - engine_text->sh_addr;

  // Проходимся по всем функциям, заменяем первые пять байт на инструкцию call
  // для вызова engine, обновляем хэш-таблицу.
  for(size_t i = 0; i < elf->num_symbols; i++) {
    if(ELF64_ST_TYPE(elf->symbols[i].st_info) != STT_FUNC)
      continue;

    if(!jump_to_engine(elf, funcs, &elf->symbols[i], engine_func_addr))
      goto exit;
  }

  // Вычисляем смещение хэш-таблицы относительно .text секции и
  // копируем туда только что созданную нами.
  uint8_t *funcs_table_start = engine->mem + engine_text->sh_offset +
                               engine_funcs_table->st_value -
                               engine_text->sh_addr;
  memcpy(funcs_table_start, funcs->nodes, engine_funcs_table->st_size);

  // Reverse text infection

  // Обновляем заголовки секций
  // (не обязательная операция, однако помогает потом при отладке)
  elf->ehdr->e_shoff += rounded_size;
  for(size_t i = 0; i < elf->ehdr->e_shnum; i++) {
    if(elf->shdrs[i].sh_offset >=
        elf->phdrs[0].p_offset + elf->phdrs[0].p_filesz)
      elf->shdrs[i].sh_offset += rounded_size;
  }
  Elf64_Shdr *text_section = section_by_name(elf, ".text");
  if(text_section) {
    text_section->sh_addr -= rounded_size;
    text_section->sh_size += rounded_size;
  }

  // Делаем наш исполняемый сегмент доступным для записи,
  // иначе расшифровка упадёт с ошибкой.
  elf->phdrs[0].p_flags = PF_X | PF_W | PF_R;
  // Уменьшаем начальный адрес исполняемого сегмента на rounded_size,
  // увеличиваем его размер в памяти и в файле на rounded_size.
  elf->phdrs[0].p_vaddr -= rounded_size;
  elf->phdrs[0].p_paddr -= rounded_size;
  elf->phdrs[0].p_filesz += rounded_size;
  elf->phdrs[0].p_memsz += rounded_size;

  // Увеличиваем смещение всех сегментов, которые идут
  // после исполняемого на rounded_size.
  for(size_t i = 1; i < elf->ehdr->e_phnum; i++) {
    if(elf->phdrs[i].p_offset > elf->phdrs[0].p_offset)
      elf->phdrs[i].p_offset += rounded_size;
  }

  // Создаём новый файл.
  fd = open(patched_executable, O_CREAT | O_TRUNC | O_WRONLY, 0755);
  if(fd < 0) {
    perror("infect_engine (open)");
    goto exit;
  }

  // Пишем в него все данные до заголовков программы включительно.
  size_t to_program_headers = elf->phdrs[0].p_offset;
  if(!write_all(fd, elf->mem, to_program_headers))
    goto exit;
  // Пишем .text секцию движка.
  if(!write_all(fd, engine->mem + engine_text->sh_offset, engine_text->sh_size))
    goto exit;
  // Пишем нули, чтобы размер был кратен размеру страницы.
  size_t num_zeros = rounded_size - engine_text->sh_size;
  if(num_zeros != 0) {
    if(!write_all(fd, zeros, num_zeros))
      goto exit;
  }
  // Пишем остальную часть оригинального ELF файла.
  if(!write_all(fd, &elf->mem[to_program_headers], elf->size - to_program_headers))
    goto exit;

  result = true;
exit:
  if(funcs) funcs_free(funcs);
  if(fd >= 0) close(fd);
  return result;
}

Теперь добавим в main новый аргумент командной строки -p:

typedef enum {
  ENCRYPT,
  INFECT,
  HELP
} action_t;

int main(int argc, char **argv) {
  int result = EXIT_FAILURE;
  elf_t elf, engine;
  action_t action = HELP;
  char *executable, *table, *addr_round_key, *patched_executable, *engine_path;

  engine.mem = MAP_FAILED; 

  if(argc == 6 && !strcmp(argv[1], "-e")) {
    executable = argv[2];
    table = argv[3];
    addr_round_key = argv[4];
    patched_executable = argv[5];
    action = ENCRYPT;
  } else if(argc == 5 && !strcmp(argv[1], "-p")) {
    executable = argv[2];
    patched_executable = argv[3];
    engine_path = argv[4];
    action = INFECT;
  }

  if(action == HELP) {
    fprintf(
      stderr,
      "Usage:\n\t%s -e <executable> <table.inc> <add_round_key.c> "
      "<patched_executable>\n"
      "\t%s -p <executable> <patched_executable> <engine>\n",
      argv[0],
      argv[0]
    );
    return EXIT_FAILURE;
  }

  if(!open_elf(executable, &elf))
    goto exit;

  if(action == ENCRYPT) {
    size_t num_functions = count_functions(&elf);
    if(!encrypt_and_generate_add_round_key(&elf, num_functions, addr_round_key))
      goto exit;
    num_functions = get_func_table_size(num_functions);
    if(!write_table_inc(table, num_functions))
      goto exit;
    if(!write_encrypted_exec(&elf, patched_executable))
      goto exit;
  } else if(action == INFECT) {
    if(!open_elf(engine_path, &engine))
      goto exit;
    if(!infect_engine(&elf, &engine, patched_executable))
      goto exit;
  }

  result = EXIT_SUCCESS;
exit:
  if(elf.mem != MAP_FAILED) close_elf(&elf);
  if(engine.mem != MAP_FAILED) close_elf(&engine);
  return result;
}

Осталось добавить новую цель в Makefile и готово:

CC=gcc
CFLAGS=-Wall -std=c11 -fno-stack-protector -Wno-builtin-declaration-mismatch \
       -fno-stack-protector -ffunction-sections -O0
LDFLAGS=-nostdlib -nostartfiles -nodefaultlibs -static -z noexecstack \
        -Wl,-z,noseparate-code
AS=as

.PHONY: all clean
all: crackme
crackme: crackme-encrypted engine/engine utils/patcher/patcher
        ./utils/patcher/patcher -p crackme-encrypted crackme engine/engine
engine/engine: engine/engine.o
        $(CC) $(LDFLAGS) -T engine/linker.ld -pie -Xlinker --no-dynamic-linker \
                engine/engine.o -o engine/engine
engine/engine.o: crackme-encrypted engine/engine.c
        $(CC) $(CFLAGS) -fno-function-sections -O3 -mno-sse -fno-omit-frame-pointer -Wno-overflow -fPIC -c \
                engine/engine.c -o engine/engine.o
crackme-encrypted: crackme-unencrypted utils/patcher/patcher \
        utils/gen-add-round-key/build
        cd utils/gen-add-round-key && ../patcher/patcher -e ../../crackme-unencrypted \
                ../../engine/table.inc ../../engine/AddRoundKey.c ../../crackme-encrypted
crackme-unencrypted: linker.ld start.o crackme.o
        $(CC) $(LDFLAGS) -T linker.ld start.o crackme.o -o crackme-unencrypted
linker.ld: start.o crackme.o
        readelf --wide -S start.o crackme.o | awk -f create_linker_script.awk > linker.ld
crackme.o: crackme.c
        $(CC) $(CFLAGS) -c crackme.c -o crackme.o
start.o: start.s
        $(AS) -c start.s -o start.o
utils/patcher/patcher: FORCE
        $(MAKE) $(MFLAGS) -C utils/patcher
utils/gen-add-round-key/build: FORCE
        @test "$(shell cd utils/gen-add-round-key && cabal build | tee /dev/fd/2)" = "Up to date" \
                || { touch utils/gen-add-round-key/build; } \
                && { test -f utils/gen-add-round-key/build || \
                touch utils/gen-add-round-key/build; }
FORCE:

clean:
        $(MAKE) -C utils/patcher clean
        rm -f start.o crackme.o linker.ld utils/gen-add-round-key/build \
                engine/table.inc engine/AddRoundKey.c engine/engine.o engine/engine \
                crackme-unencrypted crackme-encrypted crackme

Запускаем наш crackme: ./crackme "Hello Habr!?" и ничего не происходит, как же так? Вспоминаем эту иллюстрацию и понимаем, что в функции _start нужно поменять код, отвечающий за передачу argc и argv:

.text
.globl _start
.type _start,@function
.section .text._start
_start:
    # Call main
    movq 16(%rbp), %rdi
    leaq 24(%rbp), %rsi
    callq main

    # Exit
    movq %rax, %rdi
    movq $60, %rax
    syscall
.size _start,.-_start

Компилируем всё снова, и оно начинает работать.

Заключение

В этой статье я показал простой способ шифрования всех функций в исполняемом файле с последующей расшифровкой только на время исполнения. Очевидно, это самый простой и не самый лучший вариант, потому что в итоге из всех функций путь ведёт в одно и то же место - в движок. Возможно, когда-нибудь позже я придумаю, что посложнее, но пока живём с тем, что имеем. В заключительной третьей статье речь пойдёт про добавление наномитов в наш crackme для усложнения отладки. Надеюсь, вам было весело.

Комментарии (0)