Y aquí aparece un error que puede complicar una investigación: copiar únicamente el fichero principal de la base de datos y dar por hecho que contiene toda la información disponible.
Hoy quería detenerme en un detalle que parece menor, pero que puede cambiar la interpretación de una evidencia: el funcionamiento de Write-Ahead Logging (WAL) y la importancia de adquirir sus ficheros asociados.
El problema: la base de datos no siempre está completa por sí sola
SQLite puede trabajar con distintos modos de registro. Cuando utiliza WAL, una transacción confirmada puede quedar escrita en un fichero adicional antes de que sus cambios se incorporen al fichero principal.
Por ejemplo, una aplicación puede tener estos tres ficheros:
| Fichero | Función | Interés forense |
|---|---|---|
actividad.db | Base de datos principal. | Contiene las páginas incorporadas hasta ese momento. |
actividad.db-wal | Registro de escritura anticipada. | Puede contener transacciones confirmadas todavía no trasladadas al fichero principal. |
actividad.db-shm | Índice compartido que facilita el acceso al WAL. | Es auxiliar y puede regenerarse en determinadas condiciones. |
El proceso por el que SQLite transfiere cambios del WAL a la base principal se denomina checkpoint.
Por tanto, una operación de COMMIT no implica necesariamente que el fichero .db ya contenga esos cambios.
Esto no significa que todas las bases SQLite utilicen WAL, ni que siempre existan los tres ficheros. Depende del modo de registro, de la aplicación y del momento en que se realiza la adquisición.
Un laboratorio para comprobarlo
Vamos a reproducirlo sin utilizar datos reales ni herramientas externas. Solo necesitamos Python 3 con el módulo estándar sqlite3.
El siguiente laboratorio es completamente sintético.
Crearemos una base de datos de prueba, escribiremos un primer registro y forzaremos un checkpoint. Después confirmaremos un segundo registro en WAL, mantendremos la conexión abierta y compararemos dos adquisiciones simuladas:
Una que contiene únicamente el fichero
.db.Otra que conserva el fichero principal y sus archivos asociados.
Importante: el checkpoint se ejecuta exclusivamente sobre la base sintética creada por el laboratorio. No debe aplicarse sobre evidencias originales.
Guardamos el siguiente código como laboratorio_sqlite_wal_dfir.py:
#!/usr/bin/env python3
"""Laboratorio sintético SQLite WAL para formación DFIR.
No utilizar sobre evidencias reales ni sobre bases de datos de terceros.
"""
from pathlib import Path
from tempfile import TemporaryDirectory
from contextlib import closing
import shutil
import sqlite3
def leer_eventos(ruta: Path):
# URI de solo lectura. Se ejecuta exclusivamente sobre copias del laboratorio.
uri = ruta.resolve().as_uri() + "?mode=ro"
with closing(sqlite3.connect(uri, uri=True)) as db:
return db.execute(
"SELECT id, descripcion FROM eventos ORDER BY id"
).fetchall()
def main():
with TemporaryDirectory(prefix="dfir_sqlite_wal_") as temporal:
raiz = Path(temporal)
original = raiz / "original"
solo_db = raiz / "solo_db"
conjunto = raiz / "conjunto"
for carpeta in (original, solo_db, conjunto):
carpeta.mkdir()
db_path = original / "actividad.db"
conexion = sqlite3.connect(db_path)
try:
modo = conexion.execute("PRAGMA journal_mode=WAL").fetchone()[0]
assert modo.lower() == "wal", modo
conexion.execute("PRAGMA wal_autocheckpoint=0")
conexion.execute(
"CREATE TABLE eventos (id INTEGER PRIMARY KEY, descripcion TEXT NOT NULL)"
)
conexion.execute(
"INSERT INTO eventos(descripcion) VALUES (?)", ("evento_base",)
)
conexion.commit()
# Solo en esta base SINTÉTICA: fijamos un estado base.
conexion.execute("PRAGMA wal_checkpoint(TRUNCATE)")
# Este commit queda en el WAL mientras la conexión sigue abierta.
conexion.execute(
"INSERT INTO eventos(descripcion) VALUES (?)", ("evento_reciente",)
)
conexion.commit()
shutil.copy2(db_path, solo_db / db_path.name)
for sufijo in ("", "-wal", "-shm"):
origen = Path(str(db_path) + sufijo)
if origen.exists():
shutil.copy2(origen, conjunto / origen.name)
print("SQLite:", sqlite3.sqlite_version)
print("Solo actividad.db:", leer_eventos(solo_db / "actividad.db"))
print("DB + WAL + SHM: ", leer_eventos(conjunto / "actividad.db"))
finally:
conexion.close()
if __name__ == "__main__":
main()Lo ejecutamos:
python3 laboratorio_sqlite_wal_dfir.pyEn una prueba realizada con SQLite 3.46.1, obtenemos:
SQLite: 3.46.1
Solo actividad.db: [(1, 'evento_base')]
DB + WAL + SHM: [(1, 'evento_base'), (2, 'evento_reciente')]El resultado es importante.
El segundo registro estaba confirmado en la base lógica, pero todavía no se había incorporado al fichero principal.
Si hubiéramos analizado exclusivamente actividad.db, habríamos visto un conjunto de datos incompleto.
La versión de SQLite y los detalles de salida pueden variar según el entorno.
El objetivo del laboratorio no es simular una adquisición forense fiable de un sistema en producción, sino demostrar el mecanismo de WAL en un escenario controlado y sin escrituras concurrentes.
¿Qué cambia en una investigación real?
Pensemos en una base SQLite utilizada por un navegador o una aplicación.
Un analista puede consultar una copia aislada y no encontrar un registro que esperaba ver.
Esa ausencia, por sí sola, no permite concluir que la actividad no existió.
Antes de llegar a esa conclusión debemos comprobar si el artefacto utilizaba WAL, si se adquirieron sus ficheros asociados y si la copia es coherente.
También conviene recordar que la estructura de tablas y el significado de los registros dependen de cada aplicación y de su versión.
Una consulta SQL muestra el estado lógico que SQLite reconstruye a partir de los ficheros disponibles.
No equivale a un análisis de todas las páginas, fragmentos o registros antiguos que puedan permanecer en el WAL.
La recuperación de información eliminada requiere técnicas y validaciones adicionales. La presencia de un WAL no garantiza que puedan recuperarse registros borrados.
Adquisición: el orden importa
En una intervención real seguiría, como mínimo, estos criterios:
Identificar el conjunto de artefactos. Localizar el fichero principal y comprobar la presencia de
-wal,-shmu otros ficheros de diario que correspondan al modo utilizado.Adquirir una copia coherente. Copiar los tres ficheros de forma secuencial mientras la aplicación escribe puede generar una combinación inconsistente. Cuando sea posible, utilizar una instantánea del sistema de archivos o un procedimiento de adquisición compatible con el estado de la aplicación, documentando sus limitaciones.
Preservar los originales. Registrar rutas, tamaños, marcas temporales, método de adquisición y hashes SHA-256 de los ficheros obtenidos, incluidos los auxiliares que existan.
Analizar copias de trabajo. No abrir ni ejecutar operaciones de mantenimiento sobre la evidencia original. Incluso una apertura aparentemente de solo lectura puede necesitar gestionar ficheros auxiliares según la versión y los permisos de SQLite.
Contrastar el resultado. Verificar consistencia, revisar las tablas y correlacionar los hallazgos con otros artefactos antes de atribuir acciones o establecer una secuencia temporal.
Una opción para aplicaciones activas es la Online Backup API de SQLite, que permite obtener una copia lógica consistente.
Es útil en determinados procedimientos de adquisición en vivo, pero no sustituye la conservación de los ficheros originales si queremos estudiar estructuras físicas, frames del WAL o posibles restos de información.
Tampoco debemos forzar un checkpoint, ejecutar VACUUM o cambiar el modo de registro sobre una evidencia original: son operaciones que pueden modificar precisamente el estado que intentamos preservar.
Una nota técnica de 2026
SQLite documentó en 2026 un fallo de concurrencia denominado WAL-reset bug, corregido en la versión 3.51.3 y en determinadas versiones anteriores mediante backports.
Requiere una combinación poco frecuente de escrituras y checkpoints concurrentes y puede provocar corrupción de la base de datos.
No es una explicación genérica para cualquier discrepancia encontrada en una investigación, pero sí un recordatorio de que la versión del motor y las condiciones de acceso también forman parte del contexto técnico de la evidencia.
Conclusión
En DFIR solemos dedicar mucho tiempo a interpretar artefactos, pero la interpretación solo es tan buena como la adquisición que la precede.
Con SQLite, el fichero principal puede ser únicamente una parte del estado que necesitamos reconstruir.
Si ignoramos WAL, podemos perder visibilidad sobre transacciones confirmadas y llegar a conclusiones incorrectas.
La lección no es adquirir siempre más por sistema, sino entender cómo almacena sus datos cada artefacto y comprobar qué hemos recogido realmente.
Y eso, en una investigación forense, marca la diferencia.
Referencias técnicas
SQLite — Write-Ahead Logging. Funcionamiento de WAL, checkpoints, ficheros auxiliares y nota de 2026 sobre WAL-reset.
SQLite — Online Backup API. Copias lógicas consistentes de bases SQLite activas.
SQLite — URI Filenames. Apertura de bases de datos mediante URI y modo de solo lectura.
Chromium — HistoryDatabase. Ejemplo de uso de una base SQL para almacenar actividad de navegación.
0 Comentarios