1. Introdução
Dumps de memória, discos ou arquivos binários em geral podem conter outros arquivos dentro. Por exemplo, um arquivo TAR (agrupado, sem compactação) praticamente concatena vários arquivos e um só. O arquivo resultante contém todos os bytes dos arquivos de entrada e mais algumas informações que o formato TAR exige. O mesmo acontece com dumps feitos de memória gerados com software de cópia bit a bit, como o dd, parte integrante do coreutils [1], do projeto GNU. Estes podem, e geralmente é que acontece, conter vários arquivos de diferentes tipos. Neste artigo mostrarei uma forma manual, usando ferramentas básicas, para extrair dados de dentro destes dumps.
Ao ministrar um dos cursos de segurança na 4Linux, gerei este desafio: O objetivo é extrair dados válidos de um dump de 1 MB fornecido. Dentro dele, há dois arquivos. Não sabemos que tipo de arquivo há dentro do dump, então temos que procurar.
NOTA: Este texto é didático e você pode seguir utilizando uma máquina com Linux para fazer a análise do dump. Basta baixar o binário dump.bin.
2. Como procurar tipos de arquivo
Antes de tudo, é preciso saber o que se vai procurar. Você pode querer procurar milhares de tipos diferentes, mas terá que conhecer o formato de cada um deles para identificar possíveis bytes dentro do dump. Neste artigo vamos buscar por arquivos BMP e GIF mas este texto pode servir de guia para buscar qualquer tipo. Para isso precisamos estudar o a especificação de cada formato. Este tipo de documento quem fez é o desenvolvedor do formato e é particularmente interessante para desenvovledores de software que trabalharão com imagens como editores, visualizadores, browsers etc. A Wikipedia é uma ótima fonte para isso, mas o site dos consórcios que desenvolvem e mantêm os padrões também podem ser utilizados. Basta googlar por “<extensão> file format” e você achará muitos links.
3. Analisando a especificação do bitmap (BMP)
Para o nosso interesse, que não é desenvolver um aplicativo que trabalhe com o formato, mas sim entender o básico do formato, a parte mais interessante está na estrutura de um arquivo bitmap [2], onde cada grupo de bytes de um arquivo deste tipo ganha um significado. O que queremos é saber os bytes que definem o início de um arquivo bitmap. Em outras palavras, queremos saber que bytes estão no cabeçalho (header) de um arquivo bitmap. Veja que na especificação consta:
“the header field used to identify the BMP & DIB file is 0x42 0x4D in hexadecimal, same as BM in ASCII”
Achamos. Isso nos leva a crer que um bitmap tradicional começa com os bytes 0x42 e 0x4d, o mesmo que BM em ASCII. Sabendo disso podemos checar as strings dentro de nosso dump e fazer um filtro pela string “BM”, certo?
3.1 Buscando a string que identifica um bitmap
Para isso, nada mais indicado que o strings, do conjunto binutils [3], também do projeto GNU (thanks FSF!).
$ strings -t x dump.bin | grep BM 6ae6d BMP& 8f699 g,UBM_ be081 ?O~BMY ed589 YBMG f63c2 xBM5
A opção “-t x” do strings faz com que ele imprima os endereços (posições) em hexa das strings que encontrou.
NOTA: Já se perguntou como funciona o strings? Uma ideia é ler cada byte e testar se este é imprimível, ou seja, tem representação gráfica na tabela ASCII. Caso seja imprimível, só mandar pra tela. Um código de exemplo seria:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[1]) { unsigned char byte; FILE *fp = fopen(argv[1], “rb”); while (fread(&byte, sizeof(unsigned char), 1, fp)) putchar(isprint(byte) ? byte : ‘n’); fclose(fp); }
Código apenas para fins didáticos. Precisaria de melhorias se fosse usado pra valer.
De acordo com a saída do strings, parece que temos 5 bitmaps dentro deste arquivo. Será? Na verdade não podemos garantir. Os bytes referentes aos caracteres ‘B’ e ‘M’ podem ser parte de outro arquivo, de uma música, de um texto ou simplesmente parte de uma instrução de algum programa. Não pode-se garantir que é um bitmap. Temos que analisar!
3.2 Analisando a primeira ocorrência da string BM
Sabemos a posição da primeira ocorrência porque o comando strings nos deu. Agora vamos usar um visualizador hexa/ascii para ver o que há próximo destes bytes. No conjunto coreutils há o od, mas eu estou acostumado ao hexdump (hd), do BSD:
$ hd -s 0x6ae6d -n 32 dump.bin 0006ae6d 42 4d 50 26 bf 29 c1 f3 f3 81 f0 99 aa f0 aa fd |BMP&.)……….| 0006ae7d d3 d1 e0 3e 92 54 84 b1 18 74 4e b5 00 c1 ce 06 |…>.T…tN…..|
A opção “-s 0x6ae6d” faz com que o hd pule (skip) essa quantidade de bytes a partir do início do arquivo, caindo diretamente em nossa string. Já a opção “-n 32” faz com que o hd mostre apenas bytes à frente. Só estamos dando uma olhada, não precisamos ver o arquivo inteiro neste momento.
A documentação também diz que os próximos 4 bytes de um bitmap representam o tamanho do arquivo em bytes. Então o tamanho da nossa *possível* imagem seriam os 4 bytes “50 26 bf 29”. Organizando-dos em little-endian (de trás para frente) pois estes representam um número inteiro e não texto, temos 0x29bf2650. Quanto dá isso em decimal? O bash responde:
$ echo $((0x29bf2650)) 700393040
O bc também responderia (mas as letras precisam estar em maiúsculo):
$ echo “ibase=16;29BF2650” | bc 700393040
Quanto dá isso em megabytes?
$ echo $((0x29bf2650 / 1024 / 1024)) 667
Então nosso provável bitmap tem mais de 700 milhões de bytes, aproximadamente 667 MB. Impossível para um dump de 1 MB, certo? Descartado… esses bytes não fazem parte de um bitmap e foi somente coincidência a string “BM”, ainda mais seguida de um “P” (eu fiz isso sem querer hehe).
Deixarei para o leitor analisar as outras ocorrências da string “BM”. Vá se divertir!
4. Buscando a string que identifica um GIF
Agora vamos buscar GIF. Assim como fizemos com o bitmap, vamos olhar a especificação do GIF [4]. De cara, vemos o header da última versão do formato, que deve ser composto pelos bytes 0x47 0x49 0x46 0x38 0x39 0x61, que representam em ASCII a string “GIF89a”. Se quiser confirmar, pode usar o comando ascii do Linux:
$ sudo apt-get install ascii $ ascii
Então vamos ao grep:
$ strings -t x dump.bin | grep GIF89a 80000 GIF89ae
Opa, casamos com a string inteira. Seria muita coincidência esses 6 caracteres casarem se isso não for o início de um GIF? Não sei. Vamos analisar:
$ hd -s 0x80000 -n 64 dump.bin 00080000 47 49 46 38 39 61 65 01 1a 02 f7 00 00 bb 9c 91 |GIF89ae………| 00080010 6e 5b 54 d9 ad 9c 6b 69 69 ca a4 94 e3 8c 7d 3f |n[T…kii…..}?| 00080020 1e 0e 33 0d 09 f6 cb ba e1 b4 a3 e3 bb aa eb ca |..3………….| 00080030 8a c8 44 39 0b 05 04 6b 26 21 44 27 19 fa f2 e4 |..D9…k&!D’….|
A documentação diz que depois do header de 6 bytes, temos 2 bytes identificando a largura (0x165) e mais 2 para a altura (0x21a), o que definiria um GIF de 357×538 pixels. Aceitável…
O próximo byte, segundo o exemplo na documentação, quando é 0xf7, diz que o GIF tem 256 cores, dentre outras informações. Este byte bate com o nosso 0xf7. Parece mesmo ser um GIF…
Vamos testar se é um GIF mesmo? Não precisamos ir até o final checando todos os campos. A maioria dos formatos de arquivo definem um ou mais bytes para marcar o fim do arquivo. No caso do GIF, veja na documentação que os últimos bytes são 0x00 (end) e 0x3b (GIF terminator). Lembre-se que isso é *específico* do formato GIF e não é regra para qualquer tipo de arquivo. Cada um tem seu formato.
NOTA: Os formatos que não possuem bytes que marcam o fim geralmente possuem um campo que diz o tamanho completo do arquivo, como vimos no bitmap.
4.1 Estratégia para extrair o possível GIF do dump
Podemos usar a estratégia de extrair os bytes desde o ‘G’ do “GIF89a” até a primeira sequência 0x00 0x3b. Se for um GIF real, funcionaria, concorda?
Vamos buscar então:
$ hd -s 0x80000 dump.bin | grep –color “00 3b” 00082b60 3d 97 87 80 00 00 3b 55 d9 60 ab 23 eb 24 ac f1 |=…..;U.`.#.$..| 000ef4e0 2b 61 8d 77 00 3b 7c 58 62 23 4a dc 4c 15 42 da |+a.w.;|Xb#J.L.B.|
A primeira sequência que “00 3b” que o grep encontrou após o offset 0x80000 (início do GIF) está em 0x82b65, conseguiu visualizar? É só ir somando 1 em hexa, para cada byte. Neste offset está o 0x00 e em 0x82b66 está o 0x3b. Seguindo nossa teoria, o próximo byte, 0x55 em 0x82b67 não faz parte do GIF.
Se diminuirmos este valor de 0x80000, teremos o tamanho total do suposto GIF. Vamos ver:
$ echo $((0x82b67-0x80000)) 11111
A resposta é em decimal. Só lembrando algumas informações que leventamos:
Tipo: GIF
Resolução: 357×538 pixels
Tamanho: 11111 bytes (11 KB)
4.2 Extração com o dd
Se estivermos certo, veremos um GIF quando extrairmos esses bytes. Para isso, podemos usar o dd:
$ dd if=dump.bin of=possivel.gif bs=1 count=11111 skip=$((0x80000)) 11111+0 registros de entrada 11111+0 registros de saída 11111 bytes (11 kB) copiados, 0,036675 s, 303 kB/s
A opção bs (block size) informa ao dd para assumir blocos de 1 byte. A opção count diz quantos blocos ele vai ler, então vão dar exatamente 11111 bytes. E por fim a opção skip, que só aceita números em decimal (por isso a conversão com o bash) informa quantos bytes o dd vai pular para começar a ler, assim como a opção “-s” do hd. Temos que começar a extração do byte 0x80000, que é onde começa o GIF, lembra?
Vamos conferir:
$ file possivel.gif possivel.gif: GIF image data, version 89a, 357 x 538 $ wc -c possivel.gif 11111 possivel.gif
Agora é só visualizar…
5. Ferramentas para automatizar o trabalho
Agora a notícia chata depois que você leu tudo isso: existem ferramentas que fazem tudo isso de maneira automatizada. Uma delas é o foremost [5], que conhece vários tipos de arquivo e é capaz de extrair com precisão a partir de dumps como este. No entanto, considero que mais importante que conhecer ferramentas é saber como elas funcionam e/ou como fazer as coisas sozinho. Sem elas. Say no to script kiddies!
6. Referências
A ilustração utilizada neste artigo foi gentilmente cedida por Anderson Barros [6] e seu uso foi autorizado.
[1] http://www.gnu.org/software/coreutils/
[2] http://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header
[3] http://www.gnu.org/software/binutils/
[4] http://en.wikipedia.org/wiki/Graphics_Interchange_Format#Example_GIF_file
[5] http://foremost.sourceforge.net/
[6] http://anderson-barros.blogspot.com
- 1