2016年6月17日金曜日

cipherコマンドでドライブ内のデータを完全に削除する

ファイルやフォルダを削除しても運が良ければ復元できます。誤って削除してしまった場合には復元できた方が良いのですが、機密情報の入っているHDDを廃棄したい場合は復元できてしまっては困ります。HDDに入っていたものを復元できないようにするためにcipherというコマンドが利用できるのですが、これは使用する上で色々と注意が必要です。
今回はその注意点について解説します。(次の記事でそれを気にしなくて良いサンプルを公開します)
2016/06/17追記:ストレージの廃棄を目的としてcipherを実行する場合に必要な下準備からcipher実行までを自動化するサンプルを作成しました。
2016/07/04追記:記事末尾にも追記しましたが、cipherで処理したHDDからでも復元できるソフトがあるようです。。
2016/07/06追記:cipherに比べて非常に時間がかかりますが復元ソフトで元のファイルを取り出すことができない状態にすることができました。記事末尾に詳細を追記しました。
2016/07/07追記:アロケーションユニットサイズを大きくすることで、高速に埋め尽くすことが出来ることを確認しました。埋め尽くすためのプログラムを公開しました。
2016/07/13追記:埋め尽くす.htaのソースを修正しました。(処理中断ができるようにした。処理終了or中断時に、WSFファイルを削除するようにした。空き容量が0になったらHTA側も停止するようにした)


cipherコマンドはドライブの空き領域を埋め尽くすサイズのファイルを1つ作成します。(ファイルは作成後に削除されます。3種類のファイルを作っては削除するので、全体としては3つのファイルを作って消します)
ファイルサイズの上限よりもドライブの空き容量が大きい場合、ファイルサイズの上限までしかcipherコマンドは実行されません。
例えばファイルシステムがFAT32のドライブではファイルサイズの上限は4GBytesですので、空き容量が4GBytes以上ある場合はcipherコマンドの影響を受けない領域が生じます

空き領域ではない領域は、cipherコマンドによる影響を受けません。
上記理由により、ドライブ内のデータを全て削除したい場合は、cipherコマンドを利用する前に、DISKPARTコマンドで以下を実行すると目的の達成が確実になります。
  1. select vol 選択したドライブレター
  2. list partition ←でリストアップされるパーティションを全て「delete par * override」で削除する
  3. create partition primary ←サイズを指定しなければ現在の領域に未割り当て領域がなくなるまでパーティションが作成されます
  4. format fs=ntfs quick ※NTFSは10MB16E(エクサ)Bまで対応します
ディスクの初期化作業の頻度が低いと忘れてしまいそうです。
そしてcipherコマンドはどんな状況でも特にエラーメッセージを出さず、出来るところまでやったら黙って終了する感じなので不適切な状態で実行した場合は気が付きにくいので危険です。

安全に、確実にすべてを削除するために、確認から削除まで自動で実行してくれるサンプルを作りたいです。
目的はドライブ内のデータ削除ですので、ファイルシステムの変更が必要な場合でもユーザへの確認メッセージは表示せず自動で変更して処理を継続させたいです。


サンプルは以下。
<package>
<job>
<script language="VBScript">
function vbMsgBox(prompt, button, title)
vbMsgBox = MsgBox(prompt, button, title)
end function
</script>
<script language="JScript">
jsMsgBox=function(prompt, button, title){return vbMsgBox(prompt, button, title)}
arg = WScript.Arguments
if(arg.length != 1){
WScript.Echo('初期化したい物理ドライブに含まれるドライブレターを1つだけ指定してください')
WScript.Quit()
}
if(jsMsgBox('このプログラムはストレージ内のデータを完全に削除します。\n使うタイミングとしては「機密情報の入ったHDDを廃棄することになった。中身を完全に削除して復元も不可能にしたい」という時に使うものです。\n実行しますか?', 1, '!重要警告!')!=1){
WScript.Quit()
}
// DISKPARTは管理者権限がないと起動できないので昇格する。
shellApp = new ActiveXObject('Shell.Application')
shellApp.ShellExecute('wscript', WScript.ScriptFullName+' //job:main '+arg(0)+' uac', '', 'runas')
</script>
</job>
<job id=main>
<script language="VBScript">
function vbMsgBox(prompt, button, title)
vbMsgBox = MsgBox(prompt, button, title)
end function
</script>
<script language="JScript">
jsMsgBox=function(prompt, button, title){return vbMsgBox(prompt, button, title)}
shell = new ActiveXObject('WScript.Shell')
objExec = shell.exec('diskpart')
// objExecの標準入出力を扱う関数
readStr = function(){
var ret='', ret0, i=0, sw空行
while(true){
// ReadLine()は呼んだら相手側が何か返すまで待ちになってしまう。
// 確実にリターンがある時にしか使えない。。
ret0 = objExec.StdOut.ReadLine()
ret += ret0 + '\r\n'
// 2度目の空行ならbreakするというのは場当たり的な対応なので、コマンドによっては不都合あるのかも。。
if(ret0.indexOf('DISKPART>')==0 || (!ret0 && sw空行)){break}
if(!ret0){sw空行 = true}
if(++i>50){return ret}
}
return ret
}
sendStr = function(str){ objExec.StdIn.Write(str+'\r\n') }
// 起動時のテキストを読み込む
WScript.Echo('起動時のテキストを読み込む\n\n'+readStr())
/*
Microsoft DiskPart バージョン 6.1.7601
Copyright(C) 1999-2008 Microsoft Corporation.
コンピューター:PC名
*/
// ボリュームを選択する
sendStr('select vol '+WScript.Arguments(0))
// 上記のリターンを表示する
WScript.Echo('ボリュームE:を選択する\n\n'+readStr())
/*
ボリューム 4 が選択されました。
*/
// 選択中のボリュームと同じ物理ドライブ内のパーティションリストを表示する
sendStr('list partition')
WScript.Echo('パーティションリストを表示する\n\n'+readStr())
/*
Partition### Type Size Offset
------------ ------------------ ------ -------
* Partition 1 プライマリ 37GB 1024KB
*/
// 何故か1回遅れでしかテキストが取得できないので、同じコマンド2回うつ。。
sendStr('list partition')
ret = readStr()
WScript.Echo('パーティションリストを表示する2\n\n'+ret)
if(jsMsgBox(WScript.Arguments(0)+'ドライブを含むディスク内パーティションを全て削除します。\nここでキャンセルしない場合、操作のやり直しは不可能になりますが、処理を継続しますか?', 1, '!!最終警告!!')!=1){
// 第2引数:1 = OK Cancel。戻り値:1 = OK
sendStr('exit')
WScript.Quit()
}
reg = /Partition (\d+)/
arr = []
i = 0
while(ret.match(reg)){
arr[i++] = RegExp.$1
ret = ret.replace(reg, '')
}
WScript.Echo('パーティション配列:'+arr)
for(i=0,L=arr.length;i<L;i++){
sendStr('select partition '+arr[i])
WScript.Echo('select partition '+arr[i]+'\n'+readStr())
/*
パーティション 1 が選択されました。
*/
sendStr('delete partition override')
WScript.Echo('delete partition override\n'+readStr())
/*
DiskPart は選択されたパーティションを正常に削除しました。
*/
}
// サイズ指定なし(可能な限り大きく)パーティションを作成する
sendStr('create partition primary')
WScript.Echo('create partition primary\n'+readStr())
/*
DiskPart は指定したパーティションの作成に成功しました。
*/
// NTFS形式でフォーマットする
sendStr('format fs=ntfs quick')
WScript.Echo('format fs=ntfs\n'+readStr())
/*
1% 完了しました
DiskPart は、ボリュームのフォーマットを完了しました。
*/
// 元のドライブレターを設定する
sendStr('assign letter='+WScript.Arguments(0))
WScript.Echo('format fs=ntfs\n'+readStr())
/*
*/
// DISKPARTも終了させる
sendStr('exit')
// cipherを実行する
shell.run('cipher /w:'+WScript.Arguments(0))
</script>
</job>
</package>

StdOut.ReadLine()にかなり苦しめられました。
結局納得のいく形にはできませんでしたが、最低限必要な処理ができるようにはなりました。



初期化に成功したら、あとはcipherを実行するだけですが、cipherは特にログを出力しないので、動作中に容量をモニタしてちゃんと全容量を埋め尽くしたかどうか確認するスクリプトを作成すると安心です。

2016/06/17追記:よく考えたらcipherを使わなくてもWSHで0x00と0x11とランダムパターンのファイルをバイナリ形式で作成すれば状態も確認しやすいかと思いましたが、下準備がちゃんとできていればcipherが失敗することは無いので、シンプルにcipherを実行するだけにしました。


例えば「ディスクの管理」での表示が以下のようになっている場合



cmdで以下のように起動すると


まず「E」を含む「ディスク 2」内の全てのパーティションを削除して1つのパーティションに作り替えた後、NTFS形式にしてフォーマットされます。


あとは、cipherで空き領域を埋め尽くして、ドライブ内にあった情報の復元が不可能になります。


2016/07/04追記
この記事を作成した時は廃棄したいHDDがあったため、上記サンプルを作成してHDD内のデータを削除しました。
その後、今度は「間違って削除してしまったデータを復元して欲しい」というお話があり、復元ソフトを試すことになりました。

cipherコマンドで処理したHDDはまだ手元にあったので、試しにそちらも復元ソフトで処理してみました。


おう。。


見えてる見えてるー

cipherで一回処理したぐらいでは消しきれないのかも、と思ってもう一度上記サンプルを使ってcipherを実行しました。

そして再び復元ソフトでテスト。



・・・・・・一個減った。



これはちょっと他の方法を考えた方が良さそうです。
0x00、0x11、ランダムパターンを書いて「すぐに消す」のが良くないような気がします。
セクターにクセがつくまで時間を置く方が良いような……しかしそれではゴミがいつまで経っても捨てられませんし、ずっとPCでドライブレターを占有されるのも困ります。

物理的に破壊するのが良いのかもしれませんが、あんな金属の塊を再起不能にするような力をかけるのも大変ですし、バネやら何やらが飛散して周囲に危険が及びそうなのも怖いです。


2016/07/06追記
元のファイルを復元不可能にしたいドライブに対して、ドライブ全体を埋め尽くすようにファイルを作成しまくるプログラムを作って動作確認しました。

とりあえず結果から



上記のようになったのは、対象のドライブが以下のように空き容量がゼロになるまで



中身が「000000…」というテキストファイルを作成したからです。


ちなみに、このドライブは1998年製の容量3GBのHDDです。年代物です。



3GBの容量を埋め尽くすのにかかった時間はほぼ丸一日です。。
昔はこれが普通の速度だったのか、あるいは年数が経っていることによる劣化なのか確認する気力もありませんが……古いHDDを捨てるのは大変だということは分かりました orz

埋め尽くすプログラムは既に作成済みですが公開するなら少し修正したい箇所があるので修正して動作確認してから公開します。


2016/07/07追記
動作確認のために何度もファイルを作って消してを繰り返しているうちにHDDから異音が聞こえ始めました。それはさておき。
アロケーションユニットサイズを変更するとディスクアクセスが高速になります。

NTFSではアロケーションユニットサイズは最大64KBです。



HDDをファイルで埋め尽くすプログラムを作成しました。
<html>
<title>埋め尽くす</title>
<body>
処理対象のドライブ<select id=sel></select>
<div>
アロケーションユニットサイズ:<input id=inp><button id=btnアロケーションユニットサイズ>確認</button><br>
ドライブを埋め尽くすファイルについて、1ファイルあたりの容量はアロケーションユニットサイズ(クラスターあたりのバイト数)の倍数にするのが理想的です。<br>
<button id=btnドライブ詳細>ドライブの詳細情報を表示</button>
<div id=div style="border:solid 1px #000;font-family:MS ゴシック;padding:5px;"></div>
</div>
<br>
<button id=btn実行>埋め尽くす</button>
<br>
<div id=div進捗 style="border:solid 1px #000;font-family:MS ゴシック;padding:5px;"></div>
</body>
<script>
onload = function(){
// ドライブのリスト表示を更新する。
var drives=new Enumerator(fs.Drives), i=0, ops=sel.options
for(; !drives.atEnd() ;drives.moveNext()){
ops[i++] = new Option(drives.item().DriveLetter)
}
}
btnアロケーションユニットサイズ.onclick=function(){
var drv文字=selOps(sel).text, drv=fs.GetDrive(drv文字), path=drv文字+':\\1'
var 前size = drv.FreeSpace
Write(path, 1)
var 後size = drv.FreeSpace
fs.DeleteFile(path)
inp.value = 前size - 後size
}
btnドライブ詳細.onclick=function(){
var path=gsf2+'/'+fs.GetTempName(), drive=selOps(sel).text
div[iT] = getリダイレクト('cmd /C fsutil fsinfo ntfsinfo '+drive+':')
}
後始末 = function(){
btn実行[iT] = '埋め尽くす'
var m=arguments.callee
fs.DeleteFile(m.pathWSF)
m.pathWSF = ''
}
btn実行.onclick=function(){
if(this[iT]=='中断'){return 後始末()}
var drive=selOps(sel).text, 空き容量=fs.GetDrive(drive).FreeSpace, アロケーションユニットサイズ=inp.value
if(!isFinite(アロケーションユニットサイズ)){return alert('アロケーションユニットサイズは半角数字で記入してください')}
if(アロケーションユニットサイズ<=0){return alert('アロケーションユニットサイズは1以上の整数にしてください')}
div進捗[iT] = [
'対象ドライブ:'+drive,
'空き容量:' +空き容量+' バイト',
'アロケーションユニットサイズ:'+アロケーションユニットサイズ,
'処理開始日時:'+get日時()
].join('\n')
if(空き容量%アロケーションユニットサイズ!=0){
div進捗[iT] += '\n\n中断'
return alert('アロケーションユニットサイズで空き容量を割ると余りがゼロになるようにしてください')
}
this[iT] = '中断'
var fun=function(){
arg=WScript.Arguments
ドライブ = arg(0)+':\\'
サイズ = arg(1)-0
空き容量 = arg(2)-0
for(str='',i=0; i<サイズ ;i++){str+='0'}
fs = new ActiveXObject('Scripting.FileSystemObject')
リセット = function(){
i = 0
dat = new Date()
path = ドライブ+dat.getTime()+'\\'
fs.CreateFolder(path)
}
リセット()
pathWSF = WScript.ScriptFullName
fun = function(){
// 空き容量が残っているのに「ディスクに十分な空き領域がありません。」のエラーが生じる場合がある。
try{
with(fs.CreateTextFile(path+j)){Write(str); Close()}
}
catch(e){}
if(i++ > 10000){
// 自身のWSFが削除されている = 中断指令
if(!fs.FileExists(pathWSF)){WScript.Quit()}
// 中断指令が無い場合はリセットして継続。
リセット()
}
}
for(j=0; j<空き容量 ;j+=サイズ){ fun() }
// 上記forループで、空き容量とピッタリ一致する量のファイルを出力している筈だが、
// あと少しのところで「ディスクに十分な空き領域がありません。」のエラーが出てしまうことがある。
// 本当に0になるまで埋め尽くす。
objDrive = fs.GetDrive(ドライブ.slice(0,1))
while(j=objDrive.FreeSpace){ fun() }
}
var pathWSF=gsf2+'/'+fs.GetTempName()+'.wsf'
var str=['<'+'job>','<'+'script>','fun='+fun,'fun()','<'+'/script>','<'+'/job>'].join('\r\n')
Write(pathWSF, str)
shell.run('cmd /C wscript '+pathWSF+' '+drive+' '+アロケーションユニットサイズ+' '+空き容量, 0)
後始末.pathWSF = pathWSF
var 桁数=(fs.GetDrive(drive).FreeSpace+'').length, スペース=''
for(var i=0;i<桁数;i++){スペース+=' '}
桁合わせ=function(v){var str=スペース+v, len=str.length;return str.slice(len-桁数, len)}
var 間隔=1000 * 60 * 5
setTimeout(function(){
var 空き容量=fs.GetDrive(drive).FreeSpace
div進捗[iT] += '\n\n'+get日時()+':空き容量…'+桁合わせ(空き容量)
if(!空き容量){ return 後始末() }
if(!後始末.pathWSF){ return }
setTimeout(arguments.callee, 間隔)
},1)
}
get日時=function(dat){
dat=dat || (new Date())
var 二桁=function(s){s='0'+s; return s.slice(s.length-2, s.length)}
return [dat.getFullYear(), 二桁(dat.getMonth()+1), 二桁(dat.getDate())].join('/')+' '+[二桁(dat.getHours()), 二桁(dat.getMinutes()), 二桁(dat.getSeconds())].join(':')
}
getリダイレクト=function(cmd){
var pathWSF=gsf2+'/'+fs.GetTempName()+'.wsf', pathTXT=pathWSF+'.txt'
var fun0=function(){
pathTXT = WScript.Arguments(0)
pathTXT_ = pathTXT+'_'
fs = new ActiveXObject('Scripting.FileSystemObject')
if(fs.FileExists(pathTXT_)){ fs.DeleteFile(pathTXT_) }
shellApp = new ActiveXObject('Shell.Application')
shellApp.ShellExecute('wscript', WScript.ScriptFullName+' //job:main '+pathTXT+' uac', '', 'runas')
// 呼出し元は、この関数の完了を待機している。
// この関数が完了したらリダイレクトで作成されたファイルを読み込むため。
// 昇格が必要なので、UACが動作して「OK」「キャンセル」の選択肢が表示される。
// 「OK」が選択されたら昇格状態のプロセスが起動するが、
// それはこの関数とは別プロセスのため、別プロセス側が完了する前に
// この関数が終了すると、別プロセスのリダイレクトが出力される前に
// この関数の呼出し元がリダイレクトを読みに行ってしまう。
// 上記理由により、昇格後の処理完了したらファイルを作って合図を送るようにして
// この関数はその合図が来るまで待機するようにしたい…けど、
// ShellExecute実行時に表示されるUACで「キャンセル」が選択されるなどして
// 昇格状態の処理が始まらなかった場合、無限ループになってしまう。
// ShellExecuteの戻り値は成功しても失敗してもundefinedなので判別不能。
// 合図があったらすぐにリターンするけど、なくても1秒経過したらリターンする。
dat = (new Date()).getTime()
while(!fs.FileExists(pathTXT_)){
WScript.Sleep(1)
dat_ = (new Date()).getTime()
if((dat_ - dat) > 1000){WScript.Quit()}
}
fs.DeleteFile(pathTXT_)
}
var fun1=function(){
fs = new ActiveXObject('Scripting.FileSystemObject')
Read = function(path){var s=fs.OpenTextFile(path),t=s.AtEndOfStream?'':s.ReadAll();s.Close();return t}
pathTXT = WScript.Arguments(0)
pathTXT_ = pathTXT+'_'
cmd = Read(pathTXT)
shell = new ActiveXObject('WScript.Shell')
shell.run(cmd, 1, true)
fs = new ActiveXObject('Scripting.FileSystemObject')
fs.CreateTextFile(pathTXT_).Close()
}
var str=['<'+'package>','<'+'job>','<'+'script>','fun0='+fun0,'fun0()','<'+'/script>','<'+'/job>','<'+'job id=main>','<'+'script>','fun1='+fun1,'fun1()','<'+'/script>','<'+'/job>','<'+'/package>'].join('\r\n')
Write(pathWSF, str)
Write(pathTXT, cmd+' > '+pathTXT)
shell.run('cmd /C cscript '+pathWSF+' '+pathTXT, 0, true)
var リダイレクト=Read(pathTXT)
fs.DeleteFile(pathWSF)
fs.DeleteFile(pathTXT)
return リダイレクト
}
fs = new ActiveXObject('Scripting.FileSystemObject')
shell = new ActiveXObject('WScript.Shell')
Write = function(path, str){with(fs.CreateTextFile(path)){Write(str);Close()}}
Read = function(path){var s=fs.OpenTextFile(path),t=s.AtEndOfStream?'':s.ReadAll();s.Close();return t}
gsf2 = fs.GetSpecialFolder(2)
selOps= function(sel){return sel.selectedIndex==-1?undefined:sel.options[sel.selectedIndex]}
iT='innerText', iH='innerHTML'
</script>
</html>


起動して、実行した結果が以下です。



ファイルをバンバン作成する処理はWSFで実行していて、HTAは2分おきに空き容量を確認しています。HTAでファイルの作成もやってしまうと画面更新されなくなってフリーズしたようになってしまうので、そうしています。

17:19に開始して18:44には空き容量がゼロになっています。
18:54から空き容量が少し増えているのは謎ですが……システムファイル的なものが削除されたか退避されて空き容量が作られたのでしょうか?不明です。

とにかく、アロケーションユニットサイズを64KBにして、書き込むファイルのサイズもピッタリ65536バイトにしたら80分程度で埋め尽くすことに成功しました。

もちろん、その後ドライブをフォーマットして、復元も試してみました。



64KBの無意味なファイルをすばらしいコンディションで復元できるそうです。
それ以外の「64KBのファイルが作られる前にあったファイル」はリストに表示されていませんでした。
まったく、すばらしいですね。


ちなみに、本日SanDiskのウルトラ II SSD SDSSDHII-960G-J26を入手したので、それだとどれくらいの速度でやれるのか試してみました。



これだけだと速度がよく分からないので、空き容量が減った量を計算してHDDと比較してみました。
容量の単位はバイトです。
SSDやHDDの下に記載している「4KB」と「64KB」はアロケーションユニットサイズです。
経過時間[min] SSD
[64KB]
SSD
[4KB]
HDD
[64KB]
HDD
[4KB]
2 9,974,382,592 886,898,688 218,955,776 33,832,960
4 8,381,661,184 727,580,672 207,421,440 25,604,096
6 8,328,249,344 709,976,064 184,287,232 25,006,080
8 8,251,113,472 726,462,464 188,416,000 25,038,848
……桁が違います。色々。HDDとSSDはもちろん、アロケーションユニットサイズでも。
ちなみにSSD[64KB]で最高速度の「9,974,382,592」は664Mbpsという計算(容量/1000/1000/2分/60秒 * 8ビット)になります。
メーカーの製品情報ページに「シーケンシャル 書込み(最大)500MB/秒」って記載されているのに上回っちゃってる感じなんですが…
単位を「MB/秒」にするなら9,974,382,592は(容量/1000/1000/2分/60秒)の計算で83.1MB/秒でした。今度は逆にシーケンシャル書き込み(最大)500MB/秒に対して大幅に低い値になってしまいますが、そこはWSHでファイルをCreate、Write、Closeなどしているあたりがボトルネックになっているのではと思います。
HDD[64KB]の218,955,776も同様に計算すると1.8MB/秒ということになり、3GBのHDDを埋め尽くすのにかかる時間は27分という計算になりますが、上記の表にもある通り速度は一定ではなく徐々に下がっている(プラッタの外周から内周に向かって書き込んでいる?回転速度が一定の時、内側より外側の方が単位時間あたりの距離が長くなるので読み書きが高速になる)ので、低速領域まで書き込むと全体としては80分程度かかるということで正解のようです。


あと、ストレージ容量の増大によってJavaScriptの変数で扱える数値の大きさも問題になってくるかも…と思いましたが、整数として精度が保障されるのは2の53乗(9,007,199,254,740,992)だそうですので、まだまだ大丈夫なようです。

0 件のコメント:

コメントを投稿