ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

【お知らせ】2021年5月10日~リニューアルオープン!今後はこちらで新しい記事を公開します。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:ペネトレーションテスト

「書き込み」チェックの謎


※こちらの記事は2021年3月29日公開note版「ラック・セキュリティごった煮ブログ」と同じ内容です

デジタルペンテストサービス部のkjです。
突然ですが、次の画像は「C:\ProgramData」フォルダーのプロパティです。画像を見て、あたなはこのフォルダーにファイルを作成できるか、できないか、わかりますか?(あなたはUsersグループのメンバーでかつAdministratorsグループには属さないとする)

私は書き込みできないと思いました。が、実際は書き込めました。
よくわからん。ということで、フォルダーのアクセス権について調べてみました。(以降、私の理解において記述しているため、間違いが含まれている可能性があります。ご注意ください)

ファイルもフォルダーもオブジェクト

Windowsの中では、ファイルやフォルダーは「オブジェクト」として表現されます。そして、大体のオブジェクトは(特に名前を持つオブジェクトはすべて)「セキュアブルオブジェクト」です。具体的には「セキュリティディスクリプタ」を持つオブジェクトのことをセキュアブルオブジェクトといいます。

セキュリティディスクリプタ構造体
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-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;

セキュアブルオブジェクトに対する操作にはアクセス制御が伴います。その動作を規定しているのが、セキュリティディスクリプタ構造体の「Dacl」メンバーです。

DACLとACE

「DACL」(Discretionary Access Control List)、日本語では「随意アクセス制御リスト」と呼ばれます。このリストの中に、アクセスの許可・拒否を決定する要素「ACE」(Access Control Entry)が並んでいます。このACEが肝で、アクセスの許可は「許可ACE」、拒否は「拒否ACE」に設定されます。そして、それぞれのエントリの中に、誰に対して何を(どのような操作を)許可・拒否するのかが設定されます。

実際のところ、ACEはその用途に応じて複数の種類が定義されているようです。winnt.hをgrepしたところ、以下のdefineが見つかりました。

ACCESS_ALLOWED(許可ACE)系

#define ACCESS_ALLOWED_ACE_TYPE                 (0x0)
#define ACCESS_ALLOWED_COMPOUND_ACE_TYPE        (0x4)
#define ACCESS_ALLOWED_OBJECT_ACE_TYPE          (0x5)
#define ACCESS_ALLOWED_CALLBACK_ACE_TYPE        (0x9)                                  
#define ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE (0xB)

ACCESS_DENIED(拒否ACE)系

#define ACCESS_DENIED_ACE_TYPE                  (0x1)
#define ACCESS_DENIED_OBJECT_ACE_TYPE           (0x6)
#define ACCESS_DENIED_CALLBACK_ACE_TYPE         (0xA)
#define ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE  (0xC)

各タイプごとに構造体も定義されており、例えばACCESS_ALLOWED_ACE_TYPEに該当するのは次の構造体です。

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

同じように、ACCESS_ALLOWED_OBJECT_ACE_TYPEに対応するのは次の構造体です。

typedef struct _ACCESS_ALLOWED_OBJECT_ACE {
   ACE_HEADER Header;
   ACCESS_MASK Mask;
   DWORD Flags;
   GUID ObjectType;
   GUID InheritedObjectType;
   DWORD SidStart;
} ACCESS_ALLOWED_OBJECT_ACE, *PACCESS_ALLOWED_OBJECT_ACE;

どちらの構造体も先頭2つのメンバーは同じです。

1つ目のHeaderメンバーは「ACE_HEADER」型で、定義は次のとおりです。

typedef struct _ACE_HEADER {
   BYTE  AceType;
   BYTE  AceFlags;
   WORD   AceSize;
} ACE_HEADER;
typedef ACE_HEADER *PACE_HEADER;

AceTypeメンバーにACEの種類が設定され、そのサイズがAceSizeメンバーに設定されるようです。

2つ目のMaskメンバーは「ACCESS_MASK」型で、定義は次のとおりです。

//  Define the access mask as a longword sized structure divided up as                                    
//  follows:                                                                                              
//                                                                                                        
//       3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1                                                      
//       1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0                                  
//      +---------------+---------------+-------------------------------+                                 
//      |G|G|G|G|Res'd|A| StandardRights|         SpecificRights        |                                 
//      |R|W|E|A|     |S|               |                               |                                 
//      +-+-------------+---------------+-------------------------------+                                 
//                                                                                                        
//      typedef struct _ACCESS_MASK {                                                                     
//          WORD   SpecificRights;                                                                        
//          BYTE  StandardRights;                                                                         
//          BYTE  AccessSystemAcl : 1;                                                                    
//          BYTE  Reserved : 3;                                                                           
//          BYTE  GenericAll : 1;                                                                         
//          BYTE  GenericExecute : 1;                                                                     
//          BYTE  GenericWrite : 1;                                                                       
//          BYTE  GenericRead : 1;                                                                        
//      } ACCESS_MASK;                                                                                    
//      typedef ACCESS_MASK *PACCESS_MASK;                                                                
//                                                                                                        
//  but to make life simple for programmer's we'll allow them to specify                                  
//  a desired access mask by simply OR'ing together mulitple single rights                                
//  and treat an access mask as a DWORD.  For example                                                     
//                                                                                                        
//      DesiredAccess = DELETE | READ_CONTROL                                                             
//                                                                                                        
//  So we'll declare ACCESS_MASK as DWORD 
typedef DWORD ACCESS_MASK;

具体的な構造の説明が書かれてますが、「生きやすいようにDWORDにしたよ」とのこと。助かる。

また、位置は違いますが、どちらの構造体にもSidStartメンバー(SID)が定義されています。

大雑把にまとめると、
● 「許可・拒否」を示すのはAceType
● 「誰」を示すのはSidStart
● 「何を」を示すのはMask
ということがわかりました。

Windowsは、ユーザーがファイルやフォルダーにアクセスする際、DACLに設定されているACEを一つ一つチェックし、操作の許可・拒否を決定します。

初めの画像では「書き込み」にチェックが入っていませんでした。このチェックの有無は、おそらくMaskの値が反映されてるはず、ということで、ACCESS_MASK型を調べてみます。

ACCESS_MASK

「rwxだろ」とか思ってたらそんな単純な話ではありませんでした。

ACCESS_MASKフォーマット
https://docs.microsoft.com/en-us/windows/win32/secauthz/access-mask-format
ACCESS_MASKのフォーマットを説明している上記ページでは、以下の3種類のアクセス権が出てきています。

● Generic access rights
https://docs.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights
● Standard access rights
https://docs.microsoft.com/en-us/windows/win32/secauthz/standard-access-rights
● Object-specific access rights
https://docs.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants

ACCESS_MASKの下位16ビットが「Object-specific access rights」、次の8ビットが「Standard access rights」、上位4ビットが「Generic access rights」だそうです。どうやら3つのアクセス権はそれぞれ独立しているわけではなく、Genericアクセス権はStandardアクセス権とObject-specificアクセス権から成り立っているとのこと。ですが、単純にビットの論理和(GENERIC_XXX = STANDARD_XXX | OBJECT_SPECIFIC_XXX)というわけではないようです。話違うじゃん。

これは、Windowsがファイルやフォルダーだけでなく様々なものをオブジェクトとして表現しているため、各オブジェクトにあわせた「具体的な意味」に変換する(マッピングする)必要があるためです。
例えば、ファイルを読み込む際、CreateFile関数に「GENERIC_READ」を渡しますが、GENERIC_READは

#define FILE_GENERIC_READ         (STANDARD_RIGHTS_READ     |\
                                   FILE_READ_DATA           |\
                                   FILE_READ_ATTRIBUTES     |\
                                   FILE_READ_EA             |\
                                   SYNCHRONIZE)
// 内訳
// STANDARD_RIGHTS_READ -> Standard access rights
// FILE_READ_DATA       -> (File) Object specific access rights
// FILE_READ_ATTRIBUTES -> (File) Object specific access rights
// FILE_READ_EA         -> (File) Object specific access rights
// SYNCHRONIZE          -> Standard access rightsに

に変換されます。

本題 C:\ProgramDataのDACLはどうなっているのか

要素技術をざっと調べたので、本題に入ります。まず、適当なスクリプトを書き、C:\ProgramDataのDACLに設定されるACEが、どのようなアクセスマスクを持っているかを調べます。

import sys


import win32security as win32sec
import ntsecuritycon as ntsc

ACE_TYPES = (
   "ACCESS_ALLOWED_ACE_TYPE",
   "ACCESS_ALLOWED_CALLBACK_ACE_TYPE",
   "ACCESS_ALLOWED_CALLBACK_OBJECT_ACE_TYPE",
   "ACCESS_ALLOWED_OBJECT_ACE_TYPE",
   "ACCESS_DENIED_ACE_TYPE",
   "ACCESS_DENIED_CALLBACK_ACE_TYPE",
   "ACCESS_DENIED_CALLBACK_OBJECT_ACE_TYPE",
   "ACCESS_DENIED_OBJECT_ACE_TYPE",
   )
ACE_TYPE_NAME_MAPPING = {getattr(ntsc, i):i for i in ACE_TYPES}


sd = win32sec.GetNamedSecurityInfo(
   sys.argv[1],
   win32sec.SE_FILE_OBJECT,
   win32sec.DACL_SECURITY_INFORMATION)

view = []
dacl = sd.GetSecurityDescriptorDacl()
for i in range(dacl.GetAceCount()):
   ace_header, mask, sid = dacl.GetAce(i)
   ace_type, ace_flags = ace_header
   mask = int.from_bytes(mask.to_bytes(4, "little", signed=True), "little", signed=False)
   name, domain, _ = win32sec.LookupAccountSid(None, sid)
   
   if domain:
       principal = f"{domain}\\{name}"
   else:
       principal = name
   
   view.append((ACE_TYPES[ace_type], principal, mask))
   
type_padlen = max([len(a[0]) for a in view])
principal_padlen = max([len(a[1]) for a in view])
for v in view:
   print(f"{v[0].ljust(type_padlen)} | {v[1].ljust(principal_padlen)} | {v[2]:032b}")


Usersグループに対して、2つの許可ACEが存在します。1つ目はフォルダーに対する読み込み、2つ目はフォルダーに対する書き込み許可を示しているようです。

書き込み許可ACEは設定されており、実際、フォルダー内に書き込み出来ることはわかってます。なぜプロパティ画面の「書き込み」にチェックが入ってないのでしょうか?

アクセスマスクの継承

結論から言うと、「書き込み」チェックの有無はMaskの値だけで決定されるものではなく、「ACEのフラグ」も加味されて決定されているようです。(もしかするとそれ以外もあるかもしれません。)
「ACEのフラグ」とは、前述したACE_HEADER構造体のAceFlagsメンバーのことです。先程のコードにAceFlagsメンバーの値も出力するよう改修し、再度実行します。

1つ目のAceFlagsは3(0b11)、2つ目のAceFlagsは2(0b10)でした。

ACCESS_HEADER構造体
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-ace_header
ACE_HEADER構造体のページを調べると、AceFlagsに設定されるフラグが確認できます。AceFlagsの1ビット目(LSB) は「OBJECT_INHERIT_ACE」、2ビット目 は「CONTAINER_INHERIT_ACE」のようです。このフラグは、フォルダー内に作成されたファイル・フォルダーにMaskの値を引き継ぐ(継承する)よう指定します。

Windowsでは、アクセス権の継承という考え方があります。継承フラグが設定されたフォルダー(親)配下にファイル・フォルダ(子)ーを作成すると、親フォルダーのアクセスマスクを引き継ぐ形でACEが設定されます。もちろん、継承させたくない場合は、継承フラグを落とすことでアクセスマスクを継承させないようにすることも出来ます。

C:\ProgramDataフォルダーの場合、1つ目のAceFlagsは3ですから、「OBJECT_INHERIT_ACE」、「CONTAINER_INHERIT_ACE」の2つが設定されていました。この場合、C:\ProgramData配下に作成されるファイル・フォルダーに同じアクセスマスク(読み込み許可)を持った許可ACEが設定されます。
一方、2つ目のAceFlagsは2なので「CONTAINER_INHERIT_ACE」のみが設定されていました。この場合はCONTAINER(=フォルダー)にのみ、書き込み許可のアクセスマスクを持った許可ACEが設定されます。ファイルには設定されません。

試しに、3つのフォルダーを作成し、そのフォルダーのAceFlagsに「OBJECT_INHERIT_ACE」、「CONTAINER_INHERIT_ACE」、「OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE」をそれぞれ設定しました。


よくわからないのですが、「OBJECT_INHERIT_ACE」と「CONTAINER_INHERIT_ACE」の両方を持たないと「書き込み」にチェックが入らないようです。

若干話がそれますが、おそらく、C:\ProgramData配下は様々な権限のプロセスがファイルを作成するので、他ユーザーが勝手にファイルの内容を書き換えられると困ります。この問題を防ぐため、Usersグループに対しての書き込み許可はフォルダーに対してのみ設定されていると思われます。

まとめ

プロパティ画面で「書き込み」にチェックが入っていなくても書き込める場合があります。その理由は「書き込み許可ACE」がちゃんと設定されているからでした。
わかりずらいなーと思うのですが、最初の画像をよく見ると「特殊なアクセス許可」にチェックが入っています。さらに、その下に「詳細設定」ボタンがあります。

「詳細設定」ボタンを押すと、DACLが見えます。「Usersグループに対して書き込み許可」を示す許可ACEもちゃんと表示されてます。

ちなみに、icaclsやaccesschkの結果は次のとおりです。

icacls


icaclsでは、スクリプトで確認した際と同じように2つの許可ACEが表示されており、一つ目はOI(=OBJECT_INHERIT_ACE)、CI(=CONTAINER_INHERIT_ACE)のフラグが立っている読み込み許可ACE、二つ目はCIのみの書き込み許可ACEが確認できました。

accesschk

accesschkでは、Usersグループに読み書きが許可されていることを確認できました。

結論

隅々までちゃんとよく確認しましょう。