Será que estou num mundo virtual?

Existem publicações na web com “macetes” para determinar se seu processo está rodando numa máquina real ou virtual. Alguns parecem ser bem simples:

#ifndef (__linux__)
#error Esse código compila apenas em Linux.
#endif

int swallow_red_pill(void)
{
  /* No modo 32 bits 'long' tem 32 bits.
     No modo 64 bits 'long' tem 64 bits 
       (no Windows, continua com 32 bits). */
  struct {
    unsigned long offset;
    unsigned short selector;
  } idtr;

  __asm__ __volatile__ ( "sidt %0" : "=m" (&idtr) );

  return (idtr.selector >> 8) == 0xd5;
}

Esse esquema funcionava em certos ambientes, mas deixou de funcionar há anos… Macetes similares eram usados com a leitura dos registradores LDTR e GDTR. E, do mesmo jeito, não funcionam mais!

Infelizmente, o método mais seguro de verificar se estamos numa máquina virtual ou real é verificando características dessa máquina. Uma das coisas que uma máquina virtual não compartilha com a máquina real é a ROM-BIOS. Já que o objetivo da virtualização é emular um PC, não há sentido em compartilhar a ROM-BIOS da máquina hospedeira… Os softwares de virtualização fornecem sua própria ROM-BIOS.

Fica claro que tudo o que precisamos fazer é obter uma descrição da ROM-BIOS e verificarmos se bate com as descrições presentes em VMs como VirtualBox e VMWARE. Esses são os dois ambientes que tratarei aqui. Outros ambientes, como Xen e KVM, podem ter características diferentes e deixo por sua conta os testes referentes a ROM-BIOS (pode ser que esses ambientes comparilhem, afinal de contas, a ROM-BIOS do hospedeiro!).

Lendo a memória física, no LINUX

Linux permite, ao usuário privilegiado, a leitura da memória física. Num ambiente virtual a “memória física” refere-se à máquina virtual. Essa leitura pode ser feita pelo device “/dev/mem”. Como estamos interessados apenas na memória do ROM-BIOS, que tem uns 128 kBytes de tamanho, vamos mapear a memória entre os endereços físicos 0xE0000 até 0xFFFFF usando um descritor de arquivos aberto com a função open (para acessar “/dev/mem”) e mapear esse descritor com a função mmap:

#define BIOS_SIZE 128*1024

int fd;
void *memptr;

if ((fd = open("/dev/mem", O_RDONLY)) == -1)
  return -1;    /* -1 = ERRO */

if ((memptr = mmap(NULL, BIOS_SIZE, PROT_READ, MAP_SHARED, 
                   fd, 0xE0000)) == MAP_FAILED)
{
  close(fd);
  return -1;
}

/* ... Neste ponto 'memptr' aponta para o endereço 
   físico 0xE0000. */

Obtendo a versão da BIOS…

Uma vez que obtivemos o ponteiro para o início da ROM-BIOS o trabalho realmente começa… Temos que procurar por uma substring “_SM_” que aparece alinhada por parágrafo (de 16 em 16 bytes, com os 4 bits inferiores zerados). Isso é importante porque essa substring pode aparecer várias vezes na ROM-BIOS (de fato, na minha BIOS, aparece duas vezes!), mas somente a estrutura que queremos contém essa substring iniciando um bloco alinhado por parágrafo:

/* Estrutura que estamos procurando... */
struct smbios_info_s {
  char           sm_signature[4];   /* "_SM_" */
  unsigned char  sm_checksum;
  unsigned char  length;
  unsigned char  major_version;
  unsigned char  minor_version;
  unsigned short max_structure_size;
  unsigned char  revision;
  char           reserved[5];
  char           dmi_signature[5];  /* "_DMI_" */
  unsigned char  dmi_checksum;
  unsigned short table_length;
  unsigned int   table_address;     /* Este endereço nos 
                                       interesssa! */
  unsigned short number_of_structures;
  unsigned char  unused;
};

/* Retorna NULL se não achou a estrutura smbios_info_s
   na ROM ou o ponteiro para a estrutura encontrada. */
struct smbios_info_s *find_smbios_info_struct(void *memptr)
{
  unsigned offset;

  for (offset = 0; offset < BIOS_SIZE; offset += 16)
    if (memcmp(memptr + offset, "_SM_", 4) == 0)
    {
      /* É interessante verificar o sm_checksum aqui 
         para termos certeza que é a estrutura correta, 
         mas deixei isso de lado por questões 
         de brevidade. */

      return (struct smbios_info_s *)(memptr + offset);
    }
  return NULL;
}

Uma vez obtido o endereço da estrutura, usaremos o campo “table_address” para localizar endereços de outras estruturas… O “table_address” aponta para um endereço na ROM-BIOS que contém informações sobre a BIOS, Chassis, Placa-Mãe, entre outras coisas… Estamos interessados em apenas uma informação: Na versão da BIOS.

A estrutura para qual “table_address” aponta é essa:

struct dmi_hdr_s {
  unsigned char type;
  unsigned char length;
  unsigned short handle;

  /* Segue mais um monte de dados e, por fim as 
     strings terminadas com ''. Essa tabela 
     termina com um último byte 0. */
};

A informação sobre a versão da BIOS está na tabela cujo “type” é zero. Assim, ao obter o endereço em “table_address”, acima, teremos que “pular” as tabelas que não tem tipo ‘0’. A rotina abaixo faz isso:

/* Essa função "pula" uma tabela, apontando para 
   a próxima. O motivo do ponteiro 'void **ptr' é 
   que devemos ser capazes de alterar o ponteiro que 
   aponta para a tabela corrente, como ficará claro na 
   próxima função. */
static void skip_table(void **ptr)
{
  unsigned char *p = *ptr;
  unsigned char len;

  len = ((struct dmi_hdr_s *)*ptr)->length;
  p += len;
  while (1)
  {
    while (*p++);

    if (*++p == 0)
      break;
  }

  *ptr = p;
}

A função que procura pela tabela das informações da BIOS fica então assim:

static struct dmi_hdr_s *find_bios_info_structure(void *ptr)
{
  /* De novo, por motivo de simplificação, 
     não codifiquei a verificação do número de tabelas
     checadas. Isso pode ser feito contando quantos 
     "pulos" demos contra o campo "table_length" da
     estrutura 'smbios_info_s'.

     Mas, toda BIOS tem informções sobre versão para o
     tipo 0. Assim, essa função não apresentará 
     problemas! */
  while (ptr->type != 0)
    skip_table((void *)&ptr);
  return ptr;
}

Tipicamente a estrutura de informações da BIOS contém três strings terminadas em ”. A primeira é a do fabricante (vendor), a segunda é a da versão (a que queremos!) e a terceira é a data de fabricação. Uma vez obtido o ponteiro para a estrutura ‘dmi_hdr_s’, podemos obter a string com a versão assim:

static char *get_bios_version(struct dmi_hdr_s *ptr)
{
  char *p;

  p = (char *)ptr + ptr->length;
  p += strlen(p) + 1; /* Pulamos a primeira string... */
  return strdup(p);   /* ... e copiamos a segunda! */
}

Nosso código para verificarmos a versão da ROM-BIOS fica, então, assim:

/* Macro usada para localizar o offset, com base 
   num endereço da ROM-BIOS. */
#define OFFSET_OF(x) ((x) - 0xE0000)

int is_virtual_rombios(void)
{
  int fd;
  void *memptr;
  struct smibios_info_s *smptr;
  struct dmi_hdr_s *dmiptr;
  char *version;
  int retvalue;

  if ((fd = open("/dev/mem", O_RDONLY)) == -1)
    return -1; /* -1 = ERRO */

  if ((memptr = mmap(NULL, BIOS_SIZE, 
                     PROT_READ, MAP_SHARED, 
                     fd, 0xE0000)) == MAP_FAILED)
  {
    close(fd);
    return -1;
  }

  if ((smptr = find_smbios_info_struct(memptr)) == NULL)
  {
    munmap(memptr, BIOS_SIZE);
    close(fd);
    return -1;
  }

  dmiptr = (struct dmi_hdr_s *)(memptr + 
              OFFSET_OF(smptr->table_address));
  dmiptr = find_bios_info_structure(dmiptr);

  version = get_bios_version(dmiptr);

  munmap(memptr, BIOS_SIZE);
  close(fd);

  retvalue = 1; /* 1 = Máquina REAL */
  if ((strncmp(version, "VirtualBox", 10) == 0) || 
      (strncasecmp(version, "VMWARE", 6) == 0))
    retvalue = 0; /* 0 = Máquina VIRTUAL */

  free(version);

  return retvalue;
}

Algumas considerações finais

Eu ainda não testei o código contra uma VM da VMWare e sequer contra o Xen e o KVM. Ainda, a função skip_table foi criada a partir de experimentações minhas com dumps das BIOS de minha máquina e do VirtualBox, ela pode ser um cadinho mais complicada se sua BIOS implementar essas tabelas de forma diferente do padrão atual (minha BIOS, por exemplo, parece ter um “tipo” que não está listado no padrão!).

A especificação Desktop Management Interface (DMI) pode ser encontrada aqui, com tudo o que você precisa saber sobre como lidar com a SMBIOS.

Um código mais detalhado (e menos amigável) que usa essa técnica pode ser observado no utilitário dmidecode, do pacote com mesmo nome, disponível em qualquer repositório de distribuições linux. Lá você lerá, por exemplo, que o mapeamento de página para acessar o endereço físico a partir de 0xE0000 é feito porque o uso de lseek e read nem sempre funcionam.

Ahhh. esqueci de mencionar: Infelizmente, para ler /dev/mem, você só pode fazer como root… Por isso é interessante checar:

if (getuid())
{
  fprintf(stderr, "ERROR: root privilege needed!\n");
  exit(EXIT_FAILURE);
}
Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s