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.