iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
AI Security

一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天系列 第 5

我把被改回去的那五版全找回來了:從寫檔紀錄重建版本鏈

  • 分享至 

  • xImage
  •  

昨天結尾我留了一題:檔案被改壞、又被改回去,現在看起來完全正常,中間那一版還查不查得到?
話不多說,老樣子,先上圖 :

https://ithelp.ithome.com.tw/upload/images/20260919/20183541Fis6Cu2WhI.png
今天我去查了。答案跟我原本想的相反。

查得到。 而且整條版本鏈都在,五次「改回去」一次都沒漏。

這是這個系列開始以來,第一次有東西真的成立。前四天我查一次、撞牆一次:hook 裝了不叫、兩份紀錄給出矛盾的時間、標準刻意不記我要的欄位、同一個寫檔動作分不出改的是測試還是正式碼。今天終於接上了一塊。

所以這篇的前半是好消息,後半是我接著撞上的那面牆——我手上有那份內容,卻沒辦法證明它就是我磁碟上那個檔。


先把昨天的題目結掉

我挑了一個 AI 這幾天改最多次的檔,billing.py。它在我的軌跡裡出現 15 次。

我把這 15 次寫入的內容各算一次雜湊(可以想成內容的指紋,內容一樣指紋就一樣),排成一條版本鏈:

第 4 版  291 bytes  c1b92c4414ec
第 5 版  336 bytes  0c7d7e5d8bd6
第 6 版  527 bytes  96201b1bef58
第 7 版  291 bytes  c1b92c4414ec   ← 跟第 4 版一模一樣
...
第 9 版  336 bytes  0c7d7e5d8bd6   ← 跟第 5 版一模一樣

15 版裡有 5 次「內容完全回到先前某一版」。第 7 版退回第 4 版,第 9 版退回第 5 版,第 13 版又退回第 5 版一次。

這正是昨天問的那個情況:改壞了,改回去,現在看起來沒事。而軌跡上不但看得出「改回去」這個動作,被改回去之前那一版的完整內容也還在

為什麼會在?因為 Cursor 的 hook 在每次寫檔之後,會把實際寫進去的完整內容交給我的腳本。所以我手上不是一句「這個檔被寫過 15 次」,是 15 份完整副本。

我以為今天要寫的缺口,其實不存在。


然後我試著證明它

有副本,跟舉得出證,是兩件事。

假設有人問我:「你說第 6 版被退掉了,證據呢?」我要回答的不是檔案內容長什麼樣。我要證明的是這一句:

我手上這份紀錄,對應的就是磁碟上那個檔。

標準做法是算雜湊。兩邊指紋一樣,就認定是同一份東西。這套做法在鑑識裡叫監管鏈,用來證明證物從現場到法庭之間沒被掉包。

我算了。對不上。

磁碟 627 bytes sha256 7c77f331395d
軌跡 613 bytes sha256 23bfa51923b3

差 14 個位元組。我以為是軌跡漏收了最後一次修改,於是逐行比對,準備找出多出來的那幾行。

結果是 0 行不同。14 行,每一行都一模一樣。

14 行,差 14 個位元組。

原因是換行符。磁碟上那個檔用的是 Windows 的 CRLF(兩個位元組),軌跡收到的內容用的是 LF(一個位元組),每行差一個,14 行剛好差 14 個。我把換行統一之後再算一次,兩邊的指紋都是 23bfa51923b3,完全相同。

內容沒有任何差異,差的是寫法。這個寫法不影響程式怎麼跑,卻讓雜湊比對這個舉證手段用不了。

一個檔可能是巧合。我把軌跡裡 98 個被寫過的檔全跑了一遍:

裁決 檔數 佔比
逐位元組相同 7 7.1%
只差換行符 74 75.5%
內容真的不同 15 15.3%
磁碟上已不存在 2 2.0%

雜湊直接對得上的只有 7 個檔。 其餘 91 個裡,有 74 個內容一字不差,指紋卻是錯的。


這是同一個病的第四次

Day 1,我裝的 hook 一筆都沒收到,而且沒有任何錯誤訊息。原因是腳本讀不掉開頭那三個看不見的位元組就當掉了,而 Cursor 的 hook 失敗會照樣放行,所以整件事無聲。

Day 2,我設陷阱測 AI 會不會作弊,順便測我的規則抓不抓得到。規則裡有一個字寫錯(content 寫成 contents),「跳過測試」那條就永遠不會響。它每次都回報正常,因為它根本沒在看。

Day 4,有一種事件其實帶了「改了哪幾行」,但我的腳本用錯欄位去讀,整筆變成空白。不是沒記,是接錯了。

今天,內容記對了,指紋對不上。

四次都有紀錄。問題是那份紀錄回答不了我要問的問題。而且四次都沒有紅燈,沒有錯誤訊息,要自己動手比對才會發現。

220 筆紀錄裡有 201 筆(91%)在傳給我的腳本途中被重新編碼過一次,中文全變成亂碼。我還原得回來,因為位元組沒有真的損失。但我知道這件事,只是因為我當初多記了一個「這筆被改過編碼」的欄位。沒有那個欄位,我會拿著一批看起來很正常的亂碼資料繼續往下查。


那 15% 才是我還沒解決的

回到上面那張表。「內容真的不同」的 15 個檔,我目前分不出是哪一種情況。一種是軌跡漏收,AI 改了但 hook 沒攔到;另一種是那次修改根本不是 AI 做的,是我自己在編輯器裡改的,或是某個指令寫進去的。

這兩件事的嚴重程度差很多。第一種代表我的監視器有洞,第二種代表監視器沒問題,只是它本來就只看 AI。

我現在的腳本分不出來,所以我不會假裝分得出來。要分辨得把整條版本鏈重新套用一次,看斷在哪一步,那是後面重放那幾天的工作。

我最近在看 TypeSafe 九月中發表的 Jev,它不生成文字,只回校準過的機率。也許能拿來分這 15 個檔。API 我今天剛拿到,一次都還沒跑過,所以今天不會給你任何數字。而且就算跑出來了,還有一個更麻煩的問題在等著:機率算不算證據。


今天的成果,你可以直接拿去用

今天真正做出來的東西是兩個判斷式,都短到可以貼在這裡。

第一個,從寫檔紀錄找出「改回去」:把每次寫入的內容算一次雜湊,出現重複就代表內容回到了先前某一版。

import hashlib
seen = {}
for i, content in enumerate(versions, 1):      # versions = 每次寫入的內容
    h = hashlib.sha256(content.encode()).hexdigest()
    if h in seen:
        print(f"第 {i} 版 = 第 {seen[h]} 版,被改回去了")
    else:
        seen[h] = i

第二個,比對軌跡與磁碟時先把換行正規化,否則你會像我一樣白追一輪:

def fingerprint(data: bytes) -> str:
    return hashlib.sha256(data.replace(b"\r\n", b"\n")).hexdigest()[:12]

這兩個放進你自己的專案就能跑,不需要裝任何 agent。 拿你這週改過的檔試一次:算一次原始 sha256、再算一次正規化後的,如果你手上有任何「備份」或「稽核紀錄」宣稱保存了內容,兩邊對一次。對不上的話先別認為備份壞了,先看是不是換行符。

順便打開編輯器右下角,確認它存檔用的是 LF 還是 CRLF。這一格決定你未來所有雜湊比對會不會白做。


誠實欄

  • hook 是 9 月 14 日才修好開始收的。在那之前這個專案被改過多少次,軌跡上一個字都沒有,今天這些比例只涵蓋 9 月 14 日之後。
  • 我只比對了每個檔的最後一版跟磁碟。中間那些版本忠不忠實,我沒有驗。
  • 「把換行統一之後再比」是我自己選的判準。在別的情境下,換行符本身可能正好是要查的東西,這樣比就會把證據抹掉。
  • 「磁碟上已不存在」那 2 個檔,不代表軌跡救得回來,只代表我沒有可以比對的對象。
  • 98 這個數字是一台機器、一個專案、五天。不是統計結論。

明天我要把軌跡的欄位表定下來。這五天我查的都是它做了什麼,還沒碰到它憑什麼那樣做——而那一欄,就是我填不滿的那一欄。

對了,今天我剛好拿到 TypeSafe 的 Jev,一個不生成文字、只回機率的模型。我想知道它補得上哪幾欄、補不上哪幾欄。答案明天給你,包含它答錯的那幾題。


今天對應的威脅: T3(藉 Agent 之手規避控制)。規避之所以划算,前提是事後舉不出證。今天量到的是:在我這台機器上,這個前提成立的比例是 93%。

實驗與程式: 2026-09-19。原始輸出 research/samples/day05-chain.txtday05-replay-billing.txt;腳本 day05_replay.pyday05_chain.pyday05_bytediff.py。軌跡來源是 Cursor postToolUse hook 落下的 hooks.jsonl,220 筆寫入與刪除紀錄、98 個檔。本篇沒有引用外部來源。


上一篇
它刪的是測試還是垃圾檔?同一個 write_file,log 裡長得一模一樣
下一篇
欄位的三層境界,與Jev 模型到底能補上哪一欄
系列文
一個 Agent 我查不動,一個我改得動:內部威脅偵測的 30 天9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言