
Quiero contar cuántas veces ocurre una determinada secuencia de bytes dentro de un archivo que tengo. Por ejemplo, quiero saber cuántas veces \0xdeadbeef
aparece el número dentro de un archivo ejecutable. Ahora mismo estoy haciendo eso usando grep:
#/usr/bin/fish
grep -c \Xef\Xbe\Xad\Xde my_executable_file
(Los bytes se escriben en orden inverso porque mi CPU es little-endian)
Sin embargo, tengo dos problemas con mi enfoque:
- Esas
\Xnn
secuencias de escape sólo funcionan en el caparazón del pescado. - grep en realidad está contando la cantidad de líneas que contienen mi número mágico. Si el patrón ocurre dos veces en la misma línea, solo contará una vez.
¿Hay alguna manera de solucionar estos problemas? ¿Cómo puedo hacer que este delineador se ejecute en Bash Shell y contar con precisión la cantidad de veces que ocurre el patrón dentro del archivo?
Respuesta1
Esta es la solución de una sola línea solicitada (para shells recientes que tienen "sustitución de procesos"):
grep -o "ef be ad de" <(hexdump -v -e '/1 "%02x "' infile.bin) | wc -l
Si no hay ninguna "sustitución de proceso" <(…)
disponible, simplemente use grep como filtro:
hexdump -v -e '/1 "%02x "' infile.bin | grep -o "ef be ad de" | wc -l
A continuación se muestra la descripción detallada de cada parte de la solución.
Valores de bytes de números hexadecimales:
Su primer problema es fácil de resolver:
Esas secuencias de escape \Xnn solo funcionan en el caparazón del pescado.
Cambie el superior X
por uno inferior x
y use printf (para la mayoría de los shells):
$ printf -- '\xef\xbe\xad\xde'
O usar:
$ /usr/bin/printf -- '\xef\xbe\xad\xde'
Para aquellos shells que eligen no implementar la representación '\x'.
Por supuesto, traducir hexadecimal a octal funcionará en (casi) cualquier shell:
$ "$sh" -c 'printf '\''%b'\'' "$(printf '\''\\0%o'\'' $((0xef)) $((0xbe)) $((0xad)) $((0xde)) )"'
Donde "$sh" es cualquier shell (razonable). Pero es bastante difícil mantenerlo citado correctamente.
Archivos binarios.
La solución más sólida es transformar el archivo y la secuencia de bytes (ambos) a alguna codificación que no tenga problemas con valores de caracteres impares como (nueva línea) 0x0A
o (byte nulo) 0x00
. Ambos son bastante difíciles de gestionar correctamente con herramientas diseñadas y adaptadas para procesar "archivos de texto".
Una transformación como base64 puede parecer válida, pero presenta el problema de que cada byte de entrada puede tener hasta tres representaciones de salida dependiendo de si es el primer, segundo o tercer byte de la posición mod 24 (bits).
$ echo "abc" | base64
YWJjCg==
$ echo "-abc" | base64
LWFiYwo=
$ echo "--abc" | base64
LS1hYmMK
$ echo "---abc" | base64 # Note that YWJj repeats.
LS0tYWJjCg==
Transformación hexagonal.
Es por eso que la transformación más sólida debería ser aquella que comience en cada límite de byte, como la representación HEX simple.
Podemos obtener un archivo con la representación hexadecimal del archivo con cualquiera de estas herramientas:
$ 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
En este caso, la secuencia de bytes a buscar ya está en hexadecimal.
:
$ var="ef be ad de"
Pero también podría transformarse. A continuación se muestra un ejemplo de un viaje de ida y vuelta hex-bin-hex:
$ echo "ef be ad de" | xxd -p -r | od -vAn -tx1
ef be ad de
La cadena de búsqueda se puede configurar a partir de la representación binaria. Cualquiera de las tres opciones presentadas anteriormente od, hexdump o xxd son equivalentes. Solo asegúrese de incluir los espacios para garantizar que la coincidencia se ajuste a los límites de bytes (no se permite el desplazamiento de nibble):
$ a="$(printf "\xef\xbe\xad\xde" | hexdump -v -e '/1 "%02x "')"
$ echo "$a"
ef be ad de
Si el archivo binario se ve así:
$ 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
Luego, una simple búsqueda grep dará la lista de secuencias coincidentes:
$ grep -o "$a" infile.hex | wc -l
2
¿Una línea?
Todo se puede realizar en una sola línea:
$ grep -o "ef be ad de" <(xxd -c 1 -p infile.bin | tr '\n' ' ') | wc -l
Por ejemplo, buscar 11221122
en el mismo archivo necesitará estos dos pasos:
$ a="$(printf '11221122' | hexdump -v -e '/1 "%02x "')"
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ') | wc -l
4
Para "ver" los partidos:
$ grep -o "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
3131323231313232
3131323231313232
3131323231313232
3131323231313232
$ grep "$a" <(xxd -c1 -p infile.bin | tr '\n' ' ')
… 0a3131323231313232313132323131323231313232313132323131323231313232313132320a
Almacenamiento en búfer
Existe la preocupación de que grep almacene en búfer todo el archivo y, si el archivo es grande, cree una carga pesada para la computadora. Para eso, podemos usar una solución sed sin búfer:
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
El primer sed no tiene búfer ( -u
) y se usa solo para inyectar dos nuevas líneas en la secuencia por cadena coincidente. El segundo sed
solo imprimirá las líneas coincidentes (cortas). El wc -l contará las líneas coincidentes.
Esto almacenará en buffer sólo algunas líneas cortas. Las cadenas coincidentes en el segundo sed. Esto debería ser bastante bajo en recursos utilizados.
O algo más complejo de entender, pero la misma idea en un solo sed:
a='ef be ad de'
hexdump -v -e '/1 "%02x "' infile.bin |
sed -u '/\n/P;//!s/'"$a"'/\n&\n/;D' |
wc -l
Respuesta2
Con grep
el indicador GNU -P
(perl-regexp)
LC_ALL=C grep -oaP '\xef\xbe\xad\xde' file | wc -l
LC_ALL=C
es para evitar problemas en configuraciones regionales de varios bytes donde, grep
de otro modo, se intentaría interpretar secuencias de bytes como caracteres.
-a
trata los archivos binarios de manera equivalente a archivos de texto (en lugar del comportamiento normal, donde grep
solo imprime si hay al menos una coincidencia o no)
Respuesta3
PERLIO=:raw perl -nE '$c++ while m/\xef\xbe\xad\xde/g; END{say $c}' file
Que trata los archivos de entrada como binarios (sin traducción para avances de línea o codificaciones, consultePerlrun) luego recorre los archivos de entrada que no imprimen incrementando un contador para todas las coincidencias del hexadecimal dado (o cualquier forma, consulteperla).
Respuesta4
Con GNU awk
, puedes hacer:
LC_ALL=C awk -v 'RS=\xef\xbe\xad\xde' 'END{print NR - (NR && RT == "")}'
Sin embargo, si alguno de los bytes son operadores ERE, habría que escaparlos (con \\
). Como 0x2e
cuál .
debería ingresarse como \\.
o \\\x2e
. Aparte de eso, debería funcionar con valores de bytes arbitrarios, incluidos 0 y 0xa.
Tenga en cuenta que no es tan simple NR-1
porque hay un par de casos especiales:
- cuando la entrada está vacía, NR es 0, NR-1 daría -1.
- cuando la entrada termina en el separador de registros, no se crea un registro vacío después de eso. Probamos eso con
RT==""
.
También tenga en cuenta que en el peor de los casos (si el archivo no contiene el término de búsqueda), el archivo terminará cargándose completo en la memoria).