バランスの良い難読化の使い方 - 金賞
#include <stdio.h>
#include <stdlib.h>
#define s(_)sizeof(_)
#define n void*
#define z(_)_,_,_
#define x (s*)__
#define y (s*)_
#define h C(y,y)
#define o &d
#define t() (p)
#define w(_)_,_
typedef n (*(*(*(*p)(n,n))(n,n))(n,n))(n,n);
typedef struct s { struct s* a ; struct s* UNUSED; } s;
typedef struct t { struct s* UNUSED; struct s* a ; } *t;
n __(n _,n __) { return _;}n _(n _,n __){return __; }
typedef unsigned char e;
#define _(_)((*_).a)
s*
w,
a={x ,x},
b={x ,y},
c={y,x},d={y,y};s l[]={&b,&d,w (w(w(w(w(w(&d)))))),w(&d),&c,&d,w(w(w(
&d))),&c,w(&b),&d,&a,z(&d),z(w (w(w(w(w(&d)))))),&b,&b,w(&d),&a,&b,w(&
d), z(w
(w( w(w
(&d))))),w(w(w(&d))), &b,&c,&d, &d,&a,&c ,w(w(&d)), &c,z(&b) ,w(&d),w(&a)
};s* C(s* a, s* b) {s* _=malloc(s(s));_(_)=a;_((t)_)=b;return _;}e k2=s(l
);p f(p
a,p b)
{ e k;s d;p v,r, q,i,C,c,u,g,m=t( )
_ ( _(_(w)));C=t() _(_(&l[fread(&k,s(e )
,s ( e),stdin)]));v =C(_,__);d=l[k=(e )
C ( k2,k)];c=(u=a( b,_),i=(t()_((t )
_ ( _(w))))(_,_(_(o)) ),(a(_(_((t)o)),(t( )
_ ( _((t)o)))(_,__) ))(b(_((t)_((t)o) )
, ( t()_((t)_((t)o))) (_,__)),_));{p a=t( )
_ ( (t)_(o));{p b= C(_,i(u(_,__),_) )
; { p u=C(_,(t()_(_(o) ))(_,__));_(_(_(w)) )
= ( s*)i(__,_(_( _(w))));r=b(m(c,_ )
,C ( m(_,(t()_(( t)_(_(w))))(_,__) )
, ( f)));v=b(_,v);i =b(a(_,__),_);g=(b )
( a(m(c(_,__),__ ),_),_);q=u(_((t )
_ ( o)),_)((t()_( (t)_(_(w))))((t( )
_ ( _((t)o)))(_(_((t )_(w))),(t()_(_((t )
_ ( w))))(_,__))( _,__),_)(_,__),_ )
;_ ( _((t)_(w))) = (s*)u(_((t)_(o)),_ )
( (t()_((t)_(_(w)))) (_,__),_)(_(_((t)o) )
,_ ( _((t)_(w))));} }_((t)_(_(w)))=(s* )
q ( a=(t()_((t)_(_(w))) )(_,__),_((t)_(_(w))) )
; fwrite((k=k,&k2) ,s(e),u(_,g)(s(e )
, ( e)s(s[s(s[s(s[s (s)])])])),stdout )
; fwrite((k2=k,&k) ,s(e),u(__,g)(s(e )
, ( e)s(s[s(s[s(s[s (s)])])])),stdout )
;_ ( _(_((t)w)))=(s*) u((t()_(_(_((t)w))) )
( _,__),_(_(_((t)w ))));_(_((t)_(w)) )
= ( s*)q(a(_(_((t) o)),_(_((t)_(w))) )
,_ ( _((t)_(w))));} _((t)_(_((t)w)))=(s* )
( t()_(_(_((t)w))) )(_((t)_(_((t)w)) )
,u ( (t()_((t)_(_(( t)w))))(_,__),_((t )
_ ( _((t)w)))));g =u((q=u(C(__,(t( )
_ ( _(_((t)w))))(_((t )_(_((t)w))),_)),_) )
( _,__),g);v=g( __,i(u(_,i(f,_) )
( _(_((t)o)),_( (t)_((t)o))),v) )
,r= q(_
,g( f,i
(v(f,(_(_(_(w)))=(s*)m,_)),r)));return q(_,v)(r(g(__,a),g(__,b)),r);}
int main
(){w=C(C(h,h),C(h,h));return printf((e*)f(_,_)("OK\n","\n ^ Error\n"
"" ))
;}
引用元: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つの関数からなる。
__
_
数字を使わないという制約のために、これらの関数を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
に読み込み、それに応じて処理を振り分ける。
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として、次のように解釈される。
グローバル変数w
は、2要素のstruct
を3段にネストして使っているので、8ビットの情報になっているが、見落としがなければ4ビットしか使っていないと思う。
1ビットめは最初のカッコに到達したかどうかのフラグ、2ビットめはおそらくクォートの種別、3ビットめと4ビットめはエラー状態だと思う。
このあたりは解析を簡単にするために無視したので、やや自信がない。