実行可能属性のページとDEP

何をもってしてコードセクションとするか、を会社の昼休み中に調べていたら、なんだかいつの間にかDEPという深淵を覗き込んでいたので、そのままDEPについて調べてみた。

まずコードセクション(実行可能セクション)はIMAGE_SCN_MEM_EXECUTEが設定してあるセクションだと思って良い。IMAGE_SCN_CNT_CODEという似たようなフラグもあるが、これは少なくとも実行可能属性には影響しない。IMAGE_SCN_MEM_EXECUTEさえ設定されていればそのまま実行できる。

ただここでDEPが話に絡んでくる。DEPが有効でなければ、IMAGE_SCN_MEM_EXECUTEがあろうがなかろうが、アクセス可能なページは常に実行可能となる。そして何をもって「DEPが有効」とするかについてが、いくぶん深淵を覗き込むような話になる。


結論から言えば、64ビットプロセスは常にDEPが有効で、32ビットプロセス(WOW64も含む)はProcess ExplorerDEP列に「DEP」もしくは「DEP(parmanent)」と表示されていればDEPが有効だといえる。この記事では以降、このProcess Explorerの表示の意味、挙動、判定条件を掘り下げていく。

用語

まずDEPに関するいくつかの用語と要素をMicrosoftのページ*1を参考に整理する。

ハードウェアDEP(以後HWDEP)
プロセッサがNXビットをサポートしている場合に使えるDEP。ページを管理するPage Table Entryというデータ構造の実行禁止(NX)ビットを利用して実現されている。HWDEPが利用可能か否かは、システムのプロパティから「データ実行防止」タブページで確認でき、XP系では何も表示されてない場合はHWDEPが利用可能、Vista系では明示的にHWDEPが使えるかどうかが表示される。このHWDEPが記事の話題の中心である。
ソフトウェアDEP(以後SWDEP)
HW要件がなく、HWDEPが使えない場合にも機能するDEP。これは構造化例外を利用したexploitationを防止する仕組であるが、この記事では範囲外として扱わない。概要を示しておくと、SWDEPが有効になっている場合、/SAFESEHが指定されたプログラムの構造化例外ハンドラはあらかじめPE内に登録されていなければならなくなり、/SAFESEHが指定されていないプログラムの構造化例外ハンドラは実行可能なページなければならなくなる*2
システムの設定状態:OptIn/OptOut/AlwaysOn/AlwaysOff
システム全体としてのDEPの適用範囲で、boot.iniやbcdeditから確認/変更することができる。それぞれの意味は、既定のプログラムに対してのみ適用、指定したプログラム以外全適用、例外なく全適用、例外なく適用しない。「既定のプログラム」の内容に関してはid:EijiYoshidaさんがリストしている*3
プロセスの設定状態
64ビットプロセスは常に適用される。これには、システムの設定状態やプロセスごとの設定などの影響を受けない。一方32ビットプロセスの挙動は複雑である。後述する。


64ビットプロセスとカーネルは常にDEPが適用されるという明確なルールがあるため、これ以降は特に断らない限り32ビットプロセス(WOW64を含む)のみを取り扱う。また、32ビットカーネルは非PAEカーネル使用時のみDEPが無効化されるという、ユーザー空間とは違う規則が適用されるが、これもこの記事では取り扱わない。

DEPが機能しているか否か

「プロセスにHWDEPが適用される」とは、実行可能属性がないページを実行しようとするとSTATUS_ACCESS_VIOLATIONを引き起こして、実行が禁止される動作を指している。


Process Explorer*4Vista以降のタスクマネージャーで各プロセスのDEPの状態を確認することができる。Process Explorerでは DEP / DEP (parmanent) / 空白(以後Disabled)の3種類の状態で示し、タスクマネージャーでは単に 有効 / 無効 と示される。タスクマネージャーにおける「有効」はProcess Explorerの「DEP」か「DEP(parmanent)」のいずれかにあたる。


それぞれの意味と表示される条件を以下に示す*5

プロセス 表示 条件(lpFlags) 条件(lpPermanent) 意味
32ビット DEP & PROCESS_DEP_ENABLE == FALSE HWDEPが適用されているが無効化することができる。
32ビット DEP(parmanent) & PROCESS_DEP_ENABLE == TRUE HWDEPが常に適用される。
32ビット Disabled == 0 問わず HWDEPが適用されない。
64ビット DEP - - HWDEPが常に適用される。

表の条件列はGetProcessDEPPolicy*6の実行結果を示している。ただし64ビットプロセスはこのAPIを使うことはできない。DEPに関連するAPIについては後のセクションで詳述する。

DEP」と「DEP(parmanent)」の違い

DEP」と「DEP(parmanent)」の違いは、そのプロセスのDEPを動的に無効にできるか否かである。「DEP」表示のプロセスは自分自身がSetProcessDEPPolicy*7を呼び出すことで、その状態を「Disable」もしくは「DEP(parmanent)」にすることができる。なお、64ビットプロセスは常に「DEP」表示であるが実質的には「DEP(parmanent)」である(上記の表のとおり)。

DEP(parmanent)」になるのはどのようなときか

DEP(parmanent)」表示になるのは、以下の場合である*8

  1. AlwaysOnでシステムを起動したとき
  2. OptInかOptOutでシステムを起動し、SetProcessDEPPolicyでPROCESS_DEP_ENABLE(DEP有効化)を設定したとき
  3. OptInかOptOutでシステムを起動し、OSがVista以降でレジストリImage File Execution Optionsに登録されたプロセスであるとき
  4. OptInかOptOutでシステムを起動し、OSがVista SP1以降で、プロセスのPEにNX compatibleフラグが立っているとき

1は前述のとおりで自明なので割愛する。
2のSetProcessDEPPolicyは原則的には他のプロセスに対して実行できないので開発者が明示的に実装した場合である。
3はレジストリキー HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options にプロセス名のサブキーを作り、ExecuteOptions=0を設定した場合である。この設定は再ログオンなどをせずに適用される。


4のNX compatibleフラグはプログラムのリンク時に /NXCOMPAT (データ実行防止との互換性) オプションを指定することでIMAGE_OPTIONAL_HEADER::DllCharacteristicsに設定されるIMAGE_DLLCHARACTERISTICS_NX_COMPAT (0x0100)のことを指している。このフラグはバイナリを直接編集して与えることもできる。/NXCOMPAT オプションは、VS2005以降であればデフォルトで有効であるため、特に意識せずに作られたプログラムは、Vista SP1以降では自動的にDEP(parmanent)で保護されることになる。

DEP(parmanent)」とは何か

特徴としては、以下の2点である。

  1. HWDEPが常に適用される
  2. HWDEPを無効にすることができない

Process Explorerが「DEP(parmanent)」と表示するのは、前述のとおりGetProcessDEPPolicyが変更不能と報告してきた場合である。


ちなみに、Process Explorerの実装は2点興味深いところがある。
1つは、SetProcessDEPPolicyでDEPを有効にし変更不能状態を作り出しても、Process Explorerは「DEP(parmanent)」と表示してくれない。「DEP(parmanent)」と表示してくれるのは、プロセスのエントリーポイントに到達する前(たとえば静的リンクされたDLLのDllMainなど)でSetProcessDEPPolicyを使った場合らしい。

もうひとつは、GetProcessDEPPolicyはXP SP3ではプロセスを指定できない(最初ドキュメントのミスかと目を疑いました(笑))にもかかわらず、全プロセスの情報を取ってきているところ。その仕組みを推測するために次にGetProcessDEPPolicyを調べてみる。

Get/SetProcessDEPPolicyは何をやっているのか

フローは以下のとおりどちらも簡単なもの。

GetProcessDEPPolicy
  +--ZwQueryInformationProcess(PROCESSINFOCLASS 34)
       +--NtQueryInformationProcess
            if XP+--MmGetExecuteOptions
                 +--KeGetExecuteOptions
SetProcessDEPPolicy
  +--ZwSetInformationProcess(PROCESSINFOCLASS 34)
       +--NtSetInformationProcess
            if XP+--MmSetExecuteOptions
                 +--KeSetExecuteOptions

XPかそれ以外かで最後の関数名は変わるものの、どちらのケースでも最終的には _KPROCESS::Flags を操作する。以下はこの構造体のx64 Win7での定義である。

0: kd> dt nt!_kprocess  -r1
    ...
   +0x0d2 Flags            : _KEXECUTE_OPTIONS
      +0x000 ExecuteDisable        : Pos 0, 1 Bit   ; GetProcessDEPPolicy lpFlags = PROCESS_DEP_ENABLE
      +0x000 ExecuteEnable         : Pos 1, 1 Bit   ; GetProcessDEPPolicy lpFlags = 0
      +0x000 DisableThunkEmulation : Pos 2, 1 Bit   ; GetProcessDEPPolicy lpFlags = PROCESS_DEP_DISABLE_ATL_THUNK_EMULATION
      +0x000 Permanent             : Pos 3, 1 Bit   ; GetProcessDEPPolicy lpPermanent
      +0x000 ExecuteDispatchEnable : Pos 4, 1 Bit   ; (SafeSEH関係。実行可能でないページ上のSEHハンドラを許可するか)
      +0x000 ImageDispatchEnable   : Pos 5, 1 Bit   ; (SafeSEH関係。例外発生元PEの以外のアドレス上のSEHハンドラを許可するか)
      +0x000 DisableExceptionChainValidation : Pos 6, 1 Bits  ; (構造化例外処理を上書き保護 (SEHOP)関係。詳細は未調査)
      +0x000 Spare                 : Pos 7, 1 Bits  ; 未使用領域
      +0x000 ExecuteOptions        : UChar

各ビットフィールドの意味はコメントの通りである。各フィールドの使われ方を調査すると、

  • ExecuteDisableとPermanentが1になっている場合「DEP(parmanent)」となり、
  • ExecuteEnableが1になっている場合「Disable」であり、
  • ExecuteDisableもExecuteEnableも0の場合は「DEP

となるらしい。AlwaysOffを指定するとExecuteEnableとPermanentが1になるが、この場合も単に「Disable」である。Process Explorerはドライバを持っているので、XP SP3で情報を取得するときには、この構造体を自力調査しているのではないかと推測している。


ちなみにXPで他プロセスにGetProcessDEPPolicyできない理由は、MmGetExecuteOptionsが _KPCR.PrcbData.CurrentThread.ApcState.Process.Flags のフローで決め打ちしていて、カレントプロセスの情報以外をとってくることができないためである。Vista以降ではNtQuery/SetInformationProcessで引数を解釈し、KeGet/SetExecuteOptionsはそれを利用するように変更されている。


以下に OptOut な XP SP3上で動作しているプロセスの_KEXECUTE_OPTIONS構造体の値の変化の例を示す。はじめは「DEP」表示の状態にある。

  +0x06b Flags            : _KEXECUTE_OPTIONS
     +0x000 ExecuteDisable   : 0y0
     +0x000 ExecuteEnable    : 0y0
     +0x000 DisableThunkEmulation : 0y0
     +0x000 Permanent        : 0y0
     +0x000 ExecuteDispatchEnable : 0y0
     +0x000 ImageDispatchEnable : 0y0
     +0x000 Spare            : 0y00
  +0x06b ExecuteOptions   : 0 ''


そこで SetProcessDEPPolicy(PROCESS_DEP_ENABLE) を実行すると、ExecuteDisableとPermanentが1になる。これは「DEP(parmanent)」表示の状態である。

  +0x06b Flags            : _KEXECUTE_OPTIONS
     +0x000 ExecuteDisable   : 0y1
     +0x000 ExecuteEnable    : 0y0
     +0x000 DisableThunkEmulation : 0y0
     +0x000 Permanent        : 0y1
     +0x000 ExecuteDispatchEnable : 0y0
     +0x000 ImageDispatchEnable : 0y0
     +0x000 Spare            : 0y00
  +0x06b ExecuteOptions   : 0x9 ''


プロセスを再起動し、今度は SetProcessDEPPolicy(0) を実行する。ExecuteEnableは1になるが、Permanentは変わらない。

 +0x06b Flags            : _KEXECUTE_OPTIONS
    +0x000 ExecuteDisable   : 0y0
    +0x000 ExecuteEnable    : 0y1
    +0x000 DisableThunkEmulation : 0y0
    +0x000 Permanent        : 0y0
    +0x000 ExecuteDispatchEnable : 0y1
    +0x000 ImageDispatchEnable : 0y1
    +0x000 Spare            : 0y00
 +0x06b ExecuteOptions   : 0x32 '2'

また、ExecuteDispatchEnable、ImageDispatchEnableが1になっている。この値が1になるということは、SafeSEHによる制限が緩まることを意味する*9。このことからSafeSEHはDEPが有効な場合により効果的に(あるいは有効な場合のみかもしれない。SafeSEHについては調査していない。)機能するのだと予想できる。なおXPはSEHOP*10をサポートしないためDisableExceptionChainValidation フィールドは存在しないが、Windows 7で試したところSetProcessDEPPolicyを使ってもこの値は変化しなかった。


例示にカーネルデバッガを利用しているが、NtQueryInformationProcessを使って自分自身に紐付く構造体を取得することができる。以下はそのサンプルコード。

typedef NTSTATUS (NTAPI*TypeNtQueryInformationProcess)(
    __in       HANDLE ProcessHandle,    // require PROCESS_QUERY_INFORMATION
    __in       PROCESS_INFORMATION_CLASS ProcessInformationClass,
    __out      PVOID ProcessInformation,
    __in       ULONG ProcessInformationLength,
    __out_opt  PULONG ReturnLength
);
typedef union 
{
    ULONG ExecuteOptions;
    struct 
    {
        ULONG ExecuteDisable                    : 1;
        ULONG ExecuteEnable                     : 1;
        ULONG DisableThunkEmulation             : 1;
        ULONG Permanent                         : 1;
        ULONG ExecuteDispatchEnable             : 1;
        ULONG ImageDispatchEnable               : 1;
        ULONG DisableExceptionChainValidation   : 1;
        ULONG Spare                             : 1;
    } Field;
} KEXECUTE_OPTIONS, *PKEXECUTE_OPTIONS;


int main()
{
    TypeNtQueryInformationProcess NtQueryInformationProcess = (TypeNtQueryInformationProcess)
        GetProcAddress(GetModuleHandle(TEXT("ntdll")), "NtQueryInformationProcess");
    if (!NtQueryInformationProcess)
        return 1;

    KEXECUTE_OPTIONS options = {0};
    NTSTATUS status = NtQueryInformationProcess(GetCurrentProcess(), 34, &options, sizeof(options), NULL);
    if (status)
        return 1;
    printf( "ExecuteDisable                     %d\n"
            "ExecuteEnable                      %d\n"
            "DisableThunkEmulation              %d\n"
            "Permanent                          %d\n"
            "ExecuteDispatchEnable              %d\n"
            "ImageDispatchEnable                %d\n"
            "DisableExceptionChainValidation    %d\n"
            "Spare                              %d\n", 
            options.Field.ExecuteDisable,
            options.Field.ExecuteEnable,
            options.Field.DisableThunkEmulation,
            options.Field.Permanent,
            options.Field.ExecuteDispatchEnable,
            options.Field.ImageDispatchEnable,
            options.Field.DisableExceptionChainValidation,
            options.Field.Spare);
    return 0;
}

他のプロセスの情報を取得したい場合は、リモートスレッドを生成して実行させることになるだろう。

DEPの表示状態の決まり方

最後に表示状態の決まり方を擬似コードで示す。

// 初期化

// X => Y はXと表示されるが実質的な動作はYの状態を指していることを意味する
if (is kernel) {
    goto DEP => DEP(parmanent)
}
if (is 64bit process) {
    goto DEP => DEP(parmanent)
}

switch (system wide setting) {
case AlwaysOn:
    goto DEP(parmanent)

case AlwaysOff:
    if (is Vista and later) and (is included registry keys) {
        goto DEP(parmanent) => Disable(parmanent)   // Why?
    } else {
        goto Disable(parmanent)
    }

case OptIn:
    if (is Vista and later) and (is included registry keys) {
        goto DEP(parmanent)
    }
    
    if (is predefined file) {
        if (is Vista SP1 and later) {
            if (is marked NX compatible flag) {
                goto DEP(parmanent)
            } else {
                goto Disable   
            }
        } else {
            if (entry poiny is excutable page) {
                goto DEP
            } else {
                goto Disable     
            }
        }
    } else {
        if (is Vista SP1 and later) and (is marked NX compatible flag) {
            goto DEP(parmanent)
        } else {
            goto Disable 
        }
    }

case OptOut:
    if (is Vista and later) and (is included registry keys) {
        goto DEP(parmanent)
    }

    if (is Vista SP1 and later) and (is marked NX compatible flag) {
        goto DEP(parmanent)
    } else {
        if (entry poiny is excutable page) {
            goto DEP
        } else {
            goto Disable   
        }
    }
}
// 動的遷移
if (state is DEP) and ((SetProcessDEPPolicy(0))
    goto Disable

if ((state is DEP) or (state is Disable)) and (SetProcessDEPPolicy(PROCESS_DEP_ENABLE))
    goto DEP(parmanent)


// (10/31追記)OptIn/OptOutポリシー時に、DEPの状態が動的に変更される細則を追記しました。

AlwaysOff状態でレジストリImage File Execution Optionsに登録すると、_KEXECUTE_OPTIONS構造体の値(と表示)は「DEP(parmanent)」実際にはDEPは機能していなかった。この理屈はわからない。
また、これまでに書いていない特殊ケースとして、「DEP」扱いになるべきプログラムのエントリーポイントが実行可能でない場合、そのプログラムは「Disable」として起動してくる。そのため、OptOutなシステムでも、NX compatibleフラグが立っておらず、エントリーポイントが実行可能でない場合、そのプログラムは「Disable」な状態で起動してくる(これはもしかしたらPACKされたファイルへの配慮かもしれないが、挙動としてはややキモイ)。


次のソースはこれらの規則を確認するためにコードで、エントリーポイントが実行可能でないプログラムを生成する。プログラムは最初にMessageBoxを表示した後、SetProcessDEPPolicyでDEPを有効にする。

// selfDEP.cpp
//
// NX compatible有>cl selfDEP.cpp /link /NXCOMPAT
// NX compatible無>cl selfDEP.cpp /link /NXCOMPAT:NO
// NX compatible無>cl selfDEP.cpp
//
#include <windows.h>
#include <tchar.h>
#pragma comment(linker, "/subsystem:windows")
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,r")      // READ only
#pragma comment(linker, "/defaultlib:user32.lib /defaultlib:kernel32.lib")

typedef BOOL (WINAPI* TypeSetProcessDEPPolicy)(__in  DWORD dwFlags);
static const ULONG PROCESS_DEP_ENABLE = 0x00000001;


EXTERN_C
void __cdecl WinMainCRTStartup()
{
    MessageBox(0, TEXT("StartUp"), TEXT("StartUp"), 0);

    DWORD Code = 0;
    TypeSetProcessDEPPolicy SetProcessDEPPolicy =
        (TypeSetProcessDEPPolicy)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "SetProcessDEPPolicy");
    if (!SetProcessDEPPolicy || !SetProcessDEPPolicy(PROCESS_DEP_ENABLE))
    {
        Code = GetLastError();
    }
    TCHAR Text[260];
    wsprintf(Text, "status: 0x%08X", Code);
    MessageBox(0, Text, (Code == ERROR_SUCCESS) ? TEXT("OK!") : TEXT("NG!"), 0);

    ExitProcess(0);
}

このプログラムをOptInかOptOutな環境で動作させる。

もし /NXCOMPAT オプションをつけてリンクし、Vista SP1以降のOSで起動したならば、NX compatibleフラグでDEPが有効になるため、最初のメッセージボックスを表示することなくクラッシュする。
XPや2003などで起動したならば、NX compatibleフラグは無視され、先ほどの特殊な振る舞いによってDEPは無効化されるため実行可能である。最初のメッセージボックスを表示し、SetProcessDEPPolicyを実行する。これが成功するとDEPが有効になり、2番目のメッセージボックスは表示せずにクラッシュする。
/NXCOMPAT オプションをつけなかった場合も、特殊な振る舞いによってDEPは無効化され、どのOSでもSetProcessDEPPolicyまで実行する(そしてSetProcessDEPPolicyが成功すればクラッシュする)。

まとめ

  1. 開発者は、64ビット化する、32ビットなら/NXCOMPATでビルドする、SetProcessDEPPolicyを使うなどしてDEPを有効にしよう(たとえばfirefox.exeはSetProcessDEPPolicyで保護されている)。
  2. 実行可能属性のセクションを持たないからといって、そのPEが動作しないわけではない。システムでDEPを有効にしていても、イメージローダーが特別に無効にしたりする。


と。

10/31+11/30追記

Bypassing Browser Memory Protectionsによると、システムがOptInまたはOptOutポリシーのとき、プロセスにDLLが読み込まれたときに以下の確認処理が行われ、場合によりDEPが無効になることがある。

  1. DLLにNX compatibleフラグがたっているのであれば状態は維持される。
  2. DLLが"SafeDisc"であるならば、DEPは無効化される。
    • "SafeDisc"とはExport Directory Table内に"secserv.dll"という名前を持ち、".txt"と".txt2"という名前のセクションを持つファイルである。
    • この確認はLdrpCheckNXCompatibility -> LdrpCheckSafeDiscDll(LDR_DATA_TABLE_ENTRY*)で行われている。
  3. DLLがDEP非互換リストに含まれているならば、DEPは無効化される。
    • 非互換リストはレジストリキー HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\DllNXOptions に登録されている。。
    • この確認はLdrpCheckNXCompatibility -> LdrpCheckAppDatabase(LDR_DATA_TABLE_ENTRY*)で行われている。
  4. DLLがDEP非互換セクションを持っているならば、DEPは無効化される。
    • 非互換セクションとは、セクション名が".aspack", ".pcle", ".sforce" のものである。
    • この確認はLdrpCheckNXCompatibility -> LdrpCheckNxIncompatibleDllSection(LDR_DATA_TABLE_ENTRY*)で行われている。

実際の動作としては確認はしていませんが。