Monday, July 10, 2017

Basicando o Assembly

Introdução ao Assembly

Fala galera, eu aqui de novo trazendo mais tópicos básicos de engenharia reversa, básicos por enquanto, viu ?! Quero trazer algo mais desafiador nos posts futuros, porém deve haver uma introdução primeiro. Desculpem qualquer bobeira que eu disser durante o post e se eu fizer algo por favor fiquem à vontade para me comunicar. Engenharia reversa só pode ser feita com conhecimento em Assembly, não tem outro jeito, ou aprendemos ou aprendemos.

Caso você não tenha lido o meu último post, eu recomendo. Eu tento abordar de uma forma mais dinâmica a computação. Então tudo no fim é assembly como você já deve saber, não importa que linguagem você utiliza ela vira assembly no fim ou no mínimo é interpretada através dela. De qualquer modo é bom saber assembly, fora que não é tão difícil entender assembly ainda mais se você já usou alguma linguagem funcional, como pascal por exemplo.

Assembly não é universal, diferentes assemblers compilam para assembly de diferentes formas e com base na arquitetura utilizada. Afinal alguns processadores interpretam as instruções de forma diferente, assim como é nas linguagens de alto nível. O que muda é a sintaxe, os conceitos permanecem.  Nas minhas postagens trarei a arquitetura IA-32 (que é mais comum pelo mundo afora). Lembrando que esse post não é uma aula completa, mas abordarei os conceitos para que possamos iniciar posts mais técnicos.

Como vimos no meu post passado, o computador trabalha com o conceito de Memória-Dados X Instruções. Ele armazena tudo na memória e depois interpreta os dados e instruções de acordo com a necessidade e a forma como comandamos ele. Se você passa determinado para a CPU dizendo que é pra ela executar, ela vai tentar executar. Mais pra frente veremos como isso acontece.

 
Computador / Memória, CPU, Dispositivos IO(Entrada-Saida) e BUS

O processador possui alguns ponteiros que são utilizados para ajudar a CPU durante toda a sua execução, uns apontam para as instruções e outros para os dados. Basicamente é o que eu já havia dito, faça determinada operação com os determinados dados. Então temos o ponteiro de instrução instruction pointer e o ponteiro de dados data pointer. Nós trabalharemos muito com eles nos posts futuros. O ponteiro de instrução como o nome diz, aponta para a memória que contém as instruções e o ponteiro de dados aponta para a memória que contém os dados, e com essas informações a CPU faz os procedimentos necessários.

 
Unidades da CPU X Memória

Não se assustem explicarei em mais detalhes. A CPU guarda os valores desses ponteiros em registradores (registers). Conforme o processador executa as instruções o ponteiro de instrução (instruction pointer) aponta para a próxima instrução, assim como o ponteiro de dados. Uma instrução possui entre 1 e 3 bytes e é chamada de opcode (operation code(código de operação)). Então basicamente o assembly possui 3 partes principais, opcodes (operações), data sections(seções de dados) e directives.

Opcode


Código mnemônico, é uma representação mais compreensível dos opcodes. É uma forma mais tranquila para se ler do que o código de máquina. No exemplo abaixo temos do lado esquerdo o código de máquina e do lado direito os mnemônicos. Temos por exemplo o opcode/instrução '89' que é o a instrução MOV. Diferentes "tipos" de assembly representam esses valores de formas diferentes.

 
OllyDbg / Direito Assembly, Esquerdo OpCodes

Data


As data sections são espaços utilizados para armazenar os dados que as instruções precisam buscar para executar. Então os dados podem estar em alguma seção da memória ou ele pode usar a stack (mais depois). Todos os dados são armazenados utilizando sua representação em hexadecimal, e são referenciados para serem utilizados por seus endereços em memória. Tudo que é armazenado na memória possui um endereço. Veremos que os dados ou endereços podem ser requisitados através de uma immediate constant que está explícito diretamente na linha de execução ou pode estar em alguma parte da memória, isso inclui o stack frame.


Directives


Directives são elementos utilizados em assembly para informar ao assembler (compilador do assembly) como esse determinado dado deve ser utilizado. Por exemplo se você precisa armazenar um número float o assembler precisa saber que tipo de dado é para armazená-lo de forma correta. Ou quando você quer utilizar um dado que está em um determinado endereço de memória, por exemplo. As directives criam seções na memória para armazenar os tipos de dados. Uma das directives mais importantes é a ".section" que cria seções em memória para os determinados tipos de dados.

Sections


Nós temos vários tipos de seções, podemos até criar outras se quisermos. Contudo as seções listadas abaixo são padrão:

  • .text:
    • Todos as instruções que o processador executará são armazenadas nessa seção. Dados não são permitidos aqui, a não ser os immediate constants que como eu disse antes são representados diretamente na linha de instrução. Como uma atribuição de valor a = 5, mas isso depende do programador assembly ou do compilador nas linguagens de alto-nível.
  • .data:
    • Esta seção é responsável por armazenar todos os dados que serão utilizados pela seção .text. Serão referenciados através de endereços de memória. Como program.ADDRESS, por exemplo.
  • .bss:
    • Esta seção é utilizada geralmente para dados não inicializados, que são armazenados durante a execução do programa. Acredito que o nome da seção mude de acordo com as linguagens.


Arquitetura IA-32


A arquitetura IA-32 foi desenvolvida para os processadores pentium pela Intel. Não tenho certeza se é realmente a mais utilizada atualmente, contudo é MUITO conhecida, possui bastante documentação e bem utilizada. Quando estamos aprendendo assembly a arquitetura é só a sintaxe de como escrever, para migrar para outras arquiteturas fica mais fácil depois que aprendemos uma bem, pois o conceito é sempre o mesmo o que muda é como escrever. Algumas arquiteturas possuem mais instruções ou instruções mais elaboradas e outras menos. A arquitetura IA-32 é dividida basicamente em 4 partes:

  • Control unit (Unidade de controle):
    • A unidade de controle é responsável por trazer da memória os dados e as instruções. Ela então traduz essas instruções em micro-operações e passa para unidade de execução. Após a execução o resultado retorna para a unidade de controle que armazena o resultado.
  • Execution unit (Unidade de execução):
    • Executa todas as micro-operações e retorna o resultado.
  • Registers (Registradores):
    • Os registradores são responsáveis por armazenar dados e endereços de memória. Esses registradores estão na memória interna do processador. O processador mantém os dados na memória interna para deixar a sua execução mais rápida, pois o processo de ir buscar na memória RAM do computador é muito mais lenta.
    • Para manter o post objetivo não escrevei extensamente sobre todos os registradores, contudo estou deixando as referências no final do post com um conteúdo mais acurado. Basicamente há 4 tipos de registradores (Tem mais, veja nas referências):
        • General Purposes (Propósitos gerais): 8 registradores de 32-bits. Como o próprio nome diz, são registradores gerais, podem armazenar dados e endereçõs de memória.
        • Segment (Segmentos): 6 registradores de 16-bits. Usados para acessos de memória.
        • Instruction Pointer (Ponteiro de instrução): 1 registrador de 32-bits. Aponta para a instrução que deve ser executada pelo processador.
        • Floating-point (Ponteiro de Float): 8 registradores de 80-bits. Para trabalhar com os números de ponto flutuantes.
  • Flags:
    • Flags são utilizadas para manter controle das operações executadas pelo processador. Através dela sabemos se as operações funcionaram ou não. Certas flags são marcadas de acordo com a operação executada. Veremos melhor sobre elas. 

Registers


General Purposes


Esse tipo de registradores são utilizados principalmente para trabalhar com os dados que as instruções/operações usam. Esses registradores possuem um tamanho de 32-bits, porém são subdivididos em 16-bits e 8-bits. 

  • EAX (32-bits):
    • AH (16-bits):
      • AL (8-bits)
  • EBX (32-bits):
    • BH (16-bits):
      • BL (8-bits)
  • ECX (32-bits):
    • CH (16-bits):
      • CL (8-bits)
  • EDX (32-bits):
    • DH (16-bits):
      • DL (8-bits)
  • EDI (32-bits):
    • DI (16-bits)
  • ESI (32-bits):
    • SI (16-bits)
  • EBP (32-bits):
    • BP (16-bits)
  • ESP (32-bits):
    • SP (16-bits)

OllyDbg / Registradores e Flags

Esses são registradores que trabalharemos na maior parte do tempo. É importante ressaltar que modificar um registrador da cadeia mais alta modifica também a cadeia mais baixa. Por exemplo se armazenarmos um valor em AL e então colocarmos outro valor em EAX o valor que havíamos colocado em AL foi modificado pelo novo valor colocado em EAX já que AL é EAX.

Alguns desses registradores são utilizados de forma padrão, como é o exemplo dos registradores EBP e ESP que são utilizados para controlar o stack frame. O stack frame é um bloco de memória utilizado para controlar o contexto de valores e dados de uma determinada function/method, por exemplo quando declaramos uma variável dentro de uma função, esse valor é armazenado na stack frame. Utilizamos a stack  também para passar valores via parâmetro na chamada de outras funções, utilizando a instrução push que armazena os valores na stack, veremos melhor na prática no decorrer dos posts.

Como eu sempre digo, é importante programar uma linguagem "baixo-nível" tipo C ou C++ que a curva de conversão para o assembly é menor, além do que você tem uma prática de manuseio diretamente com a memória. Com isso você tem uma visão melhor de como esse código fica em assembly. No decorrer das minhas postagens trarei bastante código C para analisarmos.

Segment


Antigamente podia-se escrever diretamente na memória física, pois o processador permitia esse tipo de ação, esse modo é chamado de real mode. Atualmente o real mode ainda funciona porém de forma limitada. O modo que é utilizado atualmente pelos sistemas operacionais (Windows NT em diante) é o protected mode com o conceito de paginação de memória, dessa forma fica explícito que em segmento de código não pode ser escrito nada, somente em segmento de dados. Pode-se também descrever blocos de memórias com determinados níveis de acesso. O assunto é extenso, vamos com calma ;p

Portanto os segmento de registros são utilizados para identificar onde os dados estão localizados em memória. Cada registro de segmento possui um ponteiro para a section (seção) onde ele irá pegar os dados necessários. Os segmentos são:

  • CS: Segmento de Código.
  • DS: Segmento de Dados
  • SS: Segmento da Stack
  • ES: Segmento Extra
  • FS: Segmento Extra
  • GS: Segmento Extra
Cada um deles é usado em casos específicos. Por exemplo, se você possui um endereço dentro do registrador EAX que é um endereço de memória dentro da seção de dados e você precisa guardar a informação que está em EBX, você verá algo como:

MOV dword ptr ds:[EAX], EBX

Com isso você está movendo a informação que EBX contém para dentro do endereço de memória que está localizado em EAX e dentro do segmento DS (segmento de dados), veja que você não está guardando dentro do registrador EAX, mas sim dentro do endereço que EAX aponta e na seção em que o segmento DS aponta.

 
OllyDbg / Immediate constant sendo movido para dentro da stack

Veremos muitos exemplos práticos. Que é o melhor meio de aprender!

Flags


As flags são mantidas em um único registrador chamado EFLAGS e cada flag é representada por um 1 bit dentro desse registrador. Como eu disse antes as flags são utilizadas para controlar o sucesso ou a falha das operações/instruções que o processador executa. Por exemplo os JUMPS (saltos) condicionais, eles usam algumas flags como referência para saber se o salto irá ser realizado ou não. Jump como o nome diz é um salto para algum endereço de memória, saltar para outro endereço de memória com outras instruções, ou seja continuar a execução a partir de outro endereço, outro ponto dentro do programa. Flags são divididas em 3 grupos:

  • Status Flags
  • Control Flags
  • System Flags

Vou falar basicamente sobre as status flags. Nas referências no fim do post tem bons livros com mais informações e de forma mais acurada, a ideia aqui é objetividade e macro-informação. As flags são um "sinal" ou status do resultado das operações matemáticas executadas pelo processador. As flags são :
  • CF: Carry Flag.
    • Carry flag é usado como suporte para as operações binárias salvando o carry ou o borrow. Utilizado para "carregar" o bit que sobra da operação. (Post introdutório de computação) Geralmente usado em operações unsigned.
  • PF: Parity Flag.
    • Quando a soma de 1s do valor resultante da operação é par.
  • AF: Adjust Flag.
    • Usada no Binary Coded Decimal (BCD) quando é um borrow ou carry. Como o próprio nome diz flag de ajuste do valor referente ao byte que representa a grandeza dentro do BCD. É um assunto complicadinho, indico leitura na referência (Richard Blum).
  • ZF: Zero Flag.
    • Usada quando o resultado de uma operação binária é 0.
  • SF: Sign Flag.
    • É usada em operações com números signed, é algo como números sinalizados (positivo ou negativo). Leia o post introdutório que tem um bom conteúdo lá. Esta flag é marcada quando a operação resulta em um número negativo.
  • OF: Overflow Flag.
    • Esta flag é utilizada em número signed quando a operação resulta em um número maior do que pode ser armazenada, ou seja deu erro. 

O assunto é complicadinho mesmo, mas com a prática a mais pesquisas chegaremos em um entendimento bacana. Qualquer dúvida estamos ai.

Stack


Stack é muito importante se tratando de engenharia reversa ela é utilizada o tempo todo durante a execução de qualquer programa. É utilizada para guardar dados de curto-prazo por assim dizer. Como eu disse antes, quando a execução entra dentro de uma função é criado uma nova stack frame  que seria um pedaço ou um bloco dentro da stack reservado para a função que está sendo executada. Stack geralmente é utilizada para:
  • Salvar os valores dos registradores:
    • Geralmente é utilizada para guardar valores de um registrador quando este mesmo registrador precisa ser utilizado em outra operação. Depois esse valor pode ser recuperado.
  • Alocação de variáveis locais:
    • Como já disse, suas variáveis locais ficam alocadas dentro da stack frame quando a execução entra em determinada função. E quando você precisa utilizar esses valores é usado o segmento que falamos antes, SS, stack segment.
  • Passar parâmetros para funções:
    • Para chamar determinada função geralmente é utilizado a instrução PUSH para enviar os valores para a stack e então chamar a função com a instrução CALL. Geralmente esses parâmetros são passados de forma reversa, da direta para a esquerda. f(p1, p2, p3) então passamos PUSH p3,p2,p1 e efetuamos a CALL.
  • Guarda o endereço de execução após a instrução CALL:
    • Quando o programa executa uma instrução CALL o curso de execução é alterado, então antes de uma CALL ser efetuada ela salva o endereço de execução que está na linha abaixo da CALL, pois quando a função termina de executar ela precisa retornar pro FLOW de executação na qual ela estava antes, isso acontece depois da instrução RETN ser executada.

Quando o programa entra em uma função as variáveis do escopo dessa função são armazenadas na stack. A stack nada mais é do que um pedaço de memória que o programa utiliza guardando os dados necessários. Para guardar os dados na stack utilizamos a instrução push e para resgatar os valores da stack utilizamos a intrução pop.

Toda vez que a execução do programa entra em uma função um stack frame é configurado. Stack frame nada mais é do que um bloco de dentro da stack. O stack frame é limitado pelos registradores ESP e EBP. ESP aponta para o topo da stack e o EBP para a base da stack. Vejamos um exemplo de código em assembly que realiza o stack frame:

PUSH EBP
MOV EBP,ESP
SUB ESP, SIZE

Primeiro o valor de EBP é salvo na própria stack para quando sairmos da execução da função o antigo stack frame possa ser restaurado. Então o valor que ESP está apontando agora é a nossa nova base do stack frame. Lembrando que o ESP é o topo da stack, então imagine que estamos colocando uma nova pasta de arquivos em cima de uma pasta de arquivos já existente. Quando a execução da função terminar imagina que retiramos essa pasta de arquivos que foi colocada em cima e sobra a pasta que já estava lá. Desta forma vemos que a stack funciona como LIFO (last in, first out) último que entra, é o primeiro que sai. Então por último é utilizado a instrução SUB que faz com que ESP aumente de tamanho, aumentando o tamanho do nosso stack frame.

Geralmente esse SUB é feito quando já se reserva o espaço da variável então somente é necessário acessar esse espaço dentro do stack frame diretamente usando nosso segmento de stack, SS, que vimos anteriormente. Teriamos algo parecido com isso:

mov EAX, PTR SS:[ESP+4]

Nessa última instrução o valor que está dentro de ESP somado com 4 bytes de posição, nos dá o endereço da variável desejada é movido para o registrador EAX. Nesse caso a variável é [ESP+4]. Se tivéssemos mais uma variável poderíamos ter outro endereço para ela como [ESP+8].

Uma das coisas que temos que entender é que a stack cresce para os endereços de memória menores, ou seja ela cresce para "baixo". Quando um programa começa sua execução a stack começa em seus maiores endereços e vai crescendo para os endereços menores. Vamos ao exemplo:

 
Funcionamento da stack

Heap


A Heap é uma área da memória onde os programas utilizam para alocação dinâmica de memória. O sistema operacional geralmente cuida dessa parte para os programas, então quando eles são iniciados e precisam de uma HEAP por alguma razão, o próprio sistema operacional cria este espaço em memória e entrega um ponteiro para esse espaço ao programa. Quando temos uma constante ou uma variável já iniciada como por exemplo:

char newText[] = "Testing how code storing data.";

O compilador já se encarrega de colocar o valor desta variável na seção de .data que é a seção onde ficam os dados inicializados do programa. Então dentro da execução do programa o compilador já coloca o endereço direto na linha da instrução que está sendo executada, pois o programa já sabe onde armazenou esta informação. Esse endereço podemos chamar de immediate constant dentro do programa ficaria algo como  program.ADDRESS, sendo ADDRESS o endereço da variável dentro da seção.

Contudo quando você vai carregar um arquivo externo o programa precisa alocar um espaço para esse arquivo e o tamanho que será necessário é dinâmico, então a HEAP será necessária. Se você abrir um arquivo de 5Kb então uma HEAP com o tamanho necessário será disponibilizada. Se você carregar um arquivo de 1Mb, então uma HEAP com o tamanho necessário será disponibilizada com o tamanho necessário. Heap nada mais é do que uma alocação dinâmica de memória.

Temos que ficar atentos quando se trata de HEAP em engenharia reversa, pois muitas informações importante são registradas nas HEAPs, veremos mais no decorrer dos posts e na prática como funciona.

Conclusão


Bom galera é isso! Quero pedir desculpa de antemão para qualquer erro ou mal-entendido durante a explicação. Estou tentando ser o mais objetivo e didático possível, contudo passar tudo isso em um post só é MUITO complicado, pois há muita informação. Acredito que com esse macro de informação irá facilitar e direcionar as pesquisas em cima do tema, pois sei que não da pra entender tudo só com esse post.

Qualquer dúvida em cima do tema é só me procurar, se eu puder ajudar ajudarei com maior prazer. E se eu não souber, pesquisaremos juntos. O intuito é só aprender. Quem sabe se juntar uma galera legal não montamos um grupo de estudo. Fica a dica. Qualquer coisa "tamo ai". :thumbs_up: 👍


Referências

  • Google
  • Eldad Eilam - Reversing: Secrets of Reverse Engineering
  • Reverse Engineering Code With IDA Pro
  • Professional Assembly Language - Richard Blum


No comments:

Post a Comment

Windows Objects

Objects in windows are referred as kernel objects . They provide a link or an way to use any objects functionality according with the object...