Jak odzyskać usunięte z dysku pliki

Spis treści

Całkowite usuwanie plików (shred) jak i zerowanie całych nośników ma na celu nieodwracalne zniszczenie danych. W tych podlinkowanych artykułach próbowaliśmy zatrzeć ślady po skasowanych plikach. W tym wpisie zaś prześledzimy sobie co tak naprawdę się dzieje po utworzeniu i skasowaniu pliku, a także spróbujemy odzyskać te z nich, które już nie istnieją w naszym systemie. Ten artykuł będzie dotyczył jedynie systemu plików z rodziny ext , głównie ext4 .

Czy usunięcie pliku niszczy dane?

By być w stanie zapisywać informacje na jakimkolwiek nośniku danych, potrzebny jest odpowiedni system plików, a tych jest oczywiście cała masa. System plików, to metadane opisujące zbiory binarnych jedynek i zer, które my rozumiemy pod nazwą pliku lub katalogu (katalog to też plik). W przypadku gdy system nie ma informacji o pewnym interesującym nas pliku, znaczy to tylko tyle, że ten plik zwyczajnie nie jest opisany w strukturze systemu plików. Oczywiście w takim przypadku nie możemy zwyczajnie uzyskać do niego dostępu, no bo niby w jaki sposób mielibyśmy to zrobić? Pod tym linkiem znajduje się ciekawe doświadczenie, które postanowiłem odtworzyć na mojej maszynie. Chodzi generalnie o zademonstrowanie, że usunięcie pliku z dysku nie niszczy danych oraz, że jesteśmy w stanie takie pliki bez problemu przywrócić. Stwórzmy sobie przykładowy plik tekstowy i dodajmy do niego jakąś treść:

# echo "Przykladowy plik tekstowy" > plik-tekstowy

Spróbujmy dowiedzieć się czego więcej o tym pliku. Potrzebujemy zatem ustalić najpierw numer i-węzła (i-node) odpowiedzialnego za ten plik. Możemy to zrobić za pomocą ls . Mając numer i-węzła, informacje o nim możemy uzyskać z istat oraz z fsstat , przykładowo:

# ls -li plik-tekstowy
262882 -rw-r--r-- 1 root root 26 2015-11-25 14:30:10 plik-tekstowy

# istat /dev/mapper/debian_laptop-root 262882
inode: 262882
Allocated
Group: 32
Generation Id: 582150946
uid / gid: 0 / 0
mode: rrw-r--r--
Flags: Extents,
size: 26
num of links: 1

Inode Times:
Accessed:       2015-11-25 14:30:10.762195266 (CET)
File Modified:  2015-11-25 14:30:10.762195266 (CET)
Inode Modified: 2015-11-25 14:30:10.762195266 (CET)
File Created:   2015-11-25 14:30:10.762195266 (CET)

Direct Blocks:
1094714

# fsstat /dev/mapper/debian_laptop-root
...
Group: 32:
  Block Group Flags: [INODE_ZEROED]
  Inode Range: 262145 - 270336
  Block Range: 1048576 - 1081343
  Layout:
    Data bitmap: 1048576 - 1048576
    Inode bitmap: 1048592 - 1048592
    Inode Table: 1048608 - 1049119
    Data Blocks: 1056800 - 1081343
  Free Inodes: 7 (0%)
  Free Blocks: 3821 (11%)
  Total Directories: 1193
  Stored Checksum: 0x632F
...

W systemie plików ext4 , i-węzły mają rozmiar 256 bajtów, co daje 16 i-węzłów na jeden 4KiB blok. Zatem, w którym dokładnie bloku znajduje się szukany przez nas i-węzeł? Zakres i-węzłów tej grupy zaczyna się na 262145 , zaś w ls został podany numerek 262882 . Odejmujemy pierwszą wartość od drugiej: 262882-262145=737. Dzielimy teraz tę wartość przez 16 i otrzymujemy 46.0625. Zatem, ten i-węzeł znajduje się gdzieś w bloku 46 w tablicy i-węzłów. Jeśli pierwszy i-węzeł w tej tablicy ma numer 1048608, to szukany i-węzeł znajduje się gdzieś w między 256-512 bajtami bloku 1048654. Chodzi o to, że blok może mieć 16 części po 256 bajtów. Jako, że dzielenie 737/16 nie dało nam liczby całkowitej, oznacza to, że szukany i-węzeł jest gdzieś w środku tego bloku. Gdzie dokładnie, to można ustalić dzieląc 1/16. Każda z tych szesnastu części odpowiada 0.0625, zatem wiemy, że są to bajty 256-512.

Zróbmy sobie zatem kopię całego bloku 1048654:

# blkcat /dev/mapper/debian_laptop-root 1048654 > blk-1048654

# ls -al | grep blk
-rw-r--r--   1 root root 4.0K 2015-11-25 15:12:17 blk-1048654

Musimy poddać ten plik analizie w edytorze hexalnym, np. w wxhexeditor, który jest standardowo dostępny w repozytoriach debiana. Każdy i-węzeł rozpoczyna się od A4 81 . Wiemy także, że rozmiar takiego i-węzła to 256 bajtów, zatem ładujemy plik i oszukujemy tę wspomnianą wartość. Pamiętajmy, że interesuje nas drugi i-węzeł, a nie pierwszy. Zaznaczony niżej fragment odpowiada rozmiarowi pliku:

i-wezel-edytor-hex-ext4-1

W tym przypadku jest to 1A co oznacza liczbę 26 i jeśli wrócimy wyżej do logu ls , to faktycznie nasz plik tekstowy ma 26 bajtów.

Jako, że system plików ext4 używa extent'ów, to 60 bajtów na pozycji od 40-99 włącznie (licząc w stosunku do konkretnego i-węzła) przechowuje informacje o zakresach bloków:

i-wezel-edytor-hex-ext4-2

W sumie jest to pięć wpisów, każdy po 12 bajtów. Z tym, że pierwsze 12 bajtów to nagłówek, pozostałe bajty są w stanie opisać maksymalnie 4 zakresy bloków. Rzućmy zatem okiem na sam nagłówek. Mamy tam następujące bajty:

  • 296-297: magiczna liczba (Magic number), 0xF30A = 62218
  • 298-299: liczba zakresów 0x0001 = 1
  • 300-301: maksymalna liczba zakresów 0x0004 = 4
  • 302-303: głębokość drzewa (Depth of tree) 0x0000 = 0
  • 304-307: ID 0x00000000 = 0

Gdy do systemu plików są dodawane nowe rzeczy lub zmianie ulegają stare, magiczna liczba może ulec zmianie i w ten sposób wiemy, że mamy do czynienia z dwiema różnymi implementacjami zakresów. Liczba zakresów odpowiada tej aktualnie zarezerwowanej przez plik i w tym przypadku, jako że plik nie jest duży, to system plików jest w stanie go opisać za pomocą jednego zakresu. Maksymalna liczba zakresów w ext4 wynosi póki co 4.

Wiemy zatem, że nasz plik ma jeden zakres, wobec czego musimy przebadać dodatkowo kolejne 12 bajtów, bo to one opisują ten zakres. Poniżej opis poszczególnych bajtów:

  • 308-311: numer bloku logicznego (Logical block number), 0x00000000
  • 312-313: liczba bloków w zakresie0x0001
  • 314-315: górne 16 bitów adresu fizycznego bloku 0x0000
  • 316-319: dolne 32 bity adresu fizycznego bloku 0x0010B43A

Numer bloku logicznego informuje nas o tym gdzie ten zakres się zaczyna względem początku pliku. Ta pozycja staje się o wiele bardziej ważna, gdy w grę wchodzi wiele zakresów. W tym przypadku mamy do czynienia z jednym i ten numer zawsze będzie wskazywał na początek pliku. Liczba bloków w zakresie określa ile bloków zajmuje dany plik. Tutaj mamy mały plik tekstowy i nie przekracza on 4096 bajtów, zatem zajmuje tylko jeden blok. Ostatnie 6 bajtów mówi nam gdzie ten zakres się dokładnie zaczyna podając nam adres pierwszego fizycznego bloku. Jako, że obecne systemy chcą równać wartości do 16, 32 i 64 bitów, to 48 bitowy adres bloku jest zapisywany w postaci dwóch wartości. Pierwsze 2 bajty dają górne 16 bitów adresu bloku, a kolejne 4 bajty zawierają 32 bity niższego rzędu. W taki sposób uzyskujemy adres bloku 0x00000010B43A, który odpowiada blokowi numer 1094714 .

Spróbujmy teraz usunąć z dysku ten plik cośmy go utworzyli zaraz na początku tego wpisu. Zróbmy to standardowo przy pomocy polecenia rm :

# rm plik-tekstowy

W tej chwili może się wydawać, że plik jest usunięty ale nadal jesteśmy w stanie odczytać jego zawartość udowadniając tym samym, że usunęliśmy jedynie odnośnik do tego pliku nie zmieniając praktycznie jego zawartości. By odczytać taki usunięty plik, wpisujemy w terminal to poniższe polecenie:

# blkcat /dev/mapper/debian_laptop-root 1094714
Przykladowy plik tekstowy

Jak pokazuje ten ciekawy przykład, usuwanie plików praktycznie nic nie usuwa z dysku, zatem jesteśmy w stanie te pliki odzyskać. Wystarczy by przestrzeń, na której rezyduje dany plik, nie była ponownie nadpisana. Jeśli podłączyliśmy dysk do komputera i okazało się, że jakieś pliki zwyczajnie nam z niego zniknęły, to jak najszybciej przemontumy ten dysk w tryb tylko do odczytu (lub też go całkowicie odmontujemy) przy pomocy tego poniższego polecenia:

# mount -o remount,ro /dev/sdb

Jeżeli nie jesteśmy w stanie odmontować danej partycji, być może jest to za sprawą jakiegoś procesu, który w dalszym ciągu z niej korzysta. Wtedy powinniśmy taki proces zabić i spróbować ponownie. By ustalić, które procesy korzystają z tego systemu plików, wpisujemy w terminalu to poniższe polecenie:

# fuser -vukm /dev/mapper/kabi

Powyżej mamy określoną także flagę -k , która automatycznie zabije wszystkie wypisane procesy.

Czy trudno jest odzyskać usunięte pliki?

Jak mogliśmy zobaczyć na powyższym przykładzie, skasowane pliki w systemie plików ext4 w dalszym ciągu są obecne na dysku. Co jednak w przypadku, gdy usuniemy plik i nie mamy do niego łatwego odwołania, tak jak to było pokazane wyżej? W takiej sytuacji możemy posłużyć się paroma technikami. Pierwsza z nich nazywa jest File Carving , gdzie przeszukujemy ciąg bitów w poszukiwaniu dopasowań do wzorców pliku. Każdy plik zawiera nagłówek (i bardzo często też stopkę), na podstawie którego to jesteśmy w stanie określić typ pliku analizując jedynie same zera i jedynki. Ta metoda znajduje głównie zastosowanie w przypadku plików ciągłych, gdzie dane następują po sobie i nie są w żaden sposób pofragmentowane. Jeśli mamy do czynienia z pofragmentowanym plikiem, wtedy mogą się pojawić problemy z jego odczytem, lub też odczytanie takiego pliku może w ogóle nie być możliwe. Inna metodą odzyskiwania danych jest analiza dziennika, o ile dany system plików go udostępnia, a tak jest w przypadku ext4 . W tym przypadku, ciągłość pliku nie ma większego znaczenia.

Odzyskiwanie plików metodą File Carving

By móc się bawić w odzyskiwanie plików, potrzebujemy odpowiednich narzędzi. W debianie musimy sobie zainstalować pakiet sleuthkit oraz foremost. W pakiecie sleuthkit mamy szereg narzędzi, w tym też te, które zostały wykorzystane w doświadczeniu powyżej.

Zatem do dzieła. Weźmy sobie dla przykładu jakieś plik graficzny i umieśćmy go w przykładowym katalogu na nieużywanym systemie plików, po czym go skasujmy, tak by nie pozostał po nim żaden widoczny ślad, przykładowo:

# rm plik.pdf

Tutaj już nie znamy lokalizacji pliku, tak jak to miało miejsce w doświadczeniu, które przeprowadziliśmy na samym początku. Jako, że jest to symulacja przypadkowego usunięcia ważnego pliku, to musimy postępować tak jak to robilibyśmy gdyby faktycznie coś ważnego nam uległo skasowaniu. Dlatego też pamiętajmy, by przemontować system plików w tryb tylko do odczytu. Pamiętajmy też, by nigdy nie operować na faktycznych danych, a jedynie na backup'ie. W przypadku partycji czy całych dysków musimy zrobić sobie kopię binarną takiego nośnika za pomocą dd , poniżej przykład:

# dd if=/dev/mapper/grafi of=/media/sdb/grafi.img

Mając zrobiony obraz, możemy na nim przeprowadzić poniższe czynności. Na początek przy pomocy fls skanujemy system plików obrazu pod kątem usuniętych plików:

# fls -dr grafi.img | grep pdf
r/r * 12:       plik.pdf

Liczba 12 to numer i-węzła, który musimy poddać analizie przy pomocy istat :

# istat grafi.img 12
inode: 12
Not Allocated
Group: 0
Generation Id: 3317407099
uid / gid: 0 / 0
mode: rrw-r--r--
Flags: Extents,
size: 0
num of links: 0

Inode Times:
Accessed:       2015-11-28 15:55:38.612100126 (CET)
File Modified:  2015-11-28 15:56:41.131424193 (CET)
Inode Modified: 2015-11-28 15:56:41.131424193 (CET)
File Created:   2015-11-28 15:55:38.612100126 (CET)
Deleted:        2015-11-28 15:56:41 (CET)

Direct Blocks:

Wyżej widzimy szereg informacji opisujących plik, gdzie mamy również pozycję Deleted , która nam mówi, że ten plik został skasowany o określonej godzinie. Lista Direct Blocks jest pusta, zatem nie mamy możliwości określenia, które bloki ten plik zajmował. Nas jednak bardziej interesuje grupa bloków, którą w tym przypadku ma numer 0 . Odszukujemy jej opis przy pomocy fsstat :

# fsstat grafi.img
....
Group: 0:
  Block Group Flags: [INODE_ZEROED]
  Inode Range: 1 - 8192
  Block Range: 0 - 32767
  Layout:
    Super Block: 0 - 0
    Group Descriptor Table: 1 - 1
    Group Descriptor Growth Blocks: 2 - 64
    Data bitmap: 65 - 65
    Inode bitmap: 73 - 73
    Inode Table: 81 - 592
    Uninit Data Bitmaps: 73 - 80
    Uninit Inode Bitmaps: 81 - 88
    Uninit Inode Table: 4177 - 8272
    Data Blocks: 8289 - 32767
  Free Inodes: 8029 (98%)
  Free Blocks: 26535 (80%)
  Total Directories: 6
  Stored Checksum: 0xB0D7
...

Mamy tam podaną informację, że bloki danych znajdują się na pozycji 8289 - 32767 . Generalnie można by przebadać cały ten obszar ale nas interesują jedynie te bloki, które nie są aktualnie przydzielone, bo to w nich znajduje się nasz skasowany plik. By wyciągnąć te nieprzydzielone bloki, posługujemy się blkls :

# blkls -A grafi.img 8289-32767 > free.img

# ls -al free.img
-rw-r--r-- 1 root root 89M 2015-11-28 16:58:06 free.img

Teraz już zostało nam jedynie przeszukać te 89 MiB danych przy pomocy foremost . Pamiętajmy, że szukamy pliku .pdf :

# foremost -d -i free.img -o /media/sdb/odzysk -t pdf
Processing: free.img

Po chwili proces powinien się zakończyć, a w katalogu /media/sdb/odzysk/ powinny znajdować się jakieś pliki:

# tree /media/sdb/odzysk/
/media/sdb/odzysk
├── audit.txt
└── pdf
    └── 00017960.pdf

1 directory, 2 files

Plik wprawdzie nie ma starej nazwy ale za to został w pełni odzyskany i nadaje się do odczytu. Mamy też plik audit.txt , który zawiera log z operacji. W tym powyższym przykładzie chcieliśmy odzyskać tylko jeden plik. W przypadku, gdy plików byłoby więcej i obszar do przeszukania również byłby znacznie większy, wtedy najlepiej przebadać cały obraz i w opcji -t dać all . W taki sposób będziemy próbować odzyskać wszystkie skasowane pliki z całego obrazu.

Odzyskiwanie plików przy pomocy dziennika

System plików ext4 ma również dziennik, który loguje przeprowadzane operacje. Dzięki temu, system jest w stanie podnieść się po awarii i odzyskać uszkodzone za jej sprawą pliki. Skoro system potrafi odzyskać pliki na podstawie dziennika, to my też powinniśmy być w stanie to zrobić. Trzeba tylko pamiętać, że wielkość dziennika zależy od rozmiaru partycji. Na tych większych partycjach rozmiar dziennika przy standardowym formatowaniu sięga 128MiB. Nie jest to zatem dużo. Oczywiście, "dużo" to pojęcie względne, bo to jakie dane będą logowane do dziennika zależy od konfiguracji umieszczonej w pliku /etc/fstab . Zakładając, że mamy domyślne ustawienia, to wszystkie partycje są skonfigurowane z opcją data=ordered , która ma na celu logowanie jedynie metadanych z operacji bez załączania faktycznych danych. W ten sposób 128MiB wydaje się być sporym zapasem.

Warto zaznaczyć, że większość kroków, które przeprowadzaliśmy w poprzednim rozdziale jest dokładnie taka sama i nie będę ich tutaj opisywał ponownie. Generalnie rzecz biorąc, musimy utworzyć sobie jakiś plik, następnie go skasować, po czym stworzyć obraz zawierający kopię partycji przy pomocy dd . Tam dszukujemy oczywiście i-węzeł i patrzmy w fsstat na opis grupy zawierającej ten i-węzeł:

# fls -rd grafi.img
r/r * 167:      plik.mp3

# istat grafi.img 167
inode: 167
Not Allocated
Group: 0
...

# fsstat grafi.img
...
Group: 0:
  Block Group Flags: [INODE_ZEROED]
  Inode Range: 1 - 8192
  Block Range: 0 - 32767
  Layout:
    Super Block: 0 - 0
    Group Descriptor Table: 1 - 1
    Group Descriptor Growth Blocks: 2 - 128
    Data bitmap: 129 - 129
    Inode bitmap: 145 - 145
    Inode Table: 161 - 672
    Uninit Data Bitmaps: 129 - 144
    Uninit Inode Bitmaps: 145 - 160
    Uninit Inode Table: 161 - 8352
    Data Blocks: 8353 - 32767
  Free Inodes: 7839 (95%)
  Free Blocks: 24381 (74%)
  Total Directories: 14
  Stored Checksum: 0x26EF

Musimy ustalić pozycję i-węzeła. Każdy blok ma 4096 bajtów, każdy i-węzeł ma 256 bajtów i w sumie na jeden blok składa się 16 i-węzłów. Tutaj mamy numerek 167 , zatem interesuje nas dziesiąty blok w tablicy i-węzłów, który ma numer 171 (161+10). Tu właśnie do gry wchodzą logi z dziennika, które musimy prześledzić w poszukiwaniu tego numeru 171 . By przejrzeć dziennik, w terminalu wpisujemy jls podając w argumencie plik wcześniej utworzonego obrazu, przykładowo:

# jls grafi.img | grep -i " 171"
181:    Allocated FS Block 171
191:    Allocated FS Block 171
364:    Allocated FS Block 171
368:    Allocated FS Block 171
375:    Allocated FS Block 171

Jest kilka wpisów dotyczących tego bloku. Im wcześniejsze tym większe prawdopodobieństwo, że plik ma pierwotną formę, bo kolejne wpisy oznaczają, że został on w jakiś sposób zmieniony. Weźmy zatem wpis numer 181 . Musimy podejrzeć cały ten blok w edytorze hexalnym. Możemy zwyczajnie wyświetlić ten plik i wyjście przesłąć do xxd albo też możemy zapisać cały blok do pliku i wczytać go w wxHexEditor . Ja sobie zapisałem wszystkie te powyższe wpisy do osobnych plików przy pomocy tego poniższego polecenia:

# jcat grafi.img 8 181 > blok-171-1

Cyfra 8 jest zwykle numerem i-węzła dziennika systemu plików. Jest to standardowa wartość ale zawsze można ją ustalić w poniższy sposób:

# fsstat grafi.img | grep -i journal
...
Journal Inode: 8

Jako, że i-węzeł miał numer 167, to odszukujemy 7 (7/16) i-węzeł w edytorze. Pamiętajmy, że są one numerowane od zera, zatem jest to 1536 bajt. Dla pewności można też sprawdzić rozmiar pliku (offset od 4-7 bajta włącznie, przykładowo:

jak-odzyskac-pliki

Powyższy zapis 0x001A260D odpowiada za 1713677 bajtów i faktycznie tyle miał ten plik .mp3 . Zatem udało się odnaleźć odpowiedni blok. Teraz musimy jeszcze ten plik odzyskać.

Na początek patrzymy w adresy bloków. W tym przypadku mamy poniższy zapis:

0A F3 01 00 04 00 00 00 00 00 00 00
00 00 00 00 A3 01 00 00 27 6F 02 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00

Pierwsza linijka odpowiada za nagłówek. Druga za pierwszy zakres bloków. Jako, że pozostałe mają same zera, to oznacza to, że plik jest opisany jednym zakresem bloków. Ustalmy teraz początek zakresu. Drugą linijkę dzielimy na kilka części:

00 00 00 00
A3 01
00 00
27 6F 02 00

Adres pierwszego bloku to te dwie ostatnie pozycje, zatem w zapisie hexalnym będzie to liczba 0x000000026f27 , czyli 159527 w zapisie dziesiętnym. Natomiast 0x01A3 to ilość kolejnych bloków, które plik zajmuje, w tym przypadku jest to 419 . Możemy przemnożyć tę liczbę przez 4096 i uzyskamy wtedy rozmiar pliku, oczywiście w zaokrągleniu do pełnego bloku.

Znając początkowy blok oraz ilość następnych bloków, możemy ten plik teraz wykroić z dysku przy pomocy blkls , podając zakres bloków:

# blkls -e grafi.img 159527-159946 > utracona.mp3

Jako, że kopiowaliśmy całe bloki, to plik wynikowy będzie troszeczkę większy niż ten oryginalny. Możemy go oczywiście przyciąć przy pomocy dd . Rozmiar pliku uzyskaliśmy wcześniej z dziennika i znamy go. Jest to 1713677 bajtów . Plik który uzyskaliśmy ma 1720320 . By teraz przyciąć ten plik, wklepujemy w terminal to poniższe polecenie:

# dd if=./utracona.mp3 of=./utracona-przycieta.mp3 bs=1 count=1713677
Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.