> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# 磁碟底層與資料救援（三）：救回記憶卡裡的五張照片
- URL: https://taiwanding.com/ci-die-di-ceng-yu-zi-liao-jiu-yuan-san-jiu-hui-ji-yi-qia-li-de-wu-zhang-zhao-pian/
- Published: 2026-09-03T08:41:29.000Z
- Updated: 2026-09-03T08:41:29.000Z
- Author: Kevin Chen
- Tags: 磁碟底層, 資料救援, 檔案系統, 自製靶機

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

[roulab-recovery-1.imgroulab-recovery-1.img.gz1 MBdownload-circle](https://taiwanding.com/content/files/2026/09/roulab-recovery-1.img.gz "Download")

## 為什麼第三課是照片

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

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

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

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

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

## 環境

```bash
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/`。

```bash
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：列名單

```bash
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 對、倒出來的東西打得開，這關過了，後面救出來的圖打不開，你就知道問題在圖不在流程。

```bash
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 本身沒有這個數字。

![](https://taiwanding.com/content/images/2026/09/fig7-ext4-vs-fat.jpg)

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

倒出來，順便丟到 Windows 看圖：

```bash
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 關，正常刪除

```bash
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 少，死了以後反而留得多。

```bash
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 連目錄格子都沒有，`fls`、`istat`、`icat` 全部沒用，它們都靠帳本，手上唯一剩的線索是內容本身。

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

```bash
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，換個寫法：

```bash
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`。

```bash
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：

```bash
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 關的流程走：

```bash
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 關那張的開頭已經告訴你了。對著複本寫，證物不動：

```bash
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)

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

```bash
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、大小也對，儀器全部亮綠燈，打開，只有上半張，下半張是雜訊。

![](https://taiwanding.com/content/images/2026/09/fig8-fragment-vs-guess.jpg)

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

要修，得回答兩個問題。

### 前半在哪裡結束

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

```bash
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

![](https://taiwanding.com/content/images/2026/09/fig9-ffd9-ruler.jpg)

三段各自一件事：

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

驗證：

```bash
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，輸出接在同一個檔後面：

```bash
( 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！