gccが生成するELF付随品について探る
この記事はTSG Advent Calendarの21日目の記事として書かれたものです。
CTFやらでReversingをしていると、いつも見るframe_dummyやinit、本質ではない、的な感じでスルーをしているんですが、それなりに気になるところでもあり、少し深く見てみようかなと思います。
今回は、とりあえず
#include <stdio.h> int main(void) { printf("advent calendar 2016\n"); return 0; }
このソースコードをコンパイルして、中身を調査していきたいと思います。
$ gcc hello.c -o hello $ uname -a Linux vagrant-ubuntu-trusty-64 3.13.0-101-generic #148-Ubuntu SMP Thu Oct 20 22:08:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.5 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4 Copyright (C) 2013 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.
これをdisassembleすると、main以外にもいくつか(むしろmainは5,6%程度にすぎない)の部分が含まれていることがわかります。普段は、あまり本質ではないので気にしないのですが(よくない)、今回は、大雑把な、初期化処理をしている〜、みたいな理解よりももう少しだけ、よく理解してみようと思います。
pltセクションの内容は今回の興味ではないので、.textと.initの部分にmain以外にどんな部分があるのかといえば、列挙すると以下になります。
00000000004003e0 <_init>: 0000000000400440 <_start>: 0000000000400470 <deregister_tm_clones>: 00000000004004a0 <register_tm_clones>: 00000000004004e0 <__do_global_dtors_aux>: 0000000000400500 <frame_dummy>: 0000000000400550 <__libc_csu_init>: 00000000004005c0 <__libc_csu_fini>:
初期化処理付近
まずは、_startについて。これは言うまでもなくEntrypointであり、
$ readelf -h hello ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x400440 Start of program headers: 64 (bytes into file) Start of section headers: 4472 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 30 Section header string table index: 27
0000000000400440 <_start>: 400440: 31 ed xor ebp,ebp 400442: 49 89 d1 mov r9,rdx 400445: 5e pop rsi 400446: 48 89 e2 mov rdx,rsp 400449: 48 83 e4 f0 and rsp,0xfffffffffffffff0 40044d: 50 push rax 40044e: 54 push rsp 40044f: 49 c7 c0 c0 05 40 00 mov r8,0x4005c0 400456: 48 c7 c1 50 05 40 00 mov rcx,0x400550 40045d: 48 c7 c7 2d 05 40 00 mov rdi,0x40052d 400464: e8 b7 ff ff ff call 400420 <__libc_start_main@plt> 400469: f4 hlt 40046a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0]
確かにこの0x400440
は、_startを指していて、Entrypointです。_start自体に関しては、gdbを使って追ってみます。実行時引数として、「advent calendar」を与えて、実行します。まず、_startについた段階ではスタックの様子は
[------------------------------------stack-------------------------------------] [------------------------------------stack-------------------------------------] 0000| 0x7fffffffdfd0 --> 0x3 0008| 0x7fffffffdfd8 --> 0x7fffffffe24f ("/home/vagrant/host-share/adv/hello") 0016| 0x7fffffffdfe0 --> 0x7fffffffe272 --> 0x6300746e65766461 ('advent') 0024| 0x7fffffffdfe8 --> 0x7fffffffe279 ("calendar") 0032| 0x7fffffffdff0 --> 0x0 0040| 0x7fffffffdff8 --> 0x7fffffffe282 ("XDG_SESSION_ID=3") 0048| 0x7fffffffe000 --> 0x7fffffffe293 ("PYENV_ROOT=/home/vagrant/.pyenv") 0056| 0x7fffffffe008 --> 0x7fffffffe2b3 ("SHELL=/bin/bash") [------------------------------------------------------------------------------][------------------------------------------------------------------------------]
となっています。この_startでは、__libc_start_mainを呼び出すための引数を整えています。具体的には、x64のcalling conventionがrdi, rsi, rdx, rcx, r8, r9の順に積み上がっていき、__libc_start_mainの引数が、
- mainアドレス
- argc
- argv
- initアドレス
- finiアドレス
- stack_end
であり、実際、__libc_start_mainの呼び出しの直前で止めたときにのレジスタは
RAX: 0x1c RBX: 0x0 RCX: 0x400550 (<__libc_csu_init>: push r15) RDX: 0x7fffffffdfd8 --> 0x7fffffffe24f ("/home/vagrant/host-share/adv/hello") RSI: 0x3 RDI: 0x40052d (<main>: push rbp) RBP: 0x0 RSP: 0x7fffffffdfc0 --> 0x7fffffffdfc8 --> 0x1c RIP: 0x400464 (<_start+36>: call 0x400420 <__libc_start_main@plt>) R8 : 0x4005c0 (<__libc_csu_fini>: repz ret) R9 : 0x7ffff7dea530 (<_dl_fini>: push rbp) R10: 0x14 R11: 0x1 R12: 0x400440 (<_start>: xor ebp,ebp) R13: 0x7fffffffdfd0 --> 0x3 R14: 0x0 R15: 0x0 EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
となっていてmain、__libc_csu_initや__libc_csu_finiなどが正しく渡されていることがわかります。
関数のリストにはないですが、このまま__libc_start_mainの挙動を追ってみます。これは、Cのソースコードを見てみることができ、実際に、mainが第一引数argc, 第二引数argv,そしてenvironが第三引数として渡されていることが確認できます。また、mainの返り値をexitに引数として与えているのも見て取れます。
/* Run the program. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
さらに、引数として渡した__libc_csu_initは、
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
として、呼び出されていて、gdbで確認してみると、
0x7ffff7a36ecc <__libc_start_main+124>: mov edi,DWORD PTR [rsp+0x14] 0x7ffff7a36ed0 <__libc_start_main+128>: mov rdx,QWORD PTR [rax] => 0x7ffff7a36ed3 <__libc_start_main+131>: call rbp 0x7ffff7a36ed5 <__libc_start_main+133>: mov rax,QWORD PTR [rip+0x39bf84] # 0x7ffff7dd2e60
で、
gdb-peda$ p $rbp $1 = (void *) 0x400550 <__libc_csu_init>
となっていて、中身が呼び出されます。__libc_csu_initのソースコードが見れるので、これを、コメントなどを取り除いて、また必要ない部分はなくし、小さくまとめたものが次のコードです(LIBC_NONSHAREDのifdefの部分は、今見ている部分には無さそうという理由でカットしています)。
void __libc_csu_init (int argc, char **argv, char **envp) { _init (); const size_t size = __init_array_end - __init_array_start; for (size_t i = 0; i < size; i++) (*__init_array_start [i]) (argc, argv, envp); }
これと、__libc_csu_initをdisassembleした結果を照らし合わせてみると、まず、_initを呼び出す操作があって、
40057e: e8 5d fe ff ff call 4003e0 <_init>
もう一つの処理として*__init_array_start配列に含まれる関数群を呼び出す操作としては、
0000000000400550 <__libc_csu_init>: 400561: 4c 8d 25 a8 08 20 00 lea r12,[rip+0x2008a8] # 600e10 <__frame_dummy_init_array_entry> 400568: 55 push rbp 400569: 48 8d 2d a8 08 20 00 lea rbp,[rip+0x2008a8] # 600e18 <__init_array_end> 400570: 53 push rbx 400571: 4c 29 e5 sub rbp,r12 400574: 31 db xor ebx,ebx 400576: 48 c1 fd 03 sar rbp,0x3 40057a: 48 83 ec 08 sub rsp,0x8 [省略] 400590: 4c 89 ea mov rdx,r13 400593: 4c 89 f6 mov rsi,r14 400596: 44 89 ff mov edi,r15d 400599: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 40059d: 48 83 c3 01 add rbx,0x1 4005a1: 48 39 eb cmp rbx,rbp 4005a4: 75 ea jne 400590 <__libc_csu_init+0x40>
となっています。ここでは前半部分で、__init_array_endから__frame_dummy_init_array_entryの間にあるポインタの数を計算して、後半のfor文でそれらを呼び出すという処理がされているということがわかります。これらの実行を実際にgdbで確認してみて、何が呼び出されるのかを見てみます。
gdb-peda$ p 0x600e18 - 0x600e10 $1 = 0x8 gdb-peda$ x/x 0x600e10 0x600e10: 0x0000000000400500 gdb-peda$ x/10i 0x0000000000400500 0x400500 <frame_dummy>: cmp QWORD PTR [rip+0x200918],0x0 # 0x600e20 0x400508 <frame_dummy+8>: je 0x400528 <frame_dummy+40> 0x40050a <frame_dummy+10>: mov eax,0x0 0x40050f <frame_dummy+15>: test rax,rax 0x400512 <frame_dummy+18>: je 0x400528 <frame_dummy+40> 0x400514 <frame_dummy+20>: push rbp 0x400515 <frame_dummy+21>: mov edi,0x600e20 0x40051a <frame_dummy+26>: mov rbp,rsp 0x40051d <frame_dummy+29>: call rax 0x40051f <frame_dummy+31>: pop rbp
すると、このテーブルにはframe_dummyが登録されていることがわかります(他にはこの状況では何も登録されていないこともわかります)。 では、ある意味で今回の主役であるframe_dummyはどういうことをしているのか、について調べて行きます。
0000000000400500 <frame_dummy>: 400500: 48 83 3d 18 09 20 00 cmp QWORD PTR [rip+0x200918],0x0 # 600e20 <__JCR_END__> 400507: 00 400508: 74 1e je 400528 <frame_dummy+0x28> 40050a: b8 00 00 00 00 mov eax,0x0 40050f: 48 85 c0 test rax,rax 400512: 74 14 je 400528 <frame_dummy+0x28> 400514: 55 push rbp 400515: bf 20 0e 60 00 mov edi,0x600e20 40051a: 48 89 e5 mov rbp,rsp 40051d: ff d0 call rax 40051f: 5d pop rbp 400520: e9 7b ff ff ff jmp 4004a0 <register_tm_clones> 400525: 0f 1f 00 nop DWORD PTR [rax] 400528: e9 73 ff ff ff jmp 4004a0 <register_tm_clones>
gdbで実行してみると、__JCR_END__が
gdb-peda$ x/x 0x600e20 0x600e20: 0x0000000000000000
となっていますが、__JCR_END__が0であろうとなかろうと結局やっていることは、register_tm_clonesにjumpするということで、frame_dummyは今回ただregister_tm_clonesにjumpするだけのもののようです。結局これだけしかやってないのかあという感じです。
では、次に_initが__libc_csu_initで呼び出されるので、_initがどうなっていくかを追ってみます。 _initのdisassemble結果は短いので、貼ってしまいますが、
00000000004003e0 <_init>: 4003e0: 48 83 ec 08 sub rsp,0x8 4003e4: 48 8b 05 0d 0c 20 00 mov rax,QWORD PTR [rip+0x200c0d] # 600ff8 <_DYNAMIC+0x1d0> 4003eb: 48 85 c0 test rax,rax 4003ee: 74 05 je 4003f5 <_init+0x15> 4003f0: e8 3b 00 00 00 call 400430 <__gmon_start__@plt> 4003f5: 48 83 c4 08 add rsp,0x8 4003f9: c3 ret
となっています。結局_DYNAMIC+0x1d0が0以外のときは__gmon_start__を呼ぶし、0ならば呼ばないということをしているようです。__gmon_start__の実装を見てみると、
void __gmon_start__ (void) { #ifdef HAVE_INITFINI static int called; if (called) return; called = 1; #endif /* Start keeping profiling records. */ __monstartup ((u_long) TEXT_START, (u_long) &etext); atexit (&_mcleanup); }
一回だけしか呼び出されないようになっていて、また結局_monstartupを呼び出すのが本質のようです。これ自体は、コメントにもあるように、Profiling記録を開始する関数のようで、これ以上は、とりあえずおいておくことにして次の関数に行きたいと思います。
とりあえず、__libc_start_mainまで話を戻します。ここでは、__libc_csu_initの他にも__libc_csu_finiを呼び出す関数を呼び出しています。Cのソースコードでいうと、
if (fini) _cxa_atexit ((void (*) (void *)) fini, NULL, NULL);
という部分です。これもlibcの実装を見てみると、
int attribute_hidden __internal_atexit (void (*func) (void *), void *arg, void *d, struct exit_function_list **listp) { struct exit_function *new = __new_exitfn (listp); if (new == NULL) return -1; #ifdef PTR_MANGLE PTR_MANGLE (func); #endif new->func.cxa.fn = (void (*) (void *, int)) func; new->func.cxa.arg = arg; new->func.cxa.dso_handle = d; atomic_write_barrier (); new->flavor = ef_cxa; return 0; } int __cxa_atexit (void (*func) (void *), void *arg, void *d) { return __internal_atexit (func, arg, d, &__exit_funcs); }
これをみると、exit_funcsのリンクリストに新しいエントリとして渡されたfuncをつないでいる(多分)ようです。
Transactional Memory系
具体的に言えば
- register_tm_clones
- deregister_tm_clones
ですが、これに関してはあんまり挙動がよくわからないというのが正直なところで、詳しいことがあんまよくわからないです。マルチスレッドにおいて、共有するメモリへのアクセスが考えられる時に、TMが使われる、ということですが、今回のプログラムとどう関連しているかはよくわからないです。。
さいごに
なんか最後の方疲れてしまった。期限も過ぎているのでとりあえず公開しますが、余力があったらいつか追記します。