Skip to content

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.

Publicado el 8 min de lectura

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)

CampoNotas
Número mágico0x377f0682 o 0x377f0683 (determina el orden de bytes de la suma de comprobación)
Versión del formato3007000
Tamaño de páginaEl mismo que el de la base de datos
Secuencia de checkpointSe incrementa con cada checkpoint
Salt-1, Salt-2Valores aleatorios que cambian cada vez que el WAL se reinicia
Checksum-1, Checksum-2Sobre 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 frameNotas
Número de páginaQué página de la base de datos sustituye este frame
Tamaño de la base de datosTamaño en páginas tras el commit, en los frames de commit; 0 en los demás
Salt-1, Salt-2Deben coincidir con la cabecera del WAL para que el frame sea válido
Checksum-1, Checksum-2Suma 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:

OrigenQué contieneProbabilidad
Commits anteriores en el WAL actualCopias antiguas de la misma página, anteriores a un commit posterior en el mismo WALHabitual cuando hubo varios cambios entre checkpoints
Frames de una generación antiguaFrames de una generación anterior del WAL (salts distintos) que aún no se han sobrescritoHabitual: tras un checkpoint, el WAL se reutiliza y se reinicia con salts nuevos, no se pone a cero
Frames sin confirmarFrames posteriores al último frame de commitOcasional; cambios que nunca se confirmaron
Páginas liberadas del archivo principalPáginas liberadas cuando se redujeron filas o tablasOcasional; 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:

  1. 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.
  2. 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.
  3. 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, csreq o last_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:

Origenserviceclientauth_valuelast_modified (UTC)
WAL, commit anteriorkTCCServiceAccessibility/Users/dana.whitlock/Library/Caches/.sync/sync-helper2 (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.yaml de UAC, el módulo TCC de Aftermath) copian solo TCC.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, con expired_at.
  • Registro unificado: decisiones de tccd en com.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

  1. Recopile TCC.db, TCC.db-wal y TCC.db-shm juntos, del sistema y de cada usuario, antes de que nadie ejecute tccutil o abra los archivos.
  2. Calcule los hashes y trabaje sobre copias de las copias.
  3. Cargue la carpeta en TCC Parser y abra History.
  4. Para cada fila de alto impacto eliminada o modificada, anote el origen (commit del WAL, generación antigua, sin confirmar, página liberada).
  5. Busque el cambio correspondiente en el registro unificado, el historial de shell y los datos del EDR.
  6. Revise los snapshots y las copias de seguridad en busca de una copia anterior completa.

Glosario: WAL de SQLite, tccutil.

Artículos relacionados

Artículos relacionados

Cómo cambió la tabla access de TCC.db de Mojave a Big Sur y Sonoma: allowed y prompt_count, auth_value y auth_reason, pid y last_reminded, y las demás tablas.
Paso a paso: cargue los TCC.db de macOS y su WAL en un parser gratuito en el navegador, lea los hallazgos, revise permisos e historial, y exporte.
Cómo leer los blobs csreq e indirect_object_code_identity de TCC.db: requisitos compilados, anclas, identificadores y clientes ad hoc con solo cdhash.