O banco SQLite não é um arquivo isolado: WAL, journals e estados concorrentes na análise forense

Bancos de dados SQLite estão presentes em navegadores, aplicativos de mensagens, sistemas operacionais, dispositivos móveis, programas corporativos e equipamentos embarcados. Eles podem armazenar históricos de navegação, contatos, conversas, chamadas, localizações, credenciais, configurações, arquivos recentes e diversos outros artefatos de interesse forense.

Por ser frequentemente apresentado como um banco de dados contido em um único arquivo, o SQLite pode induzir a uma simplificação perigosa: considerar que a abertura do arquivo principal revela todo o estado disponível da base.

Em muitos casos, isso não é verdade.

O estado lógico de um banco SQLite pode estar distribuído entre o arquivo principal, o Write-Ahead Log — WAL —, o rollback journal, estruturas internas de páginas, regiões não alocadas e artefatos transitórios. Uma consulta SQL convencional normalmente apresenta apenas a visão ativa e coerente que o mecanismo decidiu expor naquele momento.

Registros eliminados, versões anteriores de páginas e transações ainda não incorporadas ao arquivo principal podem permanecer fora dessa visão, embora continuem fisicamente presentes no conjunto adquirido.

O desafio forense não consiste apenas em “abrir o banco de dados”, mas em reconstruir quais estados podem ser tecnicamente sustentados a partir dos arquivos e estruturas preservados.

Estado lógico e estado físico não são equivalentes

Uma consulta como:

SELECT * FROM messages;

retorna as linhas que pertencem ao estado lógico reconhecido pelo SQLite. Ela não percorre automaticamente:

  • páginas livres;
  • blocos livres dentro de páginas ativas;
  • regiões não alocadas;
  • versões anteriores armazenadas no WAL;
  • páginas preservadas em rollback journals;
  • fragmentos existentes em arquivos temporários;
  • conteúdos desvinculados da estrutura B-tree;
  • dados presentes além do limite lógico de um arquivo.

Assim, uma linha ausente do resultado de uma consulta pode:

  • nunca ter existido;
  • ter sido eliminada;
  • estar em uma transação ainda não incorporada ao arquivo principal;
  • pertencer a uma versão anterior de uma página;
  • permanecer parcialmente recuperável em espaço livre;
  • ter sido sobrescrita;
  • ter sido zerada por configuração de exclusão segura;
  • estar armazenada em outro banco utilizado pelo mesmo aplicativo.

A consulta SQL responde ao estado lógico atual. A análise forense procura também compreender os vestígios deixados pelas transições entre estados.

O arquivo principal e sua organização em páginas

O arquivo principal do SQLite é dividido em páginas de tamanho fixo. O tamanho da página é definido no cabeçalho do banco e geralmente permanece constante durante sua existência.

Cada página possui uma função estrutural. Segundo a especificação oficial do formato SQLite, uma página pode representar, entre outras possibilidades:

  • página interna de B-tree;
  • página folha de tabela;
  • página folha de índice;
  • página da freelist;
  • página de overflow;
  • página de mapa de ponteiros;
  • região reservada para bloqueio.

As tabelas comuns são organizadas em B-trees. Nas páginas folha, cada registro é armazenado como uma célula composta por elementos como:

  • tamanho do conteúdo;
  • identificador da linha, quando aplicável;
  • cabeçalho do registro;
  • tipos seriais das colunas;
  • valores armazenados;
  • eventual referência para páginas de overflow.

Diversos desses campos são codificados por inteiros de comprimento variável, denominados varints. A correta interpretação de um registro exige respeitar a estrutura da página, o esquema da tabela e as regras de codificação do formato.

Uma sequência de caracteres legível encontrada no arquivo não é, por si só, um registro SQLite validamente reconstruído.

Registros ativos, blocos livres e regiões não alocadas

Dentro de uma página B-tree, o SQLite mantém uma matriz de ponteiros para as células ativas. Quando registros são eliminados ou reorganizados, podem surgir regiões que já não pertencem a células ativas.

A documentação do formato diferencia:

  • região não alocada entre a matriz de ponteiros e a área de conteúdo;
  • freeblocks encadeados dentro da página;
  • pequenos fragmentos livres;
  • páginas inteiras inseridas na freelist.

Essas estruturas possuem implicações forenses diferentes.

Freeblocks

Um freeblock representa uma região livre dentro de uma página B-tree ainda ativa. Ele contém um pequeno cabeçalho que indica seu tamanho e o próximo bloco da cadeia.

Parte do conteúdo anteriormente armazenado nessa região pode permanecer depois da exclusão ou reorganização. Entretanto, os primeiros bytes podem ter sido substituídos pelo cabeçalho do próprio freeblock, comprometendo o início do registro anterior.

A recuperação pode, portanto, resultar em:

  • registro completo;
  • registro sem o cabeçalho original;
  • apenas algumas colunas;
  • texto isolado;
  • fragmento sem associação segura a uma tabela;
  • combinação de bytes antigos e novos.

Região não alocada da página

O espaço entre a matriz de ponteiros e o início da área de células é considerado não alocado. Esse espaço pode conter zeros, resíduos de operações anteriores ou bytes ainda não utilizados.

Sua mera localização em uma página de determinada tabela não demonstra automaticamente que todo conteúdo encontrado pertenceu àquela tabela. Reorganizações internas e reutilização de páginas podem modificar o contexto histórico.

Freelist

Quando uma página deixa de ser necessária, ela pode ser inserida na freelist. O cabeçalho do banco mantém o número da primeira página-tronco e a quantidade total de páginas livres.

A documentação do SQLite esclarece que as páginas da freelist não estão em uso ativo e podem ser reutilizadas quando o banco precisar de espaço adicional.

Embora a estrutura lógica já não dependa de seu conteúdo anterior, bytes residuais podem permanecer até que a página seja reutilizada, reorganizada ou zerada.

A presença de um registro em página livre demonstra que seus bytes foram preservados, mas não determina, isoladamente:

  • quando o registro foi eliminado;
  • qual operação causou sua liberação;
  • quanto tempo permaneceu ativo;
  • se a página já foi utilizada por outra tabela;
  • se o conteúdo foi parcialmente sobrescrito;
  • se a linha chegou a integrar uma transação confirmada.

O modo WAL e a distribuição do estado

No modo Write-Ahead Logging, as alterações não são gravadas imediatamente sobre as páginas correspondentes no arquivo principal. Elas são acrescentadas ao arquivo com sufixo -wal.

De acordo com a documentação oficial do modo WAL, uma transação pode ser confirmada pela inserção de um marcador de commit no WAL sem que as páginas modificadas tenham sido imediatamente transferidas para o banco principal.

O estado atual pode ser representado, de forma simplificada, como:

[
D_t = D_0 \oplus W_t
]

onde:

  • (D_0) representa o estado das páginas no arquivo principal;
  • (W_t) representa o conjunto de quadros válidos do WAL até o marcador de confirmação aplicável;
  • (D_t) representa a visão lógica observada por uma conexão em determinado instante.

Consequentemente, copiar apenas o arquivo principal pode produzir uma visão antiga da base.

A documentação do SQLite é explícita ao afirmar que o WAL integra o estado persistente do banco e deve acompanhar o arquivo principal quando ele é copiado ou movido. Separá-los pode causar perda de transações já confirmadas ou produzir um banco inconsistente.

Para a análise forense, isso significa que arquivos como:

messages.db
messages.db-wal
messages.db-shm

devem ser identificados e preservados de maneira coordenada.

O arquivo -shm funciona principalmente como índice de memória compartilhada e mecanismo de coordenação. Ele não contém o conteúdo principal do banco e não é necessário, segundo a descrição do formato WAL, para a recuperação depois de uma falha. Ainda assim, sua presença, tamanho e metadados podem contribuir para documentar o estado em que o conjunto foi encontrado.

Quadros, páginas e transações no WAL

O WAL possui um cabeçalho seguido por uma sequência de quadros. Cada quadro contém, essencialmente:

  • número da página do banco;
  • tamanho do banco após um eventual commit;
  • valores de controle;
  • cópia completa da página modificada.

A mesma página pode aparecer várias vezes no WAL. Quando isso acontece, versões sucessivas daquela página podem estar presentes no mesmo arquivo.

Para construir uma visão lógica, o SQLite precisa determinar:

  1. quais quadros pertencem à geração válida do WAL;
  2. quais quadros possuem verificações consistentes;
  3. onde estão os marcadores de commit;
  4. qual é o ponto final aplicável à leitura;
  5. qual versão de cada página deve prevalecer.

Uma leitura iniciada em determinado instante utiliza um “marcador final” correspondente ao último commit visível naquele momento. Conexões simultâneas podem, portanto, observar instantâneos diferentes enquanto novos quadros são acrescentados.

Esse comportamento é importante para a análise temporal. O WAL pode conter várias versões de uma página, mas a ordem física dos quadros não deve ser convertida automaticamente em horário civil.

A sequência permite estabelecer uma ordem transacional relativa. Para associá-la a datas e horas, são necessários elementos adicionais, como:

  • colunas temporais da própria aplicação;
  • logs do sistema;
  • registros de sincronização;
  • metadados do sistema de arquivos;
  • eventos correlatos;
  • comportamento documentado do aplicativo.

Checkpoint não é simples cópia

O processo que transfere páginas confirmadas do WAL para o arquivo principal é denominado checkpoint.

Após o checkpoint, as páginas correspondentes podem estar presentes no arquivo principal, mas o WAL não é necessariamente apagado imediatamente. A documentação informa que o arquivo pode ser reutilizado a partir do início, pois sobrescrever o arquivo existente costuma ser mais eficiente do que recriá-lo.

O SQLite pode executar checkpoints automaticamente, inclusive quando o WAL atinge determinado número de páginas ou quando a última conexão é encerrada. Aplicações também podem alterar esse comportamento.

Isso produz diferentes cenários forenses:

  • transações confirmadas presentes apenas no WAL;
  • páginas já incorporadas ao arquivo principal, mas ainda existentes no WAL;
  • quadros antigos parcialmente sobrescritos;
  • WAL truncado;
  • WAL eliminado após encerramento normal;
  • WAL preservado em razão de encerramento anormal;
  • WAL crescente por impedimento de conclusão do checkpoint.

A inexistência de um arquivo WAL no momento da coleta não demonstra que o banco nunca operou nesse modo. Ele pode ter sido processado e eliminado durante o encerramento normal da aplicação.

Da mesma forma, o tamanho do WAL não corresponde diretamente à quantidade de transações ainda não incorporadas. O arquivo pode conter páginas já processadas, áreas reutilizadas ou estruturas que não pertencem ao último estado confirmado.

O risco de abrir o banco original

Abrir um banco SQLite com uma ferramenta convencional pode modificar o próprio conjunto que se pretende examinar.

Dependendo do estado do banco e da forma de abertura, o mecanismo pode:

  • executar recuperação automática;
  • processar um hot journal;
  • realizar checkpoint;
  • criar ou alterar o arquivo -shm;
  • eliminar o WAL depois do fechamento;
  • atualizar estruturas auxiliares;
  • modificar metadados do sistema de arquivos.

Por isso, não é recomendável abrir diretamente os arquivos originais coletados apenas para “verificar se o banco funciona”.

Uma abordagem tecnicamente controlada deve:

  1. preservar os arquivos relacionados;
  2. calcular hashes individualmente;
  3. trabalhar sobre cópias;
  4. registrar a ferramenta e a versão;
  5. controlar o modo de abertura;
  6. comparar os arquivos antes e depois do procedimento;
  7. manter uma cópia não processada para análise estrutural.

Uma versão reconstruída para consulta SQL e o conjunto original não são o mesmo objeto. Ambos podem ser necessários, mas devem permanecer claramente diferenciados.

Rollback journal e páginas anteriores

No modo tradicional de rollback, o SQLite preserva versões anteriores das páginas antes de modificá-las no arquivo principal.

Se a transação for interrompida, essas páginas podem ser utilizadas para restaurar o banco ao estado anterior. Um hot journal indica que houve interrupção durante uma operação que ainda precisa ser revertida.

A descrição do processo de confirmação atômica do SQLite explica que, ao detectar um hot journal, o mecanismo pode copiar as páginas anteriores de volta para o banco e, depois, excluir, truncar ou invalidar o journal.

Do ponto de vista forense, o rollback journal é diferente do WAL:

  • o WAL tende a armazenar novas versões das páginas;
  • o rollback journal preserva páginas anteriores necessárias para desfazer alterações;
  • a existência de um journal não significa que todas as alterações chegaram a ser confirmadas;
  • a ausência do journal não exclui transações anteriores;
  • o processamento automático pode alterar o conjunto coletado.

Uma página encontrada no rollback journal deve ser interpretada como imagem anterior de determinada página do banco, dentro de um mecanismo de recuperação transacional. Ela não pode ser tratada automaticamente como prova de que todos os registros contidos nela estavam ativos no momento final investigado.

Transações confirmadas e não confirmadas

Um dos maiores riscos interpretativos está em confundir bytes presentes com dados logicamente confirmados.

Uma sequência pode estar fisicamente registrada em:

  • quadro pertencente a transação confirmada;
  • quadro posterior ao último marcador válido;
  • página de transação abortada;
  • rollback journal;
  • área residual de WAL reutilizado;
  • fragmento parcialmente sobrescrito;
  • cache ou arquivo temporário.

A presença física comprova que os bytes foram encontrados naquela estrutura. A atribuição de estado transacional exige análise adicional.

Para sustentar que determinada linha integrou um estado confirmado do banco, é necessário examinar:

  • validade do cabeçalho do WAL;
  • valores de salt;
  • verificações dos quadros;
  • sequência de páginas;
  • marcadores de commit;
  • tamanho lógico do banco;
  • relação entre versões sucessivas da mesma página;
  • esquema aplicável naquele estado;
  • integridade das células reconstruídas.

Uma ferramenta pode exibir um registro recuperado sem informar se ele pertenceu a uma transação confirmada. A apresentação visual não substitui a validação estrutural.

Exclusão lógica não significa remoção física imediata

Uma instrução como:

DELETE FROM messages WHERE id = 1042;

remove a linha do estado lógico da tabela. Isso não implica, necessariamente, que todos os bytes anteriormente associados ao registro sejam imediatamente apagados.

Dependendo da estrutura, do modo de journal e das configurações, fragmentos podem permanecer em:

  • freeblocks;
  • regiões não alocadas da página;
  • páginas da freelist;
  • páginas de overflow;
  • versões anteriores no WAL;
  • rollback journals;
  • espaço não alocado do sistema de arquivos.

Entretanto, a recuperação não é garantida. O conteúdo pode ser:

  • reutilizado por novas células;
  • sobrescrito por novos quadros;
  • reorganizado;
  • truncado;
  • zerado;
  • eliminado por VACUUM;
  • afetado pelo mecanismo de armazenamento subjacente.

Portanto, “registro excluído” e “registro recuperável” não são categorias equivalentes.

secure_delete, VACUUM e redução de vestígios

O comportamento de exclusão pode ser alterado pelo PRAGMA secure_delete.

Segundo a documentação do SQLite, quando essa opção está ativada, o conteúdo eliminado de tabelas comuns é sobrescrito com zeros. Na configuração FAST, determinados vestígios podem ser removidos das páginas B-tree enquanto permanecem em páginas da freelist.

Mesmo com exclusão segura, podem existir limitações. A documentação ressalta que tabelas virtuais, como algumas estruturas de pesquisa textual, podem manter conteúdo em tabelas auxiliares.

O comando VACUUM, por sua vez, reconstrói o banco em um arquivo temporário e copia o resultado de volta ao arquivo original. Esse processo reorganiza páginas e tende a eliminar áreas livres internas que poderiam conter vestígios.

A ausência de registros recuperáveis após VACUUM ou secure_delete não demonstra necessariamente que a informação nunca existiu. Ela pode indicar apenas que os resíduos estruturais foram reduzidos ou eliminados.

Também é necessário considerar o sistema de arquivos e a mídia. A reconstrução pode criar arquivos temporários, e versões anteriores podem permanecer em cópias de segurança, instantâneos, armazenamento não alocado ou outras fontes.

Recuperação de registros não é simples busca de texto

Procurar palavras conhecidas em um banco pode localizar conteúdo relevante, mas não equivale à reconstrução de um registro.

Um registro SQLite possui:

  • cabeçalho;
  • quantidade e tipos das colunas;
  • codificação de inteiros;
  • comprimentos variáveis;
  • identificador de linha;
  • conteúdo local;
  • eventual cadeia de overflow;
  • relação com uma página e um esquema.

Uma cadeia de caracteres encontrada isoladamente pode ser:

  • valor de uma coluna ativa;
  • conteúdo eliminado;
  • parte de um índice;
  • duplicação em tabela auxiliar;
  • versão anterior no WAL;
  • fragmento de página;
  • valor armazenado em cache;
  • texto sem relação com a tabela presumida.

A reconstrução tecnicamente robusta deve buscar consistência entre:

  1. estrutura da página;
  2. posição da célula;
  3. codificação dos tipos seriais;
  4. quantidade de colunas;
  5. esquema da tabela;
  6. comprimento do conteúdo;
  7. páginas de overflow;
  8. contexto transacional;
  9. fontes correlatas.

Quanto mais elementos forem preservados, maior a força da associação.

Falsos positivos na recuperação automática

Ferramentas de recuperação podem interpretar combinações de bytes como registros eliminados mesmo quando não existe sustentação estrutural suficiente.

Esse risco aumenta quando:

  • o esquema foi inferido incorretamente;
  • há páginas parcialmente sobrescritas;
  • o conteúdo é procurado fora de páginas compatíveis;
  • varints são interpretados a partir do deslocamento errado;
  • registros têm número variável de colunas;
  • páginas foram reutilizadas por tabelas diferentes;
  • dados comprimidos ou cifrados produzem padrões coincidentes;
  • fragmentos são combinados artificialmente.

Um resultado recuperado deve ser acompanhado, sempre que possível, de:

  • arquivo de origem;
  • número da página;
  • deslocamento;
  • tipo de página;
  • estado da página;
  • tabela atribuída;
  • método de reconstrução;
  • bytes originais;
  • campos decodificados;
  • integridade estrutural;
  • nível de confiança;
  • limitações encontradas.

A transparência do processo permite distinguir uma reconstrução verificável de uma interpretação produzida apenas pela ferramenta.

Reconstrução de versões históricas

O WAL pode permitir a reconstrução de estados sucessivos do banco porque diferentes versões da mesma página podem aparecer em quadros distintos.

Uma metodologia conceitual pode seguir estas etapas:

  1. preservar o banco principal e seus arquivos auxiliares;
  2. identificar a geração válida do WAL;
  3. validar quadros e marcadores de confirmação;
  4. estabelecer os limites de cada transação;
  5. aplicar progressivamente as páginas confirmadas;
  6. gerar estados intermediários;
  7. comparar tabelas e registros entre os estados;
  8. documentar alterações, inserções e exclusões.

Pesquisas como SQLite Forensic Analysis Based on WAL exploram justamente a reconstrução de versões históricas a partir do banco principal e dos quadros do WAL.

Contudo, uma sequência de versões não é automaticamente uma cronologia absoluta. Ela estabelece precedência transacional:

[
V_0 \rightarrow V_1 \rightarrow V_2
]

Para transformar essa sequência em:

[
10h02 \rightarrow 10h05 \rightarrow 10h09
]

são necessários timestamps confiáveis ou correlação com fontes externas.

A ordem das transações e o horário em que ocorreram são dimensões diferentes.

Exemplo: mensagem editada e posteriormente eliminada

Considere um aplicativo que armazena mensagens em chat.db e utiliza WAL.

A consulta à tabela ativa não apresenta determinada mensagem. No entanto, a análise estrutural identifica:

  • uma versão da página no WAL contendo o texto original;
  • uma versão posterior contendo texto editado;
  • outra versão na qual a linha já não está presente;
  • fragmento residual em um freeblock;
  • evento correlato no registro de notificações.

Esses elementos podem sustentar a hipótese de que a mensagem passou por diferentes estados.

Ainda assim, é necessário responder:

  • os quadros pertencem à mesma geração válida do WAL?
  • cada versão está dentro de transação confirmada?
  • o esquema permaneceu igual?
  • o identificador da linha é consistente?
  • os textos pertencem à mesma célula lógica?
  • houve sincronização capaz de alterar o banco automaticamente?
  • os horários armazenados representam criação, edição, recebimento ou sincronização?
  • o fragmento residual contém estrutura suficiente para associação?

Uma formulação tecnicamente prudente seria:

Foram identificadas três versões estruturalmente consistentes da página associada à tabela de mensagens. Os quadros válidos indicam, em ordem transacional, a presença do texto original, sua substituição por outro conteúdo e a posterior ausência da linha. A atribuição dos horários decorre dos campos mantidos pela aplicação e da correlação com o registro de notificações.

Essa descrição separa observação física, reconstrução lógica e atribuição temporal.

Preservação adequada do conjunto SQLite

Quando um banco SQLite for identificado, a coleta não deve se limitar automaticamente ao arquivo com extensão .db ou .sqlite.

Devem ser procurados, conforme o caso:

  • arquivo principal;
  • arquivo -wal;
  • arquivo -shm;
  • arquivo -journal;
  • super-journals;
  • bancos auxiliares;
  • tabelas de pesquisa textual;
  • diretórios de anexos;
  • caches;
  • arquivos de configuração;
  • cópias de segurança;
  • registros de sincronização;
  • artefatos temporários.

Em uma coleta ao vivo, esses arquivos podem mudar durante a cópia. Adquirir o banco em um instante e o WAL vários segundos depois pode produzir um conjunto que nunca existiu de forma coerente.

Quando possível, devem ser utilizados recursos de snapshot, mecanismos nativos de backup ou técnicas capazes de reduzir a diferença temporal entre os componentes. Quando isso não for possível, a ordem, os horários e as alterações observadas devem ser documentados.

A aquisição perfeita pode ser inviável, mas a limitação precisa permanecer visível.

Linguagem recomendada no relatório técnico

Em vez de:

O banco de dados não continha a mensagem.

É mais preciso afirmar:

A mensagem não estava presente na visão lógica ativa obtida pela consulta à tabela messages.

Quando arquivos auxiliares não foram preservados:

O conjunto recebido continha apenas o arquivo principal. Não foram disponibilizados WAL, journal ou outros arquivos auxiliares. Por esse motivo, não foi possível avaliar transações ainda não incorporadas ao banco principal nem versões de páginas eventualmente existentes nesses arquivos.

Quando houver recuperação residual:

Foi identificado um fragmento textual em região livre da página 428. O fragmento não preserva integralmente o cabeçalho da célula e, isoladamente, não permite confirmar todas as colunas nem estabelecer se o registro integrou uma transação confirmada.

Quando houver validação suficiente:

O registro foi reconstruído a partir de quadro válido do WAL, anterior a marcador de confirmação consistente. O número da página, o esquema, os tipos seriais, o identificador da linha e o conteúdo apresentam compatibilidade estrutural.

Quando houver versões sucessivas:

Os quadros do WAL sustentam uma ordem relativa entre as versões identificadas. O arquivo não fornece, por si só, horário civil para cada alteração.

Essas formulações preservam a diferença entre presença de bytes, validade estrutural, estado transacional e atribuição temporal.

Síntese e implicações técnico-forenses

Um banco SQLite não deve ser tratado apenas como uma tabela aberta por uma interface gráfica. Seu estado pode estar distribuído entre diferentes arquivos, páginas e mecanismos transacionais.

O arquivo principal pode representar um estado anterior ao que estava visível para a aplicação. O WAL pode conter transações confirmadas ainda não incorporadas, versões sucessivas de páginas e resíduos de ciclos anteriores. O rollback journal pode preservar páginas anteriores necessárias à reversão. Freeblocks, regiões não alocadas e páginas da freelist podem conservar fragmentos de registros eliminados.

Nenhuma dessas estruturas deve ser interpretada isoladamente.

A presença de bytes não demonstra, por si só, que uma linha:

  • pertenceu à tabela presumida;
  • esteve ativa;
  • integrou uma transação confirmada;
  • existiu no horário atribuído;
  • foi criada pelo usuário;
  • foi eliminada intencionalmente.

A análise forense exige relacionar estrutura física, esquema lógico, mecanismo transacional, ordem de páginas, configurações do banco e comportamento da aplicação.

O ponto central não é apenas perguntar quais registros aparecem na consulta, mas:

Quais estados do banco podem ser reconstruídos, quais estruturas sustentam essa reconstrução e qual grau de confiança pode ser atribuído a cada registro?

No SQLite, o estado visível é apenas uma das camadas disponíveis. O valor forense frequentemente reside nas diferenças entre aquilo que o banco apresenta, aquilo que suas estruturas ainda preservam e aquilo que já não pode ser recuperado.

Referências


Secret Link