[Win32] [C++] CreateProcessAsUser – #4 セキュリティ記述子

年内に書ききろうと思っていたのに、結局年を跨いでしまった。不吉だ。
皆さま明けましておめでとうございます。

たぶん CreateProcessAsUser シリーズはこれで最後です。

ソースファイル中に、_GUI と _TRACING という定数を定義しています。

  • _GUI ・・・ DACL への ACE 追加を行なうかどうか
  • _TRACING ・・・ デバッグ用の情報を出力するかどうか

_TRACING で出力される情報を使って、セキュリティ記述子に関する補足です。

-runas オプションを付けてメモ帳を別ユーザーで実行します。

>logue -runas kimaber@contoso password c:\windows\syswow64\notepad

SID: S-1-5-21-2857284654-3416964824-2551679015-513
SID: S-1-1-0
SID: S-1-5-32-545
SID: S-1-5-4
SID: S-1-2-1
SID: S-1-5-11
SID: S-1-5-15
SID: S-1-5-5-0-4408862 (Logon)

PID      : 0x948
HWINSTA  : 0xd8
HDESK    : 0xd4
Logon SID: 0079ACE8
—–

青字で示した部分は、CreateProcessUser が返すプロセス トークンに含まれる SID の一覧です。
ログオン SID が S-1-5-5-0-4408862  であることが分かります。

エンターキーを押し、先に進みます。

Original SD: 0079A8B8
New SD     : 0079ACC8
–>

AddAceToWindowStation の中で、ウィンドウ ステーション オブジェクトのセキュリティ記述子を変更する直前のタイミングで止まります。Original SD は GetUserObjectSecurity API で取得されるポインタで、New SD は SetSecurityDescriptorDacl で 新しい DACL を設定した後のセキュリティ記述子を示すポインタです。

ユーザー モード デバッガーで、!sd を使ってみます。

0:002> !sd 79a8b8
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8004
            SE_DACL_PRESENT
            SE_SELF_RELATIVE
->Owner   :  is NULL
->Group   :  is NULL
->Dacl    :  is NULL
->Sacl    :  is NULL

0:002> !sd 79acc8
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x4
            SE_DACL_PRESENT
->Owner   :  is NULL
->Group   :  is NULL
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x11c
->Dacl    : ->AceCount   : 0xb
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x4
->Dacl    : ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[0]: ->AceSize: 0x24
->Dacl    : ->Ace[0]: ->Mask : 0x00000024
->Dacl    : ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

(略)

->Sacl    :  is NULL

おかしいですね。Original SD の ACL が NULL です。
セキュリティ記述子は _SECURITY_DESCRIPTOR という構造体なので、!sd ではなく dt コマンドで見てみます。

0:002> dt _security_descriptor 79a8b8
ntdll!_SECURITY_DESCRIPTOR
   +0x000 Revision         : 0x1 ”
   +0x001 Sbz1             : 0 ”
   +0x002 Control          : 0x8004
   +0x004 Owner            : (null)
   +0x008 Group            : (null)
   +0x00c Sacl             : (null)
   +0x010 Dacl             : 0x00000014 _ACL
0:002> dt _security_descriptor 79acc8
ntdll!_SECURITY_DESCRIPTOR
   +0x000 Revision         : 0x1 ”
   +0x001 Sbz1             : 0 ”
   +0x002 Control          : 4
   +0x004 Owner            : (null)
   +0x008 Group            : (null)
   +0x00c Sacl             : (null)
   +0x010 Dacl             : 0x0079a9f0 _ACL

Original SD の方は、DACL が 0x14 という値です。ポインタではないみたいです。 ここで重要になってくるのが Control の値で、見てみると、0x8000 のビットが違います。これは !sd の結果に出ていますが、SE_SELF_RELATIVE というフラグです。

セキュリティ記述子には、absolute と self-relative という 2 種類のフォーマットがあり、実はユーザーモードの !sd コマンドは self-relative フォーマットを正しく解釈できないようです。

Absolute and Self-Relative Security Descriptors
http://technet.microsoft.com/library/aa374807

上のページに記載がある通り、absolute フォーマットは各種情報をポインターとして保持しますが、self-relative はオフセットとして保持しています。self-relative は、セキュリティ記述子が 1 まとまりのメモリ ブロック (a contiguous block of memory) としてバッファー上に確保されています。

!sd コマンドが使えないので、!acl コマンドを使って DACL を見る必要があります。

0:002> !acl 79a8b8+14
ACL is:
ACL is: ->AclRevision: 0x2
ACL is: ->Sbz1       : 0x0
ACL is: ->AclSize    : 0x11c
ACL is: ->AceCount   : 0x9
ACL is: ->Sbz2       : 0x0
ACL is: ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[0]: ->AceFlags: 0x4
ACL is: ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
ACL is: ->Ace[0]: ->AceSize: 0x24
ACL is: ->Ace[0]: ->Mask : 0x00000024
ACL is: ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

(略)

ACL is: ->Ace[7]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[7]: ->AceFlags: 0xb
ACL is: ->Ace[7]:             OBJECT_INHERIT_ACE
ACL is: ->Ace[7]:             CONTAINER_INHERIT_ACE
ACL is: ->Ace[7]:             INHERIT_ONLY_ACE
ACL is: ->Ace[7]: ->AceSize: 0x14
ACL is: ->Ace[7]: ->Mask : 0xf0000000
ACL is: ->Ace[7]: ->SID: S-1-5-18

ACL is: ->Ace[8]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[8]: ->AceFlags: 0x4
ACL is: ->Ace[8]:             NO_PROPAGATE_INHERIT_ACE
ACL is: ->Ace[8]: ->AceSize: 0x14
ACL is: ->Ace[8]: ->Mask : 0x000f037f
ACL is: ->Ace[8]: ->SID: S-1-5-18

0:002> !acl 0x0079a9f0
ACL is:
ACL is: ->AclRevision: 0x2
ACL is: ->Sbz1       : 0x0
ACL is: ->AclSize    : 0x11c
ACL is: ->AceCount   : 0xb
ACL is: ->Sbz2       : 0x0
ACL is: ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[0]: ->AceFlags: 0x4
ACL is: ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
ACL is: ->Ace[0]: ->AceSize: 0x24
ACL is: ->Ace[0]: ->Mask : 0x00000024
ACL is: ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

(略)

ACL is: ->Ace[7]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[7]: ->AceFlags: 0xb
ACL is: ->Ace[7]:             OBJECT_INHERIT_ACE
ACL is: ->Ace[7]:             CONTAINER_INHERIT_ACE
ACL is: ->Ace[7]:             INHERIT_ONLY_ACE
ACL is: ->Ace[7]: ->AceSize: 0x14
ACL is: ->Ace[7]: ->Mask : 0xf0000000
ACL is: ->Ace[7]: ->SID: S-1-5-18

ACL is: ->Ace[8]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[8]: ->AceFlags: 0x4
ACL is: ->Ace[8]:             NO_PROPAGATE_INHERIT_ACE
ACL is: ->Ace[8]: ->AceSize: 0x14
ACL is: ->Ace[8]: ->Mask : 0x000f037f
ACL is: ->Ace[8]: ->SID: S-1-5-18

ACL is: ->Ace[9]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[9]: ->AceFlags: 0xb
ACL is: ->Ace[9]:             OBJECT_INHERIT_ACE
ACL is: ->Ace[9]:             CONTAINER_INHERIT_ACE
ACL is: ->Ace[9]:             INHERIT_ONLY_ACE
ACL is: ->Ace[9]: ->AceSize: 0x1c
ACL is: ->Ace[9]: ->Mask : 0xf0000000
ACL is: ->Ace[9]: ->SID: S-1-5-5-0-4408862

ACL is: ->Ace[10]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
ACL is: ->Ace[10]: ->AceFlags: 0x4
ACL is: ->Ace[10]:             NO_PROPAGATE_INHERIT_ACE
ACL is: ->Ace[10]: ->AceSize: 0x1c
ACL is: ->Ace[10]: ->Mask : 0x000f037f
ACL is: ->Ace[10]: ->SID: S-1-5-5-0-4408862

CreateProcessAsUser が作ったログオン SID である S-1-5-5-0-4408862 に対する ACE が、正しく 2 つ追加されていることが確認できました。

エンターキーを押して、プログラムを進めます。今度はデスクトップ オブジェクトの DACL です。

Original SD: 00796140
New SD     : 0079ACC8
–>

デスクトップ オブジェクトもウィンドウ ステーションと同様なので省略し、さらにエンターを押すと、メモ帳が起動します。プログラムは WaitForSingleObject で、プロセスが終了するまで待機します。

ここでカーネル デバッガーを使って、実際のカーネル オブジェクト上の DACL が変わっているかどうかを確認します。
最初の出力から、プロセス ID が 0x948、ウィンドウ ステーションのハンドルが d8 、デスクトップは d4 と分かっているので・・・

kd> !handle d8 7 0x948

Searching for Process with Cid == 948
PROCESS fffffa8001caa060
    SessionId: 1  Cid: 0948    Peb: 7efdf000  ParentCid: 0910
    DirBase: 19c90000  ObjectTable: fffff8a001efa9b0  HandleCount:  58.
    Image: Logue.exe

Handle table at fffff8a00170c000 with 58 entries in use

00d8: Object: fffffa8001a6e7c0  GrantedAccess: 00060000 Entry: fffff8a00170c360
Object: fffffa8001a6e7c0  Type: (fffffa8000ca7b40) WindowStation
    ObjectHeader: fffffa8001a6e790 (new version)
        HandleCount: 26  PointerCount: 43
        Directory Object: fffff8a0021e8720  Name: WinSta0

kd> !handle d4 7 0x948

Searching for Process with Cid == 948
PROCESS fffffa8001caa060
    SessionId: 1  Cid: 0948    Peb: 7efdf000  ParentCid: 0910
    DirBase: 19c90000  ObjectTable: fffff8a001efa9b0  HandleCount:  58.
    Image: Logue.exe

Handle table at fffff8a00170c000 with 58 entries in use

00d4: Object: fffffa8001cfe830  GrantedAccess: 00060081 Entry: fffff8a00170c350
Object: fffffa8001cfe830  Type: (fffffa8000ca79f0) Desktop
    ObjectHeader: fffffa8001cfe800 (new version)
        HandleCount: 13  PointerCount: 495
        Directory Object: 00000000  Name: Default

前の記事から、SecurityDescriptor はオブジェクト ヘッダーから 0x28 バイト目にあり、かつ _EX_FAST_REF 構造なので・・・

kd> dt _security_descriptor poi(fffffa8001a6e790+28)&0xffffffff`fffffff0
nt!_SECURITY_DESCRIPTOR
   +0x000 Revision         : 0x1 ”
   +0x001 Sbz1             : 0 ”
   +0x002 Control          : 0x8014
   +0x008 Owner            : 0x00000014`0000015c Void
   +0x010 Group            : 0x001c0002`00000030 Void
   +0x018 Sacl             : 0x00140011`00000001 _ACL
   +0x020 Dacl             : 0x00000101`00000001 _ACL
kd> dt _security_descriptor poi(fffffa8001cfe800+28)&0xffffffff`fffffff0
nt!_SECURITY_DESCRIPTOR
   +0x000 Revision         : 0x1 ”
   +0x001 Sbz1             : 0 ”
   +0x002 Control          : 0x8014
   +0x008 Owner            : 0x00000014`000000c0 Void
   +0x010 Group            : 0x001c0002`00000030 Void
   +0x018 Sacl             : 0x00140011`00000001 _ACL
   +0x020 Dacl             : 0x00000101`00000001 _ACL

おや、New SD は absolute フォーマットだったのに自動的に self-relative フォーマットに変換されています。まあ、そういうものなのでしょう。それに SACL も勝手に追加されています。おそらく継承によるものです。

カーネル デバッガーの !sd コマンドでは、self-relative フォーマットのセキュリティ記述子もダンプすることができます。
ログオン SID である S-1-5-5-0-4408862 に対する ACE が無事追加されていることが確認できました。

kd> !sd poi(fffffa8001a6e790+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x11c
->Dacl    : ->AceCount   : 0xb
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x4
->Dacl    : ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[0]: ->AceSize: 0x24
->Dacl    : ->Ace[0]: ->Mask : 0x00000024
->Dacl    : ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

(略)

->Dacl    : ->Ace[9]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[9]: ->AceFlags: 0xb
->Dacl    : ->Ace[9]:             OBJECT_INHERIT_ACE
->Dacl    : ->Ace[9]:             CONTAINER_INHERIT_ACE
->Dacl    : ->Ace[9]:             INHERIT_ONLY_ACE
->Dacl    : ->Ace[9]: ->AceSize: 0x1c
->Dacl    : ->Ace[9]: ->Mask : 0xf0000000
->Dacl    : ->Ace[9]: ->SID: S-1-5-5-0-4408862

->Dacl    : ->Ace[10]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[10]: ->AceFlags: 0x4
->Dacl    : ->Ace[10]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[10]: ->AceSize: 0x1c
->Dacl    : ->Ace[10]: ->Mask : 0x000f037f
->Dacl    : ->Ace[10]: ->SID: S-1-5-5-0-4408862

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

kd> !sd poi(fffffa8001cfe800+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x80
->Dacl    : ->AceCount   : 0x5
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize: 0x1c
->Dacl    : ->Ace[0]: ->Mask : 0x000f01ff
->Dacl    : ->Ace[0]: ->SID: S-1-5-5-0-3706178

(略)

->Dacl    : ->Ace[4]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[4]: ->AceFlags: 0x0
->Dacl    : ->Ace[4]: ->AceSize: 0x1c
->Dacl    : ->Ace[4]: ->Mask : 0x000f01ff
->Dacl    : ->Ace[4]: ->SID: S-1-5-5-0-4408862

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

メモ帳を閉じ、エンターキーを 2 回押すとプログラムが終了します。

Original SD: 0079C2A8
New SD     : 0079AD08
–> エンターを押す

Original SD: 007961B8
New SD     : 0079AD08
–> エンターを押す

このタイミングで、DACL が元に戻っているかどうかを確認します。ウィンドウ ステーションとデスクトップ オブジェクトのアドレスは変わらないので、さっきと同じコマンドで確認します。

kd> !sd poi(fffffa8001a6e790+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x11c
->Dacl    : ->AceCount   : 0x9
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x4
->Dacl    : ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[0]: ->AceSize: 0x24
->Dacl    : ->Ace[0]: ->Mask : 0x00000024
->Dacl    : ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

(略)

->Dacl    : ->Ace[7]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[7]: ->AceFlags: 0xb
->Dacl    : ->Ace[7]:             OBJECT_INHERIT_ACE
->Dacl    : ->Ace[7]:             CONTAINER_INHERIT_ACE
->Dacl    : ->Ace[7]:             INHERIT_ONLY_ACE
->Dacl    : ->Ace[7]: ->AceSize: 0x14
->Dacl    : ->Ace[7]: ->Mask : 0xf0000000
->Dacl    : ->Ace[7]: ->SID: S-1-5-18

->Dacl    : ->Ace[8]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[8]: ->AceFlags: 0x4
->Dacl    : ->Ace[8]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[8]: ->AceSize: 0x14
->Dacl    : ->Ace[8]: ->Mask : 0x000f037f
->Dacl    : ->Ace[8]: ->SID: S-1-5-18

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

kd> !sd poi(fffffa8001cfe800+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x80
->Dacl    : ->AceCount   : 0x4
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize: 0x1c
->Dacl    : ->Ace[0]: ->Mask : 0x000f01ff
->Dacl    : ->Ace[0]: ->SID: S-1-5-5-0-3706178

(略)

->Dacl    : ->Ace[3]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[3]: ->AceFlags: 0x0
->Dacl    : ->Ace[3]: ->AceSize: 0x14
->Dacl    : ->Ace[3]: ->Mask : 0x000f01ff
->Dacl    : ->Ace[3]: ->SID: S-1-5-18

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

S-1-5-5-0-4408862 に対する ACE だけが消えていることが確認できました。

最後に、セキュリティ記述子のサイズについての補足です。
セキュリティ記述子に関連する構造は、大体こんな風になっています。

  • SecurityDescriptor = _SECURITY_DESCRIPTOR strucutre + DACLs + m*SACLs
  • ACL = _ACL structure + ACEs
  • ACE = _ACE_HEADER structure + SID

将来の拡張を含めた汎用性の維持という観点から、SID のサイズは可変です。このため、SID を連続したメモリ上に並べた ACL や Security Descriptor は可変にならざるを得ません。

ここで、2 月の記事で引用したこの KB。

INFO: Computing the Size of a New ACL
http://support.microsoft.com/kb/102103/en

これは、既存の ACL に ACCESS_ALLOWED_ACE (アクセス許可 ACE) を 1 つ加えたときのサイズを計算する式です。

dwNewACLSize = AclInfo.AclBytesInUse
               + sizeof(ACCESS_ALLOWED_ACE)
               + GetLengthSid(UserSID)
               – sizeof(DWORD);

ACCESS_ALLOWED_ACE のサイズと、GetLengthSid で計算した SID のサイズを足すのは直感的に分かりますが、DWORD を引くのはなんなんだ、と。KB の本文を見ると、こう書いてあります。

Subtracting out the size of a DWORD is the final adjustment needed to obtain the exact size. This adjust is to compensate for a place holder member in the ACCESS_ALLOWED_ACE structure which is used in variable length ACEs.

ACCESS_ALLOWED_ACE のプレースホルダーらしいです。そんなわけで WinNT.h で定義されている構造を見ます。

typedef struct _ACCESS_ALLOWED_ACE {
    ACE_HEADER Header;
    ACCESS_MASK Mask;
    DWORD SidStart;
} ACCESS_ALLOWED_ACE;

プレース ホルダーは SidStart ですね。SID は、_ACCESS_ALLOWED_ACE 構造体の直後に始まるのではなく、SidStart のアドレスから始まるので、構造体のサイズと SID のサイズを足した後、重複する SidStart 分の DWORD を引く必要があるのです。

ACCESS_ALLOWED_ACE 以外にも ACE の種類はたくさんあり、一覧が以下のページにあります。
ちなみに、Object specifig ACE は、ACE の中に GUID を 2 つ含んでいるため、サイズが大きいです。

ACE
http://msdn.microsoft.com/en-us/library/aa374912(v=vs.85).aspx

もう 1 つ重要なのは _ACL 構造体です。

typedef struct _ACL {
    BYTE  AclRevision;
    BYTE  Sbz1;
    WORD   AclSize;
    WORD   AceCount;
    WORD   Sbz2;
} ACL;
typedef ACL *PACL;

まあ普通の構造体ですが、AclSize が WORD 型であるところがポイントです。つまり、ACL のサイズが 64KB を超えることは構造上不可能なのです。これは比較的有名な 64K 問題で、KB も出ています。

Maximum number of ACEs in an ACL
http://support.microsoft.com/kb/166348/en

さて、セキュリティ記述子のサイズが分かったところでプログラムに戻ります。サンプルを再掲。

Starting an Interactive Client Process in C++
http://msdn.microsoft.com/en-us/library/aa379608(v=vs.85).aspx

MSDN のサンプルでは、新たなセキュリティ記述子である psdNew という変数に対して、既存のセキュリティ記述子 psd のサイズである dwSdSizeNeeded の値をそのまま使ってヒープ メモリを確保しています。

psd = (PSECURITY_DESCRIPTOR)HeapAlloc(
      GetProcessHeap(),
      HEAP_ZERO_MEMORY,
      dwSdSizeNeeded);

if (psd == NULL)
   __leave;

psdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(
      GetProcessHeap(),
      HEAP_ZERO_MEMORY,
      dwSdSizeNeeded);

if (psdNew == NULL)
   __leave;

これはおかしな話です。なぜなら、ACE を追加すると ACL のサイズは増え、それに伴ってセキュリティ記述子のサイズは大きくなるはずだからです。なぜバッファー オーバー ラン (BOR) を引き起こさないのでしょうか。その秘密が SetSecurityDescriptorDacl の動きにあります。それをデバッガーで確認します。たまには Release ビルド版をデバッグしてみます。

advapi32!SetSecurityDescriptorDacl は、あちこち飛んだ挙句、ntdll!RtlSetDaclSecurityDescriptor に行きつきます。結局 ntdll.dll に実装されているのです。プログラムを実行して、ntdll!RtlSetDaclSecurityDescriptor が AddAccessAllowedAceBasedSID から呼ばれたときに止めます。

0:000> bl
0 e 779a2cc2     0001 (0001)  0:**** ntdll!RtlSetDaclSecurityDescriptor
0:000> k
ChildEBP RetAddr
0044f93c 76b6c6b3 ntdll!RtlSetDaclSecurityDescriptor
WARNING: Stack unwind information not available. Following frames may be wrong.
0044f954 012711ee KERNELBASE!SetSecurityDescriptorDacl+0x17
0044f9bc 012718a9 Logue!AddAccessAllowedAceBasedSID+0x1ee
0044fa5c 01271c9d Logue!RunAs+0x1e9
0044fa7c 74cd263d Logue!wmain+0x8d
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\syswow64\kernel32.dll –
0044fac8 76bb33ca MSVCR100!initterm+0x16
0044fad4 77999ed2 kernel32!BaseThreadInitThunk+0x12
0044fb14 77999ea5 ntdll!__RtlUserThreadStart+0x70
0044fb2c 00000000 ntdll!_RtlUserThreadStart+0x1b

0:000> bp 012711ee
0:000> g
Breakpoint 1 hit
eax=00000001 ebx=00000002 ecx=00000004 edx=007bdef0 esi=0044fa44 edi=007bdef0
eip=012711ee esp=0044f96c ebp=0044f9bc iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
Logue!AddAccessAllowedAceBasedSID+0x1ee:
012711ee 85c0            test    eax,eax
0:000> ub
Logue!AddAccessAllowedAceBasedSID+0x1db:
012711db 85c0            test    eax,eax
012711dd 7459            je      Logue!AddAccessAllowedAceBasedSID+0x238 (01271238)
012711df 8b45e8          mov     eax,dword ptr [ebp-18h]
012711e2 6a00            push    0
012711e4 57              push    edi
012711e5 6a01            push    1
012711e7 50              push    eax
012711e8 ff1564402701    call    dword ptr [Logue!_imp__SetSecurityDescriptorDacl (01274064)]

0:000> dt _security_descriptor poi(@ebp-18h)
ntdll!_SECURITY_DESCRIPTOR
   +0x000 Revision         : 0x1 ”
   +0x001 Sbz1             : 0 ”
   +0x002 Control          : 4
   +0x004 Owner            : (null)
   +0x008 Group            : (null)
   +0x00c Sacl             : (null)
   +0x010 Dacl             : 0x007bdef0 _ACL
0:000> r @edi
edi=007bdef0

SetSecurityDescriptorDacl が終わった直後の AddAccessAllowedAceBasedSID で止めます。それが 012711ee です。

SetSecurityDescriptorDacl に渡している引数を確認すると、第一引数の PSECURITY_DESCRIPTOR が eax レジスタで、DACL である第三引数の PACL は edi レジスタです。関数実行後に eax レジスタは変わってしまうので、その元を辿ると、ebp-18 から mov しているので、このローカル変数領域がセキュリティ記述子です。

ebp-18 のセキュリティ記述子内の DACL と、edi レジスタの DACL のポインタは同じ値 (0x007bdef0) になっています。つまり、SetSecurityDescriptorDacl は渡した DACL を別のバッファーにコピーすることなく、そのまま代入しているのです。当然、このセキュリティ記述子は absolute フォーマットになります。

MSDN のサンプルで、新規作成したセキュリティ記述子をオリジナルのセキュリティ記述子のバッファー サイズにしても問題ない理由が分かりました。この記事の最初で確かめたように、オリジナルのセキュリティ記述子は Self-relative でした。すなわち、セキュリティ記述子のバッファー サイズである dwSdSizeNeeded には ACL のサイズも含まれています。しかし、新規作成されるセキュリティ記述子は absolute フォーマットになるため、ACL のサイズは不要です。BOR になるどころか、ヒープの無駄遣いです。(といっても KB オーダーですが)

そこで、作り直したプログラムの AddAccessAllowedAceBasedSID 関数では、新しいセキュリティ記述子のバッファー サイズは SECURITY_DESCRIPTOR_MIN_LENGTH 定数を使っています。定義は以下のようになっており、各種フィールドはポインターの分のサイズが確保されます。absolute フォーマットならこれで十分です。

#define SECURITY_DESCRIPTOR_MIN_LENGTH   (sizeof(SECURITY_DESCRIPTOR))

typedef struct _SECURITY_DESCRIPTOR {
   BYTE  Revision;
   BYTE  Sbz1;
   SECURITY_DESCRIPTOR_CONTROL Control;
   PSID Owner;
   PSID Group;
   PACL Sacl;
   PACL Dacl;

   } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;

広告

[Win32] [C++] CreateProcessAsUser – #2 トークン編

次に、CreateProcessAsUser で STATUS_DLL_INIT_FAILED (=0xc0000142) エラーが発生する現象についてです。
こんなダイアログが表示されます。

「アプリケーションを正しく起動できませんでした (0xc0000142)。」
image

STATUS_DLL_INIT_FAILED エラーという名前からして、アクセス許可エラーではなく、何らかの DLL の初期化に失敗したようです。win32k とか、GUI スレッドに関連していそうです。

このダイアログを表示させているプロセスは、CreateProcessAsUser を実行したプロセスや、実行しようとしたプロセス (上の例だと notepad.exe) ではなく、csrss.exe です。CSRSS とは、Client Server Run-Time Subsystem の略で、win32 サブシステムのユーザー モード部分です。

とりあえず、デバッガーでスタックを見てみます。環境は Windows Server 2008 R2 SP1 です。tasklist で csrss を見ると Services と Console がありますが、ポップアップを表示させているのは Console の方です。

ユーザー モードで見ると、以下のスレッドがメッセージボックスを表示させていることが分かります。しかし、CsrApiRequestThread という関数からいきなりハードエラーが発生しているので、ここからは何も分かりません。カーネル モードから見ても、芳しい結果は得られず保留。CsrApiRequestThread の内容をごりごり見るしかないのかもしれない。ちょっとこれは大変そうなのでパス。

   4  Id: 560.450 Suspend: 1 Teb: 000007ff`fffdd000 Unfrozen
Child-SP          RetAddr           Call Site
00000000`00a4f198 00000000`77584bc4 USER32!ZwUserWaitMessage+0xa
00000000`00a4f1a0 00000000`77584edd USER32!DialogBox2+0x274
00000000`00a4f230 00000000`775d2920 USER32!InternalDialogBox+0x135
00000000`00a4f290 00000000`775d1c15 USER32!SoftModalMessageBox+0x9b4
00000000`00a4f3c0 00000000`775d146b USER32!MessageBoxWorker+0x31d
00000000`00a4f580 000007fe`fd702de9 USER32!MessageBoxTimeoutW+0xb3
00000000`00a4f650 000007fe`fd7031e8 winsrv!HardErrorHandler+0x33d
00000000`00a4f7f0 000007fe`fd703562 winsrv!ProcessHardErrorRequest+0xe8
00000000`00a4f860 000007fe`fd754a04 winsrv!UserHardErrorEx+0x356
00000000`00a4f8f0 000007fe`fd7550f4 CSRSRV!QueueHardError+0x138
00000000`00a4f930 00000000`777e4a00 CSRSRV!CsrApiRequestThread+0x510
00000000`00a4fc40 00000000`00000000 ntdll!RtlUserThreadStart+0x25

さて、STATUS_DLL_INIT_FAILED エラーの詳細は不明ですが、解決方法は分かっているわけです。2 月に記事を書いたときには ACL についてよく知らなかったので適当に MSDN のサンプルをコピペしましたが、もう少し真面目に見てみます。

サンプルはこれでした。

Starting an Interactive Client Process in C++
http://msdn.microsoft.com/en-us/library/aa379608(v=VS.85).aspx

CreateProcessAsUser を実行する前の主な処理は、こんな感じです。

  1. LogonUser API でログオン処理を行ない、トークンを生成
  2. GetLogonSID で、1. のトークンからログオン SID を取得
  3. AddAceToWindowStation で、ウィンドウ ステーション オブジェクトの DACL に
    2. の SID に対するアクセス許可 ACE を追加
  4. AddAceToDesktop で、デスクトップ オブジェクトの DACL に
    2. の SID に対するアクセス許可 ACE を追加
  5. CreateProcessAsUser 実行

まずは、GetLogonSID の処理から見てみます。ここで取得される SID はログオン SID といって、ユーザーやセキュリティ グループに割り当てられた SID とは別のものです。ログオン セッション毎にユニークな値になっています。つまり、CreateProcessAsUser が返すトークンに紐付いたログオン SID は毎回異なっています。

logon SID
http://msdn.microsoft.com/en-us/library/ms721592(v=VS.85).aspx#_security_logon_sid_gly

A security identifier (SID) that identifies a logon session. You can use the logon SID in a DACL to control access during a logon session. A logon SID is valid until the user logs off. A logon SID is unique while the computer is running; no other logon session will have the same logon SID. However, the set of possible logon SIDs is reset when the computer starts up. To retrieve the logon SID from an access token, call the GetTokenInformation function for TokenGroups.

ログオン SID は !token コマンドで見ることができます。そして、トークン オブジェクトのアドレスは !process コマンドで出力することができます。しかし、それだと何も面白くないので、もう少し捻ります。

プロセスに紐付く情報は、基本的に _EPROCESS 構造体から辿ることができます。トークンも例外ではありません。

kd> dt _EPROCESS Token
nt!_EPROCESS
   +0x208 Token : _EX_FAST_REF
kd> dt _EX_FAST_REF
nt!_EX_FAST_REF
   +0x000 Object           : Ptr64 Void
   +0x000 RefCnt           : Pos 0, 4 Bits
   +0x000 Value            : Uint8B

_EPROCESS::Token は単なるポインターではなく、_EX_FAST_REF という構造になっています。これは、以下のサイトの情報にあるように下位ビット (32bit なら 3、64bit なら 4 ビット) を RefCnt として保持させている構造体です。よって正しいポインターを得るためには、下位ビットを 0 にクリアする必要があります。

http://www.eggheadcafe.com/microsoft/Windows-Debugging/30558733/kernel-debug-unable-to-get-min-sid-header.aspx

適当にプロセスを 2 つ選んで、トークンを見てみます。_EPROCESS の中で Token のオフセットは 0x208 でした。

kd> !process 0 0 cmd.exe
PROCESS fffffa8001ccd060
    SessionId: 1  Cid: 0910    Peb: 7fffffde000  ParentCid: 0b80
    DirBase: 0363f000  ObjectTable: fffff8a001c59a30  HandleCount:  92.
    Image: cmd.exe

PROCESS fffffa8001c81860
    SessionId: 1  Cid: 0a1c    Peb: 7fffffdf000  ParentCid: 0b80
    DirBase: 15c1b000  ObjectTable: fffff8a001cebb60  HandleCount:  23.
    Image: cmd.exe

kd> dq fffffa8001ccd060+208 l1
fffffa80`01ccd268  fffff8a0`01d1c95b

kd> dq fffffa8001c81860+208 l1
fffffa80`01c81a68  fffff8a0`022a095f

kd> !token fffff8a0`01d1c950 -n
_TOKEN fffff8a001d1c950
TS Session ID: 0x1
User: S-1-5-21-2857284654-3416964824-2551679015-500 (no name mapped)
Groups:
00 S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
    Attributes – Mandatory Default Enabled
01 S-1-1-0 (Well Known Group: localhost\Everyone)
    Attributes – Mandatory Default Enabled
02 S-1-5-32-545 (Alias: BUILTIN\Users)
    Attributes – Mandatory Default Enabled
03 S-1-5-32-544 (Alias: BUILTIN\Administrators)
    Attributes – Mandatory Default Enabled Owner
04 S-1-5-4 (Well Known Group: NT AUTHORITY\INTERACTIVE)
    Attributes – Mandatory Default Enabled
05 S-1-2-1 (Well Known Group: localhost\CONSOLE LOGON)
    Attributes – Mandatory Default Enabled
06 S-1-5-11 (Well Known Group: NT AUTHORITY\Authenticated Users)
    Attributes – Mandatory Default Enabled
07 S-1-5-15 (Well Known Group: NT AUTHORITY\This Organization)
    Attributes – Mandatory Default Enabled
08 S-1-5-5-0-3706178 (no name mapped)
    Attributes – Mandatory Default Enabled LogonId

09 S-1-2-0 (Well Known Group: localhost\LOCAL)
    Attributes – Mandatory Default Enabled
10 S-1-5-21-2857284654-3416964824-2551679015-512 (no name mapped)
    Attributes – Mandatory Default Enabled
11 S-1-5-21-2857284654-3416964824-2551679015-520 (no name mapped)
    Attributes – Mandatory Default Enabled
12 S-1-5-21-2857284654-3416964824-2551679015-519 (no name mapped)
    Attributes – Mandatory Default Enabled
13 S-1-5-21-2857284654-3416964824-2551679015-518 (no name mapped)
    Attributes – Mandatory Default Enabled
14 S-1-5-21-2857284654-3416964824-2551679015-3683 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
15 S-1-5-21-2857284654-3416964824-2551679015-572 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
16 S-1-16-12288 Unrecognized SID
    Attributes – GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
Privs:
(略)
Authentication ID:         (0,388d63)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2000 ( Token in use )
Token ID: 38f126           ParentToken ID: 0
Modified ID:               (0, 38d8de)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7

kd> !token fffff8a0`022a0950 -n
_TOKEN fffff8a0022a0950
TS Session ID: 0x1
User: S-1-5-21-2857284654-3416964824-2551679015-500 (no name mapped)
Groups:
00 S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
    Attributes – Mandatory Default Enabled
01 S-1-1-0 (Well Known Group: localhost\Everyone)
    Attributes – Mandatory Default Enabled
02 S-1-5-32-545 (Alias: BUILTIN\Users)
    Attributes – Mandatory Default Enabled
03 S-1-5-32-544 (Alias: BUILTIN\Administrators)
    Attributes – Mandatory Default Enabled Owner
04 S-1-5-4 (Well Known Group: NT AUTHORITY\INTERACTIVE)
    Attributes – Mandatory Default Enabled
05 S-1-2-1 (Well Known Group: localhost\CONSOLE LOGON)
    Attributes – Mandatory Default Enabled
06 S-1-5-11 (Well Known Group: NT AUTHORITY\Authenticated Users)
    Attributes – Mandatory Default Enabled
07 S-1-5-15 (Well Known Group: NT AUTHORITY\This Organization)
    Attributes – Mandatory Default Enabled
08 S-1-5-5-0-3706178 (no name mapped)
    Attributes – Mandatory Default Enabled LogonId

09 S-1-2-0 (Well Known Group: localhost\LOCAL)
    Attributes – Mandatory Default Enabled
10 S-1-5-21-2857284654-3416964824-2551679015-512 (no name mapped)
    Attributes – Mandatory Default Enabled
11 S-1-5-21-2857284654-3416964824-2551679015-520 (no name mapped)
    Attributes – Mandatory Default Enabled
12 S-1-5-21-2857284654-3416964824-2551679015-519 (no name mapped)
    Attributes – Mandatory Default Enabled
13 S-1-5-21-2857284654-3416964824-2551679015-518 (no name mapped)
    Attributes – Mandatory Default Enabled
14 S-1-5-21-2857284654-3416964824-2551679015-3683 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
15 S-1-5-21-2857284654-3416964824-2551679015-572 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
16 S-1-16-12288 Unrecognized SID
    Attributes – GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
Privs:
(略)
Authentication ID:         (0,388d63)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2000 ( Token in use )
Token ID: 3a609d           ParentToken ID: 0
Modified ID:               (0, 3a5ce1)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7

どちらのプロセスも、S-1-5-5-0-3706178 というログオン SID をトークンの中に保持していることが分かります。ログオン SID は、S-1-5-5- で始まっていることから判断することができます。

Well-known security identifiers in Windows operating systems
http://support.microsoft.com/kb/243330/en

以上が GetLogonSID の処理でした。UI を持つプロセスは、デスクトップやウィンドウ ステーション オブジェクトに対してアクセス権を持っていなければならず、そのためにはログオン セッションのログオン SID に対して許可されていなければならないようです。つまり、オブジェクトの DACL に ACE を追加する必要があります。

ACL に関しては、半年ほど前に SDDL をパースするプログラムを書きました。

[Win32] [C++] CUI tool to parse SDDL Strings and account SIDs
https://msmania.wordpress.com/2011/06/30/win32-c-cui-tool-to-parse-sddl-strings-and-account-sids/

オブジェクトが持つセキュリティ記述子 (Security Descriptor) は、オブジェクト ヘッダー nt!_OBJECT_HEADER が保持しています。そこで、デスクトップとウィンドウ ステーションのセキュリティ記述子を見てみます。

winobj で見ることができるように、ウィンドウ ステーション オブジェクトは \Windows\WindowStations\WinSta0 のような名前を持っていますが、デスクトップ オブジェクトにはオブジェクト マネージャーの名前空間に名前を持っていないため、プロセスのハンドルテーブルから探します。

kd> !process 0 0 notepad.exe
PROCESS fffffa8001be6b30
    SessionId: 1  Cid: 0310    Peb: 7fffffdf000  ParentCid: 0910
    DirBase: 07a6b000  ObjectTable: fffff8a00192f600  HandleCount:  90.
    Image: notepad.exe

kd> !handle 0 7 fffffa8001be6b30
    PROCESS fffffa8001be6b30
    SessionId: 1  Cid: 0310    Peb: 7fffffdf000  ParentCid: 0910
    DirBase: 07a6b000  ObjectTable: fffff8a00192f600  HandleCount:  90.
    Image: notepad.exe

Handle table at fffff8a0019e0000 with 90 entries in use

(略)

0034: Object: fffffa8001a6e7c0  GrantedAccess: 000f037f Entry: fffff8a0019e00d0
Object: fffffa8001a6e7c0  Type: (fffffa8000ca7b40) WindowStation
    ObjectHeader: fffffa8001a6e790 (new version)
        HandleCount: 23  PointerCount: 39
        Directory Object: fffff8a0021e8720  Name: WinSta0

0038: Object: fffffa8001cfe830  GrantedAccess: 000f01ff Entry: fffff8a0019e00e0
Object: fffffa8001cfe830  Type: (fffffa8000ca79f0) Desktop
    ObjectHeader: fffffa8001cfe800 (new version)
        HandleCount: 11  PointerCount: 494
        Directory Object: 00000000  Name: Default

(略)

オブジェクトのセキュリティ記述子は、オブジェクト ヘッダー nt!_OBJECT_HEADER に含まれています。

kd> dt _object_header
nt!_OBJECT_HEADER
   +0x000 PointerCount     : Int8B
   +0x008 HandleCount      : Int8B
   +0x008 NextToFree       : Ptr64 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : UChar
   +0x019 TraceFlags       : UChar
   +0x01a InfoMask         : UChar
   +0x01b Flags            : UChar
   +0x020 ObjectCreateInfo : Ptr64 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : Ptr64 Void
   +0x028 SecurityDescriptor : Ptr64 Void
   +0x030 Body             : _QUAD

セキュリティ識別子は、!sd でダンプできます。そこで、SecurityDescriptor メンバーに対して !sd を実行すると・・・

kd> dt _object_header fffffa8001cfe800
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n494
   +0x008 HandleCount      : 0n11
   +0x008 NextToFree       : 0x00000000`0000000b Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0x15 ”
   +0x019 TraceFlags       : 0 ”
   +0x01a InfoMask         : 0xe ”
   +0x01b Flags            : 0 ”
   +0x020 ObjectCreateInfo : 0xfffff800`0166ac00 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0xfffff800`0166ac00 Void
   +0x028 SecurityDescriptor : 0xfffff8a0`01c133ee Void
   +0x030 Body             : _QUAD

kd> !sd 0xfffff8a0`01c133ee
1100000001001c: Unable to get MIN SID header
1100000001001c: Unable to read in Owner in SD

エラーになりました。ポインターが間違っているようです。ポインターの値をよくみると、64bit ポインターなのに 64bit 境界になっていないことに気づきます。64bit 境界ということでポインターは下位 4 ビットが必ず 0 か 8 になるはずですが、SecurityDescriptor の値の下位 4 ビットは e になっています。ここで思い出すのは、さっき出てきた _EX_FAST_REF 構造です。というか、上で引用したページにも書いてありましたが SecurityDescriptor フィールドは _EX_FAST_REF 構造になっているため、下位 4 ビットを 0 にすることでセキュリティ記述子のアドレスが得られます。SecurityDescriptor はオフセット 0x28 バイトなので・・・

▼ ウィンドウ ステーション "Winsta0"

kd> !sd poi(fffffa8001a6e790+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0xe4
->Dacl    : ->AceCount   : 0x9
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x4
->Dacl    : ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[0]: ->AceSize: 0x24
->Dacl    : ->Ace[0]: ->Mask : 0x00000024
->Dacl    : ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

->Dacl    : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[1]: ->AceFlags: 0xb
->Dacl    : ->Ace[1]:             OBJECT_INHERIT_ACE
->Dacl    : ->Ace[1]:             CONTAINER_INHERIT_ACE
->Dacl    : ->Ace[1]:             INHERIT_ONLY_ACE
->Dacl    : ->Ace[1]: ->AceSize: 0x1c
->Dacl    : ->Ace[1]: ->Mask : 0xf0000000
->Dacl    : ->Ace[1]: ->SID: S-1-5-5-0-3706178

->Dacl    : ->Ace[2]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[2]: ->AceFlags: 0x4
->Dacl    : ->Ace[2]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[2]: ->AceSize: 0x1c
->Dacl    : ->Ace[2]: ->Mask : 0x000f037f
->Dacl    : ->Ace[2]: ->SID: S-1-5-5-0-3706178

(略)

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

▼ デスクトップ "Default"

kd> !sd poi(fffffa8001cfe800+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x64
->Dacl    : ->AceCount   : 0x4
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize: 0x1c
->Dacl    : ->Ace[0]: ->Mask : 0x000f01ff
->Dacl    : ->Ace[0]: ->SID: S-1-5-5-0-3706178

(略)

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

これでセキュリティ記述子をダンプできました。

まず注目すべきは紫色で示した SID です。これは S-1-5-5- で始まっているのでログオン SID です。現在のログオン セッションに対して、アクセス許可の設定がされていることが分かります。

次に、ログオン SID が割り当てられている ACE に注目します。DACL の中でログオン SID が割り当てられている ACE は全て ACCESS_ALLOWED_ACE_TYPE のアクセス許可 ACE です。そして、ウィンドウ ステーションでは 2 つの ACE があるのに対して、デスクトップは 1 つです。ウィンドウ ステーションに設定された 2 つの ACE では、AceFlags と Mask の値が異なっています。AceFlags の定数を見ると分かりますが、1 つは継承可能で、もう 1 つは継承を止める ACE になっています。つまり、Mask で設定されたアクセス許可の種類に応じて、継承させるものと継承させないものを区別していることが分かります。

ACE Inheritance Rules
http://msdn.microsoft.com/en-us/library/windows/desktop/aa374924(v=vs.85).aspx

ACCESS_MASK
http://msdn.microsoft.com/en-us/library/aa374892(v=vs.85).aspx

MSDN のサンプルに戻ります。サンプルにおいては、オブジェクトの DACL に ログオン SID の ACE を追加する処理がAddAceToWindowStation と AddAceToDesktop との 2 つの関数に分けてあります。この理由が上記の内容になります。デスクトップは 1 つの ACE を追加すればいいですが、ウィンドウ ステーションには、2 つの ACE を追加しているため、関数を分けているようです。

AddAceToWindowStation では、AddAce API を 2 回呼ぶことで 2 つの ACE を ACL に追加しています。一方 AddAceToDesktop では、AddAccessAllowedAce API を 1 回呼んで ACE を追加しています。

このサンプルは実はもっと効率よく書けます。まず、AddAceToWindowStation と AddAceToDesktop とでは共通する処理がほとんどです。というのも、いずれの関数も流れは共通で、異なっているのは以下の 5. の部分だけです。

  1. オブジェクト ハンドルからセキュリティ記述子を取得 (GetUserObjectSecurity API)
  2. 新たなセキュリティ記述子を作成 (InitializeSecurityDescriptor API)
  3. 新たな ACL を作成 (InitializeAcl API)
  4. 1. で取得したセキュリティ記述子の DACL を 3. の ACL に コピー
  5. SID に対するアクセス許可 ACE を 4. の ACL に追加
  6. 2. のセキュリティ記述子に 5. の ACL を設定 (SetSecurityDescriptorDacl API)
  7. オブジェクトに 6. の セキュリティ記述子を設定 (SetUserObjectSecurity API)

AddAceToDesktop で使っている AddAccessAllowedAce API では AceFlags を指定できませんが、AddAccessAllowedAceEx API を使うと AceFlags を指定できるので、AddAceToWindowStation と AddAceToDesktop とのいずれにおいても AddAccessAllowedAceEx を使って ACE を追加すれば、わざわざ ACE のサイズを計算して AddAce を使わなくて済みます。

AddAccessAllowedAce function
http://msdn.microsoft.com/en-us/library/aa374947

AddAccessAllowedAceEx function
http://msdn.microsoft.com/en-us/library/aa374951(v=VS.85).aspx

そこで、AddAceToWindowStation と AddAceToDesktop の ACE を追加する処理以外を AddAccessAllowedAceBasedSID という関数でまとめて、ACE の追加は AddAccessAllowedAceEx を使ってプログラムを書き直してみました。

さらに、追加した ACE を削除する関数 RemoveAccessAllowedAcesBasedSID も作りました。これは、指定した SID に対するアクセス許可 ACE のみだけを削除する関数です。

AddAceToWindowStation と AddAceToDesktop は ACE のフラグとマスクの配列を渡すだけになりました。
にも関わらず、エラー処理をするとコードが長くなってしまうのが Win32 の面倒なところ。

//
// dacl.cpp
//

#include <Windows.h>
#include <Sddl.h>
#include <stdio.h>

#include "logue.h"

// add ACCESS_ALLOWED_ACEs of the specified SID to the object’s DACL
BOOL AddAccessAllowedAceBasedSID(HANDLE Object, PSID Sid, DWORD AceCount,
                                CONST DWORD AceFlags[], CONST DWORD AccessMasks[]) {
    BOOL Ret= FALSE;
    SECURITY_INFORMATION DaclInfo= DACL_SECURITY_INFORMATION;
    PACL Acl= NULL; // no need to free
    PACL AclNew= NULL;
    PSECURITY_DESCRIPTOR Sd= NULL;
    PSECURITY_DESCRIPTOR SdNew= NULL;
    DWORD SdSize= 0;
    DWORD SdSizeNeeded= 0;
    ACL_SIZE_INFORMATION AclSizeInfo;
    DWORD AclSize= 0;
    BOOL DaclPresent;
    BOOL DaclDefaulted;

    //
    // Obtain DACL from the object.
    //
http://msdn.microsoft.com/en-us/library/aa379573
    //
    if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, 0, &SdSizeNeeded) ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER )
            goto cleanup;
           
        Sd= (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
              HEAP_ZERO_MEMORY, SdSizeNeeded);
        if ( Sd==NULL ) goto cleanup;

        SdSize= SdSizeNeeded;
        if ( !GetUserObjectSecurity(Object,
                &DaclInfo, Sd, SdSize, &SdSizeNeeded) )
            goto cleanup;
    }

    // Obtain the DACL from the security descriptor.
    if ( !GetSecurityDescriptorDacl(Sd, &DaclPresent, &Acl, &DaclDefaulted) )
        goto cleanup;

    // Initialize.
    ZeroMemory(&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
    AclSizeInfo.AclBytesInUse = sizeof(ACL);
    if ( Acl ) {
        if (!GetAclInformation(Acl, (LPVOID)&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation) )
            goto cleanup;
    }

    // Create a new ACL
    // (original ACL + new ACCESS_ALLOWED_ACEs)
    AclSize= AclSizeInfo.AclBytesInUse +
        AceCount * (sizeof(ACCESS_ALLOWED_ACE) +
        GetLengthSid(Sid) – sizeof(DWORD));
    AclNew= (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclSize);
    if ( AclNew==NULL ) goto cleanup;

    if ( !InitializeAcl(AclNew, AclSize, ACL_REVISION) )
        goto cleanup;

    // If DACL is present, copy all the ACEs to a new DACL.
    if ( DaclPresent && AclSizeInfo.AceCount ) {
        for ( DWORD i=0; i < AclSizeInfo.AceCount; ++i ) {
            PVOID Ace= NULL;
            if ( !GetAce(Acl, i, &Ace) ) goto cleanup;

            if (!AddAce(AclNew, ACL_REVISION, MAXDWORD,
                        Ace, ((PACE_HEADER)Ace)->AceSize) )
                goto cleanup;
        }
    }

    // Add new ACEs of specified SID to the DACL
    for ( DWORD i=0 ; i<AceCount ; ++i ) {
        if (!AddAccessAllowedAceEx(AclNew, ACL_REVISION,
                                   AceFlags[i], AccessMasks[i], Sid) )
            goto cleanup;
    }

    // Create a new security descriptor.
    // SECURITY_DESCRIPTOR_MIN_LENGTH is enough
    // because SetSecurityDescriptorDacl creates absolute security descriptor
    SdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
      HEAP_ZERO_MEMORY, SECURITY_DESCRIPTOR_MIN_LENGTH);
    if ( SdNew==NULL ) goto cleanup;

    if ( !InitializeSecurityDescriptor(SdNew, SECURITY_DESCRIPTOR_REVISION) )
        goto cleanup;

    // Set new DACL to the new security descriptor.
    // (this security descriptor becomes an absolute SD)
    if ( !SetSecurityDescriptorDacl(SdNew, TRUE, AclNew, FALSE) )
        goto cleanup;
   
#ifdef _TRACING
    wprintf(L"Original SD: %p\n", Sd);
    wprintf(L"New SD     : %p\n", SdNew);
    wprintf(L"–>\n");
    getwchar();
#endif

    // Set the new security descriptor for the desktop object.
    if (!SetUserObjectSecurity(Object, &DaclInfo, SdNew))
        goto cleanup;

    Ret= TRUE;

cleanup:
    if ( AclNew ) HeapFree(GetProcessHeap(), 0, AclNew);
    if ( Sd ) HeapFree(GetProcessHeap(), 0, Sd);
    if ( SdNew ) HeapFree(GetProcessHeap(), 0, SdNew);

    return Ret;
}

// add ACCESS_ALLOWED_ACEs of the specified SID to the object’s DACL
BOOL RemoveAccessAllowedAcesBasedSID(HANDLE Object, PSID Sid) {
    BOOL Ret= FALSE;
    SECURITY_INFORMATION DaclInfo= DACL_SECURITY_INFORMATION;
    PACL Acl= NULL; // no need to free
    PACL AclNew= NULL;
    PSECURITY_DESCRIPTOR Sd= NULL;
    PSECURITY_DESCRIPTOR SdNew= NULL;
    DWORD SdSize= 0;
    DWORD SdSizeNeeded= 0;
    ACL_SIZE_INFORMATION AclSizeInfo;
    DWORD AclSize= 0;
    BOOL DaclPresent;
    BOOL DaclDefaulted;

    //
    // Obtain DACL from the object.
    //
http://msdn.microsoft.com/en-us/library/aa379573
    //
    if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, 0, &SdSizeNeeded) ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER )
            goto cleanup;
           
        Sd= (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
               HEAP_ZERO_MEMORY, SdSizeNeeded);
        if ( Sd==NULL ) goto cleanup;

        SdSize= SdSizeNeeded;
        if ( !GetUserObjectSecurity(Object, &DaclInfo,
                                    Sd, SdSize, &SdSizeNeeded) )
            goto cleanup;
    }

    // Obtain the DACL from the security descriptor.
    if ( !GetSecurityDescriptorDacl(Sd, &DaclPresent, &Acl, &DaclDefaulted) )
        goto cleanup;

    if ( !DaclPresent || !Acl || Acl->AceCount==0 ) {
        // nothing to do for Null DACL or Empty DACL
        //
http://technet.microsoft.com/ja-jp/query/aa379286
        Ret= TRUE;
        goto cleanup;
    }

    // Initialize.
    ZeroMemory(&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
    if (!GetAclInformation(Acl, (LPVOID)&AclSizeInfo,
            sizeof(ACL_SIZE_INFORMATION), AclSizeInformation) )
        goto cleanup;

    // Create an ACL copy
    AclSize= AclSizeInfo.AclBytesInUse;
    AclNew= (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclSize);
    if ( AclNew==NULL ) goto cleanup;

    if ( !InitializeAcl(AclNew, AclSize, ACL_REVISION) )
        goto cleanup;
   
    // do not copy ACCESS_ALLOWED_ACEs of the specified SID
    if ( DaclPresent && AclSizeInfo.AceCount ) {
        for ( DWORD i=0; i < AclSizeInfo.AceCount; ++i ) {
            PVOID Ace= NULL;
            if ( !GetAce(Acl, i, &Ace) ) goto cleanup;
           
            if ( ((PACE_HEADER)Ace)->AceType==ACCESS_ALLOWED_ACE_TYPE &&
                    EqualSid(Sid, &((ACCESS_ALLOWED_ACE*)Ace)->SidStart) )
                continue;

            if (!AddAce(AclNew, ACL_REVISION, MAXDWORD,
                        Ace, ((PACE_HEADER)Ace)->AceSize) )
                goto cleanup;
        }
    }
   
    // Create a new security descriptor.
    // SECURITY_DESCRIPTOR_MIN_LENGTH is enough
    // because SetSecurityDescriptorDacl creates absolute security descriptor
    SdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
             HEAP_ZERO_MEMORY, SECURITY_DESCRIPTOR_MIN_LENGTH);
    if ( SdNew==NULL ) goto cleanup;

    if ( !InitializeSecurityDescriptor(SdNew, SECURITY_DESCRIPTOR_REVISION) )
        goto cleanup;

    // Set new DACL to the new security descriptor.
    // (this security descriptor becomes an absolute SD)
    if ( !SetSecurityDescriptorDacl(SdNew, TRUE, AclNew, FALSE) )
        goto cleanup;
   
#ifdef _TRACING
    wprintf(L"Original SD: %p\n", Sd);
    wprintf(L"New SD     : %p\n", SdNew);
    wprintf(L"–>\n");
    getwchar();
#endif

    // Set the new security descriptor for the desktop object.
    if (!SetUserObjectSecurity(Object, &DaclInfo, SdNew))
        goto cleanup;

    Ret= TRUE;

cleanup:
    if ( AclNew ) HeapFree(GetProcessHeap(), 0, AclNew);
    if ( Sd ) HeapFree(GetProcessHeap(), 0, Sd);
    if ( SdNew ) HeapFree(GetProcessHeap(), 0, SdNew);

    return Ret;
}

BOOL AddAceToDesktop(HDESK Desktop, PSID Sid) {
    CONST DWORD AccessMasks[1] = {
        DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | DESKTOP_CREATEMENU |
        DESKTOP_HOOKCONTROL | DESKTOP_JOURNALRECORD |
        DESKTOP_JOURNALPLAYBACK |
        DESKTOP_ENUMERATE | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP |
        STANDARD_RIGHTS_REQUIRED
    };

    DWORD AceFlags[1] = {0};

    return AddAccessAllowedAceBasedSID(Desktop, Sid,
                                       1, AceFlags, AccessMasks);
}

BOOL AddAceToWindowStation(HWINSTA Winsta, PSID Sid) {
    CONST DWORD AccessMasks[2] = {
        GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | GENERIC_ALL,
        WINSTA_ENUMDESKTOPS | WINSTA_READATTRIBUTES |
        WINSTA_ACCESSCLIPBOARD |
        WINSTA_CREATEDESKTOP | WINSTA_WRITEATTRIBUTES |
        WINSTA_ACCESSGLOBALATOMS |
        WINSTA_EXITWINDOWS | WINSTA_ENUMERATE | WINSTA_READSCREEN |
        STANDARD_RIGHTS_REQUIRED};

    CONST DWORD AceFlags[2] = {
        CONTAINER_INHERIT_ACE|INHERIT_ONLY_ACE|OBJECT_INHERIT_ACE,
        NO_PROPAGATE_INHERIT_ACE
    };   

    return AddAccessAllowedAceBasedSID(Winsta, Sid,
                                       2, AceFlags, AccessMasks);
}

BOOL GetLogonSidFromToken(HANDLE Token, PSID *outSid) {
    BOOL Ret= FALSE;
    DWORD TokenGroupLength= 0;
    DWORD SidLength= 0;
    PTOKEN_GROUPS TokenGroup= NULL;
    LPWSTR SidString= NULL;

    if ( !GetTokenInformation(Token, TokenGroups,
                             (LPVOID)TokenGroup, 0, &TokenGroupLength) &&
            GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
        wprintf(L"GetTokenInformation (1st chance) failed – 0x%08x\n",
                  GetLastError());
        goto Cleanup;
    }

    TokenGroup= (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(),
                    HEAP_ZERO_MEMORY, TokenGroupLength);
    if ( !TokenGroup ) {
        wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }

    if ( !GetTokenInformation(Token, TokenGroups, (LPVOID)TokenGroup,
                              TokenGroupLength, &TokenGroupLength) ) {
        wprintf(L"GetTokenInformation failed (2nd chance) – 0x%08x\n",
                    GetLastError());
        goto Cleanup;
    }

    //
    // SE_GROUP_LOGON_ID
    // The SID is a logon SID that identifies the logon session
    // associated with an access token.
    //
http://technet.microsoft.com/en-us/library/aa379624
    //
    for ( DWORD i=0 ; i<TokenGroup->GroupCount ; ++i ) {
        if ( SidString ) LocalFree(SidString);
        ConvertSidToStringSid(TokenGroup->Groups[i].Sid, &SidString);
        wprintf(L"SID: %s", SidString);

        if ( (TokenGroup->Groups[i].Attributes&SE_GROUP_LOGON_ID)==
               SE_GROUP_LOGON_ID ) {
            SidLength= GetLengthSid(TokenGroup->Groups[i].Sid);

            *outSid= (PSID)HeapAlloc(GetProcessHeap(),
                     HEAP_ZERO_MEMORY, SidLength);
            if ( *outSid==NULL ) {

                wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
                goto Cleanup;
            }

            if ( !CopySid(SidLength, *outSid, TokenGroup->Groups[i].Sid) ) {
                wprintf(L"CopySid failed – 0x%08x\n", GetLastError());
                HeapFree(GetProcessHeap(), 0, (LPVOID)*outSid);
                goto Cleanup;
            }

            wprintf(L" (Logon)\n");
            break;
        }
        else
            wprintf(L"\n");
    }
   
    Ret= TRUE;

Cleanup:
    if ( SidString ) LocalFree(SidString);
    if ( TokenGroup ) HeapFree(GetProcessHeap(), 0, TokenGroup);
    return Ret;
}

[Win32] [C++] CreateProcessAsUser – #1 特権編

以前、LogonUser と CreateProcessAsUser を使って、別ユーザーとしてプログラムを実行する方法について書きました。あれから 1 年近くたって、背景がそれなりに理解できるようになってきたので、新たに書き直します。

[Win32] [C++] LogonUser と CreateProcessAsUser
https://msmania.wordpress.com/2011/02/06/win32-c-logonuser-%e3%81%a8-createprocessasuser/

長くなりそうなので、幾つかの記事に分けます。

まずは昔の記事のおさらいから。

LogonUser API でトークンを取得して、それを CreateProcessAsUser に渡すと ERROR_PRIVILEGE_NOT_HELD (0n1314) エラーが発生します。これは簡単で、呼び出し側プロセスが SE_INCREASE_QUOTA_NAME と SE_ASSIGNPRIMARYTOKEN_NAME を持っていないから。Administrator といえど、既定で前者は持っていても後者の特権を持っていないので、secpol.msc などから 「プロセス レベル トークンの置き換え」 特権を割り当てることでクリア。ちなみにこのエラー、Windows Server 2008 R2 に SAP NetWeaver の試用版をインストールするときにも sapinst で発生します。回避方法は同じです。

ERROR_PRIVILEGE_NOT_HELD エラーはクリアできても、UI (GUI or CUI) を持つプロセスは依然として起動できず、STATUS_DLL_INIT_FAILED (=0xc0000142) エラーが発生してしまう。これを解消するためには、MSDN のサンプルを参照して、ウィンドウ ステーションとデスクトップ オブジェクトの DACL にアクセス許可 ACE を追加する必要がある。

ポイントは上記 2 つでした。

まず、前者の特権について。
ユーザーがログオンすると、セキュリティ トークンというデータが LSA (=Local Security Authority) によって作成され、ログオン セッションに紐付けられます。このセッションの中で作成されたプロセスには、基本的にはログオン セッションのトークンが継承されて保持されます。

現在のログオン セッションの特権一覧を取得する方法で、最も簡単なのは whoami /priv コマンドを使う方法です。通常のコマンド プロンプトと、管理者として実行したコマンド プロンプトとで実行結果を比較すると、持っている特権が異なります。これが UAC の機能のキモで、前者が 「制限付きトークン」 というやつです。

> whoami /priv

PRIVILEGES INFORMATION
———————-

特権名                        説明                                            状態
============================= =============================================== ====
SeShutdownPrivilege           システムのシャットダウン                        無効
SeChangeNotifyPrivilege       走査チェックのバイパス                          有効
SeUndockPrivilege             ドッキング ステーションからコンピューターを削除 無効
SeIncreaseWorkingSetPrivilege プロセス ワーキング セットの増加                無効
SeTimeZonePrivilege           タイム ゾーンの変更                             無効

デバッガーでは !token というコマンドがあります。例えば、実行中のメモ帳のトークンを出力した例です。

kd> !process 0 0 notepad.exe
PROCESS fffffa8001b51570
    SessionId: 1  Cid: 05f8    Peb: 7fffffde000  ParentCid: 0910
    DirBase: 15090000  ObjectTable: fffff8a00228a300  HandleCount:  93.
    Image: notepad.exe

kd> !process fffffa8001b51570
PROCESS fffffa8001b51570
    SessionId: 1  Cid: 05f8    Peb: 7fffffde000  ParentCid: 0910
    DirBase: 15090000  ObjectTable: fffff8a00228a300  HandleCount:  93.
    Image: notepad.exe
    VadRoot fffffa8001b18cb0 Vads 86 Clone 0 Private 385. Modified 0. Locked 0.
    DeviceMap fffff8a001842670
    Token                             fffff8a0021c6950
    ElapsedTime                       00:00:02.846
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
(略)

kd> !token fffff8a0021c6950
_TOKEN fffff8a0021c6950
TS Session ID: 0x1
User: S-1-5-21-2857284654-3416964824-2551679015-500
Groups:
00 S-1-5-21-2857284654-3416964824-2551679015-513
    Attributes – Mandatory Default Enabled
01 S-1-1-0
    Attributes – Mandatory Default Enabled
02 S-1-5-32-545
    Attributes – Mandatory Default Enabled
(略)
Primary Group: S-1-5-21-2857284654-3416964824-2551679015-513
Privs:
03 0x000000003 SeAssignPrimaryTokenPrivilege     Attributes –
05 0x000000005 SeIncreaseQuotaPrivilege          Attributes –
(略)
28 0x00000001c SeManageVolumePrivilege           Attributes –
29 0x00000001d SeImpersonatePrivilege            Attributes – Enabled Default
30 0x00000001e SeCreateGlobalPrivilege           Attributes – Enabled Default
33 0x000000021 SeIncreaseWorkingSetPrivilege     Attributes –
34 0x000000022 SeTimeZonePrivilege               Attributes –
35 0x000000023 SeCreateSymbolicLinkPrivilege     Attributes –
Authentication ID:         (0,388d63)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2000 ( Token in use )
Token ID: 396a44           ParentToken ID: 0
Modified ID:               (0, 38d8de)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7
kd>

トークンは nt!_TOKEN という構造体で、そこに特権のリストが設定されているだけの話です。しかし whoami や !token で分かりにくいのが、トークンに含まれている特権しかダンプしてくれない点です。リストに含まれるトークンには無効と有効というスイッチがありますが、これ自体は AdjustTokenPrivileges API でプロセス自身が簡単に動的変更できるものです。

AdjustTokenPrivileges function
http://msdn.microsoft.com/en-us/library/aa375202(v=vs.85).aspx

上の MSDN に書いてありますが、リストに含まれていない特権を有効にしようとしても、戻り値こそ TRUE になるものの、GetLastError は ERROR_NOT_ALL_ASSIGNED に設定されます。

CreateProcessAsUser を実行するには SE_INCREASE_QUOTA_NAME と SE_ASSIGNPRIMARYTOKEN_NAME とが必要になりますが、ここで重要なのはこれらの特権がリストに含まれているかどうかであって、無効か有効かどうかは関係ありません。無効になっていても、リストに含まれていれば CreateProcessAsUser は成功します。CreateProcessAsUser を実行する前に、わざわざ自分で AdjustTokenPrivileges  を実行する必要はないわけです。

トークンに特権を追加するには、ローカル セキュリティ ポリシー (secpol.msc) を使います。以下のグループ ポリシーでも可能で、secpol.msc と同じことです。このポリシーはコンピューターに割り当てることに注意して下さい。

Computer Configuration > Policies > Windows Settings >
  Security Settings > Local Policies > User Rights Assignment

トークンはログオン時に作られるので、ローカル セキュリティ ポリシーを変更しても現在のログオン セッションに反映されることはなく、ログオフして再ログオンする必要があります。グループ ポリシー経由で設定した場合は、コンピューターアカウントにグループ ポリシーを反映 (gpupdate /force /target:computer) させた後、再ログオンして下さい。

プログラム的に特権を追加するには、LsaAddAccountRights API を使います。GUI の操作と同じで、指定した特権に指定した SID を追加するという機能を持っています。この API を実行した後も再ログオンが必要です。

LsaAddAccountRights function
http://msdn.microsoft.com/en-us/library/ms721786(v=vs.85).aspx

特権は SE_ASSIGNPRIMARYTOKEN_NAME というような定数で扱いますが、実体は “SeAssignPrimaryTokenPrivilege” というような文字列です。また、システム内では LUID という 64bit 整数値も割り振られています。GUID ではないので、異なるシステムでは同じ値であることが保証されません。whoami コマンドでは LUID を出力できませんが、!token では出力されます。上の出力例では、SE_ASSIGNPRIMARYTOKEN_NAME は LUID=3 でした。

03 0x000000003 SeAssignPrimaryTokenPrivilege Attributes –

whoami や !token では、トークンが保持している特権のリストしか得られませんでした。せっかくなら、トークンが保持していない特権もまとめて表示できると便利です。そんなわけで、まずは以下のような関数を書いてみました。

全ての特権を取得する方法がすぐに分からなかったので、0 から MAX_PRIVSCAN (256) までの LUID を全て舐めるといういい加減な動きにしています。本当は、システムで定義されている全ての特権を取得する方法があるはずです。

//
// priv.cpp
//

#include <windows.h>
#include <NTSecAPI.h>
#include <stdio.h>
#include <strsafe.h>

#include "logue.h"

#define MAX_PRIVNAME 32
#define MAX_PRIVSCAN 256

#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth

struct PRIVILAGENAME_MAPPING {
    WCHAR SymbolName[MAX_PRIVNAME];
    WCHAR PrivilegeName[MAX_PRIVNAME];
};

const PRIVILAGENAME_MAPPING PrivilegeNameMapping[]= {
    { L"SE_CREATE_TOKEN_NAME", SE_CREATE_TOKEN_NAME },
    { L"SE_ASSIGNPRIMARYTOKEN_NAME", SE_ASSIGNPRIMARYTOKEN_NAME },
    { L"SE_LOCK_MEMORY_NAME", SE_LOCK_MEMORY_NAME },
    { L"SE_INCREASE_QUOTA_NAME", SE_INCREASE_QUOTA_NAME },
    { L"SE_UNSOLICITED_INPUT_NAME", SE_UNSOLICITED_INPUT_NAME }, // no LUID?
    { L"SE_MACHINE_ACCOUNT_NAME", SE_MACHINE_ACCOUNT_NAME },
    { L"SE_TCB_NAME", SE_TCB_NAME },
    { L"SE_SECURITY_NAME", SE_SECURITY_NAME },
    { L"SE_TAKE_OWNERSHIP_NAME", SE_TAKE_OWNERSHIP_NAME },
    { L"SE_LOAD_DRIVER_NAME", SE_LOAD_DRIVER_NAME },
    { L"SE_SYSTEM_PROFILE_NAME", SE_SYSTEM_PROFILE_NAME },
    { L"SE_SYSTEMTIME_NAME", SE_SYSTEMTIME_NAME },
    { L"SE_PROF_SINGLE_PROCESS_NAME", SE_PROF_SINGLE_PROCESS_NAME },
    { L"SE_INC_BASE_PRIORITY_NAME", SE_INC_BASE_PRIORITY_NAME },
    { L"SE_CREATE_PAGEFILE_NAME", SE_CREATE_PAGEFILE_NAME },
    { L"SE_CREATE_PERMANENT_NAME", SE_CREATE_PERMANENT_NAME },
    { L"SE_BACKUP_NAME", SE_BACKUP_NAME },
    { L"SE_RESTORE_NAME", SE_RESTORE_NAME },
    { L"SE_SHUTDOWN_NAME", SE_SHUTDOWN_NAME },
    { L"SE_DEBUG_NAME", SE_DEBUG_NAME },
    { L"SE_AUDIT_NAME", SE_AUDIT_NAME },
    { L"SE_SYSTEM_ENVIRONMENT_NAME", SE_SYSTEM_ENVIRONMENT_NAME },
    { L"SE_CHANGE_NOTIFY_NAME", SE_CHANGE_NOTIFY_NAME },
    { L"SE_REMOTE_SHUTDOWN_NAME", SE_REMOTE_SHUTDOWN_NAME },
    { L"SE_UNDOCK_NAME", SE_UNDOCK_NAME },
    { L"SE_SYNC_AGENT_NAME", SE_SYNC_AGENT_NAME },
    { L"SE_ENABLE_DELEGATION_NAME", SE_ENABLE_DELEGATION_NAME },
    { L"SE_MANAGE_VOLUME_NAME", SE_MANAGE_VOLUME_NAME },
    { L"SE_IMPERSONATE_NAME", SE_IMPERSONATE_NAME },
    { L"SE_CREATE_GLOBAL_NAME", SE_CREATE_GLOBAL_NAME },
    { L"SE_TRUSTED_CREDMAN_ACCESS_NAME", SE_TRUSTED_CREDMAN_ACCESS_NAME },
    { L"SE_RELABEL_NAME", SE_RELABEL_NAME },
    { L"SE_INC_WORKING_SET_NAME", SE_INC_WORKING_SET_NAME },
    { L"SE_TIME_ZONE_NAME", SE_TIME_ZONE_NAME },
    { L"SE_CREATE_SYMBOLIC_LINK_NAME", SE_CREATE_SYMBOLIC_LINK_NAME },
    { L"", L"" }
};

BOOL LookupPrivilegeName(LPCWSTR SystemName, CONST PLUID Luid,
                         LPCWSTR *SymbolName,
                         LPWSTR PrivilegeName, LPDWORD PrivilegeNameLength,
                         LPWSTR DisplayName, LPDWORD DisplayNameLength,
                         BOOL NoErrMsg) {
    BOOL Ret= FALSE;
    DWORD LanguageId;
    int Index= -1;

    Ret= LookupPrivilegeName(NULL, Luid, PrivilegeName, PrivilegeNameLength);
    if ( !Ret ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER && !NoErrMsg )
            wprintf(L"LookupPrivilegeName failed – 0x%08x\n",
                    GetLastError());
        goto cleanup;
    }

    Ret= LookupPrivilegeDisplayName(NULL,}
      PrivilegeName, DisplayName, DisplayNameLength, &LanguageId);
    if ( !Ret ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER && !NoErrMsg )
            wprintf(L"LookupPrivilegeDisplayName failed – 0x%08x\n",
              GetLastError());
        goto cleanup;
    }

    Ret= FALSE;
    const PRIVILAGENAME_MAPPING *p=PrivilegeNameMapping;
    for ( Index=0 ; p->SymbolName[0]!=0 ; ++p, ++Index ) {
        if ( wcscmp(PrivilegeName, p->PrivilegeName)==0 ) {
            Ret= TRUE;
            break;
        }
    }

    if ( Ret )
        *SymbolName= PrivilegeNameMapping[Index].SymbolName;
    else if ( NoErrMsg )
        wprintf(L"%s not found\n", PrivilegeName);

cleanup:
    return Ret;
}

BOOL LookupPrivilegeValueEx(LPCWSTR SystemName, LPCWSTR Name, PLUID Luid) {
    BOOL Ret= LookupPrivilegeValue(SystemName, Name, Luid);
    if ( !Ret && GetLastError()==ERROR_NO_SUCH_PRIVILEGE ) {
        const PRIVILAGENAME_MAPPING *p;
        for ( p=PrivilegeNameMapping ; p->SymbolName[0]!=0 ; ++p ) {
            if ( wcscmp(Name, p->SymbolName)==0 )
                return LookupPrivilegeValue(
                  SystemName, p->PrivilegeName, Luid);
        }
        SetLastError(ERROR_NO_SUCH_PRIVILEGE);
        Ret= FALSE;
    }
    return Ret;
}

VOID EnumPrivileges(HANDLE Token, BOOL All) {
    BOOL Ret= FALSE;
    DWORD TokenLength= 0;
    PTOKEN_PRIVILEGES TokenPriv= NULL;
    DWORD PrivilegeNameLength= 256;
    DWORD DisplayNameLength= 256;
    PWCHAR PrivilegeName= NULL;
    PWCHAR DisplayName= NULL;
    LPCWCHAR SymbolName= NULL;
   
    // LUID = Locally Unique Identifier
    wprintf(L"——————————————————————————————————-\n");
    wprintf(L"   LUID                Symbol                           PrivilegeName                    DisplayName\n");
    wprintf(L"——————————————————————————————————-\n");

    if ( !All ) {
        if ( !GetTokenInformation(Token, TokenPrivileges, NULL, 0, &TokenLength) &&
                GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
            wprintf(L"GetTokenInformation (size check) failed – 0x%08x\n",
                    GetLastError());
            goto cleanup;
        }

        TokenPriv= (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), 0, TokenLength);
        if ( !TokenPriv ) {
            wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }

        if ( !GetTokenInformation(Token,
                TokenPrivileges, TokenPriv, TokenLength, &TokenLength) ) {
            wprintf(L"GetTokenInformation failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }

    }
    else {
        TokenPriv= (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), 0,
            sizeof(DWORD)+sizeof(LUID_AND_ATTRIBUTES)*MAX_PRIVSCAN);
        if ( !TokenPriv ) {
            wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }
       
        TokenPriv->PrivilegeCount= MAX_PRIVSCAN;
        for (  LONGLONG i=0 ; i<MAX_PRIVSCAN ; ++i ) {
            TokenPriv->Privileges[i].Luid= *(PLUID)&i;
            TokenPriv->Privileges[i].Attributes= 0;
        }
    }
   
    for ( DWORD i=0 ; i<TokenPriv->PrivilegeCount ; ++i ) {
        do {
            if ( PrivilegeName ) delete [] PrivilegeName;
            if ( DisplayName ) delete [] DisplayName;

            PrivilegeName= new WCHAR[PrivilegeNameLength];
            DisplayName= new WCHAR[DisplayNameLength];

            Ret= LookupPrivilegeName(NULL, &TokenPriv->Privileges[i].Luid, &SymbolName,
                    PrivilegeName, &PrivilegeNameLength,
                    DisplayName, &DisplayNameLength,
                    All);
        } while( !Ret && GetLastError()==ERROR_INSUFFICIENT_BUFFER );

        if ( Ret ) {
            WCHAR Mark= 0;
            if ( All ) {
                LONG l= 0;
                CheckPrivilege(Token, PrivilegeName, &l);
                Mark= l==0 ? Mark= ‘X’ :
                    l>0 ? Mark= ‘O’ : ‘-‘;
            }
            else {
                Mark= TokenPriv->Privileges[i].Attributes&SE_PRIVILEGE_ENABLED ?
                       L’O’ : L’X’;
            }

            wprintf(L" %c 0x%08x`%08x %-32s %-32s %s\n", Mark,
                TokenPriv->Privileges[i].Luid.HighPart,
                TokenPriv->Privileges[i].Luid.LowPart,
                SymbolName,
                PrivilegeName,
                DisplayName);
        }
    }

cleanup:
    if ( PrivilegeName ) delete [] PrivilegeName;
    if ( DisplayName ) delete [] DisplayName;
    if ( TokenPriv ) HeapFree(GetProcessHeap(), 0, TokenPriv);
}

// http://msdn.microsoft.com/en-us/library/ms722492(v=VS.85) InitLsaString
//
http://msdn.microsoft.com/en-us/library/ms721874(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/ms721863(v=vs.85).aspx
BOOL AddPrivilege(HANDLE Token, LPCWSTR PrivilegeName) {
    NTSTATUS Ret= 0;
    LSA_OBJECT_ATTRIBUTES ObjectAttributes;
    LSA_HANDLE PolicyHandle= NULL;
    PSID Sid= NULL;
    LSA_UNICODE_STRING Privilege[1];
    size_t PrivNameLength= 0;
    PTOKEN_USER CurrentUserSid= NULL;
    DWORD CurrentUserSidLength= 0;

    // get current user SID from the token
    if ( !GetTokenInformation(Token, TokenUser, NULL, 0, &CurrentUserSidLength) &&
            GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
        wprintf(L"GetTokenInformation (size check) failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    CurrentUserSid= (PTOKEN_USER)HeapAlloc(GetProcessHeap(), 0, CurrentUserSidLength);
    if ( !CurrentUserSid ) {
        wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    if ( !GetTokenInformation(Token, TokenUser, CurrentUserSid,
            CurrentUserSidLength, &CurrentUserSidLength) ) {
        wprintf(L"GetTokenInformation failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }
   
    PrivNameLength= StringCchLength(PrivilegeName, MAX_PRIVNAME, &PrivNameLength);
    Privilege[0].Buffer= (PWCHAR)PrivilegeName;
    Privilege[0].Length= PrivNameLength*sizeof(WCHAR);
    Privilege[0].MaximumLength= (PrivNameLength+1)*sizeof(WCHAR);
   
    ZeroMemory(&ObjectAttributes, sizeof(ObjectAttributes));
    Ret= LsaOpenPolicy(NULL, &ObjectAttributes, POLICY_ALL_ACCESS, &PolicyHandle);
    if ( Ret!=STATUS_SUCCESS ) {
        wprintf(L"LsaOpenPolicy failed – 0x%08x\n", LsaNtStatusToWinError(Ret));
        goto cleanup;
    }

    StringCchLength(PrivilegeName, MAX_PRIVNAME, &PrivNameLength);
    Privilege[0].Buffer= (PWCHAR)PrivilegeName;
    Privilege[0].Length= PrivNameLength*sizeof(WCHAR);
    Privilege[0].MaximumLength= (PrivNameLength+1)*sizeof(WCHAR);

    Ret= LsaAddAccountRights(PolicyHandle, CurrentUserSid->User.Sid, Privilege, 1);;
    if ( Ret!=STATUS_SUCCESS ) {
        wprintf(L"LsaAddAccountRights failed – 0x%08x\n", LsaNtStatusToWinError(Ret));
        goto cleanup;
    }

    wprintf(L"Privilege ‘%s’ was assigned successfully.\n", PrivilegeName);
    wprintf(L"To apply it to the token, re-log on the system.\n");

cleanup:
    if ( PolicyHandle ) LsaClose(PolicyHandle);   
    if ( CurrentUserSid ) HeapFree(GetProcessHeap(), 0, CurrentUserSid);

    return Ret==STATUS_SUCCESS;
}

// >0 Enabled
// =0 Disabled
// <0 Not assigned
BOOL CheckPrivilege(HANDLE Token, LPCWSTR PrivilegeName, LPLONG Privileged) {
    LUID luid;
    if ( !LookupPrivilegeValueEx(NULL, PrivilegeName, &luid) ){
        wprintf(L"LookupPrivilegeValue failed – 0x%08x\n", GetLastError());
        return FALSE;
    }

    PRIVILEGE_SET PrivilegeSet;
    PrivilegeSet.Control= 0;
    PrivilegeSet.PrivilegeCount= 1;
    PrivilegeSet.Privilege[0].Luid= luid;
    PrivilegeSet.Privilege[0].Attributes= 0; // not used

    BOOL Check;
    if ( !PrivilegeCheck(Token, &PrivilegeSet, &Check) ) {
        wprintf(L"PrivilegeCheck failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    if ( Check )
        *Privileged= 1;
    else {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount= 1;
        tp.Privileges[0].Luid= luid;
        tp.Privileges[0].Attributes= 0;

        if ( !AdjustTokenPrivileges(Token,
                FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) {
            wprintf(L"AdjustTokenPrivileges failed – 0x%08x\n", GetLastError());
            return FALSE;
        }

        *Privileged= (GetLastError()==ERROR_NOT_ALL_ASSIGNED) ? -1 : 0;
    }

    return TRUE;
}
 
BOOL EnablePrivilege(HANDLE Token, LPWSTR Name, BOOL Enabled) {
    LUID luid;
    if ( !LookupPrivilegeValueEx(NULL, Name, &luid) ) {
        wprintf(L"LookupPrivilegeValue failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    TOKEN_PRIVILEGES tp;
    tp.PrivilegeCount= 1;
    tp.Privileges[0].Luid= luid;
    tp.Privileges[0].Attributes=
      Enabled ? SE_PRIVILEGE_ENABLED : 0; // not use SE_PRIVILEGE_REMOVED, just disable

    if ( !AdjustTokenPrivileges(Token,
            FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) {
        wprintf(L"AdjustTokenPrivileges failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    if ( GetLastError()==ERROR_NOT_ALL_ASSIGNED ) {
        wprintf(L"The process token does not have %s (%I64d).\n", Name, luid);
        return FALSE;
    }

    wprintf(L"%s (%I64d) is temporarily %s.\n", Name, luid,
        Enabled ? L"enabled" : L"disabled");

    return TRUE;
}

ここで定義した関数 EnumPrivileges を使って、特権の一覧を出力させると、こんな感じです。
(出力が横に広い・・・)

——————————————————————————————————-
   LUID                Symbol                           PrivilegeName                    DisplayName
——————————————————————————————————-
– 0x00000000`00000002 SE_CREATE_TOKEN_NAME             SeCreateTokenPrivilege           トークン オブジェクトの作成
X 0x00000000`00000003 SE_ASSIGNPRIMARYTOKEN_NAME       SeAssignPrimaryTokenPrivilege    プロセス レベル トークンの置き換え
– 0x00000000`00000004 SE_LOCK_MEMORY_NAME              SeLockMemoryPrivilege            メモリ内のページのロック
X 0x00000000`00000005 SE_INCREASE_QUOTA_NAME           SeIncreaseQuotaPrivilege         プロセスのメモリ クォータの増加
– 0x00000000`00000006 SE_MACHINE_ACCOUNT_NAME          SeMachineAccountPrivilege        ドメインにワークステーションを追加
– 0x00000000`00000007 SE_TCB_NAME                      SeTcbPrivilege                   オペレーティング システムの一部として機能
X 0x00000000`00000008 SE_SECURITY_NAME                 SeSecurityPrivilege              監査とセキュリティ ログの管理
X 0x00000000`00000009 SE_TAKE_OWNERSHIP_NAME           SeTakeOwnershipPrivilege         ファイルとその他のオブジェクトの所有権の取得
X 0x00000000`0000000a SE_LOAD_DRIVER_NAME              SeLoadDriverPrivilege            デバイス ドライバーのロードとアンロード
X 0x00000000`0000000b SE_SYSTEM_PROFILE_NAME           SeSystemProfilePrivilege         システム パフォーマンスのプロファイル
X 0x00000000`0000000c SE_SYSTEMTIME_NAME               SeSystemtimePrivilege            システム時刻の変更
X 0x00000000`0000000d SE_PROF_SINGLE_PROCESS_NAME      SeProfileSingleProcessPrivilege  単一プロセスのプロファイル
X 0x00000000`0000000e SE_INC_BASE_PRIORITY_NAME        SeIncreaseBasePriorityPrivilege  スケジューリング優先順位の繰り上げ
X 0x00000000`0000000f SE_CREATE_PAGEFILE_NAME          SeCreatePagefilePrivilege        ページ ファイルの作成
– 0x00000000`00000010 SE_CREATE_PERMANENT_NAME         SeCreatePermanentPrivilege       永続的共有オブジェクトの作成
X 0x00000000`00000011 SE_BACKUP_NAME                   SeBackupPrivilege                ファイルとディレクトリのバックアップ
X 0x00000000`00000012 SE_RESTORE_NAME                  SeRestorePrivilege               ファイルとディレクトリの復元
X 0x00000000`00000013 SE_SHUTDOWN_NAME                 SeShutdownPrivilege              システムのシャットダウン
X 0x00000000`00000014 SE_DEBUG_NAME                    SeDebugPrivilege                 プログラムのデバッグ
– 0x00000000`00000015 SE_AUDIT_NAME                    SeAuditPrivilege                 セキュリティ監査の生成
X 0x00000000`00000016 SE_SYSTEM_ENVIRONMENT_NAME       SeSystemEnvironmentPrivilege     ファームウェア環境値の修正
O 0x00000000`00000017 SE_CHANGE_NOTIFY_NAME            SeChangeNotifyPrivilege          走査チェックのバイパス
X 0x00000000`00000018 SE_REMOTE_SHUTDOWN_NAME          SeRemoteShutdownPrivilege        リモート コンピューターからの強制シャットダウン
X 0x00000000`00000019 SE_UNDOCK_NAME                   SeUndockPrivilege                ドッキング ステーションからコンピューターを削除
– 0x00000000`0000001a SE_SYNC_AGENT_NAME               SeSyncAgentPrivilege             ディレクトリ サービス データの同期化
– 0x00000000`0000001b SE_ENABLE_DELEGATION_NAME        SeEnableDelegationPrivilege      コンピューターとユーザー アカウントに委任時の信頼を付与
X 0x00000000`0000001c SE_MANAGE_VOLUME_NAME            SeManageVolumePrivilege          ボリュームの保守タスクを実行
O 0x00000000`0000001d SE_IMPERSONATE_NAME              SeImpersonatePrivilege           認証後にクライアントを偽装
O 0x00000000`0000001e SE_CREATE_GLOBAL_NAME            SeCreateGlobalPrivilege          グローバル オブジェクトの作成
– 0x00000000`0000001f SE_TRUSTED_CREDMAN_ACCESS_NAME   SeTrustedCredManAccessPrivilege  資格情報マネージャーに信頼された呼び出し側としてアクセス
– 0x00000000`00000020 SE_RELABEL_NAME                  SeRelabelPrivilege               オブジェクト ラベルの変更
X 0x00000000`00000021 SE_INC_WORKING_SET_NAME          SeIncreaseWorkingSetPrivilege    プロセス ワーキング セットの増加
X 0x00000000`00000022 SE_TIME_ZONE_NAME                SeTimeZonePrivilege              タイム ゾーンの変更
X 0x00000000`00000023 SE_CREATE_SYMBOLIC_LINK_NAME     SeCreateSymbolicLinkPrivilege    シンボリック リンクの作成

[Win32] [C++] LogonUser と CreateProcessAsUser

2011/12/31~2012/1/1 にかけて、続きの記事を書きました。
—–
[Win32] [C++] CreateProcessAsUser – #4 セキュリティ記述子
[Win32] [C++] CreateProcessAsUser – #3 ソース
[Win32] [C++] CreateProcessAsUser – #2 トークン編
[Win32] [C++] CreateProcessAsUser – #1 特権編
—–

コマンド プロンプトから runas すると、他のユーザー アカウントでプロセスを起動することができます。ただしパスワードは手入力しないといけないので、これを自動化するための苦肉の策がいろいろと考案されています。VB スクリプトを使って SendKey する方法など。試してみましたが、確かに動きます。

SendKey ではあまりにも芸がないということで、プログラム的にやってみます。というか数千のプロセスを作る必要があったので、プログラムでやらないとしょうがない。

API としては LogonUser で取得したトークンを CreateProcessAsUser に渡すだけのようです。簡単ですね。が、それだと動かない。余裕で 1314 エラーです。

エラー 1314
「クライアントは要求された特権を保有していません。 」

なんですと、管理者ですぞ!? さて、真面目に MSDN を読まなければいけません。と、すぐ書いてあるし。

CreateProcessAsUser
http://msdn.microsoft.com/en-us/library/ms682429(VS.85).aspx

Typically, the process that calls the CreateProcessAsUser function must have the SE_INCREASE_QUOTA_NAME privilege and may require the SE_ASSIGNPRIMARYTOKEN_NAME privilege if the token is not assignable. If this function fails with ERROR_PRIVILEGE_NOT_HELD (1314), use theCreateProcessWithLogonW function instead. CreateProcessWithLogonW requires no special privileges

そこで SE_INCREASE_QUOTA_NAME と SE_ASSIGNPRIMARYTOKEN_NAME 特権を自分のユーザーに割り当てます。ローカル セキュリティ ポリシーの設定から、それぞれ以下の特権を割り当てます。

セキュリティの設定 > ローカル ポリシー > ユーザー権利の割り当て
プロセスのメモリ クォータの増加: SE_INCREASE_QUOTA_NAME 
プロセス レベル トークンの置き換え: SE_ASSIGNPRIMARYTOKEN_NAME

ちなみにクライアントは Windows 7 x86 ですが、クォータの方はすでに Administrators が入っているのに、トークンの置き換えの方にはLOCAL SERVICE と NETWORK SERVICE の 2 つしか入っていなかった。管理者なんて大したことないですね。やはりサービス起動ユーザーは強い。

ポリシーを反映させるために、一度ログオフしてから、再度ログオンして実行。すると CreateProcessAsUser は成功。別ユーザーで実行したかったプログラムは、ユーザー入力を必要としないプログラムだったので、要件としてはとりあえずこれで OK。

さて、ここでユーザー入力を必要とするプログラムを起動したい場合はかなり面倒です。もう一度 MSDN に戻り、該当部分を見てみるとこんな感じ。

By default, CreateProcessAsUser creates the new process on a noninteractive window station with a desktop that is not visible and cannot receive user input. To enable user interaction with the new process, you must specify the name of the default interactive window station and desktop, "winsta0\default", in the lpDesktop member of the STARTUPINFO structure. In addition, before calling CreateProcessAsUser, you must change the discretionary access control list (DACL) of both the default interactive window station and the default desktop. The DACLs for the window station and desktop must grant access to the user or the logon session represented by the hToken parameter.

  1. STARTUPINFO::lpDesktop
  2. ウィンドウ ステーションの DACL に実行ユーザーを追加
  3. デスクトップの DACL に実行ユーザーを追加

簡単そうじゃん、と思ってしまったが、これが超めんどくさい。ちなみにこれをやらずに CreateProcessAsUser を実行すると、次のようなエラーが出てダメです。

「アプリケーションを正しく起動できませんでした  (0xc0000142)。」
WS0002

ウィンドウステーションを持たないプログラムがウィンドウを表示しようとして落ちたのでしょうかね。これは今度デバッグしてみても面白いかも。

それはさておき、DACL への追加をしなければならないわけですが、これが実に実にめんどくさいです。サンプルは MSDN にありますが。

Starting an Interactive Client Process in C++
http://msdn.microsoft.com/en-us/library/aa379608(v=VS.85).aspx

Getting the Logon SID in C++
http://msdn.microsoft.com/en-us/library/aa446670(v=VS.85).aspx

(2014/12/29 修正) 2011 年末に作成したテスト プログラムのソースを GitHub 上で公開しています。このプログラムに関する記事については、本記事の冒頭にあるリンクをご参照ください。m(_ _)m

https://github.com/msmania/logue

ウィンドウ ステーションとデスクトップで、DACL を変更するときの処理が微妙に違う。サンプルでは関数を分けていたので、そのままだけど、一緒にしたほうがプログラムは短くなるかも。もともとオブジェクトが持っていた DACL をコピーするところまでは全く同じで、そのあと、トークンから取得した SID を pNewACL にコピーするときに、ウィンドウ ステーションでは 2 回に分けて AddAce する必要があるのです。だから、dwNewAclSize のサイズも微妙に異なる。このサイズについては、以下の KB に説明あり。

INFO: Computing the Size of a New ACL
http://support.microsoft.com/kb/102103/en