14 min read

磁碟底層與資料救援(三):救回記憶卡裡的五張照片

磁碟底層與資料救援(三):救回記憶卡裡的五張照片

聲明:本文用的靶碟是自己做的,一顆 64MB 的假記憶卡 image,不會碰到任何真實硬碟,五張照片上印的通關碼一律打碼為 Redacted,想自己先打的,靶碟和題目說明在文末。

為什麼第三課是照片

前兩課都是 ext4,這課換 FAT32,理由很簡單:真實世界最常被拜託救的東西是照片,而照片幾乎都躺在 FAT32 的記憶卡上。

靶碟的設定:一張相機記憶卡,主人說裡面有五張照片,現在只剩兩張看得到,五張照片五種狀況,由淺到深:

  1. 第 0 關:一張正常的照片,沒被刪
  2. 第 1 關:一張被正常刪除的照片
  3. 第 2 關:一張連檔名都沒有的照片
  4. 第 3 關:一張被刪掉、而且碎成兩半的照片
  5. 第 4 關:一張看得到、但打不開的照片

每張照片上都印著通關碼,救出來、打得開、讀得到碼,那關就算過。

重點:這課只用兩個工具,Sleuth Kit 和 grep,沒有 PhotoRec,先手動走一次,之後用自動工具你才知道它在替你做什麼、又在哪裡騙你。

環境

cp /mnt/d/Download/roulab-recovery-1.img.gz ~/lab/ && cd ~/lab
gunzip roulab-recovery-1.img.gz
md5sum roulab-recovery-1.img

md5 要是 6005d8729e0b4ef806e42daea12f2719,這顆 image 是證物,全程不對它寫入,所有輸出另外存到 ~/lab/out/

mmls roulab-recovery-1.img
      Slot      Start        End          Length       Description
000:  Meta      0000000000   0000000000   0000000001   Primary Table (#0)
001:  -------   0000000000   0000002047   0000002048   Unallocated
002:  000:000   0000002048   0000131071   0000129024   Win95 FAT32 (0x0c)

分割區一樣從 2048 開始,後面所有指令都帶 -o 2048,差別在最後一欄:Win95 FAT32 (0x0c),不是 Linux。

Step 1:列名單

fls -o 2048 -r -p roulab-recovery-1.img
r/r 3:  ROULAB      (Volume Label Entry)
d/d 4:  DCIM
d/d 21: DCIM/100LAB
r/r 37: DCIM/100LAB/IMG_0000.JPG
r/r * 38:       DCIM/100LAB/_MG_0001.JPG
r/r * 39:       DCIM/100LAB/_MG_0003.JPG
r/r * 40:       DCIM/100LAB/_ID_0001.BIN
r/r 41: DCIM/100LAB/IMG_0004.JPG
v/v 2032099:    $MBR
v/v 2032100:    $FAT1
v/v 2032101:    $FAT2
V/V 2032102:    $OrphanFiles

三件事:

  1. $FAT1$FAT2 FAT 的帳本(哪格接哪格)整本放在磁碟最前面,而且寫兩份,第二份是備份。
  2. 星號的名字第一個字變成底線 _MG_0001.JPG 原本是 IMG_0001.JPG,FAT 刪檔案時只把檔名的第一個 byte 改成 0xE5,代表「這格空了」;其他 10 個字、大小、時間、第一個 cluster 的位置全部留在原地,TSK 看到 0xE5 就顯示成底線。
  3. 少了 IMG_0002 0000、0001、0003、0004 都在,連底線版都沒有 0002,目錄裡完全沒有它的格子。

重點:這就是三種「不見」的差別。0001、0003 是格子還在、只是標成可回收;0002 是格子沒了,帳本完全不認識它,只剩內容可能還散在碟上。

Step 2:第 0 關,校正儀器

第 0 關不是救援,是確認流程,拿到一顆碟,先找一個好好的檔案走一遍,確認工具讀得對、offset 對、倒出來的東西打得開,這關過了,後面救出來的圖打不開,你就知道問題在圖不在流程。

istat -o 2048 roulab-recovery-1.img 37
Directory Entry: 37
Allocated
File Attributes: File, Archive
Size: 223671
Name: IMG_0000.JPG
...
Sectors:
2021 2022 2023 2024 2025 2026 2027 2028
...
2453 2454 2455 2456 2457

Size 223671,Sectors 從 2021 一路連號到 2457,數一下:2457 − 2021 + 1 = 437 格 × 512 = 223744 bytes,剛好裝得下 223671,這是「健康的檔案」的長相:帳本說它在哪,資料就連續躺在那,後面每關都拿這個當標準比。

這顆碟的 cluster 是 512 bytes,跟 sector 一樣大,所以 istat 印的 sector 號可以直接當 cluster 號用。

38 是 inode 嗎

跟第一課對照一下,ext4 是三步:名字 → inode(卡片)→ block(內容)。FAT 沒有獨立的卡片,名字、大小、時間、第一格在哪,全部擠在目錄裡的同一個 32-byte 格子,TSK 為了讓你能用同一套指令,自己把這些格子編號,38 是 TSK 給那個格子的號碼,FAT 本身沒有這個數字。

差別在最後一塊:ext4 把「內容在哪幾格」全寫在 inode 裡;FAT 只在格子裡寫「第一格」,第二格以後要去翻 $FAT1 帳本,

倒出來,順便丟到 Windows 看圖:

mkdir -p ~/lab/out
icat -o 2048 roulab-recovery-1.img 37 > ~/lab/out/L0.jpg
cp ~/lab/out/L0.jpg /mnt/d/Download/
file ~/lab/out/L0.jpg
L0.jpg: JPEG image data, JFIF standard 1.01, ... 1024x768, components 3

打開,通關碼 Redacted,儀器沒問題。

Step 3:第 1 關,正常刪除

istat -o 2048 roulab-recovery-1.img 38
Directory Entry: 38
Not Allocated
File Attributes: File, Archive
Size: 213350
Name: _MG_0001.JPG
...
Sectors:
2458 2459 2460 2461 2462 2463 2464 2465
...
2874

看起來跟第 0 關幾乎一樣:Size 還在、時間還在、Sectors 也是一整串連號,只差一行 Not Allocated

但這串 Sectors 的來源不同,第 0 關那串是 TSK 照帳本一格一格讀出來的;這張刪除時帳本的鏈已經被清成 0,TSK 手上只剩目錄格子裡的兩個數字:第一格 2458、大小 213350,它就假設「檔案是連續的」,自己往後數 417 格填給你看。

重點:Not Allocated 底下的 Sectors 是 TSK 的猜測,不是紀錄。這張碰巧連續所以猜對了,第 3 關就不會了,

順便把兩種檔案系統的刪除擺在一起:

  1. ext4 刪除:inode 整張清空,位置全沒了(第一課親眼看到的,只剩 journal 有備份)
  2. FAT 刪除:格子只被塗掉第一個字,第一格和大小都還在,只有帳本的鏈被清成 0

FAT 活著的時候資訊比 ext4 少,死了以後反而留得多。

icat -o 2048 roulab-recovery-1.img 38 > ~/lab/out/L1.jpg
cp ~/lab/out/L1.jpg /mnt/d/Download/
file ~/lab/out/L1.jpg

打得開,通關碼 Redacted,第 1 關過。

Step 4:第 2 關,沒有名字

0002 連目錄格子都沒有,flsistaticat 全部沒用,它們都靠帳本,手上唯一剩的線索是內容本身。

要靠內容找,得先知道 JPEG 長什麼樣,拿第 0 關那張好的看頭尾:

xxd -l 16 ~/lab/out/L0.jpg
xxd ~/lab/out/L0.jpg | tail -1
00000000: ffd8 ffe0 0010 4a46 4946 0001 0100 0001  ......JFIF......
000369b0: bf56 f57e 67ff d9                        .V.~g..

每張 JPEG 都是 ff d8 ff 開頭、ff d9 結尾,這叫 file signature,是檔案格式層的東西,跟檔案系統無關,所以帳本沒了它還在。

這裡把兩個詞分清楚,後面都會用到:

  1. 檔案系統(ext4、FAT32、NTFS):管「檔案放在磁碟哪裡」的帳本規則
  2. 檔案格式(JPEG、PNG):檔案內容本身怎麼排

帳本怎麼記是檔案系統的事,內容長什麼樣是檔案格式的事,救援時兩層都要看。

第一課用 grep -a -b 在整顆碟找文字,這次找的是 bytes,換個寫法:

LC_ALL=C grep -obUaP '\xff\xd8\xff' roulab-recovery-1.img
2083328:���
2307072:���
2520576:���
42991616:���

-o 只印命中的、-b 印 byte offset、-P 讓你用 \xff 寫 byte。

四個,接下來是排除法:四個裡面有三個是認識的照片,剩下的才是 0002,換算跟第一課一樣:byte offset ÷ 512 = 整顆碟的 sector 號,再減 2048 = istat 顯示的 sector 號。

  1. 2083328 → 4069 − 2048 = 2021 → IMG_0000 的第一格
  2. 2307072 → 4506 − 2048 = 2458 → _MG_0001 的第一格
  3. 2520576 → 4923 − 2048 = 2875 → _MG_0003 的第一格(等一下 istat 39 會看到)
  4. 42991616 → 83968 − 2048 = 81920 → 離大家都遠,沒人認領

81920 就是 0002 的頭。

順便留個疑點:碟上明明還有一張 IMG_0004.JPG 好好活著,為什麼只命中四個?先記著,第 4 關回來看。

知道從哪開始,但沒有目錄格子告訴你它多大,那就找尾巴:從 42991616 往後找到的第一個 ff d9

LC_ALL=C grep -obUaP '\xff\xd9' roulab-recovery-1.img | awk -F: '$1 > 42991616 {print $1; exit}'
43224787

新手坑:「往後找」成立的前提是檔案連續,你把頭到尾中間整段當成 0002,靠的是它從頭到尾躺在連續的位置上,檔案碎成兩段時,第二段可能在頭的前面、也可能隔很遠,「從頭往後找第一個 ff d9」就會切到別人的尾巴,這是 carving 的天生限制,第 3 關會親手撞一次。

切出來,43224787 是 ff 的位置,d9 在它後面一個,所以尾巴要 +2:

dd if=roulab-recovery-1.img of=~/lab/out/L2.jpg bs=1 skip=42991616 count=$((43224787 + 2 - 42991616)) status=none
cp ~/lab/out/L2.jpg /mnt/d/Download/
file ~/lab/out/L2.jpg

打得開,通關碼 Redacted,這關沒用到任何帳本,純靠檔案格式救回來的。

真實世界多一個坑:相機拍的 JPEG 裡面會內嵌一張縮圖,縮圖自己也有 ff d9。「第一個 ff d9」可能是縮圖的尾巴,切出來只有一張小圖,這顆靶碟沒放縮圖,真相機的記憶卡就有。

Step 5:第 4 關,壞掉的檔頭 (先拿0004)

回到疑點:IMG_0004.JPG 帳本說它活得好好的,但 ff d8 ff 搜不到它,照第 0 關的流程走:

icat -o 2048 roulab-recovery-1.img 41 > ~/lab/out/L4.jpg
file ~/lab/out/L4.jpg
xxd -l 16 ~/lab/out/L4.jpg
L4.jpg: data
00000000: 0000 0000 0010 4a46 4946 0001 0100 0001  ......JFIF......

file 認不出來,因為簽名沒了,只有前 4 個 byte 變成 0,第 5 個 byte 開始的 0010 4a46 4946 跟第 0 關一模一樣,圖片檢視器也是先看這 4 個 byte 決定要不要理它。

這種「只壞開頭一小段」很常見:壞軌剛好在第一個 sector、或別的檔案短暫蓋過去,剩下 99.9% 的資料都是好的,只是沒人肯開。

修法是把那 4 個 byte 補回去,該補什麼,第 0 關那張的開頭已經告訴你了。對著複本寫,證物不動:

printf '\xff\xd8\xff\xe0' | dd of=~/lab/out/L4.jpg bs=1 conv=notrunc status=none
file ~/lab/out/L4.jpg
cp ~/lab/out/L4.jpg /mnt/d/Download/

conv=notrunc 是關鍵:只覆蓋前 4 byte,不把檔案截斷,打得開,通關碼 Redacted

Step 6:第 3 關,碎成兩半 (再拿0003)

最難的一關,先撞牆,再拆。

istat -o 2048 roulab-recovery-1.img 39 | head -20
icat -o 2048 roulab-recovery-1.img 39 > ~/lab/out/L3-naive.jpg
file ~/lab/out/L3-naive.jpg
Directory Entry: 39
Not Allocated
Size: 202464
Name: _MG_0003.JPG
Sectors:
2875 2876 2877 2878 ...
L3-naive.jpg: JPEG image data, JFIF standard 1.01, ... 1024x768, components 3

istat 說一串連號、file 說是正常的 JPEG、大小也對,儀器全部亮綠燈,打開,只有上半張,下半張是雜訊。

真實情況:0003 寫到一半時,有別的檔案(_ID_0001.BIN)插進來佔了後面的位置,0003 的後半只好跳到更後面去,帳本本來記得這個跳躍,刪除時被清成 0,TSK 只能從 2875 一路往後數,數到前半結束後,接下來拿到的就是 BIN 的亂數。JPEG 解碼器解到亂數就放棄,所以你看到半張。

要修,得回答兩個問題。

前半在哪裡結束

線索就是插隊的那個檔案,它也有目錄格子,編號 40:

istat -o 2048 roulab-recovery-1.img 40 | head -14
Directory Entry: 40
Not Allocated
Size: 409600
Name: _ID_0001.BIN
Sectors:
3073 3074 3075 3076 ...

它從 3073 開始,Size 409600 剛好是 800 格(409600 ÷ 512 = 800),佔 3073 到 3872。

那 0003 的前半就是 2875 到 3072:3072 − 2875 + 1 = 198 格 × 512 = 101376 bytes,這段是連續的,也是 TSK 猜對的部分。

後半在哪

想想 0003 當初是怎麼被寫的:寫到 3072 發現下一格被 BIN 佔了,FAT 的習慣是「找下一個空格繼續」,而 BIN 後面的第一個空格是 3873,最合理的猜測:後半從 3873 開始,連續躺到結束。

但這是猜測,要驗證,方法第 2 關學過了:如果後半真的從 3873 開始、長度剛好是剩下的 bytes,那 ff d9 就應該出現在算出來的那個位置,不多不少。

剩下的 bytes:202464 − 101376 = 101088。÷512 = 197 格餘 224,最後一格只用 224 bytes,剩下 288 bytes 是空的,跟第 0 關 437 格裝 223671 一樣,最後一格永遠不會剛好填滿。

ff d9 預期的位置:

(2048 + 3873) × 512 + 101088 − 2 = 3132638

三段各自一件事:

  1. (2048 + 3873) × 512 把「分割區的第 3873 格」變成整顆 image 的 byte offset。第 2 關那個換算的反向:那時 ÷512 再 −2048,現在 +2048 再 ×512,得到 3031552,後半的起點。
    • 101088 從起點走完整個後半,走到 3132640,這個數字是「檔案結束後的第一個 byte」,不在檔案裡。
  2. − 2 檔案最後兩個 byte 是 ff d9d9 在 3132639、ff 在 3132638。grep 回報的是 ff 的位置,所以退 2。

驗證:

LC_ALL=C grep -obUaP '\xff\xd9' roulab-recovery-1.img | awk -F: '$1 > 3031552 {print $1; exit}'
3132638

一個 byte 都不差,這種「剛好」不會是巧合:從 3873 起算、走 101088 bytes,最後兩個 byte 剛好是 JPEG 的結尾記號,後半的位置和長度同時被證實。

接起來

三個數字都驗證過了:前半 2875 起 198 格、後半 3873 起 101088 bytes。兩個 dd,輸出接在同一個檔後面:

( dd if=roulab-recovery-1.img bs=512 skip=$((2048+2875)) count=198 status=none; dd if=roulab-recovery-1.img bs=1 skip=3031552 count=101088 status=none ) > ~/lab/out/L3.jpg
file ~/lab/out/L3.jpg
ls -l ~/lab/out/L3.jpg
cp ~/lab/out/L3.jpg /mnt/d/Download/

前半整格整格拿,用 bs=512;後半尾巴不滿一格,改 bs=1 精準切,ls -l 顯示 202464,跟目錄格子上的 Size 一樣,打開,下半張回來了,通關碼 Redacted

結語

這顆靶碟教的五件事:

  1. 救援要看兩層:檔案系統的帳本、檔案格式的內容,帳本壞了靠格式找(第 2 關),格式壞了靠帳本找到再修(第 4 關)
  2. FAT 刪除只塗掉檔名第一個字,第一格和大小都還在,比 ext4 留得多,所以連續的檔案 icat 直接救得回來
  3. Not Allocated 底下的 Sectors 是 TSK 從第一格往後數出來的猜測,不是紀錄。工具亮綠燈不代表對
  4. carving 的前提是檔案連續,碎掉的檔案,要靠鄰居的格子和檔案格式的尾巴一起把第二段找回來
  5. 每個猜測都要驗證,算出來的位置和 grep 找到的對上一個 byte 都不差,才算成立

Happy Carving!