AppContainer関連サンプル紹介

前々回はAppContainerでデスクトップアプリを起動する方法を説明しました。
今回はAppContainerでデスクトップアプリを作るに当たり問題になるプロセス間通信と、ファイルの読み書き権限の付与について紹介したいと思います。

いつものようにサンプルコード。
http://wordpress.click3.org/garakuta/AppContainerUtils.zip

この記事の目次

ハンドル継承

Win32APIを調べているとハンドル継承という単語をよく見ます。
しかし、そういう機能もあるんだなーどまりで実際にどうやってハンドルを継承するのか知らない人も多いと思います。
そもそも通常のMedium権限で動くアプリの場合、ハンドルの継承なんかせずとも問題なくハンドルの生成が可能だからというのもあるでしょう。
そこで今回、自由にハンドルを作成できないAppContainer用にハンドル継承を試してみようと思います。

ハンドル継承とは

親プロセスで保持しているハンドルを子プロセスでも使用できるようにする機能です。
具体的には同じ数値を持つHANDLEで同じオブジェクトにアクセスできます。
これは親プロセスから降格したなどの理由により、子プロセスでは作れないが使用したいという用途で使われるものです。
AppContainerからは読み込み不能の領域に置かれた設定ファイルの読み込みなどが想定されます。

実際にやってみよう

サンプルコードのTestSharedMemory1を開いて、ExampleApp.exeをTestSharedMemory.exeにD&Dしてみてください。
黒窓が二つ上がったと思います。
そのうちExampleApp.exeのほうに数値を打ち込んでEnterを押すとTestSharedMemory.exeにも表示されると思います。
これは共有メモリーのハンドルを継承させてプロセス間通信しています。
exitと打ち込みEnterで終了できるので、適当に数値を入れて試してみるなどしてみて下さい。

次は具体的な実装方法を説明します。

コード説明

具体的にどうすればいいのかというと、ハンドル作成時と子プロセス作成時にハンドル継承フラグを有効にしたうえで、ハンドルを子プロセスに渡すだけです。
今回のサンプルでは試しに共有メモリーをハンドル継承で受け渡し、プロセス間通信を行っていきます。
では実際のコードも見ていきましょう。

まずは共有メモリーの作成。

bool CreateSharedMemory(HANDLE &handle, const wchar_t * const sharedMemoryName, const unsigned int sharedMemorySize) {
  // bInheritHandle = TRUEにすることでハンドルを継承可能にする
  SECURITY_ATTRIBUTES attr = {0};
  attr.nLength = sizeof(attr);
  attr.bInheritHandle = TRUE;
  attr.lpSecurityDescriptor = NULL;
  handle = ::CreateFileMappingW(INVALID_HANDLE_VALUE, &attr, PAGE_READWRITE, 0, sharedMemorySize, sharedMemoryName);
  if (handle == NULL) {
    return false;
  }
  return true;
}

重要なのは
attr.bInheritHandle = TRUE;
で、ここがTRUEだとこのハンドルを継承することが可能になります。

次は子プロセスの作成です。

bool RunImpl(SHARED_HANDLE &processHandle, const SHARED_SID sid, const boost::filesystem::path &path, const HANDLE arg) {
  ...
  // ハンドルをコマンドライン引数で渡す
  std::wostringstream woss;
  woss << path.wstring() << L" " << reinterpret_cast<unsigned int>(arg);
  wchar_t buf[1024];
  ::wcscpy_s(buf, woss.str().c_str());
  // bInheritHandles = TRUEでハンドルを継承させる
  if (::CreateProcessW(path.wstring().c_str(), buf, NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, reinterpret_cast<LPSTARTUPINFO>(&startupInfoEx), &pi) == FALSE) {
    return false;
  }
  ...
}

CreateProcessWの第五引数のbInheritHandlesをTRUEにすることで、現在のプロセスで保持しているハンドル継承が有効なハンドルをすべて継承させます。
また、共有メモリーのハンドルをunsigned intにキャストしてコマンドライン引数で渡しています。
これは、ハンドル継承で使用可能になっても実際に使用できるハンドルを取得する方法がないためです。
実用する場合は後述の方法を用いてプロセス間通信を行ってハンドル本体を受け渡す方法がよいでしょう。

最後に子プロセス側でハンドルを実際に使用します。

bool CreateSharedMemory(void *&ptr, const HANDLE handle) {
  ptr = ::MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  if (ptr == NULL) {
    return false;
  }
  return true;
}

int main(const unsigned int argc, const char * const * const argv) {
  ...
  std::istringstream iss(argv[1]);
  unsigned int handleImpl;
  iss >> handleImpl;
  const HANDLE handle = reinterpret_cast<HANDLE>(handleImpl);
  void * ptr;
  if (!CreateSharedMemory(ptr, handle)) {
    std::wcout << L"Error: 共有メモリーの生成に失敗しました" << std::endl;
    std::wcin.ignore();
    return 1;
  }
  SharedStruct &sharedStruct = *reinterpret_cast<SharedStruct *>(ptr);
  ...

ここは特に説明が必要な箇所はありません、普通にHANDLEから共有メモリーを取得しているだけです。

ハンドル継承まとめ

1:ハンドル継承有でハンドルを作成
2:ハンドル継承有で子プロセスを作成
3:何らかの手段でハンドルを受け渡し
4:通常通りハンドルを使用するだけ。

でハンドル継承は行えます。
かなり簡単ですが、三つ以上のプロセスで協調したり、別々に起動したプロセス間でやり取りするには向いていません。
やはり通常のプロセス間のように名前指定でプロセス間通信をしたいものです。
そこで次はAppContainerの内外間を名前指定でプロセス間通信をする方法を紹介します。

AppContainer内外間でプロセス間通信

AppContainerの中と外で同じ名前で共有メモリーを作った場合処理は成功しますが相手側に反映されません。
しかし同じAppContainerで同じ名前の共有メモリーを使うと正常にやり取りができます。
これはなぜかというと、AppContainer内で作成したハンドルは別の名前空間にリダイレクトされており、別物として処理されるからです。
つまり、外部側がそのリダイレクトされた先を指定すればやり取りができるわけです。

Winodwsのハンドル管理の仕組み

共有メモリーなど各ハンドルを取得するAPIを調べてみると、名前指定でハンドルを開けるものは少なくありません。
しかし、MSDNなどでは同じ名前だと関連付いたハンドルが返る、\は名前には使えない、とあるだけで具体的な説明がなく、名前空間などの説明がありません。
そこでWinObjを使ってみましょう。
これはオブジェクトマネージャーの名前空間を可視化するツール(要管理権限)で、現在存在している共有メモリーなどが属する名前空間を見ることができます。

WinObjで調べたところ、通常の名前付きオブジェクトは
\Sessions\#{セッション番号}\BaseNamedObjects\#{名前}

AppContainerから作成したものは
\Sessions\#{セッション番号}\AppContainerNamedObjects\#{AppContainerのSID}\#{名前}
に作られるようです。
また\Sessions\#{セッション番号}\BaseNamedObjects\AppContainerNamedObjectsに\Sessions\#{セッション番号}\AppContainerNamedObjectsへのシンボリックリンクが用意されていました。
これはつまり「AppContainerNamedObjects\#{AppContainerのSID}\#{名前}」を指定すればAppContainer内部で作ったオブジェクトとやり取りできそうです。

実際にやってみよう

サンプルコードのTestSharedMemory2を開いて、1と同じようにExampleApp.exeをTestSharedMemory.exeにD&Dしてみてください。
1と同じく数値を打ち込んでEnterでもう片方にも表示されると思います。
これは1とは違いそれぞれが共有メモリーをオープンしてプロセス間通信しています。

コード説明

2つ前の章でわざわざ各ハンドルの属する名前空間を調べましたが実際にこれらの知識は必要なく、いきなり直に名前空間を取得できるAPIがちゃんと用意されています。

具体的な呼び出し方は以下の通り

  ...
  const SHARED_SID sid = GetAppContainerSid(appContainerName);
  wchar_t buffer[4096];
  ULONG size;
  if (::GetAppContainerNamedObjectPath(NULL, sid.get(), _countof(buffer), buffer, &size) == FALSE) {
    std::wcout << L"Error: NameObjectPathの取得に失敗しました" << std::endl;
    std::wcin.ignore();
    return 1;
  }
  // 名前空間から取得する共有メモリーの名前を生成
  std::wostringstream woss;
  woss << buffer << L"\\" << sharedMemoryName;
  HANDLE handle;
  if (!CreateSharedMemory(handle, appContainerName, woss.str().c_str(), sizeof(SharedStruct))) {
    std::wcout << L"Error: 共有メモリーの生成に失敗しました" << std::endl;
    std::wcin.ignore();
    return 1;
  }
  ...

重要なのはGetAppContainerNamedObjectPathで、AppContainerのsidを渡すとそのAppContainerが所属するハンドルの名前空間を返します。
あとは#{名前空間}\#{名前}と文字列を構築して各APIに渡してやるだけでAppContainer内とやり取りができるというわけです。
ただし、AppContainerのプロファイルが存在していないとハンドルを取得できないので、Mediumで動く側はAppContainer側のプロセスを起動してから取得しに行くようにしましょう。

その他に関してはほぼ1と同じなので割愛します。

AppContainer内外間でプロセス間通信まとめ

1:GetAppContainerNamedObjectPathで名前空間を取得
2:AppContainerのプロセスを起動
3:名前空間を指定してハンドルを作成
4:あとは通常のプロセス間通信

という手順で問題なくプロセス間通信が行えることがわかりました。
通常はAppContainer内からでは開けないハンドルを継承可能にしたうえで、この方法で作った共有メモリー上に載せてプロセス間通信で渡し、といった運用になるかと思います。
またこの方法単独でプロセス間通信を行い、上位の権限が必要な処理は全部外部側のプロセスに用意してAPIを叩くように処理するというのもありでしょう。

さて、ここまででプロセス間通信で必要なものは大体説明で来たかと思います。
次はプロセス間通信から離れ、AppContainerの権限で読み書きできるファイルの作成についてです。

ファイルアクセスへのアクセス権限追加

AppContainer内部からは自由にファイルを読み書きできません。
その変わりにアプリケーション専用のフォルダが用意され、そこに設定ファイルを置いたりできるようになってはいますが、アプリによってはそれ以外にも読み書きできる場所が必要になることもあるでしょう。
そこで、ファイルやフォルダにAppContainerから読み書きできるようアクセス権を追加してみましょう。

アクセス権限の仕組み

アクセス権限はACL型であらわされ、これにAllowやDenyといった設定を追加していったものをSecurityDescriptorに入れて各APIに渡していく形となります。
AppContainerを対象としたい場合、CreateAppContainerProfileなどで取得したSIDを指定すればそのプロファイルにだけアクセス権限を出すことができます。
今回はAppContainer一つを対象とするのではなく、AppCOntainer全体に対するアクセス権限を付与してみましょう。

実際にやってみよう

AuthzAnyPackageFolderフォルダ内のAuthzAnyPackageFolder.exeに何かファイルかフォルダをD&Dしてみましょう。
その後対象のファイル/フォルダを右クリック=>プロパティ=>セキュリティをのぞいてみると「ALL APPLICATION PACKAGES」という項目が増えているはずです。
これはAppContainer全体を指しており、AppContainer下で実行されるどのアプリからもアクセスできるようになったことを表しています。

コード説明

まずはアクセス権限一覧であるACLを構築します。

typedef boost::shared_ptr<boost::remove_pointer<PACL>::type> SHARED_ACL;
unsigned int GetACLSize(const std::vector<SHARED_SID> &list) {
  unsigned int size = sizeof(ACL) + (sizeof(ACCESS_ALLOWED_ACE) * list.size());
  BOOST_FOREACH(const SHARED_SID &sid, list) {
    size += ::GetLengthSid(sid.get()) - sizeof(DWORD);
  }
  // align
  size = (size + sizeof(DWORD) - 1) & 0xfffffffc;
  return size;
}
SHARED_ACL CreatePACL(const std::vector<SHARED_SID> &list) {
  const unsigned int size = GetACLSize(list);
  const SHARED_ACL acl(reinterpret_cast<PACL>(new unsigned char[size]));
  if (::InitializeAcl(acl.get(), size, ACL_REVISION) == FALSE) {
    return SHARED_ACL();
  }
  BOOST_FOREACH(const SHARED_SID &sid, list) {
    if (::AddAccessAllowedAce(acl.get(), ACL_REVISION, GENERIC_ALL, sid.get()) == FALSE) {
      return SHARED_ACL();
    }
  }
  return acl;
}

特に難しいことはしておらず、InitializeAclでACLを初期化しAddAccessAllowedAceでSIDに許可を出しています。
ここでは使用していませんが、AddAccessDeniedAceでアクセス拒否を付加することもできます。
次はACLを用いてSECURITY_DESCRIPTORを構築します。

bool CreateSecurityDescriptor(boost::shared_ptr<SECURITY_DESCRIPTOR> &desc, SHARED_ACL &acl, const std::vector<SHARED_SID> &sidList, const SHARED_SID owner) {
  acl = CreatePACL(sidList);
  if (!acl) {
    return false;
  }
  desc.reset(new SECURITY_DESCRIPTOR());
  if (::InitializeSecurityDescriptor(desc.get(), SECURITY_DESCRIPTOR_REVISION) == FALSE) {
    return false;
  }
  if (::SetSecurityDescriptorDacl(desc.get(), TRUE, acl.get(), FALSE) == FALSE) {
    return false;
  }
  return true;
}

こちらも特に難しいことはしておらず、InitializeSecurityDescriptorで初期化してSetSecurityDescriptorDaclでSECURITY_DESCRIPTORにACLを設定しています。
ただし、SetSecurityDescriptor*のAPIはSECURITY_DESCRIPTOR内にコピーされるのではなく、あくまでポインターを保持するだけなため、設定したからと解放してしまうとSECURITY_DESCRIPTORの状態がおかしくなってしまうので注意が必要です。
今回の場合aclを開放してしまうと正常に処理されないため、使用側に受け渡して解放しないようにしています。

次に実際にファイル/フォルダに権限を設定するところです。

SHARED_SID GetWellKnownSid(const WELL_KNOWN_SID_TYPE type) {
  SHARED_SID sid(new unsigned char[SECURITY_MAX_SID_SIZE]);
  DWORD sidSize = SECURITY_MAX_SID_SIZE;
  if (::CreateWellKnownSid(type, NULL, sid.get(), &sidSize) == FALSE) {
    return SHARED_SID();
  }
  return sid;
}
bool AuthzAnyPackage(const char * const path) {
  const SHARED_SID owner = GetCurrentUserSid();
  boost::shared_ptr<SECURITY_DESCRIPTOR> desc;
  SHARED_ACL acl;
  std::vector<SHARED_SID> sidList;
  sidList.push_back(GetWellKnownSid(WinBuiltinAnyPackageSid));
  sidList.push_back(GetWellKnownSid(WinLocalSystemSid));
  sidList.push_back(owner);
  sidList.push_back(GetWellKnownSid(WinBuiltinAdministratorsSid));
  if (!CreateSecurityDescriptor(desc, acl, sidList, owner)) {
    return false;
  }
  if (::SetFileSecurityA(path, DACL_SECURITY_INFORMATION, desc.get()) == FALSE) {
    return false;
  }
  return true;
}

WinBuiltinAnyPackageSidというのが先ほどのサンプル実行でいう「ALL APPLICATION PACKAGES」に相当します。
あとは見たままですね、単純にSetFileSecurityのAPIにSECURITY_DESCRIPTORを渡しているだけになります。
またCreateFileに渡して最初からその権限でファイルを作成することなどもできます。

ファイルアクセスへのアクセス権限追加まとめ

1:ACLを構築
2:SECURITY_DESCRIPTORを構築
3:SetFileSecurityで権限付与

という手順でアクセス権限を追加できることがわかりました。
AppContainer内から触れるようにする必要がある時は事前にMediumの権限でアクセス権を付与しておくとよいでしょう。

終わりに

いかがだったでしょうか?
前々回と合わせて、これで大体のAppContainer内で動くデスクトップアプリは作れるようになったんじゃないかと思います。
実際に組むとすれば、Mediumで動くAPIサーバーのようなプロセスを用意し、そことプロセス間通信をしつつUIやコアの処理を行うAppContainerプロセス、とわけて動かすことでセキュリティリスクを抑えるという流れになるでしょう。

一人でもAppContainerに興味を持ってくれる人がいることを信じて。
では、また。

※また今回の記事を書くにあたって前回の記事にGetAppContainerNamedObjectPathを追加したので興味があれば見てほしい。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です