iT邦幫忙

0

LogCarver —— 不用先開 Audit/CDC,也能從 SQL Server 交易記錄檔重建被刪掉的資料(目前支援常見型別,持續擴充中)

  • 分享至 

  • xImage
  •  

專案名稱:LogCarver

專案簡介 :
SQL Server 交易記錄檔解析工具。多數資料救援工具都要求你「事先」開啟 Audit、CDC 或 Change Tracking,才能追蹤資料異動;但真實世界裡,大部分中小企業自架的 SQL Server 從來沒開過這些功能——資料被誤刪之後才想到要查,通常已經來不及。LogCarver 直接解析 SQL Server 自己的 fn_dblog,不需要事先設定任何東西,就能重建一張表完整的 INSERT / UPDATE(前後值)/ DELETE 歷程,並產生可審閱的 Undo / Replay SQL。

開發狀態 :已上線持續更新。 從v0.1.0 發到 v0.1.5,包含 3 個真實環境測出來的 bug 修補跟 3 個 SQL Server 版本(2019、2016、2022)的相容性驗證。


GitHub :https://github.com/caiderek/LogCarver (MIT 授權)


##技術實作內容##

開發動機

起點是一個很常見的場景:資料庫每天備份,但自己家裡的程式備份反而很隨便,想到才會備份一下。曾經遇過一個情況 只需要還原一小段資料,手上卻只有一天前的完整備份,結果變成得把當天所有的紀錄一筆一筆比對,才能抓出真正需要復原的那幾列。

問題是,SQL Server 的交易記錄檔本身就記著這些變更歷程,只是 fn_dblog 回傳的欄位、row image 的位元組配置幾乎沒有公開文件 想要知道格式,只能自己拿真實資料庫做實驗、直接讀 log 的原始位元組去反推。這段逆向工程過程本身記在另一份研究紀錄裡,是純粹靠實驗跑出來的,不是查文件或問 AI 就能得到的答案。LogCarver 則是把這個研究成果,加上 AI 輔助把邏輯寫成程式碼,落地成一個真正能用的工具。

技術架構

  • .NET 10,單一自包式執行檔,免安裝
  • 核心解碼邏輯完全不依賴 ORM,直接對 fn_dblog 的原始 RowLog Contents 位元組做欄位級解碼
  • 透過 sys.system_internals_partition_columns(未公開文件的系統檢視表)取得表的實體 row layout,而不是寫死一份型別對照表——欄位配置本來就會因為 ALTER TABLE 而改變,寫死對照表遲早會解錯
  • 明確偵測並拒絕做不到解碼、而非用猜的:壓縮表(ROW/PAGE)、off-row LOB 值、早於某次改表結構的舊紀錄,遇到都會清楚說明原因並拒絕解碼,不會給出看似合理但其實是錯的資料

核心邏輯與技術挑戰

工具上線後,我自己拿一張不重要的表做真實測試,沒有主鍵、或主鍵是 NONCLUSTERED (這種寫法建出來的底層儲存結構其實是 Heap,不是叢集索引)。結果查出來是 0 筆事件 ,而且工具也會很明確的說:這通常代表 VLF 已經被標記可重用、資料可能已經從 fn_dblog 的可視範圍滾出去了。

但是,這個解釋是錯的。

實際去查 fn_dblog 的原始輸出才發現:Heap 表的 row 操作(INSERT/UPDATE/DELETE)在 log 裡的 Context 欄位標記是 LCX_HEAP,但程式碼裡的查詢條件只認 LCX_CLUSTERED 跟 LCX_MARK_AS_GHOST 這兩種——Heap 表的紀錄從一開始就被整批濾掉了,跟「VLF 輪替」一點關係都沒有。更麻煩的是,這個錯誤訊息看起來很合理,如果沒有動手去查 fn_dblog 的原始輸出,很容易就會相信「喔,原來資料真的救不回來了」然後放棄。

修完 Context 過濾之後,又冒出第二個問題:表上如果有任何額外的索引(不管是 Heap 表上的 NONCLUSTERED PRIMARY KEY,還是叢集索引表上隨手加的一個查詢用索引),原本用來比對「這是哪張表」的邏輯是拿表名去猜 AllocUnitName 的字串樣式,但索引自己的內部維護紀錄,用的也是「表名.索引名稱」這種格式,會被同一組樣式誤抓進來,混進錯誤的欄位配置。同一張表上,兩種完全不同用途的資料被當成同一種東西解讀。

解決方案

與其繼續用字串樣式去猜,不如換一個方向:先查 SQL Server 自己的 sys.indexes,找出這張表真正對應的儲存結構(index_id 是 0 代表 Heap、1 代表叢集索引),組出唯一、精確的 AllocUnitName 字串,再用完全比對取代模糊比對。這樣一來,不管表上有幾個額外索引,都不會再誤抓進不相關的內部紀錄。

sys.system_internals_partition_columns 這邊也有一樣的根因:原本的查詢沒有過濾 index_id,對有額外索引的表,會把索引自己的欄位配置也一併撈進來,混進正確的表結構資料。加上同一個 index_id IN (0, 1) 過濾就解決了。

這整個除錯過程有一個明確的原則貫穿始終: 每一個假設都要拿真實的 SQL Server 去驗證,不能只憑文件或直覺 。fn_dblog 本身幾乎沒有公開文件,任何的猜測都有可能是錯的,這次三個 bug,沒有一個是靠讀文件發現的,全部是實際連上真實資料庫、直接看 fn_dblog 的原始輸出才抓出來。當天在三個不同版本(SQL Server 2022、2019、2016 SP2)上重跑同一套測試矩陣,確認修法在不同版本上行為一致,才正式把這三個版本列進「已驗證版本」清單。


*提醒邦友,使用第三方服務/API 時,請務必評估資安風險與隱私保護
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言