Como um PDF adulterado se revela: forense de estrutura, não de aparência
Vim de KYC/AML e compliance fiscal antes de construir o Tamperlens, e a intuição que carrego dessa vida é chata de repetir para quem está começando a olhar documento: abrir bem não é integridade. Um PDF que abre no leitor, mostra o carimbo certo, imprime bonito e devolve “200 OK” quando você faz o download não te diz nada sobre ter sido editado depois de assinado. A aparência é a camada que o adulterador controla. A estrutura é a que ele esquece.
Este post é sobre a estrutura — como um PDF é montado por dentro, onde a edição fica gravada mesmo quando a página parece intacta, e como raciocinar sobre isso. Não é um pitch. Se você sai daqui sabendo abrir um PDF num editor de bytes e entender o que está vendo, cumpri o que vim fazer.
As quatro partes de um PDF
Um PDF, no osso, tem quatro seções, nesta ordem no arquivo:
- Header — uma linha,
%PDF-1.7ou parecido. - Body — a lista de objetos: páginas, fontes, imagens, o catálogo, os fluxos de conteúdo. Cada objeto é numerado (
12 0 obj … endobj). - Tabela de referências cruzadas (xref) — um índice que diz, para cada objeto, em que byte do arquivo ele começa. É o que permite ao leitor pular direto para a página 40 sem ler as 39 anteriores.
- Trailer — aponta para a raiz do documento e, crucialmente, diz onde a tabela xref começa, com a palavra-chave
startxrefseguida de um offset.
O leitor abre o arquivo, vai para o fim, lê o startxref, salta para a tabela xref, e a partir dela encontra todo o resto. Guarde isso: um PDF se lê de trás para frente. É essa decisão de design de 1993 que torna a adulteração visível.
O pecado original: a atualização incremental
Aqui está o detalhe que quase ninguém que não mexe com o formato conhece. Quando você edita um PDF e salva, o padrão não reescreve o arquivo. Ele anexa. Os bytes novos vão para o fim, seguidos de uma nova tabela xref que registra só o que mudou, seguida de um novo trailer com um startxref apontando para essa nova tabela.
E esse novo trailer carrega uma entrada /Prev: o offset da tabela xref anterior. Ou seja, cada revisão aponta para a que veio antes, formando uma cadeia encadeada para trás, até a original. O arquivo inteiro vira uma pilha de camadas de sedimento, e cada camada é uma vez que alguém abriu, mexeu e salvou.
Por que o padrão faz isso? Por dois motivos legítimos: permite salvar rápido (não precisa reescrever 200 páginas para trocar uma), e permite assinar sem invalidar assinaturas anteriores — o assinante seguinte anexa, não reescreve. É por isso que a atualização incremental existe, e é por isso que ela não é, por si só, prova de fraude. É prova de que o arquivo foi escrito mais de uma vez. O que estava na segunda vez é a próxima pergunta.
O Tamperlens tem uma família de sinais chamada incremental-updates que faz exatamente essa leitura: conta as revisões e, para cada uma que veio depois da primeira, lista quais objetos foram escritos e quais foram sobrescritos. Sobrescrever um objeto do tipo page, content ou image numa revisão posterior é a assinatura estrutural de “alguém mudou o que estava visível na página depois que o documento foi finalizado”. O objeto 12 existia; a revisão 3 escreveu um objeto 12 novo que a xref nova passa a apontar, e o antigo continua ali, enterrado, byte por byte, ainda legível para quem sabe onde procurar.
O primeiro jeito de errar: contar revisão demais
A parte honesta dessa história é que a contagem ingênua de revisões mente. Dois mecanismos perfeitamente normais escrevem duas seções de referência cruzada num único save:
- Linearização (o “fast web view”, que deixa o PDF começar a renderizar antes de baixar inteiro) escreve uma segunda tabela xref por desenho.
- Layout de referência híbrida — uma tabela clássica para leitores PDF 1.4 antigos, mais uma segunda que aponta para um
/XRefStm— também escreve duas.
Se você conta seções de xref, um PDF recém-exportado do Word, salvo uma única vez, aparece como “duas revisões” e você grita fraude num arquivo virgem. O incremental-updates do Tamperlens trata cada um desses pares como um save — o parser reconhece o par de linearização e o par híbrido e os colapsa — e por isso a família fica calada num “fast web view” impecável e num Word, Excel ou PowerPoint comum sem assinatura. A contagem de revisões que ele reporta é deliberadamente menor que o número de startxref que você conta no arquivo bruto, e o próprio achado explica por quê, porque um analista que compara os dois números precisa saber a razão.
Isso é o que separa forense de teatro: um sinal que dispara no documento legítimo mais comum do mercado não é um sinal, é um gerador de falso positivo. Voltarei a esse ponto — ele é o assunto de outro post inteiro.
As causas benignas têm nome
Uma revisão extra genuína tem explicações inocentes, e o correto é nomeá-las, não escondê-las: assinatura, preenchimento de formulário, anotação. Cada uma anexa bytes legitimamente. A pergunta forense nunca é “tem mais de uma revisão?” — é “o que a revisão posterior tocou?”. Uma assinatura que anexa um objeto de assinatura é uma coisa. Uma “assinatura” que, na mesma anexação, sobrescreve o objeto de conteúdo da página 2 é outra completamente diferente, e a estrutura conta as duas com a mesma clareza.
/ByteRange: por que “assinado” não fecha a conversa
Agora a parte que mais gente entende errado, inclusive gente de compliance. Uma assinatura digital num PDF cobre um intervalo de bytes declarado, o /ByteRange. Ela não cobre magicamente “o documento”. Ela cobre os bytes que existiam quando foi aplicada.
E o que impede alguém de assinar, e depois anexar uma atualização incremental com bytes novos, além do fim do intervalo assinado? Nada, na estrutura do formato. A assinatura continua criptograficamente válida — ela cobre o que cobria — mas há bytes no arquivo que ela não cobre. O signature-coverage do Tamperlens compara o /ByteRange de cada assinatura contra o tamanho do arquivo e reporta exatamente essa diferença: quantos bytes ficaram além da cobertura, e onde a cobertura termina.
Repare no que essa família não faz: ela não verifica a criptografia da assinatura. Ela reporta cobertura. É uma distinção que importa, porque muita “verificação de assinatura” por aí responde à pergunta errada — confirma que a matemática da assinatura fecha e declara o documento válido, sem nunca perguntar se a assinatura cobre o arquivo inteiro ou só os primeiros dois terços dele.
O caso que prova que a banda de risco não é um oráculo
Deixe-me dar o exemplo mais desconfortável que temos, porque ele é a melhor aula sobre “abrir bem não é integridade” — e sobre humildade em medição.
Em 11 de agosto de 2026 rodamos o engine sobre 1.317 contratos públicos do PNCP (o portal de contratações do governo brasileiro). Resultado: 77% dos contratos assinados pontuam high — a banda de risco mais alta. E o gatilho não é adulteração. É uma segunda assinatura.
O mecanismo é lindo de tão simples. Um contrato tem duas partes que assinam. O primeiro assinante assina; seu /ByteRange cobre o arquivo até ali. O segundo assinante (a contra-assinatura) anexa os bytes dele — que caem, necessariamente, além do /ByteRange do primeiro. Do ponto de vista da estrutura, isso é literalmente idêntico a alguém ter alterado o documento depois da primeira assinatura: signature-coverage dispara (há bytes além da cobertura), incremental-updates dispara (há uma revisão posterior), id-inconsistency dispara (o identificador do trailer mudou). Três sinais acendem. E, ao mesmo tempo, a família que verifica a integridade das assinaturas reporta ambas intactas.
Cada afirmação é verdadeira sobre os bytes. E, mesmo assim, num contrato de duas partes — o documento legítimo mais comum nesse mercado — a banda high é ruidosa e pouco informativa. É por isso que não trato a pontuação como veredito. A pontuação é a soma dos sinais; o que cada sinal significa exige ler o achado, não o número. Num contrato de duas assinaturas, os sinais a ler são os de integridade e de permissões da assinatura, não a banda.
Publicar esse achado sobre a própria ferramenta — “olha o falso positivo que ela produz no documento mais comum do mercado” — é o oposto de marketing. É a única postura defensável para quem vende confiança.
Como a pontuação é montada (e por que ela é conservadora)
Já que citei a banda, vale abrir a caixa. O Tamperlens agrega os sinais numa nota de 0 a 100 com um contrato explícito:
- Qualquer sinal de severidade
highforça a nota para pelo menos 70 — a bandahighcomeça em 70. - E, ao contrário, a banda
highé reservada para sinaishigh: sem nenhum deles, a nota é limitada a 69.banda === highé equivalente a “existe pelo menos um sinal que, sozinho, estabelece uma alteração”. - Vários sinais
mediumacumulam, mas com retornos decrescentes depois dos dois primeiros da mesma severidade — um monte de sinais fracos, sozinho, não alcança 100. - Bandas: abaixo de 30 é
low, 30 a 69 éelevated, 70 ou mais éhigh.
O ponto de projeto aqui é a assimetria deliberada: fraqueza não vira força por acumulação. Você não consegue empilhar dez suspeitas fracas e produzir uma certeza. A certeza — a banda alta — é reservada para o tipo de achado que, isolado, já é uma alteração. Isso é uma escolha de risco, não de engenharia: prefiro que a nota alta signifique algo firme a que ela seja fácil de atingir.
Como raciocinar sobre um PDF, na prática
Se você for olhar um PDF suspeito à mão, o roteiro é este:
- Vá para o fim. Encontre o último
startxref. Conte quantosstartxrefo arquivo tem — descontando os pares de linearização e híbrido, cada um vale um save de verdade. - Ande a cadeia
/Prevpara trás. Cada trailer aponta para a xref anterior. Cada salto é uma vez que o arquivo foi escrito. - Em cada revisão posterior à primeira, pergunte o que foi sobrescrito. Um objeto de
page,contentouimagereescrito é a pergunta que importa. - Se há assinatura, compare o
/ByteRangecom o tamanho do arquivo. Sobrou byte além da cobertura? Descubra o que é. Contra-assinatura legítima e adulteração produzem a mesma sombra aqui — a diferença está no que os bytes anexados fazem, não na sua mera existência. - Trate a nota como um resumo, nunca como um veredito. Leia os sinais.
Duas defesas de sanidade que o formato exige, porque o arquivo é escrito pelo adversário: a cadeia /Prev é limitada (o Tamperlens trunca em 64 saltos — documentos reais acumulam algumas revisões, não milhares), e a tabela xref é limitada a 250.000 entradas no total. Um arquivo de 50 KB pode declarar uma cadeia de /Prev que consome gigabytes de RAM de um parser ingênuo; essa é a primeira coisa que um parser forense precisa recusar, porque o documento hostil vai tentar.
O resumo em uma frase
Um adulterador controla o que a página mostra. Ele não controla — e quase nunca limpa — o rastro que a estrutura do PDF guarda de cada save: a cadeia de revisões, os objetos que ele sobrescreveu, os bytes que ele anexou além da assinatura. “Abre bem” e “200 OK” são afirmações sobre a superfície. Integridade é uma afirmação sobre a estrutura. São perguntas diferentes, e só a segunda importa quando o documento é dinheiro, contrato ou identidade.
Se você quer ver esse raciocínio virar JSON num arquivo real, é para isso que o tamperlens.com existe. Mas o raciocínio é seu para ficar — leve-o mesmo que nunca use a ferramenta.
Precisa de um projeto técnico sob medida?
Arquitetura, TypeScript, APIs e automação, do protótipo à produção. Quem responde o seu e-mail é quem escreve o código, e o prazo que eu prometo é o prazo que eu consigo cumprir.
Me manda uma mensagem →