← voltar

criando um hexdump simples

atualmente eu estou usando o tempo livre que tenho para programar na linguagem C, com o objetivo de aprender como as coisas funcionam por baixo dos panos.
uma das áreas que me interessa bastante é a exploração de binários e a engenharia reversa. durante esse processo, eu acabei conhecendo uma ferramenta bastante comum em sistemas unix: o hexdump.

mas como podemos visualizar os bytes reais do arquivo? analisando cada byte individualmente e sua representação em ASCII. para isso, vou usar o hexdump original com a flag -C, já que o objetivo é comparar a saída da minha implementação com a do programa real.

a ideia é chegar o mais próximo possível do comportamento original enquanto eu aprendo melhor como esse tipo de utilitário funciona.

hexdump -C teste.txt
00000000  4f 6c c3 a1 20 6d 75 6e  64 6f 21 0a 0a 45 73 73  |Ol.. mundo!..Ess|
00000010  65 20 61 72 71 75 69 76  6f 20 c3 a9 20 75 6d 20  |e arquivo .. um |
00000020  74 65 73 74 65 0a                                 |teste.|

offsets e loops

primeiramente, precisamos de uma entrada de dados. por padrão, o hexdump utiliza a entrada padrão (stdin), caso nenhum arquivo seja passado.

#include <stdio.h>
#include <stdlib.h>

int main(int argc, const char *argv[])
{
    FILE *fp = stdin;

    if (argc >= 2) {
        fp = fopen(argv[1], "rb");

        if (!fp) {
            perror("fopen");
            exit(EXIT_FAILURE);
        }
    }
}

o programa não suportará argumentos por agora, porque estou com preguiça e esse não é o objetivo kkkkkkkkkk

após isso, precisamos printar o offset atual, ou seja, qual posição em bytes do arquivo que estamos. o offset será incrementado por 16, que é o tamanho de bytes lidos por linha.

devemos declarar então uma constante que será utilizada no buffer que guardará os bytes de cada linha.

#include <stdio.h>
#include <stdlib.h>

#define MAX_BYTES 16

depois precisamos declarar e inicializar nossas variáveis de offset e de quantos bytes nos lemos, seguido do array que os guardará:

size_t offset = 0;
size_t n_bytes = 0;
unsigned char line[MAX_BYTES] = {0};

agora precisamos do loop inicial, onde iremos ler bytes até o final do arquivo (EOF), e iremos printar o offset, por enquanto.

while ((n_bytes = fread(line, 1, MAX_BYTES, fp)) > 0) {
        printf("%08zx", offset);
        offset += n_bytes;
        printf("\n");
    }
    printf("%08zx\n", offset);

o ultimo printf é para replicar a saída do programa original...

vamos compilar o programa com gcc main.c -o heckdump e executar...

./heckdump teste.txt
00000000
00000010
00000020
00000026

é... por enquanto não programamos a saída com os bytes bonitinhos em hexadecimal e o texto do lado, mas chegaremos lá.

vamos fazer um teste de sanidade rapidão:

hexdump -C teste.txt
00000000  4f 6c c3 a1 20 6d 75 6e  64 6f 21 0a 0a 45 73 73  |Ol.. mundo!..Ess|
00000010  65 20 61 72 71 75 69 76  6f 20 c3 a9 20 75 6d 20  |e arquivo .. um |
00000020  74 65 73 74 65 0a                                 |teste.|
00000026

wooooow, olha só, o offset ta correto, igualzinho B)

agora, para printar os bytes em hexadecimal é tão simples quanto:

for (size_t i = 0; i < n_bytes; i++) {
  printf("%02x ", line[i]);
}

testando:

 ./a.out teste.txt
00000000  4f 6c c3 a1 20 6d 75 6e 64 6f 21 0a 0a 45 73 73
00000010  65 20 61 72 71 75 69 76 6f 20 c3 a9 20 75 6d 20
00000020  74 65 73 74 65 0a
00000026

note que existe um espaço separando os dois blocos de 8 bytes, então colocamos um espaço caso estejamos no final do primeiro:

if (i == 7) printf(" ");

visualizando o texto

agora para terminar, precisamos visualizar a representation em ASCII dos bytes que recebemos em cada linha do arquivo.

caso o byte não seja printável (assim como os bytes de controle no começo) ele aparecerá como um ponto.

while ((n_bytes = fread(line, 1, MAX_BYTES, fp)) > 0) {
        printf("%08zx  ", offset);
    
        for (size_t i = 0; i < n_bytes; i++) {
            printf("%02x ", line[i]);
            if (i == 7) printf(" ");
        }
        printf(" |"); // caractere que determina o começo do bloco ASCII
        for (size_t i = 0; i < n_bytes; i++) {
            printf("%c", isprint(line[i]) ? line[i] : '.'); /* isprint retorna verdadeiro se o 
                                                              caractere pode ser imprimido ou não */
        }
        offset += n_bytes;
        printf("|\n"); // caractere que determina o fim do bloco e salta a linha
 }
./heckdump teste.txt                                                                    
00000000  4f 6c c3 a1 20 6d 75 6e  64 6f 21 0a 0a 45 73 73  |Ol.. mundo!..Ess|
00000010  65 20 61 72 71 75 69 76  6f 20 c3 a9 20 75 6d 20  |e arquivo .. um |
00000020  74 65 73 74 65 0a  |teste.|
00000026

pronto, agora basta alinhar a saída ascii, para que ela permaneça fixa, mesmo quando a linha lida seja menor.

precisamos printar três espaços para cada byte faltante, dois para o byte que falta e um para o espaço que os separa.

for (size_t i = n_bytes; i < MAX_BYTES; i++) {
     printf("   ");
     if (i == 7) printf(" "); // precisamos igualar o espaço extra de acima
  }

testando novamente:

./heckdump teste.txt                                                                    
00000000  4f 6c c3 a1 20 6d 75 6e  64 6f 21 0a 0a 45 73 73  |Ol.. mundo!..Ess|
00000010  65 20 61 72 71 75 69 76  6f 20 c3 a9 20 75 6d 20  |e arquivo .. um |
00000020  74 65 73 74 65 0a                                 |teste.|
00000026
voce pode encontrar o código fonte do heckdump aqui

conclusão

esse projeto embora simples acabou abrindo meus olhos para como arquitetura de software pode transparecer. começou como "e se eu lesse tantos bytes com fread e os printasse" e acabou em uma pequena ferramenta, que serviu mais para aprendizado.

espero que possa ter te inspirado a recriar ferramentas que você já utiliza, ou criar novas.

obrigado por ler até aqui.

← voltar