Um PDF não é apenas papel. É um contentor que pode transportar scripts que são executados quando o ficheiro abre, hiperligações que iniciam programas externos, hiperligações que acedem a servidores web, ficheiros aninhados dentro de outros ficheiros, e uma assinatura que atesta que o documento não foi alterado desde que alguém o validou. Quando um ficheiro provém de uma origem que não controla, o primeiro passo mais seguro não consiste em renderizá-lo. Passa sim por ler o que o ficheiro diz sobre si próprio e criar um inventário de tudo o que ele poderá tentar fazer, para que um utilizador possa decidir se este se enquadra no seu fluxo de trabalho.
Este artigo aborda uma etapa de auditoria estática e de apenas leitura sobre essa superfície de risco utilizando o componente PDFium para Delphi e Lazarus. A auditoria nunca desenha uma página. Analisa a estrutura do documento, enumera as partes do ficheiro que contêm comportamento e elabora um relatório simples. É a diferença entre pedir a um desconhecido para esvaziar os bolsos à porta e confiar nele porque sorriu.
O que é e o que não é uma auditoria
Importa definir claramente as fronteiras. Uma pré-visualização em ambiente isolado (sandbox) renderiza um ficheiro sob restrições rigorosas para que um utilizador possa olhar para ele sem que o ficheiro afete o resto da máquina. A auditoria ocorre antes disso. Trata-se de uma inspeção sem renderização cujo único resultado é uma descrição da superfície de ameaça: que scripts existem, quais as ações associadas às ligações, se o ficheiro está assinado e com que rigor, e o que está anexado. Executa-a quando um documento cruza uma fronteira de confiança, como na receção por e-mail, num formulário de carregamento ou num fluxo de parceiros, antes de qualquer etapa posterior o abrir efetivamente.
O componente carrega um documento da mesma forma para uma auditoria como para qualquer outra tarefa. Define o nome do ficheiro e ativa-o, o que analisa os dados de referência cruzada (cross-reference) e o catálogo do documento sem renderizar uma única página. Tudo o que é descrito abaixo é lido a partir desse estado carregado e não renderizado.
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'Incoming_Invoice.pdf';
Pdf.Active := True; // parses structure, renders nothing
// audit the loaded document here
finally
Pdf.Free;
end;
end;
JavaScript de documento na árvore de nomes
A primeira coisa a enumerar é o código. Um PDF pode conter JavaScript ao nível do documento: scripts que não estão associados a nenhuma página ou campo específico, mas sim ao próprio documento, guardados na árvore /Names sob uma entrada /JavaScript. Um visualizador em conformidade executa-os ao abrir o ficheiro. Este é o mecanismo subjacente a uma longa linhagem de malware em PDF, porque permite a um ficheiro executar lógica no instante em que o utilizador faz duplo clique, antes mesmo de ler uma única palavra.
Um auditor procura dois factos sobre cada um desses scripts: a sua existência e o seu conteúdo. O componente expõe a contagem e permite ler cada ação como um registo que contém o nome do script e o seu corpo completo. Ler o corpo é importante. Um script chamado Doc.0 não indica nada por si só, mas o seu texto pode chamar app.launchURL ou construir uma cadeia de texto e enviá-la para onde não deve. Extrair o código-fonte para que um revisor o possa analisar constitui o principal objetivo de sinalizar um ficheiro que executa código ao ser aberto.
var
I: Integer;
Action: TPdfJavaScriptAction;
begin
if Pdf.JavaScriptActionCount > 0 then
WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
' script(s) on open');
for I := 0 to Pdf.JavaScriptActionCount - 1 do
begin
Action := Pdf.JavaScriptAction[I];
WriteLn(' script "', Action.Name, '":');
WriteLn(Action.Script); // full body, for a human to read
end;
end;
Um ficheiro com zero scripts de documento não é automaticamente seguro, dado que também existem scripts de página e de campo, mas um ficheiro que contenha scripts ao nível do documento merece sempre atenção redobrada. A contagem de presença constitui por si só um filtro útil, e o corpo do script é o que transforma uma sinalização num veredito.
Ações Launch e URI
O comportamento seguinte a inventariar reside em hiperligações e anotações. Há dois tipos de ações particularmente importantes para um auditor. Uma ação Launch inicia um programa externo ou abre um ficheiro local quando a ligação é ativada. Uma ação URI abre um destino na web. Um revisor que analise um documento suspeito deve conseguir verificar, sem clicar em nada, se um botão na página três está configurado para iniciar cmd.exe ou para abrir um URL que não corresponda à marca exposta na página.
O auditor lê a ação a partir da estrutura e regista-a. Nunca a executa.
O controlo do visualizador que renderiza os documentos é o local onde a execução de uma ação ocorreria, e a sua postura predefinida é deliberadamente cautelosa. O controlo TPdfView possui um conjunto LinkOptions que define que tipos de ligação são ativados automaticamente ao clicar. A sua predefinição é [loAutoGoto, loAutoOpenURI], o que significa que saltos dentro do documento e URLs da web podem abrir-se, mas loAutoLaunch está ausente, pelo que as ações de lançamento nunca correm automaticamente. Para um fluxo de trabalho de auditoria, pode ir mais longe e limpar o conjunto por completo, para que absolutamente nada se ative de forma automática enquanto decide se deve confiar no ficheiro.
// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];
// The shipped default already withholds launch:
// default = [loAutoGoto, loAutoOpenURI]
// loAutoLaunch is NOT in the default set, so external programs
// are never started on a stray click out of the box.
O raciocínio para reter a ação de lançamento por predefinição é simples. Um salto dentro do documento é inofensivo e um URL é visível e cancelável, mas iniciar um programa externo arbitrário a partir de um clique é a ação mais perigosa que uma ligação PDF pode solicitar, pelo que está desativada a menos que opte por a ativar. Um auditor rejeita mesmo os comportamentos seguros, porque o seu papel é analisar, e não interagir.
O nível de permissão MDP da assinatura digital
As assinaturas alteram a perspetiva. Uma assinatura simples atesta os bytes no momento em que foi efetuada. Uma assinatura de certificação, do tipo criado com uma regra de deteção e prevenção de modificação de documentos (MDP - Modification Detection and Prevention), vai mais longe: declara o que pode legitimamente sofrer alterações após a certificação do documento, e um visualizador em conformidade emite avisos se algo fora dessa tolerância tiver sido modificado. Ler esse nível de permissão indica a um auditor se o ficheiro está certificado e, em caso afirmativo, qual o nível de restrição previsto para o mesmo.
A permissão MDP é um inteiro com três valores definidos. O nível 1 significa que não são permitidas quaisquer alterações; qualquer modificação quebra a certificação. O nível 2 permite o preenchimento de formulários e a assinatura, que é o caso comum para um contrato que deve ser preenchido e assinado, mas não alterado noutros aspetos. O nível 3 permite adicionalmente anotações a par do preenchimento de formulários e assinatura. Conhecer o nível permite à sua lógica de admissão avaliar a intenção: um documento certificado no nível 1 que, no entanto, contenha campos de formulário ou scripts está a entrar em contradição, valendo a pena sinalizar essa incoerência.
O componente lê a contagem de assinaturas e expõe cada uma como um registo cujo campo Permission transporta esse valor MDP, preenchido diretamente a partir da chamada subjacente FPDFSignatureObj_GetDocMDPPermission. Uma permissão igual a zero indica que a assinatura não é de certificação (DocMDP), pelo que não há restrições ao nível do documento a reportar.
var
I: Integer;
Sig: TPdfSignature;
begin
if Pdf.SignatureCount = 0 then
WriteLn('document is not signed')
else
for I := 0 to Pdf.SignatureCount - 1 do
begin
Sig := Pdf.Signature[I];
case Sig.Permission of
1: WriteLn('certified: no changes allowed');
2: WriteLn('certified: form fill and signing allowed');
3: WriteLn('certified: form fill, signing and annotations allowed');
else
WriteLn('signed, but not a DocMDP certification');
end;
end;
end;
Uma auditoria não valida aqui a criptografia da assinatura; a verificação da cadeia de certificados é um aspeto independente. O que ela reporta é a intenção declarada: este ficheiro indica que foi bloqueado neste nível. Trata-se precisamente do contexto de que um revisor necessita para avaliar se alterações posteriores, ou a mera presença de conteúdo ativo, são coerentes com a forma como o autor selou o documento.
O resto da superfície: ficheiros incorporados e XFA
Os ficheiros incorporados são documentos completos contidos dentro do PDF como anexos, constituindo um veículo de propagação clássico, pois um relatório com aparência inofensiva pode transportar um executável ou um segundo PDF malicioso na sua árvore de anexos. O componente expõe a contagem de anexos e o nome de cada um, para que a auditoria possa listar os elementos associados sem necessidade de extrair ou abrir nenhum deles.
A presença de XFA constitui a outra sinalização. Um formulário XFA substitui o AcroForm estático por uma arquitetura de formulários baseada em XML que introduz o seu próprio modelo de renderização e scripts, apresentando uma superfície maior e mais complexa do que um formulário simples. Não necessita de processar o XFA para constatar que está presente; a sua mera existência sinaliza que o ficheiro possui uma camada interativa mais rica que justifica uma análise atenta. O componente reporta-o sob a forma de um booleano simples.
var
I: Integer;
begin
if Pdf.XFA then
WriteLn('NOTE: document contains an XFA form layer');
if Pdf.AttachmentCount > 0 then
begin
WriteLn('embedded files: ', Pdf.AttachmentCount);
for I := 0 to Pdf.AttachmentCount - 1 do
WriteLn(' - ', Pdf.AttachmentName[I]);
end;
end;
Uma rotina de apenas leitura que elabora um relatório
Reunindo as peças, a auditoria resume-se a um único procedimento que carrega um documento, enumera os seus scripts e respetivos corpos, lista os destinos Launch e URI, reporta o nível MDP da assinatura, identifica anexos e XFA, e regista os resultados num log. Não renderiza nada, pelo que é pouco exigente em processamento e não pode ser enganado para apresentar conteúdos de página hostis. O resultado é um registo plano de leitura simples que um revisor ou uma regra automatizada podem processar.
O modelo com bons resultados na prática consiste em recolher cada conclusão numa linha, aplicar um prefixo às que acarretam riscos reais para que surjam no topo de uma fila de revisão, e guardar toda a informação junto ao ficheiro. Um documento sem scripts, sem ações de lançamento, sem anexos, sem XFA e sem assinatura (ou com uma certificação coerente) passa sem alertas. Um documento que ative diversas sinalizações simultaneamente constitui o caso que um utilizador deve avaliar antes de qualquer fase posterior o abrir. A auditoria não toma a decisão de confiança por si. Garante sim que essa decisão é informada e não cega.
Assim que um ficheiro passa na auditoria e se torna necessário analisá-lo, faça-o sob restrições e não num visualizador predefinido. A abordagem no nosso guia prático sobre a criação de uma pré-visualização segura de PDF em Delphi mostra como impedir a ativação automática de ligações e de conteúdos ativos durante uma análise controlada. Para integrar esta enumeração num fluxo de admissão completo com ferramentas de revisão, consulte o artigo workbench de revisão e admissão de PDF. Ambos assentam no mesmo princípio de apenas leitura e sem renderização, sendo disponibilizados como parte do PDFium Component para Delphi e C++Builder, a par das APIs de renderização, texto, formulário e assinatura abordadas noutros locais deste blogue.