Recuperar registros borrados de QuarantineEventsV2
Cómo sobreviven las filas borradas de QuarantineEventsV2 en freeblocks, freelist, frames WAL y diarios de SQLite, cómo preservarlas y qué prueba recuperarlas.
TL;DR. Borrar una fila de QuarantineEventsV2 con sqlite3 no la elimina del todo. El registro suele quedar en el archivo como espacio libre dentro de la página de tabla, y versiones antiguas de páginas pueden estar también en páginas de la freelist, el WAL y el diario de rollback. Quarantine Parser extrae mediante carving registros LSQuarantineEvent de todos esos lugares anclándose en los serial types del registro, que sobreviven cuando se sobrescriben los 4 primeros bytes de una celda liberada. La recuperación depende de secure_delete y de las escrituras posteriores, y la ausencia de filas recuperadas no prueba nada.
Cómo desaparecen las filas de cuarentena
La base QuarantineEventsV2 no tiene ninguna purga automática documentada; las filas suelen sobrevivir a los archivos que describen. Desaparecen de dos maneras:
- Borrado de filas. Cualquiera con acceso a los archivos del usuario puede ejecutar un
DELETEsobreLSQuarantineEventcon sqlite3 y quitar una fila comprometedora o todo el historial. Una limpieza rutinaria del historial de descargas también puede eliminar filas. - Eliminación del archivo. La base entera, con sus acompañantes, se puede borrar. Lo que queda entonces es asunto de la recuperación a nivel de sistema de archivos, no del carving de SQLite.
El carving ayuda en el primer caso, porque por defecto SQLite no borra físicamente lo que elimina.
Dónde sobreviven los registros borrados
Espacio libre dentro de las páginas de tabla
SQLite guarda las filas como celdas en páginas de tamaño fijo (4096 bytes en el sistema macOS 26.6 observado). Cuando se borra una fila, su celda no se pone a cero por defecto: el espacio pasa a una cadena de freeblocks dentro de la página y los bytes antiguos permanecen hasta que una inserción posterior los reutiliza. Es el lugar más habitual para encontrar un evento de cuarentena borrado.
Páginas de la freelist
Cuando una página ya no contiene ninguna celda viva, SQLite la pone en la freelist para reutilizarla. Su contenido antiguo puede permanecer hasta que la página se vuelva a asignar.
Copias antiguas de páginas: archivo principal frente a WAL
Con un registro de escritura anticipada (WAL), las nuevas versiones de las páginas van primero al archivo -wal y llegan al archivo principal en un checkpoint. Hasta entonces, el archivo principal conserva la versión antigua de cada página modificada y el WAL la nueva. Una fila borrada en la versión del WAL puede seguir viva en la copia del archivo principal, y al revés después de un checkpoint.
Frames WAL sustituidos y no confirmados
Un WAL puede contener varios frames para la misma página, cada uno una versión posterior. Los frames sustituidos conservan estados anteriores de la página, y los frames escritos después del último commit nunca llegaron a confirmarse. SQLite ignora ambos al leer la base; una herramienta de carving no tiene por qué hacerlo.
Páginas del diario de rollback
En el modo de diario de rollback, el que usaba el sistema macOS 26.6 observado (journal_mode=delete), SQLite copia la versión original de cada página en el archivo -journal antes de modificarla. En modo DELETE, el diario se elimina en el commit, así que un -journal junto a la base suele indicar que había una transacción en curso o interrumpida cuando se hizo la copia; sus páginas pueden contener las filas tal como estaban antes del cambio. En modo PERSIST, SQLite conserva el archivo del diario y solo invalida su cabecera, de modo que también pueden quedar imágenes antiguas de páginas.
secure_delete y escrituras posteriores
SQLite tiene un ajuste secure_delete que sobrescribe el contenido borrado. Cuando está activo, las celdas liberadas se ponen a cero y el carving del espacio libre no encuentra nada. Apple no documenta cómo lo configura para esta base, así que no dé nada por supuesto en ningún sentido. Con independencia de ese ajuste, cualquier inserción posterior puede reutilizar el espacio liberado, y una base con mucha actividad lo recicla más deprisa. Ambas razones explican por qué la recuperación es oportunista.
Por qué el carving se ancla en los serial types
Un registro SQLite empieza con la longitud de su carga útil y su rowid, seguidos de una cabecera que enumera un serial type por columna, y después los valores de las columnas. Cuando se libera una celda, SQLite escribe la cabecera del freeblock (el desplazamiento del siguiente freeblock y el tamaño de este) sobre los 4 primeros bytes de la celda. Eso destruye el comienzo de la celda, pero los serial types sobreviven.
En LSQuarantineEvent, esos serial types forman un patrón reconocible: once columnas, casi todas de texto, un REAL para la marca de tiempo (o un entero cuando el valor es entero, una optimización de almacenamiento que aplica SQLite), un entero para el número de tipo y un blob o NULL para el alias. Quarantine Parser busca ese patrón en el espacio libre y decodifica los valores de columna que le siguen para reconstruir la fila.
Registros parciales
Un registro extraído puede ser parcial: escrituras posteriores pueden haber sobrescrito parte de la celda liberada, o los bytes necesarios para las últimas columnas pueden haber desaparecido. Considere fiable cada campo solo si se decodifica limpiamente. El identificador y una URL suelen bastar para unir y pivotar. Compare los registros recuperados con las filas vivas: una copia antigua de página también puede contener una fila que sigue viva, así que busque duplicados antes de contar borrados.
Qué prueban las filas recuperadas y qué no
Una fila recuperada muestra que ese evento existió en la base de este usuario y que ya no está vivo. Es una pista, no un veredicto. No dice cuándo se borró la fila ni quién lo hizo; SQLite no lo registra. No distingue un DELETE deliberado de una limpieza rutinaria. Por eso la vista Hallazgos lo formula como «posible borrado o limpieza». El contexto decide: una sola fila que falta en un historial constante, justo después de una descarga sospechosa, no se interpreta igual que un historial vaciado por completo.
Y a la inversa: cuando no se recupera nada, no se prueba nada. El espacio liberado puede haberse reutilizado, o secure_delete puede haberlo puesto a cero.
Preservar la prueba
- Copie la base con sus archivos
-waly-journal(cp -pcon un*final, como en la guía de recopilación). - Calcule los hashes de las copias antes de cualquier análisis.
- No abra el original con sqlite3. Abrir una base puede volcar el WAL al archivo principal mediante un checkpoint o deshacer un diario activo, lo que sobrescribe justo las páginas que quiere examinar. Incluso con una copia, use sqlite3 solo sobre una segunda copia.
- Prefiera los archivos en bruto a la salida ya analizada: una exportación analizada solo contiene las filas vivas que devolvió la consulta.
Quarantine Parser nunca escribe en los archivos: usa su propio lector SQLite de solo lectura en su navegador, aplica los frames WAL confirmados como lo hace SQLite y extrae el resto mediante carving.
Ejemplo: FIN-MBP-03
El caso sintético que hay detrás de «Try a sample» en la página de inicio muestra toda la cadena. En el portátil Mac FIN-MBP-03, usuario dana.whitlock, el 2026-09-14:
- A las 10:07:12, Safari descarga
https://files.example/s/8f3k2/tools.zip(origenhttps://files.example/s/8f3k2). La Utilidad de Archivo extraetools/README.txt,tools/updater.shytools/com.example.updater.plist, cada uno con el valor de cuarentena y el UUID del archivo comprimido. - Una copia de
com.example.updater.plistestá en~/Library/LaunchAgents/y sigue llevando ese valor. - Hacia las 10:48, la fila de files.example se borra de la base con sqlite3.
En la tabla viva ya no hay ningún evento de files.example, y varios UUID de atributos no tienen fila en la base. Quarantine Parser recupera la fila del espacio libre de la página de tabla y la une con los cinco archivos que llevan su UUID, incluida la copia de LaunchAgents. La cronología procede del escenario: en un caso real, la base no le diría cuándo se produjo el borrado, ni mostraría la sesión de Terminal usada para ejecutar sqlite3. Para eso hay que mirar los unified logs, TCC.db y FSEvents.
Para la parte del atributo en la unión, vea los flags de com.apple.quarantine; para la investigación completa, la investigación de descargas en macOS paso a paso; para una ficha de referencia, macforensics.app.
Preguntas frecuentes
¿Se pueden recuperar registros borrados de QuarantineEventsV2?
A menudo, pero no siempre. Una fila borrada puede sobrevivir en el espacio libre de las páginas de tabla, en páginas de la freelist, en copias antiguas de páginas en el WAL o en el archivo principal, y en páginas del diario de rollback. El ajuste secure_delete de SQLite y las escrituras posteriores pueden destruirla.
¿Un registro recuperado demuestra que alguien manipuló la base?
No. Demuestra que la fila existió en esa base y que ya no está viva. Un borrado deliberado con sqlite3 y una limpieza rutinaria pueden parecerse; la cronología, otros artefactos y los archivos que llevan el UUID ayudan a decidir cuál fue.
Si no se recupera nada, ¿es que no se borró ninguna fila?
No. Las escrituras posteriores reutilizan el espacio liberado y secure_delete sobrescribe el contenido borrado, así que la ausencia de filas recuperadas no prueba nada.
¿Cómo evito destruir registros borrados durante la recopilación?
Copie la base junto con sus archivos -wal y -journal, calcule los hashes de las copias y trabaje sobre una copia de la copia. No abra el original con sqlite3: abrirlo puede provocar un checkpoint del WAL o el rollback de un diario y sobrescribir el espacio libre.