Blob Blame History Raw
/* Detects the presence of potential JCC vulnerabilities in a binary.
   Copyright (c) 2019 Red Hat.

  This is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published
  by the Free Software Foundation; either version 3, or (at your
  option) any later version.

  You should have received a copy of the GNU General Public
  License along with this program; see the file COPYING3. If not,
  see <http://www.gnu.org/licenses/>.

  It is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.  */

#include "annobin-global.h"
#include "annocheck.h"
#include <bfd.h>
#include <dis-asm.h>

typedef struct disas_state
{
  char *        buffer;    /* Disassembly printing buffer.  */
  ulong         alloc;     /* Size of the printing buffer.  */
  ulong         pos;       /* Where we are in the printing buffer. */
} disas_state;

typedef struct insn_state
{
  bfd_byte *    code;	   /* Instruction buffer.  */
  ulong         size;      /* Size of instruction buffer.  */
  bfd_vma       code_base; /* Address of first instruction in the instruction buffer.  */
} insn_state;

static disas_state  disas;
static insn_state   insns;
static bool         disabled = false;
static int          e_type;
static uint         num_found;

static bool
start_file (annocheck_data * data)
{
  if (disabled)
    return false;

  num_found = 0;

  int  e_machine;

  if (data->is_32bit)
    {
      Elf32_Ehdr * hdr = elf32_getehdr (data->elf);

      e_type = hdr->e_type;
      e_machine = hdr->e_machine;
    }
  else
    {
      Elf64_Ehdr * hdr = elf64_getehdr (data->elf);

      e_type = hdr->e_type;
      e_machine = hdr->e_machine;
    }

  return (e_machine == EM_X86_64 || e_machine == EM_386);
}

static bool
interesting_sec (annocheck_data *     data,
		 annocheck_section *  sec)
{
  if (disabled)
    return false;

  /* For retpolines we want to scan code sections.  */
  if (sec->shdr.sh_type == SHT_PROGBITS
      && sec->shdr.sh_flags & SHF_EXECINSTR
      && sec->shdr.sh_size > 0)
    return true;

  /* We do not need any more information from the section,
     so there is no need to run the checker.  */
  return false;
}

static int ATTRIBUTE_PRINTF_2
x86_printf (void * stream, const char * format, ...) 
{
  size_t n;
  va_list args;

  while (1)
    {
      size_t space = disas.alloc - disas.pos;

      /* Attempt to print the instruction into the allocated buffer.  */
      va_start (args, format);
      n = vsnprintf (disas.buffer + disas.pos, space, format, args);
      va_end (args);

      if (space > n)
	break;

      /* If that failed, increase the buffer size and try again.  */
      disas.alloc = (disas.alloc + n) * 2;
      disas.buffer = (char *) xrealloc (disas.buffer, disas.alloc);
    }

  disas.pos += n;
  return n;
}

static int
x86_read_mem (bfd_vma addr, bfd_byte * buffer, unsigned len, struct disassemble_info * info)
{
  if (len == 0)
    return 1;
  if (addr >= insns.size)
    return 2;
  if ((addr + len) > insns.size)
    return 3;
  if ((addr + len) <= addr)
    return 4;

  memcpy (buffer, insns.code + addr, len);
  return 0;
}

static void
x86_mem_err (int status, bfd_vma addr, struct disassemble_info * info)
{
  einfo (ERROR, "x86 disasembler: memory err %d for addr %lx (offset %lx)",
	 status, insns.code_base + addr, addr);
}

static void
x86_addr (bfd_vma addr, struct disassemble_info * info)
{
  size_t space = disas.alloc - disas.pos;

  if (space < 16)
    {
      disas.alloc  = disas.alloc * 2 + 16;
      disas.buffer = (char *) xrealloc (disas.buffer, disas.alloc);
    }

  sprintf (disas.buffer + disas.pos, " &%#lx ", insns.code_base + addr);
  disas.pos += strlen (disas.buffer + disas.pos);
  return;
}

static bool
is_affected_insn (bfd_vma offset, uint len)
{
  /* Only jump type instructions are affected.  */
  if (strchr (disas.buffer, 'j') == NULL
      && strncmp (disas.buffer, "ret", 3) != 0
      && strncmp (disas.buffer, "call", 4) != 0)
    return false;

  /* The instruction has to cross or end on a 32-byte boundary.  */
  bfd_vma start = insns.code_base + offset;
  bfd_vma end   = start + len - 1;

  if ((end > start && ((start & ~0x1F) != (end & ~0x1F)))
      || (end & 0x1F) == 0x1F)
    return true;

  return false;
}

static bool
is_affected_fused_insn (bfd_vma prev_offset, uint prev_len)
{
  /* If the current instruction is a conditional jump and the previous instruction
     could be fused with it, and the previous instruction crossed or ended
     on a 32-byte boundary, then it can be affected too.  */
  if (disas.buffer[0] != 'j' || strncmp (disas.buffer, "jmp", 3) == 0)
    return false;

  bfd_vma start = insns.code_base + prev_offset;
  bfd_vma end   = start + prev_len - 1;

  if ((end > start && ((start & ~0x1F) != (end & ~0x1F)))
      || (end & 0x1F) == 0x1F)
    return true;

  return false;
}

static bool
is_fusable (void)
{
  static const char * fusable_insns[] =
    {
     "cmp", "test", "add", "sub", "and", "inc", "dec"
    };
  int i;

  for (i = 0; i < ARRAY_SIZE (fusable_insns); i++)
    if (strncmp (disas.buffer, fusable_insns[i], strlen (fusable_insns[1])) == 0)
      return true;
  return false;
}

static bool
check_sec (annocheck_data *     data,
	   annocheck_section *  sec)
{
  if (sec->data->d_size == 0)
    return true;

  if (sec->shdr.sh_type == SHT_PROGBITS
      && sec->shdr.sh_flags & SHF_EXECINSTR
      && sec->shdr.sh_size > 0)
    {
      bfd_vma           offset;
      bfd_vma           next_offset;
      bfd_vma           prev_offset = 0;
      int               prev_len = 0;
      bool              prev_is_fusable = false;
      disassemble_info  info;

      if (disas.alloc == 0)
	{
	  disas.alloc  = 128;
	  disas.buffer = xmalloc (disas.alloc);
	}

      memset (& insns, 0, sizeof insns);
      insns.code = (bfd_byte *) sec->data->d_buf;
      insns.size = sec->shdr.sh_size;
      insns.code_base = sec->shdr.sh_addr;

      /* Initialise the non-NULL fields in the disassembler info structure.  */
      init_disassemble_info (& info, stdout, x86_printf);
      info.application_data   = & insns;
      info.stream             = & insns;
      info.endian_code        = BFD_ENDIAN_LITTLE;
      info.read_memory_func   = x86_read_mem;
      info.memory_error_func  = x86_mem_err;
      info.arch               = bfd_arch_i386;
      info.mach               = bfd_mach_x86_64;
      info.print_address_func = x86_addr;
      disassemble_init_for_target (& info);
      
      /* Walk the instructions in this section.  */
      for (offset = 0; offset < sec->shdr.sh_size; offset = next_offset)
	{
	  extern int print_insn_i386 (bfd_vma, disassemble_info *);
	  int len;

	  disas.pos = 0;
	  disas.buffer[0] = 0;
	  len = print_insn_i386 (offset, & info);

	  if (len < 1)
	    {
	      einfo (INFO, "%s: %s: WARN: Unable to classify insn at addr %lx\n",
		     data->filename, sec->secname, insns.code_base + offset);
	      break;
	    }

	  if (is_affected_insn (offset, len))
	    {
	      einfo (VERBOSE, "%s: %s: %#lx: cache line crossed/hit, insn: %s\n",
		     data->filename, sec->secname, insns.code_base + offset,
		     disas.buffer);
	      ++ num_found;
	    }

	  if (prev_is_fusable && is_affected_fused_insn (prev_offset, prev_len))
	    {
	      einfo (VERBOSE, "%s: %s: %#lx: FUSED instructions cross cache line: %s\n",
		     data->filename, sec->secname, insns.code_base + offset,
		     disas.buffer);
	      ++ num_found;
	    }

	  einfo (VERBOSE2, "%s: %s: %lx: %s\n",
		 data->filename, sec->secname, insns.code_base + offset, disas.buffer);

	  prev_offset = offset;
	  prev_len    = len;
	  prev_is_fusable = is_fusable ();
	  next_offset = offset + len;
	}
    }
  
  return true;
}

static void
usage (void)
{
  einfo (INFO, "  Detects the presence of branch instructions that cross a 64-byte cache line boundary");
}

static bool
process_arg (const char * arg, const char ** argv, const uint argc, uint * next_indx)
{
  return false;
}

static void
version (void)
{
  einfo (INFO, "Version 1.0");
}

static bool
end_file (annocheck_data * data)
{
  if (disabled)
    return false;

  free (disas.buffer);

  if (num_found == 0)
    einfo (INFO, "%s: No potential vulnerabilities found", data->filename);
  else if (BE_VERBOSE)
    einfo (VERBOSE, "%s: %u potential vulnerabilities found", data->filename, num_found);
  else
    einfo (INFO, "%s: %u potential vulnerabilities found.  Rerun with --verbose to see where", data->filename, num_found);

  return num_found == 0;
}

struct checker jcc_checker = 
{
  "JCC Checker",
  start_file,
  interesting_sec,
  check_sec, 
  NULL, /* interesting_seg */
  NULL, /* check_seg */
  end_file,
  process_arg, /* process_arg  */
  usage,
  version,
  NULL, /* start_scan */
  NULL, /* end_scan */
  NULL /* internal */
};

static __attribute__((constructor)) void
register_checker (void) 
{
  if (! annocheck_add_checker (& jcc_checker, ANNOBIN_VERSION / 100))
    disabled = true;
}