UNIX v6で削除したファイルの復元をしてみた

はじめに

先日、Lions' Commentary on UNIXの読書会に参加したときにUNIX v6のファイルシステム周りのコードを読みました。

ファイルのクローズ・削除のあたりを読んでいたときに、あることに気づきました。

「ファイル削除するときに、ブロックデバイスのデータを扱う領域からデータそのものは消していなくね?」

今回は、simh + PDP11を使った実験により、それを確かめてみました。さらに、削除したファイルの復元(?)も行ってみました。

追記:このエントリを書き終ってから気づきました。inode numberはemulator上の"ls -i"で確認できると……

まずは事前準備

simh + PDP11を使って、UNIX v6 emulatorにログインし、以下のような環境を構築しました。

# chdir /work
# ls
a.out
hoge.c
# cat hoge.c
int main( ) {
  printf( "hello\n" ) ;
  return 0 ;
}

/workにhoge.cというファイルとそれをコンパイルして生成されたa.outというファイルが存在しています。

ここで一旦sync+Ctrl-E, quitコマンドでemulatorから抜けます。

以前のエントリで紹介したfile system hack toolを使ってinode numberを確認します。

% ./analysis
# ls
0065 : bin/
0064 : dev/

... snip ...

0143 : work/

... snip ...

015B : hogehoge.c
015C : hogehoge
# cd work
# ls
015D : hoge.c
0142 : a.out

:の左はinode numberを表します。workディレクトリと、hoge.c, a.outファイルのinode numberがわかりました。

次に、hoge.cのinode情報を見ます。

# in 15d
  i_mode  : 81b6 | IALLOC | IREAD | IWRITE
  i_nlink : 1
  i_uid   : 0
  i_gid   : 3
  i_size0 : 0
  i_size1 : 35
  i_addr[0] : b9e
  i_addr[1] : 0
  i_addr[2] : 0
  i_addr[3] : 0
  i_addr[4] : 0
  i_addr[5] : 0
  i_addr[6] : 0
  i_addr[7] : 0
  i_atime[0] : adc
  i_atime[1] : ae4
  i_mtime[0] : adc
  i_mtime[1] : aae

inはfile system hack toolで使用できるコマンドの一つで、inode情報を表示します。

ここで一旦file system hack toolを終了します。

次に、hexdumpコマンドを使って、ブロックデバイス中のhoge.cデータをを見てみます。

まずはアドレスの計算をします。なんとなくpythonを使って計算してみました。

% python
Type "help", "copyright", "credits" or "license" for more information.
>>> hex( int( 'b9e', 16 ) * 512 )
'0x173c00'

hexdumpコマンドを使って、ブロックデバイスの中身をダンプします。v6rootは、simh+PDP11で使用しているディスクイメージです。

% hexdump -C v6root

...snip...

*
00173c00  69 6e 74 20 6d 61 69 6e  28 20 29 20 7b 0a 20 20  |int main( ) {.  |
00173c10  70 72 69 6e 74 66 28 20  22 68 65 6c 6c 6f 5c 6e  |printf( "hello\n|
00173c20  22 20 29 20 3b 0a 20 20  72 65 74 75 72 6e 20 30  |" ) ;.  return 0|
00173c30  20 3b 0a 7d 0a 00 00 00  00 00 00 00 00 00 00 00  | ;.}............|
00173c40  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*

...snip...


確かにデータが入っているのが確認できました。

ファイルの削除処理

シェルでrmが呼ばれたときのことを考えます。

rm内部ではunlinkシステムコールが呼ばれます。

unlinkシステムコールではiput( )が呼ばれます。

iput( )ではinodeの参照カウンタをデクリメントします。参照カウンタが0になり、どのディレクトリからも見られなくなる場合、itrunc( )を呼んで、そのinodeが使用していたデータ領域のblock numberをフリーリストに戻します。そのinodeのinode numberもフリーリストに返します。これらの処理の結果をブロックデバイスに反映させます。

これを実験で確認します。

まずはsimh+PDP11のemulator環境にログインして、hoge.cを削除します。

login: root
# chdir work
# ls
a.out
hoge.c
# rm hoge.c
# ls
a.out

そしてemulatorからログアウト。

続いてfile system hack toolを起動します。

% ./analysis
# cd work
# ls
0142 : a.out
# in 15d
  i_mode  : 0
  i_nlink : 0
  i_uid   : 0
  i_gid   : 3
  i_size0 : 0
  i_size1 : 0
  i_addr[0] : 0
  i_addr[1] : 0
  i_addr[2] : 0
  i_addr[3] : 0
  i_addr[4] : 0
  i_addr[5] : 0
  i_addr[6] : 0
  i_addr[7] : 0
  i_atime[0] : adc
  i_atime[1] : ae4
  i_mtime[0] : adc
  i_mtime[1] : ade

hoge.cは削除されているのが確認できます。inを使ってinode情報を見てみると、i_modeが0になり、i_sizeが0になり、i_addr[]もall 0になっているのが確認できます。

ここでfile system hack toolを終了させます。

つづいてhexdumpコマンドを使ってブロックデバイスを直接覗いてみます。

% hexdump -C v6root

...snip...

*
00173c00  69 6e 74 20 6d 61 69 6e  28 20 29 20 7b 0a 20 20  |int main( ) {.  |
00173c10  70 72 69 6e 74 66 28 20  22 68 65 6c 6c 6f 5c 6e  |printf( "hello\n|
00173c20  22 20 29 20 3b 0a 20 20  72 65 74 75 72 6e 20 30  |" ) ;.  return 0|
00173c30  20 3b 0a 7d 0a 00 00 00  00 00 00 00 00 00 00 00  | ;.}............|
00173c40  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*

...snip...

削除されたhoge.cのデータが、まだブロックデバイス中に残っているのが確認できます。

つまり、ファイルの削除処理が行われると、inode情報は初期化されますが、データ自体はブロックデバイス中に残ったままになります。

ついでにworkディレクトリについても見てみます。まずはfile system hack toolでworkディレクトリのinode情報を表示させます。

% ./analysis
# in 143
  i_mode  : c1ff | IALLOC | IFMT | IFBLK | IFDIR | IREAD | IWRITE | IEXEC
  i_nlink : 2
  i_uid   : 0
  i_gid   : 0
  i_size0 : 0
  i_size1 : 60
  i_addr[0] : c1f
  i_addr[1] : 0
  i_addr[2] : 0
  i_addr[3] : 0
  i_addr[4] : 0
  i_addr[5] : 0
  i_addr[6] : 0
  i_addr[7] : 0
  i_atime[0] : adc
  i_atime[1] : b12
  i_mtime[0] : adc
  i_mtime[1] : b12

file system hack toolを終了。

つづいてアドレスの計算。

% python
Type "help", "copyright", "credits" or "license" for more information.
>>> hex( int( 'c1f', 16 ) * 512 )
'0x183e00'

hexdumpコマンドでダンプ。

% hexdump -C ./v6root
*
...snip...
00183e20  00 00 68 6f 67 65 2e 63  00 00 00 00 00 00 00 00  |..hoge.c........|
...snip...
00183e50  42 01 61 2e 6f 75 74 00  00 00 00 00 00 00 00 00  |B.a.out.........|
...snip...
*

ディレクトリ内の各エントリは16byte単位で管理されており、最初の2byteがinode numberで、残りの14byteがファイル・ディレクトリ名です。

a.outにはinode numberが残っていますが(0x142)、hoge.cのinode numberはクリアされているのが確認できます。

つまり、ファイルの削除処理が行われると、ディレクトリの該当エントリのinode numberは0にセットされますが、ファイル・ディレクトリ名は残ったままになります。

ファイルの復元処理

ブロックデバイス(ディスクイメージ)から、hoge.cのデータを抽出します。

こんなCのコードを書きました。

// binary_extract.c

#include <stdio.h>
#include <stdlib.h>

int main( int argc, char **argv ) {

  FILE *fin, *fout ;
  int c, opt, from, to ;
  unsigned int i, address, size ;

  if( argc != 5 ) {
    printf( "usage : %s <input binary file> <output binary file> <address> <size>\n", argv[ 0 ] ) ;
    return -1 ;
  }

  if( ! ( fin = fopen( argv[ 1 ], "rb" ) ) ) {
    printf( "failed to open input file.\n" ) ;
    return -1 ;
  }

  if( ! ( fout = fopen( argv[ 2 ], "wb" ) ) ) {
    printf( "failed to open output file.\n" ) ;
    fclose( fin ) ;
    return -1 ;
  }

  address = ( unsigned int )strtol( argv[ 3 ], NULL, 16 ) ;
  size    = ( unsigned int )strtol( argv[ 4 ], NULL, 16 ) ;

  i = 0 ;
  while( ( c = getc( fin ) ) != EOF ) {
    if( i >= address && i < address + size )
      fputc( c, fout ) ;
    i++ ;
  }

  fclose( fout ) ;
  fclose( fin ) ;

  return 0 ;

}

このプログラムは、あるファイルのあるアドレスからサイズ分のデータを抽出します。抽出したデータは別ファイルに出力します。

あとはこれを実行するだけです。該当データのアドレスはわかっていますし、サイズもinode情報を表示させたときに取得しています。

% ./binary_extract ./v6root ./hoge.c 173c00 35
% cat hoge.c
int main( ) {
  printf( "hello\n" ) ;
  return 0 ;
}

このようにファイルの復元が実現できました。

削除したファイルをブロックデバイスから抽出することに成功した、の方がより正しい言い方かもしれません。

まとめと考察

まとめ

以下のことが実験で確かめられました。

  • ファイルを削除すると
    • inode情報はクリアされる
    • データ自体はブロックデバイスに残ったまま
    • ファイル名もディレクトリに残ったまま
完全にデータを消すには

では、完全にデータを消す(二度と見られなくする)にはどうしたらよいでしょうか。例えばこんな方法が考えられます。

  • ファイルの中身をクリアしてからrmコマンドで削除する


比較的簡単にそのようなプログラムを書くことができるでしょう。他にこんな方法もあります。

  • ファイルを削除したあとに、すぐ新しいファイルを生成する


block numberのフリーリストはスタックで管理されているので、新しいファイルを作成すれば、先ほど削除して解放されたblock numberが割り当てられます(おそらく)。

データの復元について

ファイルを削除しても、データはブロックデバイスに残ったままです。なので、復元したいファイルのアドレスとサイズさえわかっていれば簡単に抽出が可能です。

ただし、実践的なことを考えると、どうやってアドレスとサイズを取得するのかが悩みどころです。(ブロックデバイスのinode領域だけバックアップを取っておくとか?全体のバックアップを取っておくよりはコスト安なはず)

また、前述のとおり、削除したファイルはすぐに上書きされてしまう可能性が高いので、削除してから時間が経ったファイルの復元は難しいでしょう。

ファイル名の復元について

ファイル・ディレクトリ名についても、そのファイル・ディレクトリが存在していたディレクトリさえわかれば再現は可能です。それがわからない場合は再現が難しいでしょう。

データと同様に、ディレクトリにファイル・ディレクトリが追加されるとその情報が失われてしまうので、時間が経ったものも復元は難しいでしょう。

なぜデータをクリアしないのか?

おそらくレイテンシを気にしているのだと思います。

inodeをクリアするだけならば、該当inodeが存在しているブロックを読み込み、inode情報を更新してからブロックを書き込み、と、ブロックデバイスに対して2回のアクセスで済みます。

しかし、データ自体も消すとなると、データのサイズ次第ではさらに何回もブロックデバイスに対するアクセスが必要になります(ブロックデバイスのアクセス単位は512bytes)。それに、ファイルによって削除処理から戻ってくる時間が異なるのは、システムとしてあまりよくない設計だと思います。

セキュリティ上データを完全に消したいファイルもあれば、そうでないファイルもあります。全てのファイルに対してデータをクリアする処理を加えるのは無駄であると言えるでしょう。上記の通り、意味のないデータで上書きをしてから削除する(rmコマンドを呼ぶ)というのはユーザのプログラムで行えます。「必要ならばユーザが独自にやってくれ」という思想なのではないでしょうか。

終わりに

ファイル削除周りのコードを読んだ時に感じたことが、実験により確かめることができました。

最新のOS環境でも「ファイルは消したが、データはハードディスクに残ったままで、そこから情報漏えいした」なんて話が聞こえてきます。今回確かめたことは最新の環境でも通じるところがあるのではないかと思っています。