世の中にはひどい人もいたものである。素行の悪いCプログラマーがどのようなコードを書くか、その邪悪さの片鱗をご覧にいれようと思う。
言うまでもなく、初期化していない変数を参照外し (dereference) するのは「未定義の動作」を引き起こすため良くないことである。正しいCプログラマーはそんなことをしてはならない。
CERT Cコーディングスタンダードはもとより、未定義や実装定義の部分を排除するMISRA Cでも初期化していない変数を読み込んではならないとしている。
- CERT C コーディングスタンダード EXP33-C. 初期化されていないメモリからの読み込みを行わない
- MISRA C Rule 9.1 “The value of an object with automatic storage duration shall not be read before it has been set"
MISRA Cでは”objects with static storage duration” (要するに通常は.bssセクションか.dataセクションに配置されるグローバル変数)については0初期化されるから違反でないとしているし、CERTのように「メモリ」と言わず、ぼくのように「変数」とも言わず、Cの標準に則って「オブジェクト」と書いており、なかなか好感を持てるのだけど、それはそれとして...
行儀の悪いプログラマーは、これらのコーディングスタンダードから逸脱した次のようなコードを平気な顔をして書くのである。悪い猫である。
uint32_t a, b;
a ^= a;
b &= a;
実を言うと、おそらく全ての場合において、予期せぬ動作は起こらず常に「a、bを0で初期化する」という動作となる。aが何であれ、a自身とのXORは0x00000000となるからだ。また、0とANDをとれば必ず0になるのだから、bもまた0以外になりようがない。
...とは言っても、こんなコードを書くべきでない。素直に
uint32_t a, b;
a = 0;
b = 0;
と書けばよいのだ。0x00を含まないシェルコードを書くわけじゃないのだから。CTFのpwnではないのだから。
もう一つ例をあげてみる。cは1で初期化され、のちに32を再代入され、dは0x100000000000000000000000000000000 (4294967296) になるかと思います。
uint32_t c;
int64_t d;
c = -(~(c^c));
d = c;
c = ((c << c) << (c << c)) << (c << c);
d <<= c;
ローカル変数 (というかautomatic storage durationのobject) はほとんどのアーキテクチャにおいてautomatic storageとしてスタックに変数を積んでいく。そのため、スタックを意図的に溢れさせる (オーバーフローさせる) ことで変数の値を書き換える、言い換えると変数に意図した値を代入することができる。
これはセキュリティ技術者向けの頭の体操として、stack-based buffer overflowの練習問題として、猫のぼくがかつて作成したコードですが、もちろん正しいCプログラマーはこんなコードを書いてはいけない。
uint32_t a, b;
uint64_t c;
a = 0;
b = 0;
c = 1 << 32;
memcpy(&b, &c, sizeof(uint64_t));
このコードを実行すると、cの下位32ビット (0x0000...000) はbにコピーされるが、bは32ビットであってcの上位32ビット分が溢れてしまう (バッファーオーバーフロー。) この溢れた32ビット分は 0x00...01 (=1) であり、これはbの隣のaにコピーされる。その結果として、a = 1, b = 0となる。
言うまでもなく、正しいCプログラマーは
uint32_t a, b;
a = 1;
b = 0;
と書くべきでしょう。
C言語においては、配列なんてものは結局のところただのポインター演算の構文糖に過ぎない。例えば、
char *a;
...
a[i] = b;
という代入文の意味するところは、
char *a;
...
*(a + i) = b;
となんら変わらない。なので、a + i = i + aであることから、なんなら
char *a;
...
i[a] = b;
と書いても構わない。つまり、配列aの0番目の要素に10を代入したければ、
a[0] = 10;
でなく、
0[a] = 10;
と書いても構わない。とは言っても、読みにくくなるだけなので、こんなコードは書くべきでないし、次のようなコードを書くのは狂っているとしか言えない。
char a[6] = { 0 };
intptr_t i = (intptr_t) a;
char *q = NULL;
q[i] = ‘h’; // ((char*) NULL)[i] と書いても良い
memcpy(&q[i + 1], “ello”, 5);
printf(“%s¥n”, a); // “hello”と表示される
もう少しひねりを効かせて一目でNULLポインターとわからなくするには、例えばこのように書くと良い。
char a[6] = { 0 };
intptr_t i = (intptr_t) a;
char *q = &a[-i];
...
血迷って、"このように書くと良い”とは言ったが、このようなコードを書いてはならない。
ご存知の通り、確かにmainというシンボルが見つからないときにはリンカーがエラーとして扱ってしまうため、mainシンボルが存在しない場合はexecutableを作ることはできない。
が、シンボルがあれば良いのであって、誰もmain 関数 が必要だなんて言っていない。main 変数 でも良い。そして、main変数に機械語をダイレクトに書いていけば、きちんと動作するプログラムを作成することができる。
例えばこんなの。x86でDEPが無効なら動くはず。
unsigned main[11] = {
1385807921, 1633890152, 1647274100, 3817434729,
1936943186, 795370615, 1751216175, 1668572463,
196141449, 2303938898, 2424360417
};
グローバル変数は通常、初期値ありなら.dataセクション、なしなら.bssセクションに配置されるが、これらのセクションは実行可能属性が付与されずDEP (データ実行保護) が有効なときは実行時例外を吐き出してSEGVで死ぬ。DEPが有効の環境では、GCCの拡張によって配置するセクションを指定する必要がある。
これなら動く。
__attribute__((section(".text")))
unsigned main[11] = {
1385807921, 1633890152, 1647274100, 3817434729,
1936943186, 795370615, 1751216175, 1668572463,
196141449, 2303938898, 2424360417
};
動くには動くけど、言うまでもなく、こんなコードを書くのは素行の悪いプログラマーとみなされて当然であるし、人権を奪われて猫になるほかない。
ちなみに、上の機械語の列は、先日シェルコーディングの実習と称してぼくが寝転びながら (ネコがネコろぶ) てきとーに書いたもので、”/bin/cat /etc/passwd”を引数としてシステムコールexecveを呼び出す。具体的には下記のようなアセンブリから生成している。
31 c0 xor %eax,%eax
99 cltd
52 push %edx
68 2f 63 61 74 push $0x7461632f
68 2f 62 69 6e push $0x6e69622f
89 e3 mov %esp,%ebx
52 push %edx
68 73 73 77 64 push $0x64777373
68 2f 2f 70 61 push $0x61702f2f
68 2f 65 74 63 push $0x6374652f
89 e1 mov %esp,%ecx
b0 0b mov $0xb,%al
52 push %edx
51 push %ecx
53 push %ebx
89 e1 mov %esp,%ecx
cd 80 int $0x80
90 nop