Unicode
À esta altura você já pode imaginar a dificuldade que profissionais de computação enfrentam ao 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. As strings neste formato são comumente chamadas de wide strings (largas, numa tradução livre).
O Unicode define mais de um milhão de code points únicos. Para manter a compatibilidade com a codificação ASCII, os code points de 0 a 127 são iguais ao da ASCII e os code points de 128 a 255 são iguais aos da code page ISO-8859-1 (mas isso pode ser sobreposto por esquemas de codificação como o UTF-8, que veremos mais adiante).
O esforço é cobrir todos os caracteres dos idiomas e para isso, claro, não daria para se limitar a um byte por caractere. Além disso, Unicode não utiliza code pages (lembre-se que os code points são únicos).
Na teoria é tudo muito belo, mas os problemas aparecem quando pensamos em como armazenar caracteres Unicode em arquvos ou na memória do computador. Por exemplo: quantos bytes são necessários para cada caractere Unicode? Pois é... o padrão Unicode não especifica isso. Por isso foram inventamos esquemas de codificação que transforam code points Unicode em bytes. Veremos os principais agora.
UTF-8
O UTF (Unicode Transformation Format) de 8 bits foi desenhado originalmente por Ken Thompson (criador do Unix) e Rob Pike (criador da linguagem Go) para abranger todos os caracteres possíveis nos vários idiomas deste planeta.
Com 1 byte, o UTF-8 codifica ASCII somente. Os próximos caracteres utilizam 2 bytes e compreendem não só o alfabeto latino (como na ASCII estendida com code page 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 o método encode()
. Isso é necessário porque queremos ver uma string UTF-8 e não ASCII:
>>> 'mentebinária'.encode('utf-8').hex(' ')
'6d 65 6e 74 65 62 69 6e c3 a1 72 69 61'
Como dito antes, os code points 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.
Vamos ver um emoji agora:
>>> '💚'.encode('utf-8').hex(' ')
'f0 9f 92 9a'
O coração verde, que usamos bastante na Mente Binária, é codificado em quatro bytes: f0 9f 92 9a.
O UTF-8 reina na web pois seu padrão variável permite uma significativa economia de bytes para grande parte dos textos, mas existem outros esquemas de codificação de code points Unicode que vale mencionar para nossos objetivos.
UTF-16
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, mesmo que não precisem. O byte adicional estará zerado. Vamos entender melhor com a ajuda do Python.
Primeiro, exibimos os bytes em hexa equivalentes de cada caractere da string:
>>> b'mente'.hex(' ')
'6d 65 6e 74 65'
Até aí nenhuma novidade, mas vejamos como essa string seria codificada em UTF-16:
>>> 'mente'.encode('utf-16').hex(' ')
'ff fe 6d 00 65 00 6e 00 74 00 65 00'
A primeira dupla de bytes é FF FE, mas de onde ela veio? Esta é a Byte Order Mark (BOM) ou Marca de Ordem de Byte e define a ordem (ou endianness) dos bytes nos code points. Se for FF FE como neste caso, os bytes estão em little-endian, o que significa que o byte menos significativo está à esquerda. Em outras palavras, o número 0x0006d será armazenado como 6D 00. Se o BOM fosse FE FF, então esse número seria armazenado como 00 6D.
Também é possível utilizar a codificação UTF-16-LE, que já utiliza little-endian por padrão, sem precisar da BOM:
>>> 'mente'.encode('utf-16-le').hex(' ')
'6d 00 65 00 6e 00 74 00 65 00'
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. Isto é 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 estão em big-endian, ou seja, na ordem direta, com o byte mais significativo à esquerda:
>>> 'mente'.encode('utf-16-be').hex(' ')
'00 6d 00 65 00 6e 00 74 00 65'
Além disso, é importante ressaltar que em strings UTF-16 também há a possibilidade de caracteres de quatro bytes. Por exemplo, nosso coração verde de novo:
>>> '💚'.encode('utf-16-le').hex(' ')
'3d d8 9a dc'
Perceba que os bytes são totalmente diferentes da versão UTF-8 do mesmo caractere. Eu sei, não é simples, mas é o que é.
Code points da ISO-8859-1 na UTF-16
Os números (code points) utilizados pela ISO-8859-1 para seus caracteres são também os números utilizados em strings UTF-16. No Windows, como já falado, 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:
>>> 'binária'.encode('iso-8859-1').hex(' ')
'62 69 6e e1 72 69 61'
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:
>>> 'binária'.encode('utf-16-le').hex(' ')
'62 00 69 00 6e 00 e1 00 72 00 69 00 61 00'
Nesse caso, "binária" é uma string UTF-16-LE 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 na code page ISO-8859-1.
UTF-32
Também chamado de UCS-4, este padrão utiliza 4 bytes para cada caractere. Ele é o mais simples, mas ocupa mais espaço. Vamos ver como fica a string "mb" em UTF-32 com BOM:
>>> 'mb'.encode('utf-32').hex(' ')
'ff fe 00 00 6d 00 00 00 62 00 00 00'
Perceba o BOM obrigatório de quatro bytes ao invés de dois.
Agora em UTF-32-LE:
>>> 'mb'.encode('utf-32-le').hex(' ')
'6d 00 00 00 62 00 00 00'
E por fim em UTF-32-BE:
>>> 'mb'.encode('utf-32-be').hex(' ')
'00 00 00 6d 00 00 00 62'
Perceba que a BOM só é adicionada quando não especificamos o endianness.
É 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 utilizado, dentre outros fatores. Por exemplo, um programa feito em C no Windows e compilado com Visual Studio, normalmente tem as wide strings em UTF-16-LE. 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 do escopo deste livro. Se desejar se aprofundar no assunto, 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.
Last updated
Was this helpful?