拾い物のコンパス

まともに書いたメモ

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 mainC言語において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 ffb7が呼び出すアドレスになっている.ここがずれると上手く動かない場合がある.
よって,配列の要素として埋め込むときは一回適当な値でコンパイルして,どれくらいずれているのかを確認して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に代入されている-610xffffffc3で,命令としては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, CODEDATAフラグが立っていて,書き込めそうになかった.
唯一書き込めそうな.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

"main関数の無いプログラム"の解析 - みずぴー日記

Linux カーネルのコンテキストスイッチ処理を読み解く - naoyaのはてなダイアリー