Unicode

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

UTF-8

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:

>>> 'mentebinária'.encode('utf-8').hex(' ')
'6d 65 6e 74 65 62 69 6e c3 a1 72 69 61'

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.

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

>>> b'mente'.hex(' ')
'6d 65 6e 74 65'

Até aí, nenhuma novidade, mas vamos ver 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'

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:

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

>>> '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, um emoji:

>>> '💚'.encode('utf-16-le').hex(' ')
'3d d8 9a dc'

Codepoints da ISO-8859-1 na UTF-16

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:

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

UTF-32

Sendo pouco utilizado, este padrão utiliza 4 bytes para cada caractere. 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 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'

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

Last updated