Balanced use of obfuscation - Gold award
バランスの良い難読化の使い方 - 金賞
受賞者:Adar Zeitak
引用元:https://www.ioccc.org/2012/zeitak/zeitak.c
審査員・作者による説明:https://www.ioccc.org/2012/zeitak/hint.html
動作
カッコの対応をチェックしてくれるツール。
$ cat incorrect.c
#include <stdio.h>
int main() {
int i=40;
printf("%c",(char)(i)));
return 0;
}
$ gcc -o zeitak zeitak.c
$ ./zeitak < incorrect.c
));
^ Error
元ソースコードはカッコが乱用されていて非常にややこしいが、ちゃんとチェックが通る。
$ ./zeitak < zeitak.c
OK
解説
コード形状は、タブ幅4で見る必要がある。
左右に丸カッコ()
、上下に横向きにかぎカッコ[]
と波カッコ{}
が配置されていて、その真ん中に小さくイコールがある。
つまり、カッコの対応が取れていることを表現している。
コードは次の制限を課して書かれている。
- 数字を使わない
- 文字リテラルも使わない
- 標準ライブラリもほぼ使わない(
printf()
とfread()
とfwrite()
とstdout
とstderr
程度)
- 制御構造を使わない(
?:
も使わない)
- 算術演算も論理演算も使わない
残るは、関数呼び出し、キャスト、配列・ポインタ、とのこと。
文字列リテラルの中にあるカッコは無視するようにしているとのことだが、文字列リテラルの中で\"
を使っている可能性までは考慮していない。
挙動が比較的単純なので見逃しがちだが、この実装はIOCCCの中でも最悪レベルの難読度だと思う。
腕に覚えがある人は、ぜひ自分で解読を試みてほしい。
以下、解析の結果わかったことを記す。
大まかなアイデアとしては、ASCIIコードでテーブルをルックアップすることで分岐を行う。
しかし、そのアイデアがわかってもメイン処理を読み解くまでには非常に遠い。
難読化の中心は、次の2つの関数からなる。
- 2つの引数をうけとり、1つめを返す関数
__
- 2つの引数をうけとり、2つめを返す関数
_
数字を使わないという制約のために、これらの関数を1と0の代わりに使っている。
ほとんどの変数には__
または_
が入っている(変数r
だけは、__
と_
に加えて再帰のためにf
が入ることもあるので注意)。
制御構造や比較を使わずに値を選択するのも、これらの関数が肝になっている。
変数x
に__
または_
が入っているとき、x(A,B)
という式は、x==__
ならA
に、x==_
ならB
になる。
つまり、これが値の選択として使える(ただし、x(A,B)
ではA
もB
も必ず評価されることには注意する必要がある)。
変数x
をビット反転するにはx(_,__)
として呼び出す。
ほぼすべての処理は関数f
で行われる。
この関数はかんたんに言えば、1文字読み込み、期待する閉じカッコが来るかどうかを検査する。
関数f
は引数a
とb
を取るが、これは現在期待している閉じカッコの種類を表している。
a == _
かつb == _
ならば、丸カッコ()
の対応を検査している
a == __
かつb == _
ならば、かぎカッコ[]
の対応を検査している
a == _
かつb == __
ならば、波カッコ{}
の対応を検査している
なお、a == __
かつb == __
ならば、すでにカッコの対応が取れていないことが判明していて、当該箇所をもう2文字出力するための余分な再帰を行っている状態を表している。
関数f
は、文字を読み込んだら、ルックアップテーブルであるl
を文字コードで引いて、文字種を表すデータ構造(後述)を変数d
に読み込み、それに応じて処理を振り分ける。
- 文字が閉じカッコであれば、現在検査中のカッコと種類が同じかをチェックし、同じであれば1を返し、間違っていたら前後の文字を出力しつつ、0を返す
- 文字が開きカッコであれば、関数
f(新しいカッコの種類)
という再帰を行う
- 文字がカッコでなければ、次の文字を読み込む
実際には、シングルクォートやダブルクォートの場合の処理も行う。非常にややこしい。
最後に、これみよがしに定義されているstruct
について。
typedef struct s { struct s* a ; struct s* UNUSED; } s;
typedef struct t { struct s* UNUSED; struct s* a ; } *t;
UNUSED
というフィールドはアクセスされないが、これはキャストを通してアクセスする。
s
型の値から.a
のフィールドを読み出すと1つめが得られるが、t
にキャストしてから.a
を読み出すと2つめが取り出せる。
main
から読み始めると、関数C
がこの2要素のstruct
をmalloc()
しているので、Lispのコンスセルであることに気づく。
よって、非常に重要な役割をあたしているのかと思わせるが、これはひっかけであり、関数C
はmain
の中でしか使われていない(関数f
の中では、ローカル変数としてC
が再定義されている)。
この2要素のstruct
は、上述の文字種を表すデータ構造と、グローバル変数として使われているだけである。
文字種を表すデータ構造は、2要素のstruct
を2段にネストして使っており、各要素には__
か_
のどちらが入るので、4ビットの情報となっている。
各ビットをABCDとして、次のように解釈される。
- AB = 10ならば、開きカッコを表す。カッコの種類はCDに入っていて、CD = 00ならば丸カッコ、CD = 01ならば波カッコ、CD = 10ならばかぎカッコ。
- AB = 11ならば、閉じカッコを表す。カッコの種類はCDに入っていて、開きカッコと同じ。
- AB = 01ならば、クォートを表す。クォートの種類はCに入っていて、C = 0ならばダブルクォート、C = 1ならばシングルクォート。
- AB = 00ならば、それ以外の文字を表す。
グローバル変数w
は、2要素のstruct
を3段にネストして使っているので、8ビットの情報になっているが、見落としがなければ4ビットしか使っていないと思う。
1ビットめは最初のカッコに到達したかどうかのフラグ、2ビットめはおそらくクォートの種別、3ビットめと4ビットめはエラー状態だと思う。
このあたりは解析を簡単にするために無視したので、やや自信がない。