main関数の無いプログラムを動かすために奔走した話
ディレクトリを眺めていたら,セキュリティを始める直前あたりに頑張ったプログラムが見つかった.
初めの一歩みたいなものだから,改めて見直しがてらいじったことを書き残す.
内容はmain
関数が実装されていないけど動くプログラム.
確か元ネタはこれ.
そのままじゃ動かないから動くよう修正した.
失敗したやり方も書いておく.
環境
$ uname -a Linux poppycompass 4.12.5-1-ARCH #1 SMP PREEMPT Fri Aug 11 12:40:21 CEST 2017 x86_64 GNU/Linux $ gcc -v gcc (GCC) 7.1.1 20170630 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
何のプログラムか
C言語でプログラムを書くときには必ずと行っていいほどmain
関数を書く.
ライブラリでない限り,これがないとエラーを吐くはず.
頑張ればint main(void)
を書かなくても動くプログラムを書ける,という話.
通常では以下のようなコード.
/* normal.c */ #include <stdio.h> void put(char c) { putchar(c); } int main(void) { put('A'); return 0; } /* E.O.F. */
結論コード
結論としてのコードは以下の通り.x86/x86-64の両方のバージョンを作成した.
32ビット版.ret
の部分はなくても動く.
/* no_main32.c */ #include <stdio.h> void put(char c) { putchar(c); } extern int ret[1]; __attribute__((section(".text"))) unsigned int main[] = { 0x83e58955, 0xec83f0e4, 0x2404c710, 0x00000041, 0xffffcfe8, 0x90c3c9ff, }; int ret[] = {-61}; /* E.O.F. */
下は64ビット版.
/* no_main64.c */ #include <stdio.h> void put(char c) { putchar(c); } __attribute__((section(".text"))) unsigned long int main[] = {0x000041bfe5894855, 0x00b8ffffffd2e800, 0x1f0f66c35d000000, 0x909090900000441f}; /* E.O.F. */
どちらも普通にコンパイルすれば動く.
64ビット版は最初unsigned int
にしていたらエラーが出た.
64ビットでもint
は4バイトなのか・・・.
$ gcc no_main64.c /tmp/cchS2iOj.s: Assembler messages: /tmp/cchS2iOj.s:27: Warning: ignoring changed section attributes for .text $ ./a.out A%
警告が出ているが,ちゃんと__attribute__
が動いているみたい.
よくわからないツンデレメッセージ.
解説
手元のOSが64ビットだから,64ビットベースの説明する.
注目すべきは当然unsigned long int main
.
C言語においてmain関数が最初に呼ばれるのは,libcがmainというシンボルをバイナリの中から見つけて,それをエントリポイントとして処理を開始するようになっているかららしい.
よって,main
というシンボルを含んでさえ入れば,これがエントリポイントとして登録される.
配列の中身は通常のコードをコンパイルした際に生成される機械語をそのまま抜き出して,リトルエンディアンで格納しただけ.内容としては以下の通り.
$ gcc normal.c $ objdump -M intel -d ./a.out | grep "<main>:" -A10 ...(main部のみ抜粋)... 00000000000006a5 <main>: 6a5: 55 push rbp 6a6: 48 89 e5 mov rbp,rsp 6a9: bf 41 00 00 00 mov edi,0x41 6ae: e8 d7 ff ff ff call 68a <put> 6b3: b8 00 00 00 00 mov eax,0x0 6b8: 5d pop rbp 6b9: c3 ret 6ba: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] ...(snip)...
スタック処理の後は引数('A')をedi
に入れることで引数を渡し(x64では第1引数はrdi
に格納),呼び出すだけ.
ここでのポイントは,call 68a <put>
のところ.
e8 b7 ff ff ff
のb7
が呼び出すアドレスになっている.ここがずれると上手く動かない場合がある.
よって,配列の要素として埋め込むときは一回適当な値でコンパイルして,どれくらいずれているのかを確認してput関数の先頭になるよう調整する必要がある.
これで準備は整ったと思いきや,まだ動かない.今の状態では,mainはオブジェクトとして認識されて実行できない領域に格納される.
実際に確認してみると,
$ readelf -s ./a.out | grep "\<main\>" 0000000000201040 32 OBJECT GLOBAL DEFAULT 24 main 0000000000201040に配列の`main`が格納されていることがわかる. $ objdump -h ./a.out ...(snip)... 23 .data 00000044 0000000000201020 0000000000201020 00001020 2**5 CONTENTS, ALLOC, LOAD, DATA 24 .bss 00000004 0000000000201064 0000000000201064 00001064 2**0 ALLOC ...(snip)...
上記のように初期値付きのデータであるため,.dataセクションに配置されている.
.dataのフラグはCONTENTS, ALLOC, LOAD, DATA
.
詳しく調べきれていないが,この中でDATA
は実行不可だが書き込み可能.
よって実行できない.
実行するためには実行可能なコードが置かれる領域である.text
セクションへ置いてもらう必要がある.
これを指定するために__attribute__
を使っている.
実行可能なセクションに置きさえすれば実行できておしまい.
32ビット版ではret
という配列を作っているが,これは元ネタの関数ではret
が含まれていないから,その代わりにしたんじゃないだろうか.
ret
に代入されている-61
は0xffffffc3
で,命令としてはret
となる.
恐らく,__attribute__
を使わずにコンパイルしているから,配列mainとretは続けて配置されて,main配列が実行された後はret配列内の要素を実行するようになっていたんじゃなかろうか(適当な憶測).
つまり,今回のコードでは無用の不要ということだ!
元ネタコードは,extern int ret
は初期値を後で入れているから,多分.data
セクションへ配置される..data
セクションに配置されるのにどうやって実行したのか気になるところ.もしかして古い環境だから.data
も実行できたのかな.
課題
main配列を関数として実行するよう頑張ったが,問題がある.
$ objdump -M intel -d ./a.out | grep "<main>:" -A3 00000000000006c0 <main>: 6c0: 55 48 89 e5 bf 41 00 00 00 e8 d2 ff ff ff b8 00 UH.. 6d0: 00 00 00 5d c3 66 0f 1f 1f 44 00 00 90 90 90 90 ...]
ご覧の通り関数としてディスアセンブルされない.
objdumpのオプションを-d
から全セクションアセンブルの-D
に変えれば見える.
原因をちゃんと突き止められていないが,怪しそうなのはこれ.
$ readelf -s ./a.out | grep "\<main\>" 00000000000006c0 32 OBJECT GLOBAL DEFAULT 14 main
3つ目にOBJECT
とある.mainはオブジェクトとして見られてしまっている.
ここをFUNC
としたいところだけど,Cでのやり方が見つからなかった.
GNU asなどではtypeディレクティブを使って指定できる.
失敗談
.text以外のセクションに配置して頑張る
他のセクションで,書き込める場所を探したが全部READONLY, CODE
かDATA
フラグが立っていて,書き込めそうになかった.
唯一書き込めそうな.bss
セクションは初期値なしの変数しか格納できない.つまり無理.
流石現代OS,隙がない.
mainの内容を動的に確保してコピーすれば実行はできるが,アドレスが変わるから,それでは意味がない.
.dataセクションに実行権限を付与
OS機構に逆らおうとする話.
論文とかでDynamic generated code
とか呼ばれる手法の一つ.マルウェアがよく使うやつ.
mmap((void *)main, 0x20, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
でmainに書き込み権限をつけようとしたが,mmapは違うアドレスを返して頓挫.
mmapの第1引数addr
は与えたアドレスを参考にはするが,ダメなときはきっぱり違うところを返す.
まとめ
C言語がmain関数から実行するのは,libcがmainというシンボルをエントリポイントに設定するようになっているから.
関数でないmainを作成するときの条件は,
- mainという名前であること
- 実行可能領域(主に.text)に置かれること
参考
GNU Compiler Collection (GCC) Internals: Machine Modes
Using the GNU Compiler Collection (GCC): Label Attributes
linux - Flags in objdump output of object file - Stack Overflow