記号のみで任意のPowerShellコードを実行

2010-12-11

こんにちは、牟田口大介と申します。
はせがわさんが記号プログラミングを楽しんでらっしゃるのに触発されて私もやってみたくなり、得意なPowerShellで挑戦してみました。
PowerShellってどんな言語?どんな文法?ということに関しては以前ブログに書きましたのでそちらをどうぞ。
PowerShell基礎文法最速マスター - PowerShell Scripting Weblog

もっとも簡単な記号プログラミング

PowerShellコードは上から順に一行ずつ実行され、実行した行の出力オブジェクトがクラスごとに定義された書式に基づいて、逐一ホストに表示されます。
文字列(System.String)は文字列そのままの形で出力されます。なので、"Hello, world!"を表示させるには単に

"Hello, world!"

とするだけです。よって「記号だけで何か文字列を表示せよ」というお題なら

"(^_^;)"

とかでクリアです。
もちろんこれでは面白くもなんともないので、ハードルを高く設定しましょう。目指すは「任意のPowerShellコードを記号のみで記述可能にする。ただしその過程においてコマンドレットの使用禁止」です。
コマンドレットの使用を縛ったのは、なんだかずるい気がしたからです。コマンドレットは他言語の組み込み関数に相当する機能ですが、どちらかというとコマンドラインツールに近いので…。

おおまかな戦略

「任意のコードを記号だけで実行する」を実現するには

  1. 任意のアルファベットを生成する
  2. それを連結してeval()する

のが基本になるかと思います。しかし1.を頑張ったところで、PowerShellにはeval()に相当する構文がないので実現不可能です。終わり。

…というわけにもいかないのでコマンドレットを一つだけ解禁します。Invoke-Expressionコマンドレットはeval()とほぼ同じ機能で、指定文字列をPowerShellコードとみなして実行できます。これを使います。
Invoke-Expressionのデフォルトエイリアスはiexです。よって"iex"という文字列をまず作るのが目標となります。
さて、"iex"という文字列自体を実行させるにはどうするかですが、それには実行演算子&を用います。&演算子を用いると、文字列をコマンドとして実行できます。
PowerShellにおいて実行可能なコマンドは

  • コマンドレット
  • 関数
  • スクリプト
  • 外部ファイル(exe含む)

の4つだけです。これらのエイリアスもOKです。iexはコマンドレットのエイリアスなので&"iex"は実行可能です。
あとは任意のアルファベットを作ってやることですが、int型の数値はchar型にキャストして文字にすることができるのを利用しましょう。
つまり、たとえば"H"という文字を作るには、"[char]72"という文字列をiexで実行してやればいいわけです。

まとめると、

  1. 数値を作る
  2. "char"という文字列を作る
  3. "iex"という文字列を作る

これらができれば完成になるわけです。

数値を作る

数値は

  1. 0を作る
  2. 0をインクリメントして1を作る
  3. 以下、同様

という方法で作っていきます。

${;}=+$()
${=}=${;}    #0
${+}=++${;}  #1
${@}=++${;}  #2
${.}=++${;}  #3
${[}=++${;}  #4
${]}=++${;}  #5
${(}=++${;}  #6
${)}=++${;}  #7
${&}=++${;}  #8
${|}=++${;}  #9

PowerShellにはそのままでは変数名として使えない記号がほとんどですが、${変数名}と記述することで記号を含めた任意の文字を変数名にすることができます。
まず変数${;}を用意します。$()は空の部分式で、$nullと等しくなります。これに+を付けると$nullがintにキャストされて0になります。それを${;}に代入。
次に++${;}とインクリメントすることで1を作ります。
他の数値も順に作っていきそれぞれ変数に入れておきます。

"char"を作る

"r"はTrueから作ります。Trueを作るのは簡単で、自動変数$?を使います。$?は前の行でエラーが発生していなければTrueが格納されます。
"$?"とすると文字列化でき"True"が得られます。
2文字目が欲しいわけですが、文字列にインデックスを付けるとそのインデックスの場所にある文字を取得できることを利用します。つまり

"$?"[1]

ですね。1はもう作ってあるので

"$?"[${+}]

これで"r"が作れました。

"c","h","a"の文字は連想配列から作ります。PowerShellの連想配列は

$hash=@{a=1;b=2}

という感じで作りますが、ここでは連想配列ならなんでもいいので、空の連想配列@{}を用意します。
これを部分式$()の中に格納し、"$(@{})"としてstringにキャストすると"System.Collections.Hashtable"という文字列が得られます。これは連想配列クラスの完全修飾名ですね。
この文字列には8文字目に"C"、20文字目に"H"、21文字目に"a"が含まれているので"r"のときと同様に取り出します。
ここでインデックスに19など2ケタの数値を指定したい場合は、

"$(@{})"["${+}${|}"]

のように""内で数値変数を並べて書くだけで簡単に得られます。

以上をまとめると

${"}="["+"$(@{})"[${)}]+"$(@{})"["${+}${|}"]+"$(@{})"["${@}${=}"]+"$?"[${+}]+"]"

となり、これで${"}に"[CHar]"という文字列が格納されます。

"iex"を作る

"i"と"e"は"System.Collections.Hashtable"に含まれているので、あと足りない文字は"x"だけです。
ところでSystem.StringにはInsert()というメソッドがあります。このメソッドのシグネチャはPowerShell 2.0 + .NET 2.0~4では

string Insert(int startIndex, string value)

で、"x"があります。これを何とかして持ってきます。
PowerShellで

<オブジェクト>.メソッド名

とするとメソッド情報が格納されたPSMethodオブジェクトを取得できます。またPSMethodオブジェクトをstringにキャストするとそのメソッドのシグネチャが文字列として得られます。
つまり

$method="".insert
"$method"

このようなコードを記号で記述すればいいわけです。insertの部分は文字列"insert"でもOKです。
"insert"の構成文字はこれまででてきた文字列にすべて含まれているので、これまでと同様に抽出して結合して生成します。
まとめるとこんな感じです。

${;}="".("$(@{})"["${+}${[}"]+"$(@{})"["${+}${(}"]+"$(@{})"[${=}]+"$(@{})"[${[}]+"$?"[${+}]+"$(@{})"[${.}])

これで${;}にはPSMethodオブジェクトが格納され、 "${;}"["${@}${)}"]とすれば"x"が得られます。
(注:PowerShell 1.0ではシグネチャが少し違うのでインデックスの数値を変更し"${;}"["${.}${(}"]にする必要あり)
以上をまとめて

${;}="$(@{})"["${+}${[}"]+"$(@{})"[${[}]+"${;}"["${@}${)}"]

${;}に"iex"という文字列が格納できました。

任意のコードを実行

これですべての準備が整いました。あとは任意のコードを実行しましょう。任意のコード文字列を文字コード番号に変換し、

"[char]文字コード番号 + [char]文字コード番号 + ... | iex" | &"iex"

を実行します。実際には文字列部分はこれまでに記号で作った変数をあてはめます。

その前に、任意のコード文字列を文字コード番号に変換し、そのコード番号を最初に作った数値変数に置き換え、その文字列を[char]にキャストするというコード文字列を自動で作る関数を別に書いておきます。

function Get-EncodedCode
{
    param([string]$code)
    ([char[]]$code|
    %{
        '${"}'+ ([int]$_  -replace "0",'${=}' -replace "1",'${+}' -replace "2",'${@}' -replace "3",'${.}' -replace "4",'${[}' -replace "5",'${]}' -replace "6",'${(}' -replace "7",'${)}' -replace "8",'${&}' -replace "9",'${|}')
    })  -join '+'
}

この関数を使って、「"Hello, world!"」というコードをエンコード(?)します。

Get-EncodedCode '"Hello, world!"'

結果は

${"}${.}${[}+${"}${)}${@}+${"}${+}${=}${+}+${"}${+}${=}${&}+${"}${+}${=}${&}+${"}${+}${+}${+}+${"}${[}${[}+${"}${.}${@}+${"}${+}${+}${|}+${"}${+}${+}${+}+${"}${+}${+}${[}+${"}${+}${=}${&}+${"}${+}${=}${=}+${"}${.}${.}+${"}${.}${[}

のようになります。これを埋め込みましょう。

"${"}${.}${[}+${"}${)}${@}+${"}${+}${=}${+}+${"}${+}${=}${&}+${"}${+}${=}${&}+${"}${+}${+}${+}+${"}${[}${[}+${"}${.}${@}+${"}${+}${+}${|}+${"}${+}${+}${+}+${"}${+}${+}${[}+${"}${+}${=}${&}+${"}${+}${=}${=}+${"}${.}${.}+${"}${.}${[}|${;}"|&${;}

実行結果はめでたく

Hello, world!

となります。
埋め込むコードはPowerShellで動くコードならなんでもOKです。
(注:ここではやりませんでしたが、記号化コード全体を生成する関数を書いてもいいですね)

記号だけでHello, world!を表示

最後にスクリプト全体を載せておきます。

${;}=+$();${=}=${;};${+}=++${;};${@}=++${;};${.}=++${;};${[}=++${;};
${]}=++${;};${(}=++${;};${)}=++${;};${&}=++${;};${|}=++${;};
${"}="["+"$(@{})"[${)}]+"$(@{})"["${+}${|}"]+"$(@{})"["${@}${=}"]+"$?"[${+}]+"]";
${;}="".("$(@{})"["${+}${[}"]+"$(@{})"["${+}${(}"]+"$(@{})"[${=}]+"$(@{})"[${[}]+"$?"[${+}]+"$(@{})"[${.}]);
${;}="$(@{})"["${+}${[}"]+"$(@{})"[${[}]+"${;}"["${@}${)}"];
"${"}${.}${[}+${"}${)}${@}+${"}${+}${=}${+}+${"}${+}${=}${&}+${"}${+}${=}${&}+${"}${+}${+}${+}+${"}${[}${[}+${"}${.}${@}+${"}${+}${+}${|}+${"}${+}${+}${+}+${"}${+}${+}${[}+${"}${+}${=}${&}+${"}${+}${=}${=}+${"}${.}${.}+${"}${.}${[}|${;}"|&${;};

感想と課題

記号プログラミングは難読化コードが書けるという以外はあまりメリットはなさそうですが、言語仕様を深く知ったり何より頭の体操にはもってこいだと思います。
今回初めてやってみたわけですが、もう少し文字種を減らしたいですね。あとiexを使わない方法は本当にないんでしょうか。リダイレクトを使ってスクリプトファイルを生成しそれを実行するくらいでしょうか。
実はもっとずっと簡便な方法があるような気がしてなりません。アイデアありましたら是非どうぞ。