FR-177.A5 資安掃描:簽章驗證核心、啟動完整性閘、漏洞庫同步

⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。

卡片:CM-3512(母卡 CM-3507)|掃描 run ID:wf_4fa8f010-296|程式版本:bab2aa31f00d(branch ws-CM-3507-CM-3512)|2026-11-05

1. 🔴 一句話結論

沒有找到需要馬上修的問題。 工具只提出 1 條疑點,三個獨立檢查員一致投「不成立」;人工照「重點看什麼」逐條核對,7 項都守住了,另外記下 2 個很小的觀察(不急,要不要開卡由決策者決定)。

2. 這一棒在檢查什麼

Agent 裝在客戶機器上,它怎麼確認「這份東西是原廠出的、沒被改過」:

  • 信封驗章(common/integrity/verify.py):原廠用私鑰簽名,Agent 用寫死在程式裡的公鑰核對。更新清單、漏洞庫清單、啟動檢查全靠它。
  • 啟動檢查(common/integrity/startup_gate.py):Agent 每次開機前先核對自己的程式檔有沒有被改過,被改過就拒絕啟動。
  • 漏洞庫下載與解壓(guidant_agent/sync/app/vulndb_sync.py、vulndb_bundle.py):從主系統拉新版漏洞資料包、解開、驗章、切換。

3. 掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 結論
工具 C1 驗章前先把信封內容解壓,解壓沒設上限(「壓縮炸彈」:小檔解開變超大) Agent 記憶體被吃光、當掉 要先控制主系統本身(資料包走驗證過對方身分的加密連線,且指紋要符合主系統公布的值) common/integrity/verify.py:102 可選:解壓改用 zlib.decompressobj().decompress(data, max_length) 設上限 不成立(三票全否:能打的人本來就能做更糟的事)
人工 O1 啟動檢查只核對「清單上列的檔」,清單外多出的檔不會被發現 有人在程式目錄放額外檔案,開機檢查不會發現 要先能寫入 Agent 安裝目錄(通常只有 root) common/integrity/startup_gate.py:146-161 比照漏洞庫 check_files 的做法:掃目錄、清單外有多的檔就拒啟(要排除 log/暫存類) 觀察,低;是否成立要看 Nuitka 產物會不會載入多出的檔,未驗證
人工 O2 判斷「新版不能比舊版舊」時,舊版日期讀的是本機那份沒重新驗章的 manifest;它壞掉時當成「沒裝過」 可以塞回一包原廠簽過的舊版漏洞庫(降版) 要先能寫入漏洞庫目錄,且主系統要真的送出舊包 guidant_agent/sync/app/vulndb_sync.py:195-199、:254 本機 manifest 讀不到但 current 存在時,當錯誤停下而不是當首次安裝 觀察,低

4. 每條詳述

工具 C1:驗章前解壓沒有上限(不成立)

  • 白話:信封裡的內容是壓縮過的。Agent 先解壓、才驗簽名,解壓這一步沒有限制大小。
  • 出事會怎樣:一個 8MB 的簽章檔可能解成好幾 GB,把記憶體吃光。
  • 要先有什麼:漏洞庫的 manifest.sig 由主系統送來,走驗證過主系統身分的加密連線(cloud_trust.py:57-66),整包指紋要等於主系統自己公布的值(vulndb_sync.py:223-225);簽章檔上限 8MB(vulndb_bundle.py:130)。啟動檢查的 manifest.sig 在安裝目錄裡,能改它的人本來就能直接換掉程式。
  • 為什麼不成立:能送惡意檔的人只有「已經被入侵的主系統」或「本機 root」,這兩種人不必靠這招,就能讓 Agent 停擺。
  • 怎麼修(選做、加固):verify.py:102 改成 zlib.decompressobj().decompress(raw, 16*1024*1024),解完 unconsumed_tail 非空就拋 VerificationError。

人工 O1:啟動檢查不擋清單外的多餘檔案

  • 在哪裡:common/integrity/startup_gate.py:146-161 _compare_files 只跑 manifest["files"] 列出的檔。
  • 出事會怎樣:理論上,有人在 binary 旁邊多放一個會被載入的模組,開機檢查不會發現。
  • 要先有什麼:本機 root 等級的寫入權。這個人也能改 systemd 設定、直接換掉程式,所以多出的風險很有限。
  • 怎麼修:參考 vulndb_bundle.py:158-173 check_files,加一步「目錄實際檔案集合 ⊆ 清單」。
  • 不確定:Nuitka standalone 產物會不會載入清單外的檔,這次沒驗證。

人工 O2:本機 manifest 壞掉時,降版檢查會放行

  • 在哪裡:vulndb_sync.py:195-199 installed_date() 讀不到日期就回 None;vulndb_bundle.py:154 遇到 previous is None 就放行。
  • 出事會怎樣:漏洞庫退回舊版,掃描漏掉新公布的漏洞。
  • 要先有什麼:能寫本機漏洞庫目錄,而且主系統真的送了舊包(主系統送舊包時,vulndb_sync.py:109 已經先比過一次)。
  • 怎麼修:current 連結存在但 manifest 讀不到時,回報錯誤、不同步,不要當成首次安裝。

「重點看什麼」逐條人工查證

項目 結果 為什麼
verify.py:61-81 從檔案讀公鑰 不成立 預設只查寫死在程式裡的 PUBLIC_KEYS(:61-65)。從檔案讀公鑰的 release_key_lookup 只用在 Agent 更新清單(update_console.py:178),目錄寫死為 /opt/guidant-agent/current/release-keys(update_console.py:34)。上傳包裡的鑰一律不看;kid 只收 16 位十六進位(verify.py:47),擋 ../;捷徑檔(symlink)、超過 4KB、非 Ed25519 的公鑰都拒收(:78、:84)。能寫 /opt/guidant-agent 的人等同 root。附帶一點:release-keys/ 有沒有列在啟動 manifest 裡受保護,不在本棒範圍,沒查。
verify.py:114-152 alg/序列化/類別不符 不成立 alg 不是 Ed25519 一律拒收(:128),而且公鑰物件本身就是 Ed25519,不存在換演算法的問題。簽名驗的是「解壓後的原始位元組」(:152),不會在 Agent 端重新排序 JSON,所以兩邊序列化不一致的問題不存在。類別不符直接拋錯拒收(:157-158),不是只發警告。
startup_gate.py:105 測試開關、:55-58 覆寫路徑 不成立 打包模式下(由 __compiled__/frozen 判斷,resource_path.py:27-29),驗證一律開啟(:103-104),環境變數只能讓驗證更嚴、不能關掉。驗證目錄在打包模式固定在 binary 所在目錄(:60-61),不吃環境變數覆寫。
startup_gate.py:24 刪 manifest 能不能繞過 不成立 簽章檔不存在就拋錯(:118-121),清單空的也拒啟(:138-142),失敗一律 SystemExit(1) 拒絕啟動(:181),不是記 log 後繼續跑。
vulndb_bundle.py:79-94 自寫的解壓 不成立 絕對路徑、..、空字元一律拒收(:103);realpath 確認目標還在資料目錄內(:105-107);連結與特殊檔整包拒收(:95-97);實際寫入量超過 tar 標頭宣告值就拒收(:118-119),標頭說謊沒用;檔數、總量都有上限;`O_EXCL
vulndb_bundle.py:135-172 清單外的檔、切換是否原子 不成立 清單外多出的檔整包拒收(:168-170)。全部檢查做完才切換。os.rename 搬進正式目錄名,再用 os.replace 原子地換掉 current 連結(vulndb_sync.py:264-272),掃描任何時刻看到的不是舊版就是新版。
vulndb_sync.py 來源/頻率/降版/失敗是否刪舊庫 不成立(見 O2) 來源是主系統 /api/1.0/agents/vulndb/bundle(:32),走同一條驗證過身分的控制通道(:247),由心跳設定通道觸發;同一包失敗後冷卻 30 分鐘(:44)。降版有兩道擋:主系統宣告的日期(:109)、簽章內的日期(:254)。失敗時只清暫存,current 不動(:239-241),不會刪掉舊庫。
public_keys.py 幾把鑰、dev 鑰 不成立 只有一把 PROD 鑰(public_keys.py:31-38),沒有 dev/stg 鑰跟著出貨。

5. 這份結果可信到什麼程度

  • 「找到的這些存在嗎」:工具的 C1 確實存在,但三個檢查員都判定沒有實際攻擊價值。O1、O2 是人工開檔看到的,沒有經過三人投票,嚴重度是 runner 自己判的。
  • 「只有這些嗎」:只能說「這 6 個檔裡,一個研究員讀過一輪、人工對過重點清單,沒看到更多」。effort low 只派一個研究員,不是窮盡式審查。範圍外的檔(update_console.py、cloud_trust.py、resource_path.py)只為了確認呼叫端而翻過幾行,沒有審。沒有執行任何程式或測試,所有結論都來自讀程式碼。

6. 執行概況(照 stamp CLAUDE-SECURITY-REVISION-bab2aa31f00d.json)

欄位 值
status verified
reason_kind null
candidates 1(工具提出 1 條疑點)
panel_votes 3(三個檢查員對 1 條各投一票,3 票全投出,沒有漏投;結果 0 票成立)
researchers_dispatched / returned 1 / 1
範圍檔數 6
耗時 1042 秒(約 17 分鐘)
歸檔 docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-152756/

verified 在這裡的意思是「唯一的那條疑點,三人投票完整跑完」。0 條發現是「投票全數否決」,不是「沒東西可投」。