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が使われる、ということですが、今回のプログラムとどう関連しているかはよくわからないです。。

さいごに

なんか最後の方疲れてしまった。期限も過ぎているのでとりあえず公開しますが、余力があったらいつか追記します。