PDF 不僅僅是紙張。它是一個容器,可以攜帶在檔案開啟時執行的指令碼、啟動外部程式的連結、連線到網頁伺服器的連結、巢狀在檔案內部的檔案,以及聲明自有人擔保以來檔案未曾改變的簽名。當檔案來自您無法控制的來源時,最安全的第一步不是轉譯它。而是讀取檔案關於其自身的描述,並建立它可能嘗試執行的所有操作的清單,以便由人工決定它是否完全屬於您的工作流程。
本文將介紹使用適用於 Delphi 和 Lazarus 的 PDFium 元件對該風險表面進行的靜態、唯讀稽核分析。稽核從不繪製頁面。它解析檔案結構、列舉檔案中帶有行為的部分,並撰寫一份簡單的報告。這就像是在門口要求陌生人掏空口袋,與因為他們微笑而信任他們之間的區別。
什麼是稽核,什麼不是稽核
請弄清楚邊界。沙盒預覽在嚴格的限制下轉譯檔案,以便使用者在檔案不接觸電腦其餘部分的情況下查看它。稽核在此之前進行。它是一種無轉譯的檢查,其唯一的輸出是威脅表面的描述:存在哪些指令碼、哪些動作連接到連結、檔案是否已簽名以及鎖定程度如何,以及附加了什麼。當檔案越過信任邊界時,您會在從電子郵件、上傳表單或合作夥伴摘要接收檔案時執行它(在任何後續階段真正開啟它之前)。
元件為稽核載入檔案的方式與其他任何情況相同。您設定檔案名稱並將其啟用,這會解析交叉參照資料和檔案目錄,而不轉譯單個頁面。以下所有內容都從該已載入、未轉譯的狀態中讀取。
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:這些指令碼不附加到 any page or field,而是附加到檔案本身,儲存在 /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;
檔案指令碼數量為零的檔案並非自動安全,因為頁面和欄位指令碼也存在,但帶有檔案指令碼的檔案總是值得再看一眼。僅憑存在數量就是一個有用的閘口,而主體則是將閘口轉變為判斷的依據。
啟動與 URI 動作
下一個要編目清單的行為存在於連結和註解上。對稽核員而言,有兩種動作類型最重要。啟動動作在觸發連結時啟動外部程式或開啟本機檔案。URI 動作開啟網頁目標。審閱者在查看可疑檔案時,應能在不按一下任何內容的情況下看到,第三頁上的按鈕被連接到啟動 cmd.exe 或開啟與頁面上的品牌不符的 URL。
該元件對它找到的連結進行分類,並公開每個連結的動作類型和目標路徑,因此稽核可以列出每個啟動和 URI 動作及其目的地。這是報告,而不是執行。稽核員從結構中讀取動作並將其記錄下來。它絕不會跟隨它。
轉譯檔案的檢視器控制項是發生跟隨動作的地方,其預設狀態是有意謹慎的。TPdfView 控制項具有 LinkOptions 集合,該集合決定了哪些連結類型在按一下時自動觸發。其預設值為 [loAutoGoto, loAutoOpenURI],這意味著檔案內跳轉和網頁 URL 可能會開啟,但沒有 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 權限是一個具有三個已定義值的整數。層級 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 表單使用基於 XML 的表單架構取代了靜態 AcroForm,該架構帶來了其自身的轉譯和指令碼模型,是比普通表單更大、更複雜的表面。您不需要處理 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;
一個寫入報告的唯讀常式
將這些部分組合在一起,稽核就是一個單一的程序,它載入檔案、列舉其指令碼及其主體、列出其啟動和 URI 目標、報告簽章 MDP 層級、記錄附件與 XFA,並將發現寫入日誌。它不轉譯任何內容,因此成本低,且無法被欺騙顯示敵意的頁面內容。輸出是一份扁平、人類可讀的記錄,審閱者或下游規則可以對其進行處理。
在實作中運作良好的形式是將每個發現收集為一行,在真正有風險的發現前加上前綴,以便將它們排序到審閱佇列的頂部,並將整個內容保留在檔案旁邊。沒有指令碼、沒有啟動動作、沒有附件、沒有 XFA,且沒有簽章或具有一致認證的檔案會安靜地通過。一次觸發多個旗標的檔案是人員在任何後續階段真正開啟它之前應該看到的檔案。稽核不為您做出信任決定。它確保決定是有據可查的,而不是盲目的。
一旦檔案通過稽核且您確實需要查看它,請在限制下進行,而不是在預設的檢視器中。我們在 Delphi 中建立安全 PDF 預覽的逐步解說中的方法展示了如何在受控查看期間防止連結自動處理和作用中內容起作用。若要將此列舉併入包含審閱者工具的完整接收管線中,請參閱PDF 接收與審閱工作台文章。兩者都建立在相同的唯讀、無轉譯基礎之上,並作為適用於 Delphi 和 C++Builder 的 PDFium 元件 的一部分出貨,同時也提供本部落格其他地方介紹的轉譯、文字、表單和簽章 API。