Let’s make crackme. Part two: encrypting functions

This article is the second in a series on creating crackme for Linux amd64. In this part, we will create an executable file in which each function will be encrypted with its own key, and will be decrypted only during execution. The creation process will be fully automated, that is, when adding new code or changing old one, no additional actions will be necessary. The code for the entire project is in repositories on github.

Algorithm

The algorithm for creating such a binary is quite simple, let’s describe it step by step:

  1. Compile the code into an executable file so that each function has a size that is a multiple of 16. This condition comes from the fact that in CBC mode you can only work with buffers whose size is a multiple of the block size, that is, 16.

  2. Generate a key and initialization vector for each function, then encrypt them. After that, pass information about the functions (address and size), as well as keys and initialization vectors to the program for generating the AddRoundKey and get_iv functions, which I described in the previous part. After this step, the executable file will change as follows:

    img

    Step 2

  3. Compile the engine code responsible for encrypting and decrypting functions. It just uses the code generated in the previous step. So that the compiled engine code can be embedded in our executable file, we need to pass the flag to the compiler -fPICso that the code is position independent.

  4. Embed the engine code into the executable file, insert a call instruction at the beginning of each function to call the engine code. Here a problem arises due to the fact that we cannot change the size of the function, because then the addresses of other functions will change, which can lead to incorrect operation of instructions that access memory. Therefore, we will replace the first 5 bytes of each function with the call instruction, but we will put the original 5 bytes in a hash table and, when decrypting, we will copy them from it back to the beginning of the function. After this step, our executable file will look like this:

    img

    Step 4

Implementation

Create an executable file

As I wrote above, we need to compile the code so that the sizes of all functions are a multiple of 16. First, let’s write a simple code in C, which we will compile. We will not use the standard library, because otherwise we will have too many functions, and as we found out in the last chapter, the more functions, the larger the AddRoundKey size, the slower the execution and compilation. The code itself is presented below:

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));
}

There is one simple way to make the size of all functions a multiple of 16 – place each function in a separate section and set the alignment for each such section in the linker script. The first problem is solved by passing the flag -ffunction-sections the gcc compiler, the second – by generating a linker script. To generate the script, I decided to use standard console utilities – readelf and awk. First, using readelf, we obtain section names from object files, then, using awk, we generate a linker script based on them. The text of the awk program is given below.

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}"
}

We check if the second or third column starts with .text, if not then we skip the line, otherwise we print the section name along with ALIGN(16). Checking two columns is due to the fact that readelf aligns the section numbers, for sections with single digit numbers the name will be in the third column, and for sections with two digit numbers it will be in the second. Example readelf output for better understanding:

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

Why do we check that the section name starts with .text? Because when passing the flag -ffunction-sections gcc puts each function in its own section with a name corresponding to the pattern .text.function_name, which is why we explicitly specified earlier .section .text._start for the _start function. Now let’s write a Makefile to build the executable file:

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

As you can see from the compilation flags, we are building a static executable file without linking to the standard library. It’s also worth noting that readelf is called with the flag --wide, because otherwise the section names may not be fully displayed. For clarity, the following is the text of the generated linker script:

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);
    }
}

Encrypting functions

Let’s create a directory ./utils/patcher, which will store the code of the program that will modify our binary, and put the files there aes.h, aes.cwhich are modified files from the project tiny-AES-c. For a better understanding of the code, below is the signature of the function we will use for encryption:

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

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

Now let’s start creating a ransomware. Let’s start by writing a small structure to store information about the ELF file and a function that reads the ELF file into this structure:

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;
}

Now let’s write a procedure to encrypt one function:

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);
}

Next, let’s create a procedure that will encrypt all functions:

// Процедура, которая возвращает количество функций.
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;
}

Let’s proceed to the generate_add_round_key procedure. The gen-add-round-key program accepts the following command line arguments: -awhich passes the address of the function, and -s, which specifies the size of the function. The program reads the initialization vector and key from standard input. The generate_add_round_key procedure code looks like this:

// Максимальный размер строки, содержащей 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;
}

After we have encrypted all the functions, we need to save a new ELF file. The write_encrypted_exec procedure is responsible for this:

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;
}

In addition to generating AddRoundKey, we need to write in the table.inc file the size of the hash table, which will store the first five bytes of the functions that we will replace with the call instruction. The utils/patcher directory contains files funcs_table.h And funcs_table.cwhich implement a simple hash table with open addressing, the interface of which looks like this:

#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);

The procedure for recording the size of a hash table is as follows:

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;
}

Finally, let’s look at the main function:

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;
}

The makefile for building patcher is quite simple, so it is not included here. Now we need to integrate patcher into the build process. First, let’s put the code of the gen-add-round-key program in utils and create the engine directory, which will contain the code for the function decoding engine. This is where we will save the AddRoundKey function code and the table.inc file with the size of the hash table. The project structure at this stage looks like this:

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

Now let’s add a patcher call to our 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-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

Creating an engine

Now let’s start creating the engine. The algorithm for its operation is quite simple:

  1. If the function we are decrypting (hereinafter caller) was called from another function (hereinafter prev_caller), we encrypt prev_caller, otherwise we immediately go to step 2. This condition is necessary because we are also encrypting the _start function, which is not called from anywhere.

  2. We save the return address of the caller, i.e. the address from which execution will continue after exiting the caller.

  3. Deciphering caller.

  4. We call caller and remember the return value.

  5. We encrypt the caller.

  6. If we encrypted prev_caller in step 1, decrypt it back.

  7. We put the return address on the stack, so that after the instruction ret the performance continued from there.

  8. Put it in the register rax the value that we remembered in step 4 and exit the engine.

Let’s move on to implementation. Create a file engine.c in the engine directory with the following contents:

// Так как код движка будет встраиваться в наш бинарник
// мы не можем зависеть от 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"

Next come all the functions from tiny-AES-c needed for AES_CBC_decrypt_buffer and AES_CBC_encrypt_buffer to work; most of them have only changed their definition: I replaced static on INLINE static inline, this is necessary so that as a result the engine is one big function in which the devil will break his leg. Next, we will look at some significant changes in encryption functions. The parameter for the InvCipher and Cipher functions has disappeared as unnecessary 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);
}

The AES_CBC_encrypt_buffer and AES_CBC_decrypt_buffer functions have had their parameters removed as unnecessary struct AES_ctx *ctx And 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;
  }
}

Now let’s move on to the implementation of the engine itself. First, let’s look at the auxiliary functions for working with a hash table:

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;
}

Finally, let’s look at the code of the engine itself:

// Процедура для шифрования функции.
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;
}

Consider the line where we get the return address:

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

The illustration shows the state of the stack for each of the called functions, as you can see the return address we need is located exactly at rbp + 16.

img

Stack when calling an engine

However, for this instruction to work the way we need, it is necessary to compile the engine with the flag -fno-omit-frame-pointerso that gcc leaves the following instructions:

pushq %rbp
movq %rsp, %rbp

The engine code is now complete, now let’s add targets for its compilation to the Makefile. But first, let’s write a simple linker script so that the code and data are in one section, and we can easily cut and paste them into our executable file. Let’s create a file engine/linker.ld with the following content:

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

Now let’s add a couple of instructions to our 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

This concludes the consideration of the engine’s operation; let’s move on to the fourth step of the algorithm.

Embedding the engine in the executable file

Reverse text infection

This technique of embedding your own code into a ready-made binary will be described here; if you are familiar with it, feel free to skip this description. This technique is so called because the embedded code will be located before the main code of the executable file, so we increase the text segment in the opposite direction:

img

Reverse text infection

As can be seen from the illustration, with such embedding, the addresses of all functions and data remain unchanged. We can also substitute that we increase the segment size by the size of our embed code, rounded up to the page size. The fact is that the address of the beginning of the segment must be a multiple of the page size (4096 bytes); if our code is not a multiple of it, we are forced to fill the remaining space with zeros (padding in the illustration).

Now we can describe the algorithm:

  1. We calculate the length of the embedded code rounded to the page size, then rounded_size.

  2. We go through all section headings, if the section is located after the segment with the code (shdr->sh_offset >= text_phdr.p_offset + text_phdr.p_filesz), increase its displacement by rounded_size: shdr->sh_offset += round_size. Find the title of the .text section, reduce the address by rounded_size and increase the size by rounded_size.

  3. Find the title of the executable segment, reduce p_vaddr And p_paddr on rounded_sizeincrease p_filesz And p_memsz on rounded_size.

  4. We go through all the program headers; if the segment is located after the executable one, we increase its offset relative to the beginning of the file (p_offset) on rounded_size.

  5. Save a new ELF file to disk:

    1. first we write all the data up to and including the program headers,

    2. then we write the embedded code,

    3. then zeros to make the size a multiple of the page size,

    4. Finally we write all the data after the program headers from the original file.

Engine embedding algorithm

We have come to the final step, first let’s look at the algorithm for embedding the engine:

  1. We calculate the address of the engine function in our executable file.

  2. We create an empty hash table that will contain the first five bytes of functions.

  3. We go through all the functions, add the first five bytes to the hash table, replace them with the instruction call with a call to the engine function, for this we use the address from the first step.

  4. We find the funcs_table symbol in the engine and copy our hash table into it.

  5. We embed the engine code into our file using reverse text infection.

The patcher will handle the embedding, so we write the code in utils/patcher/patcher.c. First, let’s look at two helper functions:

// Эта функция находит секцию в 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;
}

Now let’s move on to the main code:

#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;
}

Now let’s add a new command line argument to 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;
}

All that’s left is to add a new target to the Makefile and you’re done:

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

Let’s launch our crackme: ./crackme "Hello Habr!?" and nothing happens, how can this be? Let’s remember this illustration and we understand that in the _start function we need to change the code responsible for passing argc and 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

We compile everything again, and it starts working.

Conclusion

In this article, I showed a simple way to encrypt all functions in an executable file and then decrypt it only during execution. Obviously, this is the simplest and not the best option, because in the end, of all the functions, the path leads to the same place – to the engine. Perhaps someday later I will come up with something more complicated, but for now we live with what we have. In the final third article we will talk about adding nanomites to our crackme to complicate debugging. Hope you had fun.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *