objcopyでバイナリにデータを埋め込む

リンカ・ローダ実践開発テクニック―実行ファイルを作成するために必須の技術 (COMPUTER TECHNOLOGY)に載っていた方法。

objcopyを使えばファイルのデータをELFのセクションとして埋め込むことができる。

以下のコード(binary.c)ではbindataセクションにデータを埋め込むことを想定しており、そのアドレスを__start_bindataによって参照している。

#include <stdio.h>

extern char __start_bindata;
extern char __stop_bindata;

int main()
{
  char *p;

  printf("__start_bindata = 0x%08x\n", (int)(&__start_bindata));
  printf("__stop_bindata = 0x%08x\n", (int)(&__stop_bindata));

  for (p = &__start_bindata; p < &__stop_bindata; p++) {
    putchar(*p);
  }

  return 0;
}

埋め込みたいデータを用意。

$ echo "Hello World" > binary.dat

オブジェクトファイルを作成。

$ gcc -c binary.c -Wall

bindataセクションにbinary.datを埋め込んだオブジェクトファイルbinary_tmp.oを生成。

$ objcopy --add-section=bindata=binary.dat \
    --set-section-flags=bindata=contents,alloc,load,readonly,data binary.o binary_tmp.o

bindataというセクションができている。

$ readelf -W -S binary_tmp.o
There are 14 section headers, starting at offset 0x1c8:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        0000000000000000 000040 00006f 00  AX  0   0  4
  [ 2] .rela.text        RELA            0000000000000000 0006f0 0000d8 18     12   1  8
  [ 3] .data             PROGBITS        0000000000000000 0000b0 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          0000000000000000 0000b0 000000 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        0000000000000000 0000b0 000033 00   A  0   0  1
  [ 6] .comment          PROGBITS        0000000000000000 0000e3 00002e 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        0000000000000000 000111 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        0000000000000000 000118 000038 00   A  0   0  8
  [ 9] .rela.eh_frame    RELA            0000000000000000 0007c8 000018 18     12   8  8
  [10] bindata           PROGBITS        0000000000000000 000150 00000c 00   A  0   0  1
  [11] .shstrtab         STRTAB          0000000000000000 00015c 000069 00      0   0  1
  [12] .symtab           SYMTAB          0000000000000000 000548 000168 18     13  10  8
  [13] .strtab           STRTAB          0000000000000000 0006b0 00003d 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

bindataセクションのオフセットは0x150となっているのでそこを見てみる。

$ hexdump -C binary_tmp.o
(略)
00000140  00 41 0e 10 86 02 43 0d  06 02 6a 0c 07 08 00 00  |.A....C...j.....|
00000150  48 65 6c 6c 6f 20 57 6f  72 6c 64 0a 00 2e 73 79  |Hello World...sy|
00000160  6d 74 61 62 00 2e 73 74  72 74 61 62 00 2e 73 68  |mtab..strtab..sh|
00000170  73 74 72 74 61 62 00 2e  72 65 6c 61 2e 74 65 78  |strtab..rela.tex|
(略)

"Hello World"が含まれている。

最後にデータを埋め込んだオブジェクトファイルから実行形式を作成。

$ gcc binary_tmp.o -o binary
$ ./binary
__start_bindata = 0x004006bb
__stop_bindata = 0x004006c7
Hello World

これを利用して関数を埋め込んでそれを呼び出すということをやってみる。

以下の関数を埋め込む。(func.c)

int func(int a, int b)
{
  return a + b;
}

オブジェクトファイルを作成。

$ gcc -c func.c

セクション情報から.textセクションはオフセット0x40から始まっていることがわかる。

$ readelf -W -S func.o
There are 11 section headers, starting at offset 0x118:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        0000000000000000 000040 000011 00  AX  0   0  4
  [ 2] .data             PROGBITS        0000000000000000 000054 000000 00  WA  0   0  4
  [ 3] .bss              NOBITS          0000000000000000 000054 000000 00  WA  0   0  4
  [ 4] .comment          PROGBITS        0000000000000000 000054 00002e 01  MS  0   0  1
  [ 5] .note.GNU-stack   PROGBITS        0000000000000000 000082 000000 00      0   0  1
  [ 6] .eh_frame         PROGBITS        0000000000000000 000088 000038 00   A  0   0  8
  [ 7] .rela.eh_frame    RELA            0000000000000000 0004c0 000018 18      9   6  8
  [ 8] .shstrtab         STRTAB          0000000000000000 0000c0 000054 00      0   0  1
  [ 9] .symtab           SYMTAB          0000000000000000 0003d8 0000d8 18     10   8  8
  [10] .strtab           STRTAB          0000000000000000 0004b0 00000c 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

関数の機械語は以下のようになっていて、

$ objdump -d func.o

func.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <sum>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   89 7d fc                mov    %edi,-0x4(%rbp)
   7:   89 75 f8                mov    %esi,-0x8(%rbp)
   a:   b8 61 1e 00 00          mov    $0x1e61,%eax
   f:   c9                      leaveq
  10:   c3                      retq

これは以下から、実際にオフセット0x40の位置にあることがわかる。

00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 3e 00 01 00 00 00  00 00 00 00 00 00 00 00  |..>.............|
00000020  00 00 00 00 00 00 00 00  18 01 00 00 00 00 00 00  |................|
00000030  00 00 00 00 40 00 00 00  00 00 40 00 0b 00 08 00  |....@.....@.....|
00000040  55 48 89 e5 89 7d fc 89  75 f8 b8 61 1e 00 00 c9  |UH...}..u..a....|
00000050  c3 00 00 00 00 47 43 43  3a 20 28 47 4e 55 29 20  |.....GCC: (GNU) |
(略)

そこで以下のように関数ポインタが埋め込み先のセクションの先頭からオフセット0x40の位置を指すようにしておく。

#include <stdio.h>

extern char __start_exec;
extern char __stop_exec;

int main()
{
  int res;
  int (*fp)(int, int);
  fp = &__start_exec + 0x40;

  res = (*fp)(10, 90);
  printf("%d\n", res);

  return 0;
}

さっきと同じようにしてファイルを埋め込む。
ただし、埋め込んだセクションを実行可能にしておく必要がある。

$ gcc -c binary.c -Wall
$ objcopy --add-section=exec=func.o \
    --set-section-flags=exec=contents,alloc,load,readonly,code binary.o binary_tmp.o
$ gcc gcc binary_tmp.o -o binary

以下のように埋め込んだ関数を実行できていることがわかる。

$ ./binary
100