Recuperar permisos TCC eliminados de TCC.db-wal
Cómo los permisos TCC borrados o modificados sobreviven en TCC.db-wal y en páginas liberadas: formato WAL de SQLite, qué deja tccutil reset y los límites.
Resumen. TCC.db solo guarda el estado actual de cada permiso: una decisión nueva sustituye a la fila, y tccutil reset la borra. Pero TCC.db funciona en el modo write-ahead log de SQLite, y el archivo TCC.db-wal que tiene al lado puede conservar todavía copias anteriores de las páginas que contenían esas filas: commits más antiguos, frames de una generación anterior del WAL y, a veces, frames sin confirmar. Las páginas liberadas del archivo principal también pueden contener restos. TCC Parser reproduce el WAL confirmado para mostrar el estado actual y coloca cada fila anterior o eliminada que consigue recuperar en la pestaña History (historial), junto al valor actual. Nada de esto tiene garantizada su supervivencia, así que recopile el WAL cuanto antes y trate lo que encuentre como evidencia complementaria.
Por qué falta el historial
La tabla access tiene una fila por cliente, servicio y destino (la clave primaria es service, client, client_type, indirect_object_identifier). Cuando un usuario cambia un interruptor, responde a una solicitud o un perfil MDM modifica una concesión, esa fila se sobrescribe y last_modified avanza. Cuando alguien ejecuta:
tccutil reset Accessibility
tccutil reset ScreenCapture com.example.agent
las filas correspondientes se borran (tccutil recibe el nombre del servicio sin el prefijo kTCCService y, opcionalmente, un bundle ID). Un restablecimiento en Ajustes del Sistema o la eliminación de una app tiene el mismo efecto. La base de datos principal ya no muestra ningún rastro de que la concesión existiera. Es un problema cuando un intruso se concede una capacidad, la usa y la elimina antes de irse.
El WAL de SQLite en cinco minutos
En modo write-ahead log, SQLite no escribe los cambios en el archivo principal de inmediato. Los añade al archivo -wal y los copia de vuelta más tarde, durante un checkpoint. El formato está documentado en sqlite.org; lo que importa para la recuperación es lo siguiente:
Cabecera del WAL (32 bytes)
| Campo | Notas |
|---|---|
| Número mágico | 0x377f0682 o 0x377f0683 (determina el orden de bytes de la suma de comprobación) |
| Versión del formato | 3007000 |
| Tamaño de página | El mismo que el de la base de datos |
| Secuencia de checkpoint | Se incrementa con cada checkpoint |
| Salt-1, Salt-2 | Valores aleatorios que cambian cada vez que el WAL se reinicia |
| Checksum-1, Checksum-2 | Sobre la cabecera |
Frames, uno tras otro: una cabecera de frame de 24 bytes seguida de una copia completa de una página de la base de datos.
| Campo de la cabecera del frame | Notas |
|---|---|
| Número de página | Qué página de la base de datos sustituye este frame |
| Tamaño de la base de datos | Tamaño en páginas tras el commit, en los frames de commit; 0 en los demás |
| Salt-1, Salt-2 | Deben coincidir con la cabecera del WAL para que el frame sea válido |
| Checksum-1, Checksum-2 | Suma de comprobación acumulada sobre los frames anteriores |
Un lector reproduce los frames cuyos salts coinciden con la cabecera y cuya suma de comprobación acumulada es válida, hasta el último frame de commit, y usa la copia confirmada más reciente de cada página. Ese es el estado actual de la base de datos.
Dónde se esconden las filas antiguas
La reproducción da el presente. El pasado está en los frames que la reproducción no utiliza:
| Origen | Qué contiene | Probabilidad |
|---|---|---|
| Commits anteriores en el WAL actual | Copias antiguas de la misma página, anteriores a un commit posterior en el mismo WAL | Habitual cuando hubo varios cambios entre checkpoints |
| Frames de una generación antigua | Frames de una generación anterior del WAL (salts distintos) que aún no se han sobrescrito | Habitual: tras un checkpoint, el WAL se reutiliza y se reinicia con salts nuevos, no se pone a cero |
| Frames sin confirmar | Frames posteriores al último frame de commit | Ocasional; cambios que nunca se confirmaron |
| Páginas liberadas del archivo principal | Páginas liberadas cuando se redujeron filas o tablas | Ocasional; depende de cómo haya reutilizado SQLite el espacio |
TCC.db es pequeña, así que las páginas de tabla que contienen filas de access se reescriben a menudo, y cada reescritura deja otra copia en el WAL. Por eso una concesión eliminada hace unos minutos u horas suele sobrevivir, mientras que una de hace meses normalmente no.
Qué hace TCC Parser con él
Cuando hay un archivo -wal junto a una base de datos (con el mismo nombre base), TCC Parser:
- Valida la cabecera del WAL y reproduce los frames confirmados, como lo haría SQLite, para construir el estado actual. No necesita el archivo
-shm. - Decodifica las celdas de fila de las demás copias de cada página: commits anteriores, frames sin confirmar y frames de generaciones antiguas, además de las páginas liberadas del archivo principal.
- Compara cada fila recuperada con el estado actual por clave primaria y la muestra en la pestaña History como fila eliminada (ya no existe) o como versión anterior (con otro
auth_value,auth_reason,csreqolast_modified).
La pestaña Findings (hallazgos) muestra "grants removed or changed" (concesiones eliminadas o modificadas) cuando una fila recuperada difiere de la actual, por ejemplo una fila Allowed de un servicio de alto impacto que ya no está en la base de datos.
Ejemplo: una concesión de Accesibilidad eliminada
En la muestra ficticia FIN-MBP-03, el estado actual de la base de datos del sistema no tiene ninguna fila de Accesibilidad para el helper identificado por ruta /Users/dana.whitlock/Library/Caches/.sync/sync-helper. El WAL cuenta otra historia:
| Origen | service | client | auth_value | last_modified (UTC) |
|---|---|---|---|---|
| WAL, commit anterior | kTCCServiceAccessibility | /Users/dana.whitlock/Library/Caches/.sync/sync-helper | 2 (permitido) | 2026-09-14 10:19:05 |
| Estado actual | (sin fila) |
En la historia de la muestra, la eliminación se produjo a las 10:47:12 UTC. Los borrados no dejan un last_modified propio en la tabla, así que la hora de la eliminación sale de otras evidencias: la cronología de la secuencia de commits del WAL, el registro unificado (subsystem == "com.apple.TCC"), un historial de shell que muestre tccutil o la telemetría del EDR. En macOS 15.4 y posteriores, Endpoint Security expone las modificaciones de TCC a las herramientas de seguridad (ES_EVENT_TYPE_NOTIFY_TCC_MODIFY), así que su EDR puede haber registrado el cambio directamente.
Límites que debe indicar en el informe
- La supervivencia es oportunista. Un checkpoint seguido de suficientes escrituras nuevas sobrescribe los frames antiguos. Una base de datos con poca actividad puede conservar un frame antiguo mucho tiempo; una con mucha actividad lo pierde enseguida.
- No hay hora de borrado fiable. Una fila recuperada da el estado y su
last_modified, no el momento en que se borró. - El orden dentro de una generación del WAL es fiable; entre generaciones, se deduce. Los frames antiguos de una generación anterior son previos a la actual, pero los huecos se desconocen.
- La recolección destruye evidencia con facilidad. Abrir la base de datos con una herramienta que escriba, o dejar que SQLite haga un checkpoint al cerrar, reescribe el WAL. Varios recolectores (
tcc.yamlde UAC, el módulo TCC de Aftermath) copian soloTCC.db. Vea ubicación y adquisición de TCC.db. - Recuperado no es lo mismo que actual. Presente las filas recuperadas como "un estado anterior recuperado del WAL", con el frame de origen, no como concesiones vigentes.
Otros lugares donde buscar
- Snapshots locales de APFS y Time Machine: copias completas y más antiguas de la base de datos. La forma más fiable de ver una concesión que se eliminó.
- La tabla
expired: clientes cuyas concesiones caducaron, conexpired_at. - Registro unificado: decisiones de
tccdencom.apple.TCC, mientras la retención lo permita. - Telemetría de Endpoint Security (macOS 15.4+): eventos de modificación de TCC.
Lista de comprobación
- Recopile
TCC.db,TCC.db-walyTCC.db-shmjuntos, del sistema y de cada usuario, antes de que nadie ejecutetccutilo abra los archivos. - Calcule los hashes y trabaje sobre copias de las copias.
- Cargue la carpeta en TCC Parser y abra History.
- Para cada fila de alto impacto eliminada o modificada, anote el origen (commit del WAL, generación antigua, sin confirmar, página liberada).
- Busque el cambio correspondiente en el registro unificado, el historial de shell y los datos del EDR.
- Revise los snapshots y las copias de seguridad en busca de una copia anterior completa.
Glosario: WAL de SQLite, tccutil.