Skip to content

Instantly share code, notes, and snippets.

@nkoneko
Last active June 15, 2023 07:49
Show Gist options
  • Save nkoneko/9d45877dc6016b152d0d09c0ef9634f4 to your computer and use it in GitHub Desktop.
Save nkoneko/9d45877dc6016b152d0d09c0ef9634f4 to your computer and use it in GitHub Desktop.
ひどいC言語、楽しく壊そう

素行の悪いCプログラミング入門

世の中にはひどい人もいたものである。素行の悪いCプログラマーがどのようなコードを書くか、その邪悪さの片鱗をご覧にいれようと思う。

未初期化変数の使用

言うまでもなく、初期化していない変数を参照外し (dereference) するのは「未定義の動作」を引き起こすため良くないことである。正しいCプログラマーはそんなことをしてはならない。

CERT Cコーディングスタンダードはもとより、未定義や実装定義の部分を排除するMISRA Cでも初期化していない変数を読み込んではならないとしている。

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;

と書くべきでしょう。

NULLポインターを配列として扱う

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変数がある

ご存知の通り、確かに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
uint32_t a, b;
uint64_t c;
a = 0;
b = 0;
c = 1 << 32;
memcpy(&b, &c, sizeof(uint64_t));
uint32_t a, b;
a = 1;
b = 0;
uint32_t a, b;
a ^= a;
b &= a;
uint32_t a, b;
a = 0;
b = 0;
uint32_t c;
int64_t d;
c = -(~(c^c));
d = c;
c = ((c << c) << (c << c)) << (c << c);
d <<= c;
unsigned main[11] = {
1385807921, 1633890152, 1647274100, 3817434729,
1936943186, 795370615, 1751216175, 1668572463,
196141449, 2303938898, 2424360417
};
__attribute__((section(".text")))
unsigned main[11] = {
1385807921, 1633890152, 1647274100, 3817434729,
1936943186, 795370615, 1751216175, 1668572463,
196141449, 2303938898, 2424360417
};
char *a;
...
a[i] = b;
char *a;
...
*(a + i) = b;
char *a;
...
i[a] = b;
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”と表示される
char a[6] = { 0 };
intptr_t i = (intptr_t) a;
char *q = &a[-i];
...
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment