Se o computador só entende números, como podemos trabalhar com texto então? Bem, não se engane, o computador realmente só entende números. O fato de você apertar uma tecla no teclado que tem o desenho de um símbolo do alfabeto utilizado no seu país não garante que é isto que de fato seja enviado para o computador e, certamente, não é. Ao invés disso, cada tecla possui um código conhecido como scan code ou make code que é enviado, entre outras informações, pelo fio teclado para a placa-mãe do computador e passa por vários estágios até chegar ao kernel, o núcleo do sistema operacional. Se sua intenção for escrever o texto "a" num editor de textos (no Bloco de Notas por exemplo), então uma tabela de conversão entra em jogo. Essa tabela vai armazenar no arquivo de texto que você está criando um ou mais números que são equivalentes ao caractere "a". Ou seja, o texto "a", na prática, não existe.
Assim como na entrada de dados pelo teclado, o tratamento da entrada do mouse ou de qualquer outro dispositivo de entrada também é numérico e, de maneira geral, o computador necessita entender que ao ler um determinado número, precisa tomar alguma ação, como desenhar o que conhecemos por caractere "a" num editor de textos. Para ele é um número, para nós, um símbolo, que é parte de um texto. Veremos nas seções a seguir como essa conversão se dá.
Na linguagem C, foi criado um padrão para saber programaticamente o fim de uma string: ela precisa ser terminada com um byte nulo, também chamado de nullbyte. Este nada mais é que um byte zerado. Sendo assim, a string ASCII "fernando", se utilizada num programa escrito em C, fica no binário compilado (no .exe gerado pelo processo de compilação, por exemplo) da seguinte forma:
É importante não confundir o nullbyte com o caractere de nova linha. Este pode ser o Line Feed (0x0a), também conhecido por newline. Já no DOS/Windows, a nova linha é definida por um par de caracteres: Carriage Return (0x0d) seguido de Line Feed, sequência conhecida por CrLf em linguagens como Visual Basic.
Se a string for UTF-16, então dois bytes nulos serão adicionados ao fim. Se for UTF-32, quatro.
Talvez você se pergunte qual a razão pela qual este conceito é útil em engenhria reversa. Bem, no caso de busca de strings num binário compilado, você pode refinar a busca por sequências de bytes na tabela ASCII, usando ou não uma codificação do padrão Unicode (já que os valores ASCII são os mesmos) terminados com por um ou mais nullbytes. Por exemplo, supondo que você esteja buscado a string "Erro" dentro de um programa, o primeiro passo é descobrir quais são os bytes equivalentes na tabela ASCII desta string. Ao invés de usar a tabela, você pode usar o Python:
Agora sabemos que a sequência de bytes a ser procurada vai depender do tipo de string. Se for UTF-16-LE (Little Endian, que é o padrão no Windows), podemos montar a string sem nem precisar do Python, bastando para isso colocar os zeros depois de cada byte:
45 00 72 00 72 00 6f 00
Mas para sermos mais assertivos, caso não haja mais nada depois do "o" da palavra "Erro" no programa, podemos adicionar o nullbyte na busca:
Em ASCII:
45 72 72 6f 00
Em UTF-16-LE:
45 00 72 00 72 00 6f 00 00 00
Claro que os programas feitos para buscarem texto dentro de arquivos já possuem esta inteligência, no entanto, a proposta deste livro é entender a fundo como a engenharia reversa funciona e por isso não poderíamos deixar de cobrir este importante assunto.
A esta altura você já pode imaginar a dificuldade que programadores enfrentam em trabalhar com diferentes codificações de texto, mas existe um esforço chamado de Unicode mantido pelo Unicode Consortium que compreende várias codificações, que estudaremos a seguir. Essas strings são comumente chamadas de wide strings (largas, numa tradução livre).
O padrão UTF (Unicode Transformation Format) de 8 bits foi desenhado originalmente por Ken Thompson (sim, o criador do Unix!) e Rob Pike para abranger todos os caracteres possíveis nos vários idiomas deste planeta.
Os primeiros 128 caracteres da tabela UTF-8 possuem exatamente os mesmos valores da tabela ASCII padrão e somente necessitam de 1 byte para serem representados. Chamamos estes números de codepoints. Os próximos caracteres utilizam 2 bytes e compreendem não só o alfabeto latino (como na ASCII estendida com codificação ISO-8859-1) mas também os caracteres gregos, árabes, hebraicos, dentre outros. Já para representar os caracteres de idiomas como o chinês e japonês, 3 bytes são necessários. Por fim, há os caracteres de antigos manuscritos, símbolos matemáticos e até emojis, que utilizam 4 bytes.
Concluímos que os caracteres UTF-8 variam de 1 a 4 bytes. Sendo assim, como ficaria o texto "mentebinária" numa sequência de bytes? Podemos ver novamente com o Python, mas dessa vez ao invés declarar um objeto do tipo bytes
com aquele prefixo b
, vamos converter um tipo str
para bytes
utilizando a função encode()
. Isso é necessário porque queremos ver uma string UTF-8 e não ASCII:
Como dito antes, os codepoints da tabela ASCII são os mesmos em UTF-8, mas o caractere 'á' (que não existe em ASCII puro) utiliza 2 bytes (no caso, C3 A1) para ser representado. Esta é uma string UTF-8 válida. Dizemos que seu tamanho é 11 porque ela contém 11 caracteres, mas em bytes seu tamanho é 12.
Também conhecido por UCS-2, este tipo de codificação é frequentemente encontrado em programas compilados para Windows, incluindo os escritos em .NET. É de extrema importância que você o conheça bem.
Representados em UTF-16, os caracteres da tabela ASCII possuem 2 bytes de tamanho onde o primeiro byte é o mesmo da tabela ASCII e o segundo é um zero. Por exemplo, para se escrever "A" em UTF-16, faríamos: 41 00. Vamos entender melhor com a ajuda do Python.
Primeiro, exibimos os bytes em hexa equivalentes de cada caractere da string:
Até aí, nenhuma novidade, mas vamos ver como essa string seria codificada em UTF-16:
Duas coisas aconteceram nesta conversão: a primeira é que uma sequência de dois bytes, FF FE, foi colocada no início da string. Esta sequência é chamada de Byte Order Mark (BOM) ou Marca de Ordem de Byte, em português. A segunda coisa é que os bytes foram sucedidos por zeros. De fato, a BOM diz se os bytes da string serão sucedidos (FF FE) ou precedidos (FE FF) por zeros quando necessário, mas também é possível utilizar uma variação da codificação UTF-16 que é a UTF-16-LE (Little Endian), onde os bytes são sucedidos por zeros, mas não há o uso de BOM:
A codificação UTF-16-LE (lembre-se: sem BOM) é a utilizada pelo Visual Studio no Windows quando tipos WCHAR
são usados, como nos argumentos das funções MessageBoxW()
e CreateFileW()
. Também é a codificação padrão para programas em .NET. Isso é importante de saber pois se você precisar alterar uma string UTF-16-LE durante a engenharia reversa, vai ter que respeitar essas regras.
Além da UTF-16-LE, temos a UTF-16-BE (Big Endian), onde os bytes são precedidos com zeros:
Além disso, é importante ressaltar que em strings UTF-16 também há a possibilidade de caracteres de quatro bytes. Por exemplo, um emoji:
Os números (codepoints) utilizados pela ISO-8859-1 para seus caracteres são também os números utilizados em strings UTF-16. No Windows, o padrão é o UTF-16-LE. Para entender como isso funciona, observe primeiro os bytes da string "binária" na codificação ISO-8859-1:
Perceba que o byte referente ao "á" é o E1. Até aí nenhuma novidadade. Sabemos que é uma string ASCII estendida que usa a tabela ISO-8859-1, também conhecida por Latin-1. Agora, vejamos como ela fica em UTF-16-LE:
Nesse caso, "binária" é uma string UTF-16-LE (cada caractere sucedido por zeros), sem BOM. Os bytes dos caracteres em si coincidem com os da ISO-8859-1. Doido né? Mas vamos em frente!
Perceba que o "á" em UTF-8 é C3 A1, mas em UTF-16 é E1 (precedido ou sucedido por zero), assim como em ISO-8859-1.
Sendo pouco utilizado, este padrão utiliza 4 bytes para cada caractere. Vamos ver como fica a string "mb" em UTF-32 com BOM:
Perceba o BOM de quatro bytes ao invés de dois.
Agora em UTF-32-LE:
E por fim em UTF-32-BE:
É importante ressaltar que simplesmente dizer que uma string é Unicode não diz exatamente qual codificação ela está utilizando, fato que normalmente depende do sistema operacional, da pessoa que programou, do compilador, etc. Por exemplo, um programa feito em C no Windows e compilado com Visual Studio tem as wide strings em UTF-16 normalmente. Já no Linux, o tamanho do tipo wchar_t é de 32 bits, resultando em strings UTF-32.
Há muito mais sobre codificação de texto para ser dito, mas isso foge ao escopo deste livro. Se o leitor desejar se aprofundar, basta consultar a documentação oficial dos grupos que especificam estes padrões. No entanto, cabe ressaltar que para a engenharia reversa, a prática de compilar programas e buscar como as strings são codificadas é a melhor escola.
American Standard Code for Information Interchange
Computadores trabalham com números, mas humanos trabalham também com texto. Sendo assim, houve a necessidade de criar um padrão de representação textual - e também de controle, que você entenderá a seguir.
O American Standard Code for Information Interchange, ou em português, Código Padrão Americano para o Intercâmbio de Informação, é uma codificação criada nos EUA (como o nome sugere), já que o berço da computação foi basicamente lá.
Na época em que foi definido, lá pela década de 60, foi logo usado em equipamentos de telecomunicações e também nos computadores. Basicamente é uma tabela que relaciona um número de 7 bits com sinais de controle e caracteres imprimíveis. Por exemplo, o número 97 (0b1100001) representa o caractere 'a', enquanto 98 (0b1100010) é o 'b'. Perceba que tais números não excedem os 7 bits, mas como em computação falamos quase sempre em bytes, então acaba que um caractere ASCII possui 8 bits mas só usa 7. A tabela ASCII vai de 0 a 127 e pode ser encontrada no apêndice Tabela ASCII, que você deve consultar agora.
Há vários testes interessantes que você pode fazer para entender melhor as strings ASCII. Ao saber que o caractere 'a'
é o número 97, você pode usar a função chr()
no Python para conferir:
Viu? Quando você digita 'a', o computador entende o byte 0x61 (97 em decimal). De forma análoga, quando um programa que exibe um texto na tela encontra o byte 0x61, ele exibe o caractere 'a'. Como você pode ver na tabela ASCII, todas as letras do alfabeto americano estão lá, então é razoável concluir que uma frase inteira seja na verdade uma sequência de bytes onde cada um deles está dentro da faixa da tabela ASCII. Podemos usar o Python para rapidamente imprimir os valores ASCII de cada caractere de uma string:
Perceba o b minúsculo antes das aspas do texto. Em Python, isso cria um objeto da classe bytes
ao invés de str
. Essa classe tem um método hex()
para imprimir cada o valor de caractere da string em hexadecimal e aceita um argumento para ser utilizado como separador entre os bytes. No exemplo, usei espaço.
É exatamente assim que um texto ASCII vai parar dentro de um programa ou arquivo.
Agora vá até o Apêndice Tabela ASCII e observe o seguinte:
O primeiro sinal é o NUL, também conhecido como null ou nulo. É o byte 0.
Outro byte importante é 0x0a, conhecido também por \n, line feed, LF ou simplesmente "caractere de nova linha".
O MS-DOS e o Windows utilizam na verdade dois caracteres para delimitar uma nova linha. Antes do 0x0a, temos um 0x0d, conhecido também por \r, carriage return ou CR. Essa dupla é também conhecida por CrLf.
O caractere de espaço é o 0x20.
Os dígitos vão de 0x30 a 0x39.
As letras maiúsculas vão de 0x41 a 0x5a.
As letras minúsculas vão de 0x61 a 0x7a.
Agora, algumas relações:
Se somarmos 0x20 ao número ASCII equivalente de um caractere maiúsculo, obtemos o número equivalente do caractere minúsculo em questão. Da mesma forma, se diminuirmos 0x20 ao valor de um caractere minúsculo, obtemos o seu maiúsculo. Perceba que basta mudar o bit 5 (da direita para a esquerda, com a contagem começando em zero) do valor para alternar entre maiúsculo e minúsculo.
Se diminuirmos 0x30 de um dígito, temos o equivalente numérico do dígito. Por exemplo, o dígito 5 possui o valor 0x35. Então, 0x35 - 0x30 = 5.
Sabe quando no Linux você dá um cat
num arquivo que não é de texto e vários caracteres "doidos" aparecem na tela enquanto você escuta alguns beeps? Esses sons são, na verdade, os bytes 0x07 encontrados no arquivo. Experimente!
Para complementar esta seção, assista ao vídeo Entendendo a tabela ASCII no nosso canal no YouTube. Nele há exemplos no Linux, mas o conceito é o mesmo.
A tabela ASCII padrão de 7 bits é limitada ao idioma inglês no que diz respeito ao texto. Perceba que uma simples letra 'á' (com acento agudo) é impossível nesta tabela. Sendo assim, ela foi estendida e inteligentemente passou-se a utilizar o último bit do byte que cada caractere ocupa, tornando-se assim uma tabela de 8 bits, que vai de 0 a 255 (em decimal).
Essa extensão da tabela ASCII varia de acordo com a codificação utilizada. Isso acontece porque ela foi criada para permitir texto em outros idiomas, mas somente 128 caracteres a mais não são suficientes para representar os caracteres de todos os idiomas existentes. A codificação mais conhecida é a ISO-8859-1, também chamada de Latin-1, que você vê no Apêndice Tabela ISO-8859-1/Latin-1.
Outro nome para ASCII é US-ASCII. Alguns textos referem-se a texto em ASCII como ANSI strings também.