Showing posts with label reversa. Show all posts
Showing posts with label reversa. Show all posts

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


Sunday, June 4, 2017

Engenharia Reversa - Ponto de vista didático ?

Olá para todos!

Bom, estou iniciando esse blog para gerar conteúdo à toda a comunidade e como forma de estudo também. Como estou iniciando o blog achei melhor trazer um texto introdutório e a ideia é ir elevando o nível com o passar dos textos. Não é nenhuma super revelação esse post, porém acho importante dissertar sobre. Meu único interesse é gerar informação para todos, quero continuar trazendo conteúdo didático e objetivo para aprendizado. Tentarei traduzir meus posts e postá-los em português e inglês.

Caso tenham recebido malwares por e-mail e quiserem compartilhá-los comigo. Podem me enviar, ficarei agradecido. pimptechh@gmail.com.

Engenharia reversa é um assunto bem polêmico, pois envolve muitas questões legais e por esta razão é um tema que deve ser tratado com muito cuidado. No caso estarei utilizando o conceito para software de computador, visto que é um termo bem abrangente e pode ser referido para qualquer tipo de artefato fruto de engenharia. Basicamente se trata da desconstrução de algo que já está pronto. Muitos utilizam deste conhecimento para quebrar software, muitas vezes citados pelo termo "Cracker" que remete a "Crack" (quebrar) unindo o "er" no final ficaria algo como "Quebrador" ou o usuário que irá quebrar o software. Contudo é um processo muito importante hoje em dia na indústria, nos quesitos de segurança de software, manutenção de software, performance de software e etc. É uma visão de como funciona para que então possamos melhorar ainda mais o software em si.

Em engenharia reversa é fundamental que se tenha curiosidade no geral, pois é basicamente dai que nasce a ideia de reverter algo. "Ora, mas como será que isso foi feito ?" ou "Ora, por que será que isso está acontecendo ?". Um simples debugar de um programa com o código fonte em mãos já se encaixa no processo, a fim de descobrir o motivo de determinado valor ou bug, por exemplo. Portanto para entender um processo reverso devemos primeiramente ter uma base de como o processo foi desenvolvido em primeiro lugar. Se há a necessidade de se reverter um software é necessário que saiba ao menos como desenvolver um software. Entender o funcionamento de um software é imprescindível, pois antes de jogar nas ferramentas que auxiliam o processo temos de ter no mínimo uma base. A base é sempre importante, tentarei dissertar da melhor forma possível para tentar passar a importância da base pelos meus estudos.

Tudo começou, bem não sei exatamente onde tudo começou (ainda estou descobrindo), mas sabemos do "primeiro" grande invento ou algo aproximado do modelo de computadores que temos hoje, o ENIAC. Esse projeto já possuía o dedo de Neumann(criador do modelo atual de computadores), ajudando a resolver alguns problemas lógico matemáticos. ENIAC basicamente fazia cálculos aritméticos complexos, porém não possuía o conceito de memória e programa. Depois do ENIAC nasceu o EDVAC um pré computador moderno, com conceito de memória e programa.

O computador moderno "começou" com John von Neumann, uma mente brilhante com um memória fantástica. Recitava capítulos inteiros de livros usando exatamente o que estava escrito sem trocar nem acrescentar. Capaz até de recitar livros em alemão traduzindo simultâneamente. Em 1927 e 1928 publicou vários "papers" sobre fundamentos matemáticos da teoria quântica e probabilidade na estatística quântica, então se você não acredita em mecânica quântica ou duvida, desliga seu pc e joga fora hahaha.. Nesses "papers" ele demonstra o conhecimento nos fenômenos físicos. A abstração vem desses problemas extremamente complexos que por sinal Von Neumann dominava. Se ele não era um cara a frente do tempo, então não sei. A história é bem interessante, vale a pena ler. Tem um livro chamado "The computer and the brain" em que ele tenta explicar os conceitos e palpites que teve durante os testes com o modelo projetado principalmente por ele, nesse livro ele tentar fazer uma analogia entre o nosso sistema nervoso de um ponto matemático, utilizando conceitos de lógica e estatística. Corpo humano, máquina biológica.

Quando pensamos na mágica que o computador faz, isso o deixa ainda mais bonito haha. Armazenar energia ao mesmo passo que armazena informação, e com essa informação resolver problemas frutos do intelecto. O computador teve sua necessidade para ajudar a resolver os problemas de forma muito mais rápida. Fazendo cálculos complexos em uma velocidade difícil de imaginar. Basicamente você insere um "input" esse valor é armazenado em memória na forma de energia/tensão, essa tensão é diferenciada entre 0's e 1's. Isso tudo armazenado dentro do seu computador, e com auxilio da lógica booliana temos nosso "output", seja ele qual for, depende do tipo de problema que você quer resolver. Quando digo isso, é necessária abstração do que você encara como um problema. Não sei como era a conversa de Von Neumann e seus amigos matemáticos, mas a conversa deveria ser bem maluca ;p. 

Nós resolvemos e criamos muitos problemas todos os dias pelo simples fato de se ter um monte de energia estocada dentro do seu computador. Um dos conceitos de programação está aqui, a abstração, quanto mais no topo você está, mais você abstrai a computação. Quando você precisa falar com alguém, você chama este alguém no seu IM de escolha e resolver o problema(falar com este alguém), apenas um exemplo. Não perceber isso dificulta a quebra de paradigma. Contas de matemática (2+2). O que você fez ? Você olhou o 2 entendeu que é um 2, armazenou essa informação, adicionou mais 2 e então operou de forma lógica "+", obtendo o 4. Isso pode ser abstraído de por exemplo, olha tenho 2 maças, se como uma sobra só mais 1. Então você observa, estoca informação e processa, exatamente igual o computador. Agora como fazer isso tudo virar informação ? Subindo as camadas de representação. Temos nesse caso 0 e 1 armazenados em forma de energia no nosso computador podendo representar informações como ligado e desligado, cheio e vazio, e etc. Quando formamos blocos destes bits/valores temos um acervo maior de possibilidades. Como números, letras e etc.

Todas as informações estão em 0 e 1, juntando todos os 0's e 1's em blocos temos as informações. Tudo depende da maneira como você deseja interpretar esta informação. Portando por serem 2 valores possíveis podemos então deduzir que é uma representação de base 2, ou seja 2 possíveis valores. Não sou super fã de matemática, porém sei apreciar a beleza. Enfim, como podemos abstrair essas informações de conjuntos de 0 e 1 ? Vamos começar pelos decimais base 10. Todo mundo, acredito eu, viu na escola sobre grandezas e formas de representar essas grandezas, por exemplo 10, 100, 1000 e assim sucessivamente. Você implicitamente entende essas representações como dez, cem e mil, e consegue discernir isso pois adicionamos um 0 no fim de cada grandeza, desta forma elevando-as. Adicionando os dígitos decimais nós podemos contar (0,1,2,3... 10,11,12...), a cada quebra de grandezas temos uma nova representação.

O mesmo se dá com binário, porém alternando o 1 e o 0 de lugar. Na imagem abaixo é possível ver uma contagem com binários:



Podemos representar ou subir para uma representação mais amigável(numérica) convertendo a base 2 para a base 10 (a qual estamos acostumados) como pode ser demonstrado a seguir:



Agora, como é possível fazer operações com formas tão primitivas de representação ? Aqui entra o que citei lá em cima, lógica booliana. O que tem dentro da sua CPU são microcircuitos de lógica cada vez mais complexos. Temos o que eu acredito ser o menor microcircuito Half-Adder:





Nesse circuito correm dois inputs (A e B) de tensão, "A" pode ser tanto 0 quanto 1, o mesmo se da para o "B". Agora como funciona esse microcircuito ? Dentro do microcircuito existem os chamados portões lógicos como OU (OR), OU Exclusivo (XOR), E (AND), Negação (NOT) e etc. Esse circuito tem 2 outputs, soma (sum) e o valor carregado (carry), como vimos na imagem anterior. Com isso podemos adicionar 2 bits, assim como fazemos com 2 números. Pegue o exemplo de (5+5=10), tenha como sum o valor 0 e o carry o valor 1. O mesmo se dá com a soma de bits, por exemplo com o nosso Half-Adder podemos somar dois bits. 1+1.



Com isso criamos outro problema, sabemos que a representação do número 2 é "10" em binário, certo ? O Half-Adder fez o trabalho dele, agora temos que dar conta desse carry ai, por esta razão veio o Full-Adder (imagem abaixo), que por sua vez recebe 3 inputs, A, B e o carry. Percebem a abstração dos problemas ? Dentro do Full-Adder temos um Half-Adder. Nossa querida CPU tem muitas camadas de abstração extremamente complexas são muitas e muitas horas de estudo aprofundado, contudo é bom saber a base pelo menos, certo ?. Sugiro quem tiver interesse, a pesquisar por organização lógica computacional. Existem vários microcircuitos embutidos um dentro do outro e a CPU em si é um arranjo de circuitos extremamente complexos. 



Certo, agora temos nossos 0's e 1's representados como dados em blocos e sendo "operados" pelos seus respectivos operadores lógicos, fazendo nossa máquina gerar nossos outputs. Contudo mesmo com as conversões de dados para decimal e posteriormente hexadecimal, que veremos mais abaixo, temos que entender que primeiramente o computador foi necessário para realizar cálculos matemáticos complexos isso inclui números com domínio nos REAIS, certo ? Cálculos complexos necessitam de números "complexos", não vou me aprofundar muito até porque não saberia explicar com prioridade. Temos dois importantes tópicos para serem discutidos antes de avançarmos para a conversão de hexadecimal que é a principal forma de representação de dados que iremos trabalhar. Houve a necessidade de representar os números negativos para realizar as operações, para isso utilizaram o sistema de "Signed" (Que também é um assunto bem discutido quando falamos da linguagem C junto com "Unsigned", vamos chegar lá!). Isto significou utilizar dentre os 8bits de dados o bit mais significativo ou o primeiro bit (0)0000000 para representar "+" e "-", sobrando então 7bits para representar o valor. Desta forma não podemos representar mais de 0 à 255, pois o bit mais significante (msb=most significant bit) está sendo utilizado para representar se ele é positivo ou negativo. De forma que agora podemos representar entre -127 à +127, pois o primeiro bit (msb) foi "cortado" para representar o sinal do número. Contanto com essa forma de representação os nossos cálculos com binários ficaram falhos, certo ? Vamos ao seguinte exemplo (+2) + (-3), deveria dar -1, correto ? Vejamos o que acontece:



Como podemos ver ao somar dois números, sendo um negativo e o outro positivo temos um resultado errado. Por esta razão foi implementado o "Complemento" (Complement). Em sua primeira "versão" chamado de "One's complement", que nada mais é do que a inversão do número signed, por exemplo o número "00000010" (2) ao receber o complemento fica "11111101" (2), mas mesmo utilizando esse conceito o resultado sai como -1 do valor correto. Vamos ao exemplo 4 + (-2):



O resultado da operação resultou em "00000001" que em decimal é 1. Lembrando que nesse sistema o último carry é sempre descartado, como podem ver na tarceira casa da direita para a esquerda nossa operação de adição gera um carry que é carrregado para as operações restantes e que no final é descartado por sair do bloco de 8bits. Então para mostrar corretamente o valor da operação é necessário adicionar +1 ao resultado final. Por esta razão o "Two's complement" veio para resolver este problema. Vejamos no exemplo:



Basicamente o "Two's complement" adicionar + 1 ao final da operação resolvendo o problema com a falha do "One's complement".
Contudo ainda sim temos um pequeno problema, mas fácil de ser resolvido. Imaginando que o resultado da operação seja negativo, ao realizá-la nos deparamos com o seguinte problema 4 + (-5):



Como vemos na imagem acima o valor fica "errado" quando tentamos utilizar a soma com o número negativo maior que o número positivo, então quando isso acontece é necessário refazer o complemento do número no resultado da operação, ou seja fazendo um flip de 0 para 1 ou 1 para 0 em cada bit, adicionando (+1) ao resultado deste flip e trazendo o carry restante da operação para o novo número de forma que ele possa ser representado em forma de signed "10000001" (-1). A CPU utiliza de uma das flags, mais precisamente a flag (S)ign Flag para fazer esta operação, veremos isso em outro texto

Bom, depois de toda essa complicação hehe, vamos seguir com a conversão para base 10 que é a forma numérica mais próxima do entendimento humano. Contudo não paramos por aqui, pois em engenharia reversa utilizamos base 16 para representação dos dados, então ao interagir com dados em memória eles estarão em hexadecimal, até quando eu não sei =). Mas antes vamos entender como é a rotulação desses blocos de binário, pois é importante saber. Cada bloco/conjunto de binários possui um nome ou tamanho, que é um parâmetro para saber a quantidade de informação que aquele bloco de binários possui.

4 bits = 1 Nibble
8 bits = 1 Byte 
16 bits = WORD 
32 bits = DWORD
64 bits = QWORD

Com a introdução do hexadecimal podemos encapsular uma maior quantidade de dados dentro de um único dígito. Por exemplo, o bloco "1111" em decimal é "15" em hexadecimal é "F", portanto podemos representar 4bits com apenas 1 dígito de hex e com 2 dígitos podemos representar 8bits de dados. Hexadecimais tem uma tabela bem tranquila de se entender:



Existem diversos sites que fazem a conversão de hex para decimal e string, seguem alguns exemplos:

http://string-functions.com/hex-string.aspx
http://string-functions.com/hex-decimal.aspx

Para fazer tudo isso funcionar precisamos estocar tudo isso e então trabalhar com essas informações, a forma como utilizamos hoje para estocar essas informações são as memórias RAM. Memórias RAM (Random Access Memory) são células que armazenam essa informação, assim como todos sabem ela é volátil, pois ao cortar a energia os dados que são a energia, são perdidos. A memória RAM precisa ser "revisada" por assim dizer, em ciclos, para checar quais os valores que estão estocados nas células e garantir que eles continuem lá. E quem estoca e admnistra essa informação é o cérebro, também conhecido como CPU. É necessário garantir que os valores que foram estocados não "descarreguem".

Bom, agora como essas informaçõs são trafegádas ? Através dos BUS de dados. Quando vemos "BUS de dados" é necessário uma abstração, imaginar um canal, um meio pelo qual todas as informações estão sendo transmitidas e recebidas entre os componentes do nosso computador. Todos nossos 0's e 1's transitam por esses BUS de dados, esse canal de informação. Ai temos um novo problema, como eu vou comandar minha CPU estocar e manipular essa informação estocada ? Como ele vai saber onde buscar e para onde levar essa informação ? Qual algoritmo utilizamos para resolver esse problema ? Multiplexer e Demultiplexer, são os circuitos que cuidam desse "transporte" ou rotulação de endereços de origem e destino e virse versa, recomendo ler ler um pouco melhor sobre eles. Nas referências listadas abaixo tem um bom conteúdo.

Até agora "sabemos" como o computador interage e como opera as informações que nele são estocadas, contudo gostaria de lembrar que é muito básico o que eu estou escrevendo aqui, é a base. Nosso atual computador é MUITO complexo com muito mais componentes, porém a base é a mesma, os bits são os mesmos, o estoque de informação é o mesmo, a forma de operar a informação ainda é a mesma. A base ainda reside no estoque de informação e operação lógica dessas informações. Eu vejo esse texto como uma forma de pensar sobre computação. Sabendo a base o resto fica por conta da imaginação. Agora como fazer para interagir com essa máquina de complexas operações ? Temos então o SO (Sistema Operacional), o sistema que facilita operarmos a máquina. Em engenharia reversa devemos ter isso muito focado em nossa mente, tudo passa pelo SO. Então uma operação específica tem um certo padrão, o que quero dizer é que todas as funções que um software desempenha é o SO quem garante isso, "abstraindo" a conexão direto com o hardware, com a linguagem de máquina.

O sistema operacional conta muito na hora de se reverter algo, ele encapsula/abstrai os métodos necessários para a interação com a máquina. Entender a forma como ele trabalha é um trabalho árduo que requer tempo de estudo, e acima disso, experiência e prática. É bom deixar isso frizado na mente, leva-se tempo para ir digerindo tudo, por isso é importante estar sempre pensando sobre o funcionamento. Se formos voltar no tempo e analisar o histórico dos SO's para o atual é uma mudança enorme, muitos paradigmas quebrados com muitas novas tecnologias implementadas. Conforme nossos problemas ficam mais complexos necessitamos de mais camadas de abstração, por isso temos linguagens de programação de alto nível que encapsulam funcionalidades para agilizar a solução dos problemas, subindo cada vez mais camadas para facilitar as nossas vidas de operar máquinas computacionais. 

Para se comandar uma máquina computacional é necessário uma linguagem que instruam comandos para a máquina, e como sabemos, a linguagem que de fato interage com o computador é a linguagem de máquina, com seus operadores e operandos. Tudo, no fim das contas é linguagem de máquina. É só o que o seu computador entende, você pode usar qualquer linguagem que no fim você está utilizando linguagem de máquina para operar sua máquina. O que temos hoje são linguagem de nível maior, ou seja, que podemos ter um contato mais "humano" mais real do nosso dia-a-dia. Linguagens como Java e C# são encapsulamento das linguagens de baixo nível.

Hoje temos essas linguagens que eu citei que acredito serem as "mais" utilizadas, pois são muito mais rápidas na solução de problemas, porém não necessariamente de performance. Pois quão mais alto você estiver nas camadas de abstração/encapsulamento, mais tecnologia você tem abaixo que resolvem muitos problemas para acelerar nossas soluções, porém tudo em computação é suscetível ao erro. Então quando falamos de linguagens de baixo nível queremos dizer que são as linguagens mais próximas da máquina. Esse é o modelo atual das linguagens:



É importante salientar que assembly é linguagem de máquina, o que ocorre é uma tradução da linguagem de máquina para mnemônicos. Chamamos esses mnemônicos de assembly. Esse fluxo da linguagens aplicam-se apenas para as linguagens compiladas como C/C++. Java e C# (entre outras) não rodam diretamente pelo computador, eles tem o intermédio da suas respectivas máquinas virtuais. Java possui a JVM (Java virtual machine) e o C# o CLR (Common language runtime), essas máquinas virtuais por sua vez é que interagem com o SO e o computador em si. Quando "compiladas" ambas geram o que chamamos de bytecode que é a "linguagem de máquina" mais alto nível, elas interagem com suas respectivas máquinas virtuais.

Em engenharia reversa temos de ter consciência da base, pois a linguagem mais próxima da leitura humana e ao mesmo tempo mais próxima da máquina é o assembly. Ele lida diretamente com a memória e operações, contudo como eu disse antes, com intermédio do SO, então mesmo em assembly temos a total interação com os métodos que o SO abstraiu/encapsulou para que possamos interagir com a máquina. Então no fim tudo é linguagem de máquina e "antes" disso é assembly. Podemos dizer também que antes de assembly temos as linguagens C e C++, sendo que C++ possui conceitos mais modernos, como programção OOP. Se eu fosse dar uma dica seria estudar C, é a melhor forma de você estar perto da máquina realmente. É o mais próximo para se trabalhar com a manipulação de memória, precisando entender os conceitos de memória e performance para se programar utilizando ela. Você resolve seus problemas estando mais próximo da camada de hardware. Abaixo segue uma imagem que mostra de forma abstrata as camadas entre Hardware e SO:



Conclusão e opinão

Bom, esse primeiro texto quis abordar apenas um pouco da parte bem introdutória da ER/computação que por sinal é muita mágica, pois imaginar energia se transformando em informação é realmente muito "dahora". Quero continuar atualizando este blog tanto para me ajudar quanto para ajudar a comunidade, acredito que talvez ajude alguém :). A minha ideia é continuar escrevendo textos tentando ser o mais objetivo e didático possível. Pretendo trazer conteúdo mais técnico nos próximos textos como programação assembly/c, análise de malware, resolução de softwares/puzzles e etc. Sintam-se à vontade para questionar ou apontar algum erro/falha. O intuito aqui é somente conhecimento e crescimento.

Grande abraço e bons estudos!

Referências:
  • Google
  • Andrew S. Tanenbaum - Structured Computer Organization
  • Eldad Eilam - Reversing: Secrets of Reverse Engineering
  • Richard Blum - Professional Assembly Language (2005)

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...