
Quero contar quantas vezes uma determinada sequência de bytes acontece dentro de um arquivo que possuo. Por exemplo, quero descobrir quantas vezes o número \0xdeadbeef
ocorre dentro de um arquivo executável. No momento estou fazendo isso usando grep:
#/usr/bin/fish
grep -c \Xef\Xbe\Xad\Xde my_executable_file
(Os bytes são escritos em ordem inversa porque minha CPU é little-endian)
No entanto, tenho dois problemas com minha abordagem:
- Essas
\Xnn
sequências de fuga só funcionam na casca do peixe. - grep está na verdade contando o número de linhas que contêm meu número mágico. Se o padrão ocorrer duas vezes na mesma linha, ele contará apenas uma vez.
Existe uma maneira de corrigir esses problemas? Como posso fazer esse liner ser executado no shell Bash e contar com precisão o número de vezes que o padrão ocorre dentro do arquivo?
Responder1
Esta é a solução de uma linha solicitada (para shells recentes que possuem "substituição de processo"):
grep -o "ef be ad de" <(hexdump -v -e '/1 "%02x "' infile.bin) | wc -l
Se nenhuma "substituição de processo" <(…)
estiver disponível, basta usar grep como filtro:
hexdump -v -e '/1 "%02x "' infile.bin | grep -o "ef be ad de" | wc -l
Abaixo está a descrição detalhada de cada parte da solução.
Valores de bytes de números hexadecimais:
Seu primeiro problema é fácil de resolver:
Essas sequências de escape \Xnn funcionam apenas na casca do peixe.
Mude o superior X
para o inferior x
e use printf (para a maioria dos shells):
$ printf -- '\xef\xbe\xad\xde'
Ou use:
$ /usr/bin/printf -- '\xef\xbe\xad\xde'
Para aqueles shells que optam por não implementar a representação '\x'.
É claro que traduzir hexadecimal para octal funcionará em (quase) qualquer shell:
$ "$sh" -c 'printf '\''%b'\'' "$(printf '\''\\0%o'\'' $((0xef)) $((0xbe)) $((0xad)) $((0xde)) )"'
Onde "$sh" é qualquer shell (razoável). Mas é muito difícil mantê-lo citado corretamente.
Arquivos binários.
A solução mais robusta é transformar o arquivo e a sequência de bytes (ambos) em alguma codificação que não tenha problemas com valores de caracteres ímpares como (new line) 0x0A
ou (null byte) 0x00
. Ambos são bastante difíceis de gerenciar corretamente com ferramentas projetadas e adaptadas para processar “arquivos de texto”.
Uma transformação como base64 pode parecer válida, mas apresenta o problema de que cada byte de entrada pode ter até três representações de saída dependendo se é o primeiro, segundo ou terceiro byte da posição mod 24 (bits).
$ echo "abc" | base64
YWJjCg==
$ echo "-abc" | base64
LWFiYwo=
$ echo "--abc" | base64
LS1hYmMK
$ echo "---abc" | base64 # Note that YWJj repeats.
LS0tYWJjCg==
Transformação hexadecimal.
É por isso que a transformação mais robusta deve ser aquela que começa em cada limite de byte, como a representação HEX simples.
Podemos obter um arquivo com a representação hexadecimal do arquivo com qualquer uma destas ferramentas:
$ od -vAn -tx1 infile.bin | tr -d '\n' > infile.hex
$ hexdump -v -e '/1 "%02x "' infile.bin > infile.hex
$ xxd -c1 -p infile.bin | tr '\n' ' ' > infile.hex
A sequência de bytes a ser pesquisada já está em hexadecimal neste caso.
:
$ var="ef be ad de"
Mas também poderia ser transformado. Segue um exemplo de hex-bin-hex de ida e volta:
$ echo "ef be ad de" | xxd -p -r | od -vAn -tx1
ef be ad de
A string de pesquisa pode ser definida a partir da representação binária. Qualquer uma das três opções apresentadas acima od, hexdump ou xxd são equivalentes. Apenas certifique-se de incluir os espaços para garantir que a correspondência esteja nos limites de bytes (nenhuma mudança de nibble é permitida):
$ a="$(printf "\xef\xbe\xad\xde" | hexdump -v -e '/1 "%02x "')"
$ echo "$a"
ef be ad de
Se o arquivo binário estiver assim:
$ cat infile.bin | xxd
00000000: 5468 6973 2069 7320 efbe adde 2061 2074 This is .... a t
00000010: 6573 7420 0aef bead de0a 6f66 2069 6e70 est ......of inp
00000020: 7574 200a dead beef 0a66 726f 6d20 6120 ut ......from a
00000030: 6269 0a6e 6172 7920 6669 6c65 2e0a 3131 bi.nary file..11
00000040: 3232 3131 3232 3131 3232 3131 3232 3131 2211221122112211
00000050: 3232 3131 3232 3131 3232 3131 3232 3131 2211221122112211
00000060: 3232 0a
Então, uma simples pesquisa grep fornecerá a lista de sequências correspondentes:
$ grep -o "$a" infile.hex | wc -l
2
Uma linha?
Tudo pode ser executado em uma linha:
$ grep -o "ef be ad de" <(xxd -c 1 -p infile.bin | tr '\n' ' ') | wc -l
Por exemplo, pesquisar 11221122
no mesmo arquivo exigirá estas duas etapas:
$ a="$(printf '11221122' | hexdump -v -e '/1 "%02x "')"
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ') | wc -l
4
Para "ver" as partidas:
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
3131323231313232
3131323231313232
3131323231313232
3131323231313232
$ grep "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
… 0a3131323231313232313132323131323231313232313132323131323231313232313132320a
Carregando
Existe a preocupação de que o grep armazene o arquivo inteiro em buffer e, se o arquivo for grande, crie uma carga pesada para o computador. Para isso, podemos usar uma solução sed sem buffer:
a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin |
sed -ue 's/\('"$a"'\)/\n\1\n/g' |
sed -n '/^'"$a"'$/p' |
wc -l
O primeiro sed é sem buffer ( -u
) e é usado apenas para injetar duas novas linhas no fluxo por string correspondente. O segundo sed
imprimirá apenas as linhas correspondentes (curtas). O wc -l contará as linhas correspondentes.
Isso armazenará em buffer apenas algumas linhas curtas. A(s) string(s) correspondente(s) no segundo sed. Isto deve ser bastante baixo nos recursos utilizados.
Ou, um pouco mais complexo de entender, mas a mesma ideia em um sed:
a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin |
sed -u '/\n/P;//!s/'"$a"'/\n&\n/;D' |
wc -l
Responder2
Com o sinalizador grep
do GNU -P
(perl-regexp)
LC_ALL=C grep -oaP '\xef\xbe\xad\xde' file | wc -l
LC_ALL=C
é evitar problemas em localidades multibyte onde, grep
de outra forma, tentaríamos interpretar sequências de bytes como caracteres.
-a
trata arquivos binários equivalentes a arquivos de texto (em vez do comportamento normal, onde grep
apenas imprime se há pelo menos uma correspondência ou não)
Responder3
PERLIO=:raw perl -nE '$c++ while m/\xef\xbe\xad\xde/g; END{say $c}' file
Que trata o(s) arquivo(s) de entrada como binários (sem tradução para feeds de linha ou codificações, consulteperlrun) então faz um loop sobre o(s) arquivo(s) de entrada sem imprimir, incrementando um contador para todas as correspondências do hexadecimal fornecido (ou qualquer forma, consulteperlre).
Responder4
Com GNU awk
, você pode fazer:
LC_ALL=C awk -v 'RS=\xef\xbe\xad\xde' 'END{print NR - (NR && RT == "")}'
Se algum dos bytes for operador ERE, ele deverá ser escapado (com \\
). Como 0x2e
qual .
teria que ser inserido como \\.
ou \\\x2e
. Fora isso, deve funcionar com valores de bytes arbitrários, incluindo 0 e 0xa.
Observe que não é tão simples NR-1
porque existem alguns casos especiais:
- quando a entrada está vazia, NR é 0, NR-1 daria -1.
- quando a entrada termina no separador de registros, um registro vazio não é criado depois disso. Testamos isso com
RT==""
.
Observe também que na pior das hipóteses (se o arquivo não contiver o termo de pesquisa), o arquivo acabará sendo carregado inteiro na memória).