Technical Article

Analisi dei rischi di sicurezza dei PDF con PDFium in Delphi

Un PDF non è semplice carta. È un contenitore in grado di ospitare script eseguiti all'apertura del file, collegamenti che avviano programmi esterni, link che si connettono a server web, file nidificati in altri file e una firma che attesta che il documento non ha subito modifiche da quando è stato convalidato. Quando si riceve un file da una sorgente non controllata, la prima mossa più sicura non è renderizzarlo, bensì leggere cosa il file dice di se stesso e creare un inventario di tutto ciò che potrebbe tentare di fare, in modo che un operatore possa decidere se inserirlo o meno nel flusso di lavoro.

Questo articolo illustra un processo di analisi (audit) statico e in sola lettura su tale superficie di rischio utilizzando il componente PDFium per Delphi e Lazarus. L'analisi non disegna mai alcuna pagina. Esegue il parsing della struttura del documento, elenca le parti del file che comportano comportamenti attivi e genera un report semplice. È la differenza che passa tra il chiedere a uno sconosciuto di svuotare le tasche all'ingresso e il fidarsi di lui solo perché ha sorriso.

Cosa rappresenta un controllo di sicurezza, e cosa non è

Definiamo chiaramente i limiti. Un'anteprima in ambiente protetto (sandbox) esegue il rendering del file con forti restrizioni, consentendo all'utente di visualizzarlo senza che questo interagisca con il resto del sistema. Un controllo di sicurezza (audit) precede questa fase. Si tratta di un'ispezione senza rendering il cui solo output è la descrizione della superficie di minaccia: quali script sono presenti, quali azioni sono collegate ai link, se il file è firmato e con quali vincoli, e quali allegati contiene. Viene eseguito quando un documento supera un limite di attendibilità (all'arrivo via e-mail, da un modulo di caricamento o da sorgenti partner) prima che qualsiasi altra fase lo apra effettivamente.

Il componente carica il documento per un controllo di sicurezza nello stesso modo in cui lo fa per qualsiasi altra operazione. Si imposta il nome del file e lo si attiva, operazione che esegue il parsing dei dati di riferimento incrociato e del catalogo del documento senza renderizzare alcuna pagina. Tutto ciò che segue viene letto da questo stato caricato e privo di rendering.

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 del documento nell'albero dei nomi

Il primo elemento da individuare è il codice. Un PDF può contenere JavaScript a livello di documento: script non associati a specifiche pagine o campi ma al documento stesso, memorizzati nell'albero /Names sotto la voce /JavaScript. Un visualizzatore conforme esegue questi script all'apertura. Questo è il meccanismo alla base di una vasta gamma di malware per PDF, poiché consente a un file di eseguire istruzioni nel momento stesso in cui l'utente fa doppio clic, prima ancora che possa leggerne il contenuto.

Un analista necessita di due informazioni su tali script: la loro presenza e il relativo contenuto. Il componente espone il conteggio e consente di leggere ciascuna azione sotto forma di record contenente il nome dello script e il relativo corpo di codice completo. La lettura del codice è importante. Uno script chiamato Doc.0 non dice nulla di per sé, ma il suo testo potrebbe richiamare app.launchURL o comporre una stringa da passare a indirizzi non sicuri. Estrarre il codice sorgente per consentirne la lettura a un revisore è l'unico scopo del segnalare un file che esegue codice all'apertura.

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;

Un file senza script a livello di documento non è automaticamente sicuro, poiché possono esistere script associati a pagine e campi, ma un file che ne contiene merita sempre un controllo approfondito. Il solo conteggio della presenza rappresenta un filtro utile, e il corpo del codice è ciò che consente di formulare una valutazione definitiva.

Azioni Launch e URI

I comportamenti successivi da inventariare risiedono su collegamenti e annotazioni. Due tipi di azione sono di particolare interesse in fase di analisi. Un'azione Launch avvia un programma esterno o apre un file locale all'attivazione del link. Un'azione URI apre una destinazione web. Un revisore che esamina un documento sospetto deve poter vedere, senza fare clic su nulla, che un pulsante a pagina tre è configurato per avviare cmd.exe o per aprire un URL non coerente con il marchio mostrato sulla pagina.

Il componente classifica i collegamenti individuati ed espone il tipo di azione e il percorso di destinazione per ciascuno di essi, consentendo all'analisi di elencare ogni azione Launch e URI con la relativa destinazione. Questa è un'attività di reportistica, non di esecuzione. L'analista legge l'azione dalla struttura e la registra, senza mai eseguirla.

Il controllo di visualizzazione che renderizza i documenti è la sede in cui avverrebbe l'esecuzione di un'azione, e la sua configurazione predefinita è volutamente prudente. Il controllo TPdfView dispone della proprietà LinkOptions che stabilisce quali tipi di collegamento si attivano al clic. Il valore predefinito è [loAutoGoto, loAutoOpenURI], il che significa che i salti interni al documento e gli URL web possono aprirsi, ma loAutoLaunch è assente, perciò le azioni di avvio non vengono mai eseguite automaticamente. Per un flusso di lavoro di analisi si può azzerare completamente l'insieme, impedendo qualsiasi attivazione automatica mentre si valuta l'attendibilità del file.

// 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.

Il motivo per cui si esclude l'avvio per impostazione predefinita è semplice. Un salto interno al documento è innocuo e un URL è visibile e annullabile, ma avviare un programma esterno arbitrario tramite un clic è l'operazione più pericolosa che un collegamento PDF possa richiedere, perciò è disattivata a meno di un'adesione esplicita. Un analista esclude anche i comportamenti considerati sicuri, poiché il suo compito è osservare, non agire.

Il livello di permessi MDP della firma digitale

Le firme modificano lo scenario. Una firma semplice attesta i byte presenti al momento della firma. Una firma di certificazione, del tipo creato con regole di rilevamento e prevenzione delle modifiche (modification detection and prevention), va oltre: dichiara cosa può legittimamente cambiare dopo la certificazione del documento, e un visualizzatore conforme segnala se elementi esterni a tale tolleranza sono stati modificati. La lettura di tale livello di permessi indica se un file è certificato e quali vincoli vi siano applicati.

Il permesso MDP è un intero con tre valori definiti. Un livello pari a 1 indica che non è consentita alcuna modifica; qualsiasi variazione annulla la certificazione. Un livello pari a 2 consente la compilazione di moduli e la firma, il caso tipico di un contratto destinato a essere compilato e firmato senza ulteriori alterazioni. Un livello pari a 3 consente in aggiunta l'inserimento di annotazioni oltre alla compilazione e alla firma. Conoscere il livello consente alla logica di ricezione di valutare l'intento: un documento certificato a livello 1 che tuttavia presenta campi modulo o script è in contraddizione con se stesso, e tale anomalia merita di essere segnalata.

Il componente legge il conteggio delle firme ed espone ciascuna come un record il cui campo Permission porta il valore MDP, ricavato direttamente dalla chiamata sottostante FPDFSignatureObj_GetDocMDPPermission. Un permesso pari a zero indica che la firma non è di certificazione (DocMDP), perciò non vi sono blocchi a livello di documento da segnalare.

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;

In questa fase l'analisi non convalida la crittografia della firma; la verifica della catena dei certificati rappresenta un'attività separata. Ciò che viene riportato è l'intento dichiarato: questo file attesta di essere stato bloccato a questo livello. Questo è esattamente il contesto richiesto da un revisore per valutare se le modifiche successive, o la sola presenza di contenuti attivi, siano coerenti con il modo in cui l'autore ha sigillato il documento.

Il resto della superficie: file incorporati e XFA

I file incorporati sono interi documenti inseriti nel PDF come allegati, e rappresentano un tipico veicolo di attacco, poiché un report apparentemente innocuo può trasportare un eseguibile o un secondo PDF dannoso nel proprio albero degli allegati. Il componente espone il conteggio degli allegati e il nome di ciascuno, consentendo all'analisi di elencare gli elementi presenti senza necessità di estrarli o aprirli.

La presenza di XFA rappresenta l'altro indicatore. Un modulo XFA sostituisce l'AcroForm statico con un'architettura basata su XML dotata di un proprio modello di rendering e scripting, una superficie più ampia e complessa rispetto a un modulo standard. Non è necessario elaborare l'XFA per rilevarne la presenza; la sua sola esistenza segnala che il file contiene uno strato interattivo complesso che merita attenzione. Il componente lo segnala tramite un singolo valore booleano.

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;

Una routine in sola lettura che genera un report

Unendo i vari elementi, il controllo si riduce a una singola procedura che carica un documento, elenca i relativi script e il loro codice, elenca le destinazioni Launch e URI, riporta il livello MDP delle firme, rileva allegati e XFA e scrive i risultati in un log. Non eseguendo alcun rendering, l'operazione è rapida ed evita il rischio di mostrare contenuti dannosi della pagina. L'output è un record semplice e leggibile che un revisore o regole automatizzate successive possono elaborare.

La struttura ottimale consiste nel raccogliere ciascun risultato come riga, anteponendo un prefisso a quelli realmente rischiosi per posizionarli in cima a una coda di revisione, e salvare l'intero file di log accanto al documento originale. Un documento privo di script, azioni di avvio, allegati, XFA, e senza firme o con una certificazione coerente, supera il controllo senza segnalazioni. Un documento che attiva diversi indicatori contemporaneamente è quello che un operatore dovrebbe esaminare prima che qualsiasi fase successiva lo apra. Il controllo di sicurezza non prende decisioni di attendibilità al posto tuo, si assicura che tali decisioni siano basate su dati e non prese alla cieca.

Una volta che un file ha superato il controllo di sicurezza e si rende necessario visualizzarlo, è opportuno farlo con opportune restrizioni piuttosto che con un lettore comune. L'approccio descritto nella nostra guida alla creazione di un'anteprima PDF sicura in Delphi mostra come impedire l'attivazione automatica di link e contenuti attivi durante la visualizzazione controllata. Per integrare questo inventario in un flusso di ricezione completo con strumenti di revisione, si veda l'articolo sull'ambiente di revisione e ricezione PDF. Entrambi si basano sulla stessa infrastruttura in sola lettura e senza rendering e sono distribuiti come parte del PDFium Component per Delphi e C++Builder, insieme alle API di rendering, modifica e gestione documenti trattate in altre sezioni di questo blog.