mrubyのバイトコードフォーマット解説その2

2014/05/14、mrubyでローカル変数情報を扱うようになりlocal_variablesメソッドも使用可能に。
それに伴いバイトコードのフォーマットも変更となり、新たにLVARセクションが追加されました。

そこで今回は追加されたLVARセクションについて、簡単にフォーマット紹介をしようと思います。

過去の記事の続きなので、まだの人はそちらからどうぞ。

いつものようにサンプルコード
http://wordpress.click3.org/garakuta/parse_mruby3.zip
内容は過去の記事の物をベースにローカル変数情報を追加してあります。
動作としては、parse_mruby.exeに.mrbファイルをD&Dするとローカル変数情報とirep情報を表示する、というものです。

この記事で対象としているmrubyは2014/05/17当時(git hash: 13db4da204d2bedec4c0c5de939e662a44d477a6 )の物です。
それ以降のmrubyを使用する場合は以下の解説の通りではない可能性があります。

セクション

ローカル変数は新設されたセクションに含まれています。
過去の記事でも書いていますがセクションには共通のセクションヘッダーと呼ぶべきものを含んでおり、それは以下の通りになります。

ubig8_t signature[4];
ubig32_t size;

LVARセクション

ローカル変数名を保持するセクションで、signatureは0x4C 0x56 0x41 0x52(ASCIIでLVAR)になります。
LVARセクション専用の追加ヘッダーは特に存在しません。

LVARセクションは一つのmrbファイル一つにつき一つのみ存在し、複数のメソッドが含まれている場合はそれらすべてのローカル変数情報を持ちます。
LVARセクションはセクションヘッダーを除くと二つの領域からなりローカル変数名を表すシンボルテーブルと、各レジスタとシンボルを紐づけるレコードテーブルに分けられます。

シンボルテーブル

ローカル変数名のシンボルを格納した領域。
含まれるirepすべてのローカル変数名情報のリストであり、重複も排除されている。
構造は以下の構造の通りになる。

ubig32_t count;
struct symbol {
  ubig16_t len;
  ubig8_t body[len];
} symbolList[count];

サンプル内で実際に読み込んでいるコードは以下の通り。

  static boost::shared_ptr<const LocalVariableSymbolList> Read(const uint8_t *ptr, unsigned int size) {
    boost::shared_ptr<const LocalVariableSymbolList> result;
    if (size < 4) {
      return result;
    }
    const unsigned int count = *reinterpret_cast<const endian::ubig32_t *>(ptr);
    ptr += 4;
    size -= 4;
    if (count * 1 > size) {
      return result;
    }
    std::vector<std::string> list(count);
    BOOST_FOREACH(std::string &str, list) {
      if (size < 2) {
        return result;
      }
      const unsigned int len = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      ptr += 2;
      size -= 2;
      if (len > size) {
        return result;
      }
      str.assign(reinterpret_cast<const char *>(ptr), len);
      ptr += len;
      size -= len;
    }
    result.reset(new LocalVariableSymbolList(list));
    return result;
  }

最初にシンボルの個数を取得し、その個数分だけループをまわして文字列長を取得した後その長さだけ文字列として読み込み、最後にそれを保持するインスタンスを作成して返しているだけです。
irepのシンボルテーブルと違い終端文字列は含まない点に注意。
また現在の版だとブロック引数に(たとえ使用していなくても)常に空文字列がローカル変数名として割り当てられており、それがこのシンボルテーブルにも含まれていることがあります。

レコードテーブル

ローカル変数シンボルと実際にローカル変数が保持されているレジスタを紐づけるテーブル。
irepデータの数だけ存在し、深さ優先探索的にならんでいる。
構造は以下の通り

ubig16_t symbolIndex;
ubig16_t registerIndex;

symbolIndexは前述のシンボルテーブル内のインデックス、registerIndexはirep内の該当するローカル変数に紐づくレジスタのインデックスです。
サンプルで実際に読み上げている処理は以下の通り。

  static boost::shared_ptr<const LocalVariableData> Read(const uint8_t *ptr, unsigned int size, const boost::shared_ptr<const IrepRecord> irep, const boost::shared_ptr<const LocalVariableSymbolList> symbolList) {
    boost::shared_ptr<const LocalVariableData> result;
    if (static_cast<unsigned int>(2 + 2) * (irep->header->localCount - 1) > size) {
      return result;
    }
    std::vector<Record> records(irep->header->localCount - 1);
    BOOST_FOREACH(Record &record, records) {
      const unsigned int nameIndex = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      if (nameIndex > symbolList->list.size()) {
        return result;
      }
      ptr += 2;
      size -= 2;
      record.name = &symbolList->list[nameIndex];
      record.registryIndex = *reinterpret_cast<const endian::ubig16_t *>(ptr);
      ptr += 2;
      size -= 2;
    }
    std::vector<boost::shared_ptr<const LocalVariableData> > childs(irep->child.size());
    for (unsigned int i = 0; i < childs.size(); i++) {
      const boost::shared_ptr<const LocalVariableData> child = Read(ptr, size, irep->child[i], symbolList);
      if (!child) {
        return result;
      }
      childs[i] = child;
      ptr += child->GetSize();
      size -= child->GetSize();
    }
    result.reset(new LocalVariableData(records, childs));
    return result;
  }

最初に該当するirepのローカル変数の数を取得している。
-1しているのは自信を指すselfの分は含まれないためだ。

次はローカル変数の数だけレコードを読み込み、シンボルとの関連付けを解決している。

その後子のirepに対して再帰実行して読み込んだのち、データを保持するインスタンスを作成して返している。

irepとの紐づけ

irepとの紐づけといってもレコードテーブルの時点で終わっているも同然です。
すでにirepごとに変数の紐付けは終わっており、あとはirepでの処理の際に該当するレコードからシンボルを引っ張ってくるだけです。

一応、サンプルにおけるローカル変数情報の表示部分を紹介します。

bool PrintLocalVariable(const boost::shared_ptr<const LocalVariableData> data, std::vector<unsigned int> &index) {
  std::cout << boost::format("lvar%s\n") % std::accumulate(index.begin(), index.end(), std::string(), IndexListJoin());
  BOOST_FOREACH(const LocalVariableData::Record &record, data->records) {
    std::cout << boost::format("reg[%d] : %s") % record.registryIndex % *record.name << std::endl;
  }
  std::wcout << L"\n";
  unsigned int i = 0;
  BOOST_FOREACH(const boost::shared_ptr<const LocalVariableData> &child, data->childs) {
    index.push_back(i);
    if (!PrintLocalVariable(child, index)) {
      return false;
    }
    index.pop_back();
    i++;
  }
  return true;
}

indexは階層表示用の配列でlvar->child[3]->child[2]->child[1]のirepのローカル変数情報を表示する際には[3, 2, 1]と保持しており、それを文字列に変換して使用しています。
あとは単にforeachで配列を展開して表示し、childがいれば再帰して呼び出しているだけです。

終わりに

いかがだったでしょうか。
新しくセクションが増えるというそれなりに大きな変更ですが、実はバージョン番号などには変化がありません。
Matzさん曰く「sectionが追加されただけで、かつ知らないsectionは読み飛ばす(はず)ですから、変更しなくても大丈夫だと思いました。」とのことで、過去の版で動作する限りは特にバージョンは上がらないようです。

今回の更新でローカル変数をevalで取得するようなコードも動くようになったので、また一歩rubyとの互換性が上がったと言えます。
この記事を読んで少しでもmrubyの動作について理解が深まれば幸いです。
では、また。

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を追加したので興味があれば見てほしい。

AppContainer関連API情報まとめ

AppContainer関連のAPIで調べたことについてまとめておく。
これらの情報は公式ではなく、あくまで自分が個人的に調べたものであるので間違っていている可能性があることに注意。
また、実際に動くコードや解説などが目的の人はAppContainerでデスクトップアプリを起動してみたAppContainer関連サンプル紹介を参照してほしい。

公開API

CreateAppContainerProfile

MSDN参照

pszAppContainerNameの名前でAppContainerを作成する。
必要なくなったらDeleteAppContainerProfileで削除すること。
AppContainerRegisterSidと違いsidはシステム側で適切に生成してくれる様子。
すでに存在する場合は失敗する、生成済みかを判別する方法は特にない様子。
pszAppContainerNameはAppContainerRegisterSidでいうmonikerだと思われる。
pCapabilitiesは指定しなくともUpdateProcThreadAttributeで指定してあれば有効になる、というかpCapabilitiesを指定して何が変わるのかわからなかった、通常のモダンアプリなどを起動する際に付与するCapabilitiesを算出するためのものだろうか?
こちらはAppContainerRegisterSidと違ってMSDNにも載っておりsidの適切な生成などがあるので、一般にはこちらを使ってほしいという意図なのだと思われる。

サンプル

  const wchar_t * const name = L"AppConteinerTest";
  PSID sid;
  if (FAILED(::CreateAppContainerProfile(name, name, name, NULL, 0, &sid))) {
    return;
  }

DeleteAppContainerProfile

MSDN参照

指定したAppContainerを削除する。
GetAppContainerFolderPathで取得できるフォルダの中身や、GetAppContainerRegistryLocationで取得できるレジストリも消える。

サンプル

  const wchar_t * const name = L"AppConteinerTest";
  PSID sid;
  if (FAILED(::CreateAppContainerProfile(name, name, name, NULL, 0, &sid))) {
    return;
  }
  ...
  ::DeleteAppContainerProfile(name);

DeriveAppContainerSidFromAppContainerName

MSDN参照

pszAppContainerNameからCreateAppContainerProfile互換のsidを取得する。
AppContainerDeriveSidFromMonikerとほぼ同一の動作をする。
存在しないAppContainerに対しても実行でき、その場合はCreateAppContainerProfileで生成した場合に割り当てられるsidが返る。
取得したsidは必要なくなったらFreeSidで解放すること。

  PSID sid;
  if (FAILED(::DeriveAppContainerSidFromAppContainerName(name, &sid))) {
    return;
  }
  ...
  ::FreeSid(sid);

GetAppContainerFolderPath

MSDN参照

sidからアプリケーションフォルダを取得する。
アプリケーションフォルダ内はAppContainer下でも自由に読み書きでき、DeleteAppContainerProfileを呼び出さなければアプリ終了後にも残る。
逆にDeleteAppContainerProfileを呼ぶと、プロセスが生きていようともその時点でファイルが削除され読み書き権限もなくなる。
取得したpathは必要なくなったらCoTaskMemFreeで解放すること。
ちなみに手元だと
C:\Users\#{ユーザー名}\AppData\Local\Packages\#{AppContainer名}\AC
に保存されていた。

サンプル

  const wchar_t * const name = L"AppConteinerTest";
  PSID sid;
  if (FAILED(::CreateAppContainerProfile(name, name, name, NULL, 0, &sid))) {
    return;
  }
  wchar_t *sidStr;
  if (::ConvertSidToStringSidW(sid, &sidStr) == FALSE) {
    ::FreeSid(sid);
    return;
  }
  wchar_t *appContainerPath;
  if (FAILED(::GetAppContainerFolderPath(sidStr, &appContainerPath))) {
    ::FreeSid(sid);
    ::LocalFree(sidStr);
    return;
  }
  ...
  ::FreeSid(sid);
  ::LocalFree(sidStr);
  ::CoTaskMemFree(appContainerPath);

GetAppContainerRegistryLocation

MSDN参照

AppContainer下でも読み書きできるレジストリキーを取得する。
AppContainer下で実行されているプロセスでしか使えず、それ以外のプロセスで実行しても失敗する。
GetAppContainerFolderPathと同様にDeleteAppContainerProfileを呼び出さなければアプリ終了後にも残る。
逆にDeleteAppContainerProfileを呼ぶと、プロセスが生きていようともその時点でレジストリは削除され読み書き権限もなくなり、APIも失敗するようになる。
MSDNには記載がないが、おそらくRegCloseKeyで閉じる必要がある。
ちなみに、手元の環境だと
HKEY_CLASSES_ROOT\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppContainer\Storage\#{AppContainer名}.#{乱数と思われるもの}
に保存されていた。

サンプル

  HKEY key;
  if (FAILED(::GetAppContainerRegistryLocation(KEY_ALL_ACCESS, &key))) {
    return;
  }
  ...
  ::RegCloseKey(key);

GetAppContainerNamedObjectPath

MSDN参照

AppContainer内プロセスで作成した各種ハンドルのリダイレクト先パス名を取得する。
外部のプロセスからこのパス名を付けてハンドルを作成することでAppContainer内部とやり取りすることができる。
手元の環境で試した範囲では「AppContainerNamedObjects\#{AppContainerのSID}」と帰ってくるようだった。
WinObjで具体的にどこにハンドルが属しているかを見ることができるので、興味があるなら見るのもよい。
詳しくはAppContainer関連サンプル紹介を参照。

サンプル

  PSID sid;
  if (FAILED(::DeriveAppContainerSidFromAppContainerName(name, &sid))) {
    return;
  }
  wchar_t buffer[4096];
  ULONG size;
  if (::GetAppContainerNamedObjectPath(NULL, sid, _countof(buffer), buffer, &size) == FALSE) {
    return;
  }
  ...
  ::FreeSid(sid);

非公開API

使用は推奨されないが、一応非公開APIについても調べたので書いておく。

AppContainerRegisterSid

HRESULT WINAPI AppContainerRegisterSid(PSID sid, LPCWSTR moniker, LPCWSTR display_name);

非公開API、GetProcAddressして使う。
sidをAppContainerとして登録する。
必要なくなったらAppContainerUnregisterSidで削除すること。
CreateAppContainerProfileで作成した場合と違いアプリケーションフォルダやレジストリへのアクセス権限は付与されないので注意。
sidやmonikerがすでに存在する場合は失敗する、すでに存在するか調べたい場合はAppContainerLookupMonikerを使用すること。
win8だとkernel32.dllにいるが8.1でkernelbase.dllに移った、おそらくVirtulDllで何か仮想dllに紐づいていてその実態が移動したとかだと思われる。
ぐぐるとわかるのだがchromiumのソースで使われていたことがある、というかほかに情報がない。

サンプル

  const wchar_t * const appContainerSid = L"S-1-15-2-1234567890-1234567890-1234567890-123456789-1234567890-123456789-1234567890";
  const wchar_t * const name = L"AppConteinerTest";
  typedef HRESULT(WINAPI* AppContainerRegisterSidPtr)(PSID sid, LPCWSTR moniker, LPCWSTR display_name);
  AppContainerRegisterSidPtr AppContainerRegisterSid = reinterpret_cast<AppContainerRegisterSidPtr>(::GetProcAddress(::GetModuleHandleW(L"kernelbase.dll"), "AppContainerRegisterSid"));
  if (AppContainerRegisterSid == NULL) {
    AppContainerRegisterSid = reinterpret_cast<AppContainerRegisterSidPtr>(::GetProcAddress(::GetModuleHandleW(L"kernel32.dll"), "AppContainerRegisterSid"));
    if (AppContainerRegisterSid == NULL) {
      return false;
    }
  }
  PSID sid;
  if (::ConvertStringSidToSidW(appContainerSid, &sid) == FALSE) {
    return false;
  }
  if (FAILED(AppContainerRegisterSid(sid, name, name))) {
    return false;
  }

AppContainerUnregisterSid

HRESULT WINAPI AppContainerUnregisterSid(PSID sid);

非公開API、GetProcAddressして使う。
sidでAppContainerに登録されている情報を削除する。
AppContainerRegisterSidと同様にwin8だとkernel32.dllにいるが8.1でkernelbase.dllに移った。

サンプル

  typedef HRESULT(WINAPI* AppContainerUnregisterSidPtr)(PSID sid);
  AppContainerUnregisterSidPtr AppContainerUnregisterSid = reinterpret_cast<AppContainerUnregisterSidPtr>(::GetProcAddress(::GetModuleHandleW(L"kernelbase.dll"), "AppContainerUnregisterSid"));
  if (AppContainerUnregisterSid == NULL) {
    AppContainerUnregisterSid = reinterpret_cast<AppContainerUnregisterSidPtr>(::GetProcAddress(::GetModuleHandleW(L"kernel32.dll"), "AppContainerUnregisterSid"));
    if (AppContainerUnregisterSid == NULL) {
      return;
    }
  }
  AppContainerUnregisterSid(sid);

AppContainerLookupMoniker

HRESULT WINAPI AppContainerLookupMoniker(PSID sid, LPWSTR* moniker);

非公開API、GetProcAddressして使う。
sidで登録されているAppContainerのmonikerを取得する。
sidに紐づくAppContainerが存在しない場合は失敗する。
monikerにはAPI内で動的に確保されたメモリーが割り当てられるため、必要なくなったらAppContainerFreeMemoryで解放すること。
AppContainerRegisterSidと同様にwin8だとkernel32.dllにいるが8.1でkernelbase.dllに移った。

サンプル

  typedef HRESULT(WINAPI* AppContainerLookupMonikerPtr)(PSID sid, LPWSTR* moniker);
  AppContainerLookupMonikerPtr AppContainerLookupMoniker = reinterpret_cast<AppContainerLookupMonikerPtr>(::GetProcAddress(::GetModuleHandleW(L"kernelbase.dll"), "AppContainerLookupMoniker"));
  if (AppContainerLookupMoniker == NULL) {
    AppContainerLookupMoniker = reinterpret_cast<AppContainerLookupMonikerPtr>(::GetProcAddress(::GetModuleHandleW(L"kernel32.dll"), "AppContainerLookupMoniker"));
    if (AppContainerLookupMoniker == NULL) {
      return;
    }
  }
  typedef BOOLEAN(WINAPI* AppContainerFreeMemoryPtr)(void* ptr);
  AppContainerFreeMemoryPtr AppContainerFreeMemory = reinterpret_cast<AppContainerFreeMemoryPtr>(::GetProcAddress(::GetModuleHandleW(L"kernelbase.dll"), "AppContainerFreeMemory"));
  if (AppContainerFreeMemory == NULL) {
    AppContainerFreeMemory = reinterpret_cast<AppContainerFreeMemoryPtr>(::GetProcAddress(::GetModuleHandleW(L"kernel32.dll"), "AppContainerFreeMemory"));
    if (AppContainerFreeMemory == NULL) {
      return;
    }
  }
  wchar_t *moniker;
  if (FAILED(AppContainerLookupMoniker(sid, &moniker))) {
    return;
  }
  ...
  AppContainerFreeMemory(moniker);

AppContainerDeriveSidFromMoniker

HRESULT WINAPI AppContainerDeriveSidFromMoniker(LPCWSTR moniker, PSID *sid);

非公開API、GetProcAddressして使う。
monikerからCreateAppContainerProfile互換のsidを取得する。
存在しないAppContainerに対しても実行でき、その場合はCreateAppContainerProfileで生成した場合に割り当てられるsidが返る。
どうやらmonikerをハッシュ関数に食わせるなどで生成しているようで、AppContainerRegisterSidで登録したmonikerを指定してもまるで違うsidが返ってくるので注意。
取得したsidは必要なくなったらAppContainerFreeMemoryで解放すること。
AppContainerRegisterSidと同様にwin8だとkernel32.dllにいるが8.1でkernelbase.dllに移った。

  typedef HRESULT(WINAPI* AppContainerDeriveSidFromMonikerPtr)(LPCWSTR moniker, PSID *sid);
  AppContainerDeriveSidFromMonikerPtr AppContainerDeriveSidFromMoniker = reinterpret_cast<AppContainerDeriveSidFromMonikerPtr>(::GetProcAddress(::GetModuleHandleW(L"kernelbase.dll"), "AppContainerDeriveSidFromMoniker"));
  if (AppContainerDeriveSidFromMoniker == NULL) {
    AppContainerDeriveSidFromMoniker = reinterpret_cast<AppContainerDeriveSidFromMonikerPtr>(::GetProcAddress(::GetModuleHandleW(L"kernel32.dll"), "AppContainerDeriveSidFromMoniker"));
    if (AppContainerDeriveSidFromMoniker == NULL) {
      return;
    }
  }
  PSID sid;
  if (FAILED(AppContainerDeriveSidFromMoniker(moniker, &sid))) {
    return;
  }
  ...
  AppContainerFreeMemory(sid);

AppContainerFreeMemory

BOOLEAN WINAPI AppContainerFreeMemory(void* ptr);

非公開API、GetProcAddressして使う。
AppContainer関連APIで確保したメモリーを開放する。
AppContainerRegisterSidと同様にwin8だとkernel32.dllにいるが8.1でkernelbase.dllに移った。

サンプル

  typedef BOOLEAN(WINAPI* AppContainerFreeMemoryPtr)(void* ptr);
  AppContainerFreeMemoryPtr AppContainerFreeMemory = reinterpret_cast<AppContainerFreeMemoryPtr>(::GetProcAddress(::GetModuleHandleW(L"kernelbase.dll"), "AppContainerFreeMemory"));
  if (AppContainerFreeMemory == NULL) {
    AppContainerFreeMemory = reinterpret_cast<AppContainerFreeMemoryPtr>(::GetProcAddress(::GetModuleHandleW(L"kernel32.dll"), "AppContainerFreeMemory"));
    if (AppContainerFreeMemory == NULL) {
      return;
    }
  }
  AppContainerFreeMemory(ptr);

AppContainerでデスクトップアプリを起動してみた

Windows8以降にAppContainerというsandbox機能が導入されているのをご存じだろうか?
このAppContainerは主にWindowsストアアプリの実行環境として使用されているのだが、最近巷を騒がせているIEの脆弱性もAppContainerを有効にすることで回避することができるらしいと聞く。
ということは、デスクトップアプリであってもAppContainerでセキュリティが向上できるかもしれない。
そこで今回はAppContainer上でデスクトップアプリを実行する方法を解説してみたいと思う。

本記事で紹介するコードやコンパイル済みのexeなどを用意しておいたので、実際に動かしてみたい方はどうぞ。
ただし、後始末やここでは解説しない機能の使用なども含んでいるため、本質的ではないコードが多いことをあらかじめ断っておく。
http://wordpress.click3.org/garakuta/StartAppContainer.zip

割り当てるCapabilityを取得する

AppContainer上で動作するアプリは既存のIntegrity Levelによる制限とは違い、Capabilityとして宣言しておくことで追加の権限を得ることができる。
そこでまずは動作させたいアプリに付与する権限を取得するところから始めよう。
なお、具体的にどのような権限があるかは解説しないので、詳しくはMSDNを参照してほしい。

実際にCapabilityを取得するコードは以下のようになる。

bool SetCapability(const WELL_KNOWN_SID_TYPE type, std::vector<SID_AND_ATTRIBUTES> &list, std::vector<SHARED_SID> &sidList) {
  SHARED_SID capabilitySid(new unsigned char[SECURITY_MAX_SID_SIZE]);
  DWORD sidListSize = SECURITY_MAX_SID_SIZE;
  if (::CreateWellKnownSid(type, NULL, capabilitySid.get(), &sidListSize) == FALSE) {
    return false;
  }
  if (::IsWellKnownSid(capabilitySid.get(), type) == FALSE) {
    return false;
  }
  SID_AND_ATTRIBUTES attr;
  attr.Sid = capabilitySid.get();
  attr.Attributes = SE_GROUP_ENABLED;
  list.push_back(attr);
  sidList.push_back(capabilitySid);
  return true;
}

前後にごちゃごちゃ長いものがついているが、本質的にはCreateWellKnownSidでSIDを取得し、SID_AND_ATTRIBUTES構造体に詰めて返している。
IsWellKnownSidはSIDがtypeと一致するかを調べるもので、間違いなく成功するので意味はないassertの一種だと思ってほしい。
その他はメモリーの開放といった後始末になる。

まずCreateWellKnownSidはシステム定義のSIDを取得するAPIでCapabilityのSIDもこれで取得できる。
取得するSIDの種類を表すWELL_KNOWN_SID_TYPEのうちWinCapabilityから始まるものがそれだ。

次にSID_AND_ATTRIBUTESだが、これは何のためのSIDなのかやどういう意図で使用するかなどを表す。
今回はCapabilityを追加したいのでAttributesにSE_GROUP_ENABLEDを設定したものを使う。

AppContainerのプロファイルを作成する

次はAppContainerのプロファイルを作成しよう。
プロファイルも識別子としてはSIDを用いるので、今回取得するのもSIDとなる。

  PSID sidImpl;
  DeleteAppContainer deleteAppContainer(AppContainerName);
  if (FAILED(::CreateAppContainerProfile(AppContainerName, AppContainerName, AppContainerName, (capabilities.empty() ? NULL : &capabilities.front()), capabilities.size(), &sidImpl))) {
    return false;
  }
  const SHARED_SID sid = ToSharedSID2(sidImpl);

CreateAppContainerProfileがプロファイルを作成するAPIで、その前後は後始末に関係する。
CreateAppContainerProfileのプロトタイプは以下のようになる。

HRESULT WINAPI CreateAppContainerProfile(
  _In_   PCWSTR pszAppContainerName,
  _In_   PCWSTR pszDisplayName,
  _In_   PCWSTR pszDescription,
  _In_   PSID_AND_ATTRIBUTES pCapabilities,
  _In_   DWORD dwCapabilityCount,
  _Out_  PSID *ppSidAppContainerSid
);

第一引数はプロファイルの識別子で、AppContainer固有のアプリケーションフォルダのパスなどで使用される。
第二第三は表示用なので割愛。
第四第五引数はCapabilityを指定するもののようだが、ここでは指定しなくとも問題なく権限は付加される。
おそらくはモダンアプリなどexplorerから起動出来るAppContainerのアプリの場合、ここで設定したCapabilityを用いて起動するのだと思われる。
コード中で指定してあるのはあくまで念のためだ。
第六引数は生成したプロファイルのSIDを受けるポインターとなる。

作成したプロファイルでアプリを起動する

さて、プロファイルも無事生成出来たのでいよいよアプリを実行しよう。
少々長いが、アプリの起動は以下のようになる。

  const unsigned int attrCount = 3;
  SIZE_T size;
  ::InitializeProcThreadAttributeList(NULL, attrCount, 0, &size);
  const SHARED_ATTR_LIST attrList = ToSharedAttributeList(reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(new unsigned char[size]));
  if (::InitializeProcThreadAttributeList(attrList.get(), attrCount, 0, &size) == FALSE) {
    return false;
  }
  SECURITY_CAPABILITIES sc;
  sc.AppContainerSid = sid.get();
  sc.Capabilities = (capabilities.empty() ? NULL : &capabilities.front());
  sc.CapabilityCount = capabilities.size();
  sc.Reserved = 0;
  if (::UpdateProcThreadAttribute(attrList.get(), 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, &sc, sizeof(sc), NULL, NULL) == FALSE) {
    return false;
  }
  STARTUPINFOEXW startupInfoEx = { 0 };
  startupInfoEx.StartupInfo.cb = sizeof(STARTUPINFOEXW); // StartupInfoのサイズではなくstartupInfoExのサイズである点に注意
  startupInfoEx.lpAttributeList = attrList.get();
  PROCESS_INFORMATION pi = { 0 };
  if (::CreateProcessW(path.wstring().c_str(), NULL, NULL, NULL, FALSE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, reinterpret_cast<LPSTARTUPINFO>(&startupInfoEx), &pi) == FALSE) {
    std::wcout << GetLastError() << std::endl;
    return false;
  }
  ::WaitForSingleObject(pi.hProcess, INFINITE);
  ::CloseHandle(pi.hThread);
  ::CloseHandle(pi.hProcess);

InitializeProcThreadAttributeListでLPPROC_THREAD_ATTRIBUTE_LISTを初期化し、UpdateProcThreadAttributeでプロファイルのsidとCapabilityを設定してCreateProcessWでアプリを起動している。
その他諸々はほかと同じく後始末になる。

InitializeProcThreadAttributeListは見たまま単に初期化を行っているだけだ。
設定する属性の数は念のため3にしてあるが、おそらく1でも動作する。
UpdateProcThreadAttributeもみたままにLPPROC_THREAD_ATTRIBUTE_LISTにsidを設定しているだけだ。
Capabilityはここで指定してあればプロファイルに指定がなくともちゃんと割り振られて動作する。
CreateProcessWはEXTENDED_STARTUPINFO_PRESENTを指定してやるとSTARTUPINFOでLPPROC_THREAD_ATTRIBUTE_LISTを受けてくれるようになる。
あとは単にSTARTUPINFOEXWにLPPROC_THREAD_ATTRIBUTE_LISTを設定して起動しているだけだ。

実際に起動してみる

http://wordpress.click3.org/garakuta/StartAppContainer.zipに完成品があるのでDLしてもらって、中のTestAppContainer.exeを普通にダブルクリックで起動してみてほしい。
どうだろうか?おそらくは「このアプリケーションはアプリ コンテナーのコンテキストでのみ実行できます。」と表示されて起動しなかったと思う。
これは/APPCONTAINERオプションを指定してコンパイルされたものでAppContainer以外では動作しない。
そこで次はTestAppContainer2.exeを起動してみてほしい。
こちらは単に/APPCONTAINERオプションを外しただけのものになる。
動作としてはいろいろなフォルダにアクセスして中身を列挙するというものだ。

では完成品であるStartAppContainer.exeにTestAppContainer.exeをドラッグ&ドロップしてみてほしい。
今度はちゃんと起動したと思う。
また動作もTestAppContainer2.exeを起動したときと違い、見えないフォルダがあったり、そもそも別フォルダを指していたりしたと思う。
それがAppContainerによるサンドボックス化された環境ということになる。

また、付与したCapabilityによる差が見たい人はTestWinRT.exeを試してみてほしい。
こちらはWinRTのAPIを用いている以外はTestAppContainer.exeと同様だが、AppContainerとして起動するとミュージックフォルダが取得できるようになっているはずだ。
これはCapabilityでMusicLibraryを付与したためにアクセス可能になったことを表している。
TestAppContainer.exeで見えていないのはCapabilityがあくまでWinRTのAPIにしか影響しないためだ。

その他cmd.exeをAppContainerで起動してみるなどもできるのでいろいろ試してみてほしい。

終わりに

どうだっただろうか。
今回はあくまでAppContainerで起動するところまでを目標としたが、zipの中では紹介していないAPIを使用するコードなども入れてあり、また次の記事では関連APIについてもまとめているので、興味があれば見ていってほしい。

最初にも書いたがIEもAppContainerにより脆弱性が見つかってもできるだけ他所に波及しないようにと作られている。
この記事を読んでそういうことに興味を持ってくれる人が一人でも増えたら幸いだ。

ではまた何か機会があればその時に。