Technical Article

Аудит безпеки та ризиків PDF з використанням PDFium в Delphi

PDF — це не просто електронний папір. Це контейнер, який може містити скрипти, що виконуються при відкритті файлу, посилання для запуску зовнішніх програм або підключення до вебсерверів, вкладені файли, а також цифровий підпис, який гарантує незмінність документа з моменту його підписання. Коли файл надходить із неконтрольованого джерела, найбезпечніший крок — не відображати його відразу, а прочитати метадані та структуру і скласти детальний список того, що він може спробувати виконати. Тоді людина зможе самостійно вирішити, чи варто взагалі допускати цей документ до вашого робочого процесу.

У цій статті описується процес статичного аудиту безпеки в режимі 'лише для читання' за допомогою компонента PDFium для Delphi та Lazarus. Цей аудит не відображає сторінки на екрані. Він аналізує структуру документа, перераховує всі частини файлу, які містять активну поведінку, та створює простий звіт. Це схоже на пропозицію незнайомцю показати вміст кишень на вході замість того, щоб сліпо довіряти йому лише за привітну посмішку.

Чим є і чим не є аудит

Важливо чітко розрізняти ці поняття. Попередній перегляд у пісочниці відображає файл із суворими обмеженнями, так що користувач може переглянути його без ризику впливу на систему. Аудит передує цьому етапу. Це перевірка без рендерингу, єдиним результатом якої є опис потенційних загроз: які скрипти присутні, які дії прив'язані до посилань, чи підписаний файл і наскільки суворо, а також що до нього додано. Ви запускаєте його, коли документ перетинає межу довіри (при отриманні електронною поштою, завантаженні через вебформу або отриманні від партнерів) ще до того, як будь-яка інша система відкриє його для роботи.

Компонент завантажує документ для аудиту так само, як і для будь-яких інших завдань. Ви вказуєте ім'я файлу та активуєте його; при цьому виконується парсинг даних перехресних посилань (xref) та каталогу документа без візуалізації сторінок. Усі наведені нижче операції зчитують дані з цього завантаженого, але не відрендереного стану.

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 у дереві імен

Перше, що потрібно перевірити — це наявність коду. PDF може містити JavaScript на рівні документа: скрипти, які прив'язані не до конкретної сторінки чи поля, а до самого документа, і зберігаються в дереві імен /Names у записі /JavaScript. Стандартні програми перегляду запускають їх відразу при відкритті файлу. Це популярний механізм для поширення шкідливого ПЗ через PDF, оскільки він дозволяє виконувати код у момент подвійного кліку по файлу, ще до того, як користувач прочитає хоч одне слово.

Аудитору потрібні два факти про кожен такий скрипт: підтвердження його наявності та його вміст. Компонент надає інформацію про загальну кількість скриптів і дозволяє прочитати кожну дію у вигляді запису з ім'ям скрипту та його повним кодом. Читання коду є надзвичайно важливим. Ім'я Doc.0 ні про що не говорить, але код може містити виклики app.launchURL або формувати й передавати небезпечні рядки. Вилучення вихідного коду для перевірки людиною — це головна мета маркування файлів, які запускають скрипти при відкритті.

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;

Документ із нульовою кількістю скриптів на рівні документа не стає автоматично безпечним, оскільки існують також скрипти сторінок та полів, але документ із такими скриптами завжди потребує уважнішої перевірки. Сама наявність коду є приводом для блокування, а аналіз коду дозволяє прийняти остаточне рішення.

Дії Launch та URI

Наступні активні елементи, які підлягають перевірці — це посилання та анотації. Для аудитора найбільше значення мають два типи дій. Дія запуску (Launch) відкриває зовнішню програму або локальний файл при активації посилання. Дія URI відкриває адресу в Інтернеті. Фахівці з безпеки повинні мати можливість побачити (без кліків по посиланнях), що кнопка на третій сторінці налаштована на запуск cmd.exe або відкриває вебсайт, який не відповідає бренду на сторінці.

Компонент класифікує знайдені посилання та надає доступ до типу дії і цільового шляху для кожного з них, дозволяючи зафіксувати кожну дію Launch та URI разом із точкою призначення. Це суто звітність, а не виконання операцій. Аудитор зчитує дію зі структури та записує її в звіт, ніколи не переходячи за посиланням.

Компонент перегляду, який відображає документи — це місце, де міг би відбутися перехід за посиланням, і його конфігурація за замовчуванням є максимально стриманою. Елемент керування TPdfView містить набір налаштувань LinkOptions, які визначають типи посилань, що автоматично спрацьовують при натисканні. За замовчуванням встановлено значення [loAutoGoto, loAutoOpenURI], що дозволяє внутрішні переходи та відкриття вебсайтів, але параметр loAutoLaunch відсутній, тому дії запуску програм ніколи не запускаються автоматично. Для процесу аудиту рекомендується повністю очистити цей набір, щоб жодні дії не запускалися автоматично, поки ви аналізуєте безпеку файлу.

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

Логіка блокування запуску за замовчуванням є очевидною. Внутрішній перехід є безпечним, а URL-адресу можна побачити та вчасно скасувати перехід; проте запуск довільної сторонньої програми при натисканні на посилання — це найнебезпечніша дія, яку може містити посилання PDF, тому вона вимкнена, якщо ви явно не дозволили її. Під час аудиту рекомендується відмовитися навіть від безпечних переходів, оскільки завдання аудитора — аналізувати, а не діяти.

Рівень дозволів MDP цифрового підпису

Підписи змінюють підхід до аналізу. Звичайний підпис просто підтверджує цілісність байтів на момент підписання. Сертифікаційний підпис, створений за правилами виявлення та запобігання змінам документів (MDP), йде далі: він визначає, які зміни є законними після сертифікації документа, а програма перегляду попереджає користувача, якщо було змінено щось інше. Читання цього рівня доступу дозволяє визначити, чи сертифіковано файл і якими є встановлені правила безпеки.

Дозвіл MDP визначається цілим числом з трьома можливими значеннями. Рівень 1 означає повну заборону будь-яких змін; будь-яке коригування скасовує дію сертифіката. Рівень 2 дозволяє заповнення форм та підписання — типовий випадок для договорів, які мають бути заповнені та підписані, але не змінені іншим чином. Рівень 3 додатково дозволяє додавання анотацій поверх заповнення та підписання. Знання цього рівня дозволяє оцінити наміри автора: документ із сертифікатом рівня 1, який містить активні поля форм або скрипти, суперечить сам собі, і цей факт слід обов'язково зафіксувати.

Компонент зчитує кількість підписів та представляє кожен у вигляді запису, де поле Permission містить це значення MDP, отримане безпосередньо з виклику функції FPDFSignatureObj_GetDocMDPPermission. Дозвіл із значенням нуль означає, що підпис не є сертифікаційним (DocMDP), тому обмежень на рівні документа немає.

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;

Аудит не займається криптографічною перевіркою підпису; перевірка ланцюжка сертифікатів є окремим завданням. Звіт містить інформацію про заявлені правила: цей файл декларує блокування на певному рівні. Це саме той контекст, який потрібен фахівцю для оцінки того, чи відповідають наступні зміни або наявність активного вмісту тому стану, в якому автор запечатав документ.

Решта поверхні: вбудовані файли та XFA

Ще два елементи доповнюють повний опис системи безпеки. Вкладені файли — це цілі документи, розміщені всередині PDF як додатки; це класичний спосіб доставки шкідливого вмісту, коли звичайний звіт може містити виконуваний файл або інший небезпечний PDF. Компонент надає інформацію про кількість додатків та ім'я кожного з них, завдяки чому аудит може зафіксувати їхню наявність без необхідності вилучення чи відкриття.

Наявність XFA є ще одним важливим сигналом. Форма XFA замінює статичну структуру AcroForm на XML-архітектуру зі власною моделлю рендерингу та скриптів, яка є набагато більшою та складнішою за звичайні форми. Вам не потрібно обробляти дані XFA для перевірки; сам факт її наявності свідчить про використання складного інтерактивного шару, який потребує уважнішого аналізу. Компонент повертає це значення як просте логічне поле.

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;

Одна процедура тільки для читання, яка створює звіт

Об'єднавши всі ці кроки, ми отримуємо єдину процедуру аудиту, яка завантажує документ, перераховує скрипти та їхній код, реєструє цілі Launch та URI, повідомляє рівень MDP підписів, вкладені файли та XFA, і записує результати в лог. Вона не виконує рендеринг сторінок, тому є швидкою та безпечною і не може бути обманута відображенням небезпечного вмісту. Результатом є звичайний людиночитаний запис, який фахівець або автоматизована система можуть використовувати для прийняття рішення.

На практиці зручно збирати кожен знайдений факт у вигляді рядка, позначаючи найнебезпечніші з них спеціальними префіксами для виведення вгору списку перевірки, та зберігати весь звіт поруч із файлом. Документ без скриптів, дій запуску програм, вкладень, шару XFA і без підписів або із коректним сертифікатом проходить перевірку автоматично. Документ, який активував кілька попереджувальних прапорців — це саме той файл, який фахівець має перевірити вручну перед його використанням у подальших етапах обробки. Аудит не приймає рішення про довіру за вас; він просто забезпечує вас повною інформацією для прийняття усвідомленого рішення.

Після успішного проходження аудиту, якщо вам дійсно потрібно переглянути файл, робіть це в обмеженому середовищі, а не у звичайній програмі перегляду. Підхід, описаний у нашій інструкції зі створення безпечного попереднього перегляду PDF у Delphi, демонструє, як заблокувати автоматичну обробку посилань та активного вмісту під час контрольованого перегляду. Щоб інтегрувати ці перевірки у повноцінний конвеєр обробки з інструментами рецензування, див. статтю Робоче місце рецензента та імпорт документів PDF. Обидва рішення базуються на тій самій основі без рендерингу в режимі 'лише для читання' та постачаються як частина компонента PDFium для Delphi та C++Builder разом з API рендерингу, тексту, форм та підписів, які висвітлені в інших публікаціях цього блогу.