Technical Article

PDF biztonsági kockázatok vizsgálata a PDFium segítségével Delphi-ben

A PDF nem csupán papír. Ez egy konténer, amely hordozhat szkripteket, amelyek a fájl megnyitásakor futnak le, külső programokat indító linkeket, webszervereket elérő linkeket, fájlokba ágyazott fájlokat, valamint egy aláírást, amely igazolja, hogy a dokumentum nem változott azóta, hogy valaki jótállt érte. Ha a fájl egy nem ellenőrzött forrásból érkezik, a legbiztonságosabb első lépés nem a renderelése. Hanem az, hogy elolvassuk, mit mond a fájl önmagáról, és leltárt készítsünk mindenről, amit megpróbálhat tenni, így egy ember eldöntheti, hogy egyáltalán beletartozik-e a munkafolyamatba.

Ez a cikk egy statikus, csak olvasható vizsgálatot mutat be ezen a kockázati felületen, a Delphi és Lazarus rendszerekhez készült PDFium komponens segítségével. A vizsgálat soha nem rajzol ki oldalt. Elemzi a dokumentum struktúráját, felsorolja a fájl aktív viselkedést hordozó részeit, és egy egyszerű jelentést készít. Ez a különbség aközött, hogy megkérünk egy idegent, hogy ürítse ki a zsebeit az ajtónál, vagy megbízunk benne csak azért, mert elmosolyodott.

Mi a vizsgálat, és mi nem az

Legyen tisztában a határokkal. Egy homokozóban (sandbox) futó előnézet szigorú korlátozások mellett rendereli a fájlt, így a felhasználó ránézhet anélkül, hogy a fájl érintené a gép többi részét. A vizsgálat ez előtt történik. Ez egy renderelés nélküli ellenőrzés, amelynek egyetlen kimenete a fenyegetési felület leírása: mely szkriptek léteznek, milyen műveletek kapcsolódnak a linkekhez, alá van-e írva a fájl és mennyire szigorúan, valamint mik a mellékletek. Ezt akkor futtatja, amikor a dokumentum átlép egy bizalmi határt – például e-mailből érkezik, feltöltő űrlapon vagy partnercsatornán keresztül –, mielőtt bármelyik későbbi szakasz valóban megnyitná.

A komponens ugyanúgy tölti be a dokumentumot a vizsgálathoz, mint bármi máshoz. Beállítja a fájlnevet és aktiválja, ami leképezi a kereszt-hivatkozási adatokat és a dokumentum-katalógust egyetlen oldal renderelése nélkül. Minden alábbi lépés ebből a betöltött, de le nem képezett állapotból olvas.

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;

Dokumentum JavaScript a név-fában

Az első dolog, amit össze kell írni, a kód. A PDF hordozhat dokumentumszintű JavaScript-et: olyan szkripteket, amelyek nem kapcsolódnak egyetlen oldalhoz vagy mezőhöz sem, hanem magához a dokumentumhoz, a /Names fában tárolva a /JavaScript bejegyzés alatt. A szabványnak megfelelő megjelenítő ezeket megnyitáskor futtatja le. Ez áll a PDF-alapú kártevők hosszú sorának hátterében, mivel lehetővé teszi a fájl számára, hogy abban a pillanatban hajtson végre logikát, amikor a felhasználó duplán rákattint, még mielőtt egyetlen szót is elolvasott volna.

A vizsgálandó személynek két tényre van szüksége minden ilyen szkriptről: hogy létezik-e, és mit tartalmaz. A komponens elérhetővé teszi a darabszámot, és lehetővé teszi az egyes műveletek beolvasását olyan rekordként, amely a szkript nevét és teljes törzsét tartalmazza. A törzs elolvasása fontos. Egy Doc.0 nevű szkript semmit sem mond, de a szövege meghívhatja a app.launchURL függvényt, vagy összeállíthat egy karakterláncot, és továbbíthatja azt oda, ahova nem kellene. A forrás kinyerése azért fontos, hogy a felülvizsgáló elolvashassa, és ez a lényege annak, hogy megjelöljük a megnyitáskor kódot futtató fájlokat.

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;

A dokumentumszkriptek nélküli fájl nem feltétlenül biztonságos automatikusan, mert oldalszintű és mezőszintű szkriptek is létezhetnek, de a dokumentumszkripteket tartalmazó fájl mindig megérdemel egy második pillantást. A jelenlét darabszáma önmagában is hasznos szűrő, és a törzs az, ami a szűrőt ítéletté alakítja át.

Launch és URI műveletek

A következő vizsgálandó viselkedés a linkeken és annotációkon él. Két művelettípus számít leginkább a vizsgálónak. A Launch (indítási) művelet elindít egy külső programot vagy megnyit egy helyi fájlt a link aktiválásakor. Az URI művelet megnyit egy webes célt. A gyanús dokumentumot vizsgáló személynek látnia kell – anélkül, hogy bármire rákattintana –, ha a harmadik oldalon lévő gomb a cmd.exe indítására vagy olyan URL megnyitására van bekötve, amely nem felel meg az oldalon lévő márkának.

A komponens osztályozza a talált linkeket, és elérhetővé teszi az egyes elemek művelettípusát és célútvonalát, így az audit listázni tudja az összes Launch és URI műveletet a célállomásukkal együtt. Ez jelentéskészítés, nem végrehajtás. A vizsgáló kiolvassa a műveletet a struktúrából és leírja. Soha nem követi azt.

A dokumentumokat leképező megjelenítő vezérlő az a hely, ahol a művelet követése történne, és annak alapértelmezett beállítása szándékosan óvatos. A TPdfView vezérlő rendelkezik egy LinkOptions készlettel, amely eldönti, hogy mely linktípusok aktiválódjanak automatikusan kattintásra. Alapértelmezése a [loAutoGoto, loAutoOpenURI], ami azt jelenti, hogy a dokumentumon belüli ugrások és a webes URL-ek megnyílhatnak, de a loAutoLaunch hiányzik, így az indítási műveletek soha nem futnak le automatikusan. Az audit munkafolyamathoz érdemes teljesen kiüríteni ezt a készletet, hogy egyáltalán semmi se aktiválódjon automatikusan, amíg döntést hoz a fájl megbízhatóságáról.

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

Az indítás (launch) alapértelmezett letiltása mögött meghúzódó érvelés egyszerű. A dokumentumon belüli ugrás ártalmatlan, az URL pedig látható és visszavonható, de egy tetszőleges külső program indítása kattintásra a legveszélyesebb dolog, amit egy PDF link kérhet, így ez ki van kapcsolva, hacsak nem dönt mellette. A vizsgáló még a biztonságos viselkedésekről is lemond, mert a feladata a megfigyelés, nem pedig a cselekvés.

A digitális aláírás MDP engedélyezési szintje

Az aláírások megváltoztatják a kérdést. Az egyszerű aláírás az aláírás időpontjában meglévő bájtokat igazolja. A tanúsító aláírás (certification signature) – az a fajta, amely dokumentummódosítás-észlelési és megelőzési szabállyal jön létre – továbbmegy: kijelenti, hogy mi változhat jogszerűen a dokumentum tanúsítása után, és a szabványnak megfelelő megjelenítő figyelmeztet, ha az engedélyen kívüli dologhoz nyúltak hozzá. Ennek az engedélyezési szintnek az olvasása megmondja a vizsgálónak, hogy a fájl tanúsítva van-e, és ha igen, mennyire szigorúan zárolt.

Az MDP engedély három definiált egész értékkel rendelkezik. Az 1-es szint azt jelenti, hogy egyáltalán semmilyen változtatás nem megengedett; bármilyen módosítás érvényteleníti a tanúsítást. A 2-es szint engedélyezi az űrlapkitöltést és aláírást, ami a kitöltendő és aláírandó, de egyébként nem módosítható szerződések általános esete. A 3-as szint ezen felül engedélyezi az annotációk elhelyezését is. Az engedélyezési szint ismerete lehetővé teszi a beléptetési logika számára a szándék elemzését: az 1-es szinten tanúsított dokumentum, amely mégis űrlapmezőket vagy szkripteket tartalmaz, ellentmondásban van önmagával, és ezt az ellentmondást érdemes megjelölni.

A komponens beolvassa az aláírások számát, és mindegyiket olyan rekordként teszi elérhetővé, amelynek a Permission mezője hordozza ezt az MDP értéket, közvetlenül az alatta lévő FPDFSignatureObj_GetDocMDPPermission hívásból feltöltve. A nulla értékű engedély azt jelenti, hogy az aláírás nem tanúsító (DocMDP) aláírás, így nincs jelentendő dokumentumszintű zárolás.

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;

A vizsgálat itt nem ellenőrzi az aláírás kriptográfiáját; a tanúsítványlánc ellenőrzése különálló feladat. Amit jelent, az a deklarált szándék: ez a fájl azt állítja, hogy ezen a szinten lett zárolva. Pontosan ez az a kontextus, amelyre a felülvizsgálónak szüksége van annak megítéléséhez, hogy a későbbi módosítások vagy az aktív tartalom puszta jelenléte összhangban van-e azzal, ahogyan a szerző lezárta a dokumentumot.

A felület többi része: beágyazott fájlok és XFA

Két további elem teszi teljessé a leltárt. A beágyazott fájlok a PDF-en belül csatolmányként hordozott egész dokumentumok, és ezek klasszikus terjesztési eszközök, mert egy ártalmatlannak tűnő jelentés végrehajtható fájlt vagy egy második, rosszindulatú PDF-et hordozhat a csatolmányfájában. A komponens elérhetővé teszi a csatolmányok számát és mindegyik nevét, így a vizsgálat kilistázhatja a csatolmányokat anélkül, hogy bármelyiket is kicsomagolná vagy megnyitná.

Az XFA jelenléte a másik jelző. Az XFA űrlap lecseréli a statikus AcroForm-ot egy XML-alapú űrlap-architektúrára, amely saját leképezési és szkriptelési modellt hoz magával, ami egy egyszerű űrlapnál nagyobb és összetettebb felület. Nem kell feldolgoznia az XFA-t ahhoz, hogy feljegyezze a jelenlétét; a puszta jelenléte jelzi, hogy a fájl gazdagabb interaktív réteget hordoz, amelyet érdemes közelebbről megvizsgálni. A komponens ezt egyetlen logikai értékként jelzi.

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;

Egyetlen csak olvasható rutin, amely jelentést ír

Rakjuk össze a darabokat, és a vizsgálat egyetlen eljárás lesz, amely betölti a dokumentumot, felsorolja a szkriptjeit és azok törzseit, listázza a Launch és URI céljait, jelzi az aláírás MDP szintjét, feljegyzi a csatolmányokat és az XFA-t, majd naplóba írja az eredményeket. Semmit nem renderel, így olcsó, és nem lehet rávenni ellenséges oldaltartalom megjelenítésére. A kimenet egy lapos, ember által olvasható rekord, amelyre a felülvizsgáló vagy egy későbbi szabály támaszkodhat.

A gyakorlatban jól működő forma az, ha minden megállapítást sorként gyűjtünk össze, a valóban kockázatosakat előtaggal látjuk el, hogy a felülvizsgálati sor elejére kerüljenek, és az egészet a fájl mellett tároljuk. A szkript, indítási művelet, csatolmány és XFA nélküli dokumentum, amely nem rendelkezik aláírással vagy koherens tanúsítással bír, csendben átmegy a szűrőn. Az a dokumentum, amely egyszerre több jelzőt is aktivál, az, amelyet egy embernek kell megnéznie, mielőtt bármelyik későbbi fázis megnyitná. A vizsgálat nem hozza meg a bizalmi döntést Ön helyett. Gondoskodik arról, hogy a döntés megalapozott legyen, ne vak.

Miután a fájl átment a vizsgálaton, és meg kell néznie, tegye azt korlátozások mellett, ne pedig alapértelmezett megjelenítőben. A biztonságos PDF előnézet készítéséről szóló útmutatónk bemutatja, hogyan lehet megakadályozni a linkek automatikus kezelését és az aktív tartalom működését az ellenőrzött megtekintés során. Ennek az összeírásnak a felülvizsgálati eszközökkel ellátott beléptetési folyamatba való beépítéséhez lásd a PDF beléptetési és felülvizsgálati munkaasztaláról szóló cikket. Mindkettő ugyanarra a csak olvasható, renderelésmentes alapra épül, és a Delphi és C++Builder platformokhoz készült PDFium Component részét képezi, a blogunkon máshol ismertetett renderelő, szöveg-, űrlap- és aláírás-kezelő API-kkal együtt.