Seções iMasters
Desenvolvimento

Virtual Machine simples

Exemplo de código para este artigo pode ser encontrado aqui.

Em computação, Virtual Machine (VM) é uma implementação de software de uma plataforma de hardware existente ou fictícia. VMs são geralmente divididas em duas classes – sistema VM (VM que é capaz de executar um sistema operacional) e processo de VM (aquele que só pode executar um executável, por assim dizer). Enfim, se você estiver interessado apenas na definição do termo, é melhor você vir aqui.

Existem toneladas de artigos dedicados a esse assunto na Internet, centenas de tutoriais e explicações. Não vejo nenhuma razão para adicionar simplesmente outro artigo “trivial” ou tutorial para a coleção. Em vez disso, acho que poderia ser mais interessante vê-lo em ação, ter um exemplo de aplicação real. Alguém pode dizer que estamos rodeados por esses exemplos -. Java, .NET etc. Está correto, no entanto, eu gostaria de tocar em uma aplicação um pouco diferente dessa tecnologia – proteger o seu software/dados de serem hackeados.

Proteção de dados

Milhões de dólares estão sendo gastos por fornecedores de software (ou conteúdo) em uma tentativa de proteger os seus produtos de serem roubados ou utilizados de qualquer forma ilegal. Existem inúmeras ferramentas e utilidades de proteção, começando com packers/scramblers simples e terminando com pacotes complexos que implementam criptografia multinível e também virtual machines. No entanto, você pode até discordar, mas não vai me convencer, que uma solução out-of-the-box é boa até que ganhe popularidade. Há evidência suficiente para essa afirmação. Em minha opinião, ninguém pode proteger o seu software melhor que você. Só depende de quão protegido você quer que ele seja.

Embora existam vários métodos de proteção e técnicas, vamos concentrar em uma virtual machine para codificação /decodificação de dados. Nada de especial, apenas um método XOR trivial, mas, em minha opinião, suficiente para demonstrar os fundamentos.

Projete sua VM

Enquanto na vida real design de hardware precede sua contraparte de software, podemos fazer nós mesmos em ordem inversa (é a nossa própria VM, afinal). Portanto, vamos começar com o pseudo formato de arquivo executável que será apoiado por nossa VM.

Pseudo formato de arquivo executável

Bem, é uma boa ideia colocar um cabeçalho no início do arquivo. Para isso, temos que pensar no que nosso arquivo vai conter. O arquivo pode ser um código bruto (lembra-se de arquivos DOS COM?), porém não seria interessante o suficiente. Então, deixe o nosso arquivo ser dividido em três seções:

  • code section – esta seção deve conter código escrito em nossa pseudo linguagem assembly (vamos abordá-la um pouco mais tarde);
  • data seçtion – esta seção deve conter todos os dados necessários para o nosso pseudo executável (PE :-));
  • export section – esta seção deve conter referências a todos os elementos que queremos tornar visíveis para o núcleo do programa.

Vamos definir o cabeçalho como uma estrutura de C:

typedef struct _VM_HEADER
{
unsigned int version; /* Version of our VM. Will be 0x101 for now */
unsigned int codeOffset; /* File offset of the code section */
unsigned int codeSize; /* Size of the code section in bytes */
unsigned int dataOffset; /* File offset of the data section */
unsigned int dataSize; /* Size of the data section in bytes */
unsigned int exportOffset; /* File offset of the export section */
unsigned int exportSize; /* Size of the export section in bytes */
unsigned int requestedStack; /* Required size of stack in 4 bytes blocks */
unsigned int fileSize; /* Size of the whole file in bytes */
}VM_HEADER;

Bem, mais uma coisa. Na verdade, a mais importante. Precisamos de um compilador para o nosso pseudo assembly que seria capaz de fazer um output arquivos deste formato. Felizmente, não temos que escrever um (embora essa possa ser uma tarefa interessante). Tomasz Grysztar tem feito um trabalho maravilhoso com seu Assembler Flat. Apesar do fato de este compilador destinar-se a compilar código assembly Intel, graças ao apoio maravilhoso da instrução macro, podemos adotá-lo em nossas necessidades. O esqueleto do nosso arquivo ficaria assim:

include 'defs.asm' ;Definitions of our pseudo assembly instructions

org 0
; Header =======================
h_version dd 0x101
h_code dd _code
h_code_size dd _code_size
h_data dd _data
h_data_size dd _data_size
h_exp dd _export
h_exp_size dd _export_size
h_stack dd 0x40
h_size dd size

; Code =========================
_code:
_function:
;some pseudo code here
_code_size = $ - _code

; Data =========================
_data:

;some data here

_data_size = $ - _data

; Export =======================
_export:

;export table structures here

_export_size = $ - _export
size = $ - h_version

Simples assim!

A seção de exportação merece atenção especial. Eu tentei torná-la tão fácil de usar quanto possível. Ela é dividida em duas partes:

  1. Array de arquivos offset para exportar entradas terminadas em 0;
  2. Entradas de exportação: Arquivo offset da função/variável exportada (4 bytes); nome público do objeto exportado (NULL terminando em string ASCII).

No exemplo acima, a seção de exportação ficaria assim:

; Array of file offsets
dd _f1 ; Offset of '_f1' export entry
dd 0 ; Terminating 0
; List of export entries
_f1 dd _function ; File offset
db 'exported_function_name',0 ; Public name

Salve o arquivo como “algumacoisa.asm” ou qualquer nome que você preferir. Compile com Fasm.

Pseudo linguagem Assembly

Agora, quando terminamos com o formato de arquivo, temos que definir nossa pseudo linguagem assembly. Isso inclui tanto a definição de comandos quanto a codificação de instruções. Como essa VM é projetada para codificar/decodificar apenas mensagem de texto curta, não há necessidade de desenvolver uma escala completa de comandos. Tudo que precisamos é MOV, XOR, ADD, LOOP e RET.

Antes de começar a escrever macros que representariam esses comandos, temos que pensar sobre a codificação de instruções. Isso não vai ser difícil – não somos Intel. Para simplicidade, todas as nossas instruções serão de dois bytes seguidas por um ou mais argumentos imediatos, se houver algum. Isso nos permite codificar todas as informações necessárias, tais como opcode, tipo de argumentos, tamanho dos argumentos e direção de operação:

typedef struct _INSTRUCTION
{
unsigned short opCode:5; /* Opcode value */
unsigned short opType1:2; /* Type of the first operand if present */
unsigned short opType2:2; /* Type of the second operand if present */
unsigned short opSize:2; /* Size of the operand(s) */
unsigned short reg1:2; /* Index of the register used as first operand */
unsigned short reg2:2; /* Index of the register used as second operand */
unsigned short direction:1; /* Direction of the operation */

}INSTRUCTION;

Defina as seguintes constantes:

/* Operand types */

#define OP_REG 0 /* Register operand */
#define OP_IMM 1 /* Immediate operand */
#define OP_MEM 2 /* Memory reference */
#define OP_NONE 3 /* No operand (optional) */

/* Operand sizes */
#define _BYTE 0
#define _WORD 1
#define _DWORD 2

/* Operation direction */
#define DIR_LEFT 0
#define DIR_RIGHT 1

/* Instructions (OpCodes) */
#define MOV 1
#define MOVI 7
#define ADD 2
#define SUB 3
#define XOR 4
#define LOOP 5
#define RET 6

Parece que não há razão para colocar todos os macros que definem os nossos pseudo opcodes assembly aqui, pois seria um desperdício de espaço. Vou só colocar um aqui como um exemplo. Esta será a definição de instrução MOV:

Constantes a serem usadas com a nossa pseudo linguagem assembly

Macro definindo a instrução MOV

Como você pode ver no código acima, tenho sido preguiçoso de novo e decidi que seria mais fácil especificar implicitamente o tamanho dos argumentos, em vez de escrever código extra para identificar seus tamanhos automaticamente. Além disso, o nome da instrução diz o que a instrução específica destina-se a fazer. Por exemplo, mov_rm – move o valor da memória para registrar e letras ‘r‘ e ‘m‘ dizem que tipos de argumentos estão em uso (registradores, memory). Nesse caso, movendo WORD da memória para um registro ficaria assim:

mov_rm REG_A, address, _WORD

e toda a seção de código (atualmente contém apenas uma função) é representada pela imagem abaixo:

carrega o endereço de message como valor imediato no registo B; carrega o tamanho da mensagem com o endereço descrito por message_len no registro C; itera message_len vezes e aplica XOR a cada byte da mensagem. “mov_rmi” executa a mesma operação como “mov_rm“, mas o endereço está no registro especificado como segundo parâmetro.

É assim que a saída fica no IDA Pro:

Cabeçalho

Código

Dados e seções de exportação

Virtual Machine

Certo, agora, quando temos algum tipo de um “compilador”, podemos começar a trabalhar na VM em si. Primeiro de tudo, vamos definir uma estrutura, que representaria a nossa CPU virtual:

typedef struct _VCPU
{
unsigned int registers[4]; /* Four registers */
unsigned int *stackBase; /* Pointer to the allocated stack */
unsigned int *stackPtr; /* Pointer to the current position in stack */
unsigned int ip; /* Instruction pointer */
unsigned char *base; /* Pointer to the buffer where our pseudo
executable is loaded to */
}VCPU

registers – registradores de propósito geral. Não há necessidade de qualquer registro adicional nessa CPU da VM;

stackBase – ponteiro para o início da região alocada que usamos como empilhar para nossa VM;

stackPtr – este é o nosso stack pointer;

ip – ponteiro de instrução. Aponta para a próxima instrução a ser executada. Ele não pode apontar para fora do buffer que contém o nosso pseudo executável;

base – ponteiro para o buffer que contém o nosso executável. Você pode dizer que esta é a memória da nossa VM.

Além disso, você deve implementar pelo menos algumas funções para o seguinte:

  1. alocar/livre CPU virtual
  2. carregar pseudo executável na memória da VM e configurar a pilha
  3. uma função para recuperar um arquivo offset ou um ponteiro normal para um objeto exportado pelo pseudo executável
  4. uma função para definir o ponteiro de instrução (embora, isso possa ser feito por acessando diretamente o campo ip do CPU virtual
  5. uma função que iria executar o nosso pseudo-código.

No meu caso, a fonte final fica mais ou menos assim:


Decidi não citar o código da VM, já que você deve ser capaz de escrevê-lo sozinho caso o assunto for bastante interessante para você. Embora, o código neste artigo não contenha quaisquer verificações para valores de retorno corretos, você deve tomar cuidado deles.

Resumo

Embora, este artigo descreva uma virtual machine trivial virtual que só é capaz de codificar/decodificar um buffer de tamanho fixo, o conceito em si pode atendê-lo bem em software/proteção de dados como hackear VM é várias vezes mais difícil que quebrar o código nativo.

Só mais uma coisa. Nosso projeto nos permite chamar procedimentos estabelecidos pelo pseudo executável, mas existem várias maneiras de permitir o mesmo de “falar conosco”. O mais simples (como parece para mim) é a implementação de interrupções.

Eu espero ter abordado tudo.

P.S. O resultado codificado seria  “V {rrq2> Iqlrz?“.

Vejo vocês no próximo artigo!

?

Texto original disponível em http://syprog.blogspot.com.br/2011/12/simple-virtual-machine.html

Comente também

1 Comentário

Qual a sua opinião?