Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
zipdefrag — Presented at Recon Montreal 2018 | Kitploit
Herramientas/GitHubGitHub/nccgroup/zipdefrag
Embedded Systems SecurityMemory ForensicsReverse EngineeringData RecoveryDigital ForensicsFirmware Analysis
GitHubnccgroup/zipdefrag

zipdefrag

Presented at Recon Montreal 2018

Ver Repositorio
74hace 8 añosAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Este volcado es un rompecabezas

o parseo avanzado a escopetazos para necios y rebeldes

érase una vez

Érase una vez, querido lector, en una noche oscura y tormentosa, su fiel autor se topó con una situación desconcertante y misteriosa: un sistema donde lo único que impedía el análisis de memoria por chip-off y la ingeniería inversa era un sistema de archivos propietario fragmentado y desconocido, junto con la compresión debida al uso de Java embebido, lo que reducía la eficacia de las herramientas existentes para el file carving.

En su momento se podría haber improvisado una solución ad hoc concatenando manualmente fragmentos que parecían encajar, con algo de horrible trabajo de consola en Python y feo scripting bash ad hoc. Era Suficientemente Bueno, pero llevaba mucho tiempo.

Si bien extraer datos planos sin comprimir en un análisis chip-off es un trabajo bastante corriente en el día a día de un hacker de hardware, la compresión plantea problemas importantes cuando sus piezas están desperdigadas por todas partes de un modo irracional y desagradable, incluso cuando no hay otras protecciones reales contra la extracción.

Pero seguro que tiene que haber una forma mejor, ¿no?

Hay algunas cosas interesantes sobre los archivos Zip en particular (que es el formato básico usado para los archivos JAR). Como estudiante entregado desde hace bastante tiempo de la International Journal of PoC||GTFO y siguiendo especialmente el trabajo sobre trucos con formatos de archivo de Ange Albertini, pensé que podría haber suficientes datos dentro de un archivo zip sobre el propio archivo zip como para hacer un trabajo decente de volver a coserlo todo.

Así que, dejando las referencias a un lado, entremos en los detalles técnicos.

Primero lo primero: puede que no conozcamos los detalles del sistema de archivos (y, desde la perspectiva de mi investigación, independientemente del sistema en el que había encontrado el problema, decidí que sencillamente era mejor no preocuparme por ellos). Pero sabemos una cosa o dos sobre cómo se implementan la mayoría de los sistemas de archivos. En particular, sabemos que tienden a escribirse en fragmentos. Los fragmentos tienen un tamaño mínimo de algún tipo, conocido como páginas, y podemos identificar ese tamaño de página examinando el volcado y determinando el tamaño mínimo de bloque que se escribe.

Algunos de estos pueden ser contiguos y otros no, sin un patrón claro sobre cuándo los bloques son contiguos.

Con todo esto quiero decir que el problema que tenemos es cómo reordenar las páginas de datos de manera que nos proporcionen imágenes válidas (o casi válidas) de los archivos que queremos extraer.

Los archivos Zip están escritos de forma que implementan una especie de jerarquía inversa. Primero los datos del archivo comprimido (envueltos en cabeceras de archivo local que los describen). Después un directorio central (que enumera los desplazamientos de las cabeceras de archivo local) y luego un registro de fin de directorio central (que, entre otras cosas, describe el número de archivos almacenados en el zip, el desplazamiento donde comienza el directorio central y el tamaño del directorio central).

Pongámoslo al revés, ahondando un poco más en el detalle:

  • El fin del directorio central nos dice:

    • La ubicación exacta, dentro del archivo Zip, del registro EOCD (el desplazamiento del CD, más la longitud del CD, que precede al EOCD)
    • Cuántos archivos (y por tanto registros CD) buscar.
    • La ubicación precisa en el archivo Zip del primer registro CD.
  • Cada registro del directorio central nos dice:

    • CRC32 de los datos del archivo comprimido
    • Marca de tiempo
    • Muchos otros metadatos (método de compresión, flags, versión del SO usada/necesaria...)
    • Un índice dentro del archivo para el fragmento LF correspondiente
    • Y lo crucial: datos suficientes para construir una imagen del fragmento LF correspondiente.
  • Cada registro de archivo local nos dice:

    • La ubicación en nuestro volcado del inicio de un archivo
    • Si es un archivo lo bastante pequeño, obtenemos el archivo completo dentro de la misma página o, ¡gracias a que la siguiente cabecera de archivo aparece en el siguiente fragmento paginado del archivo zip!
    • Si hay suficientes archivos pequeños empaquetados en suficientes páginas, podemos usar la ubicación de las páginas y los valores conocidos del directorio para crear un orden para las páginas (¡con huecos conocidos!)

Todo lo anterior nos lleva a haber reconstruido la gran mayoría del archivo.

Giro de guion: ¡tenemos que lidiar de forma realista con más de un firmware JAR!

En primer lugar, necesitamos un paso para distinguir los datos de distintos firmwares. La razón es que todos los desplazamientos solo son relevantes dentro de sus respectivos archivos zip: cualquier conflicto provocará flujos zip desajustados y corrupción, y queremos a toda costa sacar la mayor cantidad posible de datos sin corromper. También queremos tener buenas garantías de que, p. ej., cualquier vulnerabilidad que diagnosticuemos en el firmware objetivo afecta al que normalmente vemos ejecutándose y no a algún otro archivo que simplemente se ha quedado tirado por ahí.

La solución que se necesita aquí es el algoritmo kmeans (también conocido como "Algoritmo de Lloyd"). Hay un gran vídeo aquí que explica cómo funciona. SciPy tenía una buena versión lista para usar, pero tuve que identificar/ parchear la única crate de clustering/análisis que implementaba el algoritmo para que funcionara en la implementación de Rust. Por suerte no tuve que escribirlo desde cero.

Después de eso, el trabajo está hecho.

Podemos usar varias características para ello. Los campos Flags, Method y Version varían según la pila Zip usada para comprimir el archivo. Además, las cabeceras tienen marcas de tiempo y, en general, es poco probable que todos los firmwares se compilaran y comprimieran exactamente al mismo tiempo.

Como inciso, vale la pena señalar que los archivos Zip usan marcas de tiempo en formato MS-DOS, que son enteros cortos empaquetados en bits que representan año-mes-día y hora-minutos-dos-segundos. Si no se convirtieran a un valor escalar absoluto antes de usarlos como datos de clasificación, podrías dar tanto peso a la diferencia de un año como a la de un segundo, ¡y eso no sirve de nada!

Los convertimos en un Vector Euclidiano (que es una palabra elegante para un array n-dimensional de valores en ℝ, o coordenadas flotantes, pero el vídeo enlazado arriba es probablemente la explicación más sencilla) y el algoritmo de agrupamiento hace prácticamente todo lo demás por nosotros, reuniendo todas las cabeceras parseadas en el número de grupos que esperamos.

Una nota rápida sobre el parseo

Aunque he escrito esto como un script de Python bastante chapucero para prototipar el método, al alcanzar aproximadamente un 70-80% de tasa de recuperación de contenido JAR con el PoC, decidí parar ahí y pasar a implementar una versión rápida en Rust.

Rust tiene una crate llamada nom que es absolutamente fantástica para escribir verificadores de parseo. Esta fue una de las principales razones para reescribirlo en Rust, por si sirve de algo. La capacidad de escribir parsers claros y extremadamente estrictos hace que esto sea mucho más fácil en cierto modo que intentar manejarlo todo en Python (que tiende a ser mucho más tolerante, hasta el punto de que a veces es un reto estar seguro de que no estás pasando por alto fallos erróneos al intentar cazar un caso límite).

Si lo de los parsers rápidos, legibles y alucinantes te parece interesante, echa un vistazo a:

  • Writing Parsers like it's 2017
  • Nom Benchmarks - donde alguien escribió un parser http desde cero en Rust, algo más rápido que una implementación en C muy rápida, sin desbordamientos de búfer.

Además de todo lo demás, ejecutar este tipo de análisis en Python es intrínsecamente bastante lento, y nunca pretendió ser mucho más que una vía hacia un PoC para explorar la viabilidad de este enfoque.

En fin, basta de eso...

Siguiendo adelante

Los enfoques para reconstruir los fragmentos restantes incluyen filtrar primero las páginas restantes en busca de candidatos de alta entropía, rellenar primero los huecos más pequeños (eliminando tantas páginas como sea posible de la lista de búsqueda, ya que probar permutaciones sobre ellas es, en el peor de los casos, una tarea de tiempo exponencial; así que resolver rápido los casos fáciles es una prioridad y simplifica exponencialmente nuestro problema a medida que avanzamos).

También podemos conseguir una victoria rápida encontrando casos en los que una cabecera de archivo local no se puede parsear debido a un límite de página (deberíamos poder emparejarla con una contraparte con idéntica alineación, al menos en los casos en que alineaciones similares sean únicas y no colisionen con otros artefactos de corrupción).

¿Cómo comprobamos los candidatos para las páginas que faltan? Bueno, ¡tenemos nuestras sumas de comprobación CRC32 para los archivos justo ahí, en nuestro directorio central! En lugar de calcular el CRC32 sobre el archivo, probablemente la mejor manera de abordar esto es calcular el CRC32 sobre los fragmentos que ya conocemos (hacia delante desde los datos al final de la página antes de que empiece nuestro hueco, y hacia atrás desde los datos (o el fragmento DataDescriptor después del flujo deflate) y deducir a partir de ellos qué CRC32 intermedio deberíamos esperar para cada bloque de páginas que falta.

Básicamente, cada vez que elegimos el problema más simple/rápido de resolver, hacemos que los problemas más difíciles sean significativamente más simples al eliminar paja. Esa es la razón de usar la entropía de Shannon para descartar sin más las páginas vacías o casi vacías: no está garantizado que solo tengamos páginas zip de alta entropía, pero aunque haya valores atípicos, es una aceleración masiva evitar tener que lidiar con esa complicación desde el principio.

Solo quería construir esta maldita cosa, ¿qué pasa?

Si quieres trastear con ello, instala Rust (recomendado con el fantástico rustup nightly). Después:

root@kitploit:~
$ git clone [repo]
...
$ cd zipdefrag
...
$ cargo build --release

Puedes omitir el indicador release para habilitar la depuración.

Los artefactos de compilación estarán en /target/{debug,release}

Genera la documentación con cargo doc (Esta crate está muy documentada. Me gusta escribir).

Actualmente no hay salida de terminal por defecto en la versión de Rust; si quieres ejecutar el harness de CLI, tienes que establecer la variable de entorno RUST_LOG=zipdefrag, que habilita un registro verbose en terminal que muestra el análisis hasta ahora.

Próximamente:

  • Un ejecutable nativo rápido y portátil (con hooks de Python) para resolver volcados zip que son un rompecabezas procedentes de sistemas de archivos desconocidos.

  • Un volcado de demostración

Problemas conocidos

  • El rendimiento está actualmente roto en la implementación de Rust debido a un comportamiento derrochador en la búsqueda de los fragmentos LFH correspondientes. Lo arreglaré y aprenderé la lección.

  • Esta técnica no funciona bien cuando muchos de los archivos del JAR son significativamente más grandes que el tamaño de página. Como depende en gran medida de la estructura inherente de los archivos zip, los archivos con gran cantidad de datos no funcionan tan bien.

Afortunadamente, los archivos de clase suelen ser bastante pequeños en general para los midlets J2ME, pero los binarios grandes empaquetados dentro probablemente no se podrán recuperar.

Además, el PoC en Python contiene un buen número de errores aritméticos.

Descargar herramienta