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:
- Andrew S. Tanenbaum - Structured Computer Organization
- Eldad Eilam - Reversing: Secrets of Reverse Engineering
- Richard Blum - Professional Assembly Language (2005)
Esse texto foi muito foda! curti mesmo.
ReplyDeleteparabéns =)