こんにちは、羽田です。2月11日の BSides Tokyo 2023 にて、Power Automate Packing というタイトルで発表しました。
昨年 AVTokyo 2022 で発表した Power Automate C2 の延長線上にあたる検討で、Power Automate の厳しい制約のもとプリミティブな機能だけを用いて RC4 を実装した、という内容になります。実用性には若干乏しいものの、Power Automate でバイナリ文字列を扱うというハックと、その手法の限界について知見が得られたので本イベントで紹介しました。
Power Automate Packing のコンセプト
Power Automate C2 の詳細は 以前の記事 で解説したとおり、クラウド上で C2 サーバーと対話を行いながら JSON 形式のペイロードをダウンロードして実行します。このペイロードについて暗号アルゴリズムを用いてパッキングすることを考えます。
圧縮や暗号化などの機能は Desktop Flow として提供されているのですが、クラウドで完結しません。また、単に暗号アルゴリズムを実装するだけならカスタムコネクタでも実現できますが、攻撃者の視点でなるべく痕跡を残したくありません。そこで、単一フロー内で完結する暗号アルゴリズムは実装できるのか、という疑問が発端になり検討を行いました。Power Automate で実装できるフローの限界を探るという意味でも個人的には興味深いテーマでした。
実際に試してみたところ Power Automate の基本機能は想像以上に制約が厳しく、特にバイナリの取り扱いは全く想定されていないことから最初は不可能かとも考えましたが、いくつかの暗号アルゴリズムの実装を試みた中で、RC4 の実装に成功しましたので紹介します。
RC4
RC4 のアルゴリズムについて詳細な説明は割愛しますが、以下のように 3つの工程(KeySetup、PRGA、XOR)が含まれます。
今回の話に関係する部分として着目したいのは緑色にマークした部分で、ループ操作を行うこと(KeySetup で 256 回、PRGA でメッセージ長の回数、XOR でメッセージ長の回数)、Swap 操作(変数の入れ替え)を行うこと、ビット演算の XOR を行うこと、の 3点です。この部分が実装できるかどうかがカギとなります。
Power Automate の変数操作とその制約
Power Automate における変数操作は、通常のプログラミング言語と同様、変数の初期化、変数の設定などのアクションとして提供されています。また、変数に値を設定する際に、数値演算や文字列操作など、基本的な関数が用意されていて、これを使用することができます。
今回、変数操作において制約となった部分は以下のものがありました。
代入処理における制約
- 変数設定の際に、自分自身を参照できないという問題がありました。例えば、i = (i + 1) % 256 という式を考えた場合、tmp = (i + 1) % 256 と i = tmp という 2ステップが必要となります。これはフローの見通しが悪くなるだけでなく、後に処理性能にも大きくかかわることになりました。
- 式の長さの上限は 8192 文字で、これ以上長い式は登録できません。
- 'A' → 65 や 65 → 'A' など、アスキーコードに関する変換を行う関数が提供されていません。
- 65 → 1000001 や 1000001 → 65 など、2進数に関する変換を行う関数が提供されていません。
配列操作における制約
- 配列要素への要素を指定した代入ができません。例えば、a という配列に対して a[10] = 65 といった操作ができません。
- 配列の検索ができません。例えば、a = array(65, 66, 67) という配列に対して 66 が何番目にあるか検索するための関数がありません。(contains() という関数は存在しますが、要素を含むかどうかの判定しか行いませんでした)
ビット演算における制約
- xor() が存在しません。
- and()、or() は存在しますが、ビット演算ではなく論理演算を提供する関数です。要するに、引数には True / False の 2値しか取ることができません。
Power Automate の繰り返し操作とその制約
Power Automate では繰り返しの実装方法は 2種類あります。
「Apply to each」アクション
一般的な言語でいう for each に相当する基本的なループ機能を提供します。以下の例では 0 ~ 255 の配列に対してループを回し、それぞれ中のフローを実行します。これはネストすることも可能です。
「選択」アクション
一般的な言語でいう map に相当する繰り返し処理を提供します。以下の例では 0 ~ 7 の配列に対してそれぞれ「マップ」欄に指定された処理を施します。これは、ネストすることができない、1 ステップで処理を記述しなければなない、それぞれの処理が独立している必要がある、といった制約があります。実装できる内容も大きく制限されますが、「Apply to each」よりもはるかに高速なためできる限りこちらを使用することが推奨されます。
制約の回避
これらの制約に頭を悩ませつつ、課題をひとつずつクリアしてまずは愚直な実装を作ってみました。以下ではどのように制約を回避したか説明します。
型変換の問題の回避
整数から 16進数、整数から 2進数、整数からアスキー文字、の変換のためのテーブルを用意しました。このテーブルを参照することで整数からの型変換が行えるようになりました。
一方で 2進数から整数に戻すための逆引きは、単なるテーブルでは実現できませんでした。ここでは、0 ~ 255 までのビット列を全て文字列として結合した上で、検索したい文字列を探すことにしました。例えば、以下の例では 00000000000000010000001000000011... という文字列から 01000001 という文字列を検索して、そのインデックスから元の値を計算する(1 で引いて 18 で割る)と 65 という値が得られます。
配列に代入できない問題はオブジェクト(JSON)を用いて回避しました。オブジェクトであれば a[10] = 65 などの操作が可能となります。一方で配列では可能であった部分文字列の取得などの操作ができなくなりますが、今回は問題ありませんでした。
次に、整数(0 ~ 255)同士の XOR を考えます。1 bit の and()、or() を行う関数があったことを思い出し、まずは整数を 8 bit の配列に分解します。さらに xor は x^y = ^((x & y) | (^x & ^y)) という恒等式で表現できることを利用し、以下のように実装しました。
実行
事前に RC4 で暗号化した 933 バイトのペイロード(JSON データ)を用意して実行してみたところ、以下の結果となりました。正しく復号してペイロードを新規フローとして実行することはできましたが、完了するまでに 30分ほどかかってしまいました。
高速化に向けた工夫
ここでかなり頭を悩ませましたが、まずはボトルネックを探ることにしました。一般的に Apply to each は遅いと言われるためここに改善ポイントがありそうです。Aplly to each の設定では 50 まで並列化することが可能でしたが、実行順序が保証されないため、RC4 の計算では利用できません。
ここで実験するうちに経験則として分かったのですが、Power Automate の実行時間は大まかにフローのステップ数(箱の数)に大きく影響していました。逆にいうと、各ステップの中の処理の複雑さにはほとんど影響しません。Apply to each が遅いと言われるのは、ステップ数が (Apply to each 内のステップ数+Apply to each 自身のステップ数)× ループ回数 だけ必要となるためだと考えました。つまり、とにかくステップ数を削減する方針を立てました。
XOR の高速化
XOR の処理を Apply to each で実装していましたが、これは削減できそうと考えました。最初は メッセージのバイト長だけ Apply to each でループを回し、その中で 8 bit それぞれのビットに対して XOR を選択で計算する、という実装を行いました。例えば 1000 文字の XOR を計算するのに、Apply to each で 1000 回、選択で 8 回のループを処理していました。
これは以下のようにひとつの選択処理にまとめることができます。メッセージの各バイトに対してビットに変換し、各ビットの XOR を計算した後に整数に戻す、という操作を 1ステップで制限文字数内におさめながら表現することができたので、Apply to each を廃止して選択に変更しました。ここだけで 10分程度の処理が 1秒未満に短縮でき、大幅な高速化を達成しました。
KSA、PRGA のループ回数、代入回数の削減
KSA、PRGA でも Apply to each を使用していますが、このループは前の処理に依存した処理を行う必要があるため、選択に置き換えることはできません。ここでステップ数を削減するために、Aplly to each 内の処理を展開してステップ数を抑えることを考えました。例えば、以下は全て同等の処理ですが、自己参照防止のために使用していた一時変数 tmp が不要となり、ステップ数を大きく削減できていることが分かります。
swap 処理の高速化
RC4 では、テーブル(sbox)内の変数の値を交換する swap 処理が大量に発生します。一般的には 3ステップで記述するのですが、このテーブルをオブジェクトとして実装しましたので、setProperty() 関数を用いて置換処理を一気に記述することができます。
最終的な実行結果
これらの工夫により、最終的に 933 バイトの暗号文に対して実行時間を 3~4 分まで削減することができました。これ以上改善できそうなポイントがなかったので、このあたりが限界と考えて検討を終了しました。
おわりに
今回の発表では、Power Automate C2 のペイロードを暗号化することを目的として、単一フロー内でプリミティブな機能だけを用いて RC4 を実装しました。Power Automate ではバイナリを操作することは想定されていませんが、これを可能とするためのハックと、実行時間を短縮するためのアイデアを紹介しました。
今回の RC4 の実装では乱数生成にかかる時間が支配的でしたので、乱数はあらかじめ生成しておいて One-Time Pad を用いる方式にすると実用的になるのではと考えています。また、Power Automate は頻繁に機能追加がなされていますので、今回のような制約ももしかしたら緩和されていくのかもしれません。今後の動向にも注目していきたいです。