共有ELFライブラリでの関数のリダイレクト

私たちは皆、ダイナミックリンクライブラリを使用しています。 その機能は本当に素晴らしいです。 まず、このようなライブラリは、すべてのプロセスに対して一度だけ物理アドレス空間にロードされます。 第二に、この機能を提供する追加のライブラリをロードすることにより、プログラムの機能を拡張できます。 そして、プログラム自体を再起動せずにすべてこれ。 また、更新の問題も解決します。 動的に構成可能なライブラリの場合、標準インターフェイスを定義し、ライブラリバージョンを変更するだけでメインプログラムの機能と品質に影響を与えることができます。 このようなコードの再利用方法は、「プラグインアーキテクチャ」とも呼ばれます。 しかし、トピックはそれについてではありません。



ところで、せっかちな人は今すぐすべてをダウンロードして試すことができます







もちろん、その実装で動的にコンパイルされたライブラリが、それ自体、つまりプロセッサとメモリの計算能力のみに依存することはめったにありません。 ライブラリはライブラリを使用します。 または、少なくとも標準ライブラリ。 たとえば、C \ C ++プログラムは標準C \ C ++ライブラリを使用します。 ちなみに、後者は、便宜上、動的に構成可能な形式(libc.soおよびlibstdc ++。So)で編成されています。 それら自体は、特別な形式のファイルに保存されます。 私の研究はLinuxで、動的にリンクされたライブラリの主な形式はELF(実行可能およびリンク可能形式)です。

少し前に、あるライブラリから別のライブラリへの関数呼び出しをインターセプトする必要に直面しました。 特別な方法で処理するだけです。 これは、コールリダイレクトと呼ばれます。



リダイレクトの詳細



まず、特定の例を使用して問題を定式化します。 C言語の「test」と呼ばれるプログラム(ファイルtest.c)と、事前にコンパイルされた同じ内容の2つの共有ライブラリ(ファイルlibtest1.cおよびlibtest2.c)があるとします。 これらのライブラリは、それぞれ1つの関数libtest1()およびlibtest2()を提供します。 それらの実装では、それぞれがC標準ライブラリのputs()関数を使用します。







タスクは次のとおりです。

  1. 両方のライブラリのputs()関数呼び出しを、メインプログラム(test.cファイル)に実装されているredirected_puts()関数呼び出しで置き換える必要があります。この関数呼び出しは、元のputs()を使用できます。





  2. 行われた変更を破棄します。つまり、libtest1()とlibtest2()を繰り返し呼び出して、元のputs()を呼び出すようにします。







同時に、コードの変更またはライブラリ自体の再コンパイルは許可されず、メインプログラムのみが許可されます。



なぜこれが必要ですか?



この例は、このようなリダイレクトの2つの非常に興味深い機能を示しています。

  1. これは、動的ローダーの環境変数LD_PRELOADを使用する場合のように、プロセス全体ではなく、特定の動的にコンパイルされたライブラリに対してのみ実行されます。これにより、他のモジュールは元の関数を安全に使用できます。
  2. プログラムの実行中に発生し、再起動の必要はありません。
これはどこに適用できますか? たとえば、プラグインが多数あるプログラムでは、他のプラグインやアプリケーション自体に影響を与えることなく、システムリソースまたは他のライブラリへの呼び出しをインターセプトできます。 または、独自のプラグインからアプリケーションに同じことをします。

この問題を解決する法的方法はありません。 唯一のオプションは、ELFを処理し、メモリに必要な変更を自分で加えることです。



行こう!



ELFについて簡単に



ELFを理解する最善の方法は、忍耐を持ち、仕様を数回注意深く読んでから、簡単なプログラムを作成し、コンパイルして、16進エディタを使用して詳細を調べて、仕様と見たものを比較することです。 このような研究方法は、ELF用の簡単なパーサーを書くというアイデアにすぐにつながります。多くの日常的な作業があるからです。 しかし、急がないでください。 すでに作成されたこのようなユーティリティがいくつかあります。 調査のために、前のセクションのファイルを使用します。



ファイルtest.c


#include <stdio.h> #include <dlfcn.h> #define LIBTEST1_PATH "libtest1.so" //position dependent code (for 32 bit only) #define LIBTEST2_PATH "libtest2.so" //position independent code void libtest1(); //from libtest1.so void libtest2(); //from libtest2.so int main() { void *handle1 = dlopen(LIBTEST1_PATH, RTLD_LAZY); void *handle2 = dlopen(LIBTEST2_PATH, RTLD_LAZY); if (NULL == handle1 || NULL == handle2) fprintf(stderr, "Failed to open \"%s\" or \"%s\"!\n", LIBTEST1_PATH, LIBTEST2_PATH); libtest1(); //calls puts() from libc.so twice libtest2(); //calls puts() from libc.so twice puts("-----------------------------"); dlclose(handle1); dlclose(handle2); return 0; }
      
      





libtest1.cファイル


 int puts(char const *); void libtest1() { puts("libtest1: 1st call to the original puts()"); puts("libtest1: 2nd call to the original puts()"); }
      
      





libtest2.cファイル


 int puts(char const *); void libtest2() { puts("libtest2: 1st call to the original puts()"); puts("libtest2: 2nd call to the original puts()"); }
      
      





ELFの部品は何ですか?



この質問に答えるには、そのようなファイルの中を見る必要があります。 このためのユーティリティがあります:

再配置は、別のモジュールの文字を参照するELFファイル内のその場所の特別な用語です。 このような場所の直接変更は、静的(ld)または動的(ld-linux.so.2)リンカー\ローダーによって行われます。



ELFファイルは特別なヘッダーで始まります。 その構造と他の多くのELF要素の説明は、/ usr / include / linux / elf.hにあります。 ヘッダーには、セクションヘッダーテーブルファイルの先頭からのオフセットが記録される特別なフィールドがあります。 この表の各要素は、ELFのセクションを説明しています。 セクションは、ELFファイル内の最小の分割できない構造要素です。 メモリにロードするとき、セクションはセグメントに結合されます。 セグメントは、ブートローダー(ld-linux.so.2)によってメモリにマップできるELFファイルの最小の分割不可能な部分です。 セグメントはセグメントのテーブルを記述し、そのオフセットはELFファイルのヘッダーにもあります。







それらの中で最も重要なのは:

上記のファイルをコンパイルするには、次のコマンドを実行します。

 gcc -g3 -m32 -shared -o libtest1.so libtest1.c gcc -g3 -m32 -fPIC -shared -o libtest2.so libtest2.c gcc -g3 -m32 -L$PWD -o test test.c -ltest1 -ltest2 –ldl
      
      





最初のコマンドは、ダイナミックリンクライブラリlibtest1.soを作成します。 2番目はlibtest2.soです。 –fPICスイッチに注意してください。 これにより、コンパイラはいわゆる位置独立コードを生成します。 次のセクションの詳細。 3番目のコマンドは、test.cファイルをコンパイルし、作成済みのlibtest1.soおよびlibtest2.soライブラリとリンクすることにより、testという名前の実行可能モジュールを作成します。 後者は、–L $ PWDスイッチに反映されているように、現在のディレクトリにあります。 dlopen()およびdlclose()関数を使用するには、libdl.soとのリンクが必要です。



プログラムを実行するには、次のコマンドを実行する必要があります。

 export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH ./test
      
      





つまり、ライブラリを検索するためのパスとして、現在のディレクトリへのパスを動的リンカー/ローダーに追加します。 プログラムの出力は次のとおりです。



libtest1: 1st call to the original puts()

libtest1: 2nd call to the original puts()

libtest2: 1st call to the original puts()

libtest2: 2nd call to the original puts()

-----------------------------







次に、テストモジュールのセクションを見てみましょう。 これを行うには、-aスイッチを指定してreadelfを実行します。 最も興味深いものを以下にリストします。



ELF Header:

Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

Class: ELF32

Data: 2's complement, little endian

Version: 1 (current)

OS/ABI: UNIX - System V

ABI Version: 0

Type: EXEC (Executable file)

Machine: Intel 80386

Version: 0x1

Entry point address: 0x8048580

Start of program headers: 52 (bytes into file)

Start of section headers: 21256 (bytes into file)

Flags: 0x0

Size of this header: 52 (bytes)

Size of program headers: 32 (bytes)

Number of program headers: 8

Size of section headers: 40 (bytes)

Number of section headers: 39

Section header string table index: 36







実行可能モジュールの標準ヘッダー。 最初の16バイトのマジックシーケンス。 モジュールのタイプ(この場合は実行可能ファイル、オブジェクト(.o)および共有(.so))、アーキテクチャ(i386)、推奨エントリポイント、セグメントおよびセクションのヘッダーへのオフセットとそれらのサイズが示されます。 最後に-セクション名の行のテーブル内のオフセット。



Section Headers:

[Nr] Name Type Addr Off Size ES Flg Lk Inf Al

[ 0] NULL 00000000 000000 000000 00 0 0 0

[ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1

...

[ 5] .dynsym DYNSYM 08048200 000200 000110 10 A 6 1 4

[ 6] .dynstr STRTAB 08048310 000310 0000df 00 A 0 0 1

...

[ 9] .rel.dyn REL 08048464 000464 000010 08 A 5 0 4

[10] .rel.plt REL 08048474 000474 000040 08 A 5 12 4

[11] .init PROGBITS 080484b4 0004b4 000030 00 AX 0 0 4

[12] .plt PROGBITS 080484e4 0004e4 000090 04 AX 0 0 4

[13] .text PROGBITS 08048580 000580 0001fc 00 AX 0 0 16

[14] .fini PROGBITS 0804877c 00077c 00001c 00 AX 0 0 4

[15] .rodata PROGBITS 08048798 000798 00005c 00 A 0 0 4

...

[20] .dynamic DYNAMIC 08049f08 000f08 0000e8 08 WA 6 0 4

[21] .got PROGBITS 08049ff0 000ff0 000004 04 WA 0 0 4

[22] .got.plt PROGBITS 08049ff4 000ff4 00002c 04 WA 0 0 4

[23] .data PROGBITS 0804a020 001020 000008 00 WA 0 0 4

[24] .bss NOBITS 0804a028 001028 00000c 00 WA 0 0 4

...

[27] .debug_pubnames PROGBITS 00000000 0011b8 000040 00 0 0 1

[28] .debug_info PROGBITS 00000000 0011f8 0004d9 00 0 0 1

[29] .debug_abbrev PROGBITS 00000000 0016d1 000156 00 0 0 1

[30] .debug_line PROGBITS 00000000 001827 000309 00 0 0 1

[31] .debug_frame PROGBITS 00000000 001b30 00003c 00 0 0 4

[32] .debug_str PROGBITS 00000000 001b6c 00024e 01 MS 0 0 1

...

[36] .shstrtab STRTAB 00000000 0051a8 000160 00 0 0 1

[37] .symtab SYMTAB 00000000 005920 000530 10 38 57 4

[38] .strtab STRTAB 00000000 005e50 000268 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)







ここでは、実験的なELFファイルのすべてのセクション、メモリへのロードのタイプとモード(R、W、X、およびA)のリストを見ることができます。



Program Headers:

Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align

PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 RE 0x4

INTERP 0x000134 0x08048134 0x08048134 0x00013 0x00013 R 0x1

[Requesting program interpreter: /lib/ld-linux.so.2]

LOAD 0x000000 0x08048000 0x08048000 0x007f8 0x007f8 RE 0x1000

LOAD 0x000ef4 0x08049ef4 0x08049ef4 0x00134 0x00140 RW 0x1000

DYNAMIC 0x000f08 0x08049f08 0x08049f08 0x000e8 0x000e8 RW 0x4

NOTE 0x000148 0x08048148 0x08048148 0x00020 0x00020 R 0x4

GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4

GNU_RELRO 0x000ef4 0x08049ef4 0x08049ef4 0x0010c 0x0010c R 0x1







これはセグメントのリストです-メモリ内のセクションの一種のコンテナです。 特別なモジュールである動的リンカー\ローダーへのパスも示されています。 このELFファイルの内容をメモリに配置するのは彼次第です。



Section to Segment mapping:

Segment Sections...

00

01 .interp

02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame

03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss

04 .dynamic

05 .note.ABI-tag

06

07 .ctors .dtors .jcr .dynamic .got







そして、ロード中のセグメントによるセクションの分散がどのように行われるかを示します。

しかし、インポートおよびエクスポートされた動的にリンクされた関数に関する情報を保存する最も興味深いセクションは、「。dynsym」と呼ばれます。



Symbol table '.dynsym' contains 17 entries:

Num: Value Size Type Bind Vis Ndx Name

0: 00000000 0 NOTYPE LOCAL DEFAULT UND

1: 00000000 0 FUNC GLOBAL DEFAULT UND libtest2

2: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__

3: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses

4: 00000000 0 FUNC GLOBAL DEFAULT UND dlclose@GLIBC_2.0 (2)

5: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (3)

6: 00000000 0 FUNC GLOBAL DEFAULT UND libtest1

7: 00000000 0 FUNC GLOBAL DEFAULT UND dlopen@GLIBC_2.1 (4)

8: 00000000 0 FUNC GLOBAL DEFAULT UND fprintf@GLIBC_2.0 (3)

9: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (3)

10: 0804a034 0 NOTYPE GLOBAL DEFAULT ABS _end

11: 0804a028 0 NOTYPE GLOBAL DEFAULT ABS _edata

12: 0804879c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used

13: 0804a028 4 OBJECT GLOBAL DEFAULT 24 stderr@GLIBC_2.0 (3)

14: 0804a028 0 NOTYPE GLOBAL DEFAULT ABS __bss_start

15: 080484b4 0 FUNC GLOBAL DEFAULT 11 _init

16: 0804877c 0 FUNC GLOBAL DEFAULT 14 _fini







プログラムの正しいロード/アンロードに必要な他の機能の中で、おなじみの名前を見つけることができます:libtest1、libtest2、dlopen、fprintf、puts、dlclose。 それらのすべてについて、FUNCタイプがリストされ、それらがこのモジュールで定義されていないという事実-セクションインデックスはUNDとしてマークされています。



「.rel.dyn」および「.rel.plt」セクションは、レイアウト中に通常再配置が必要な「.dynsym」からの文字の再配置テーブルです。



Relocation section '.rel.dyn' at offset 0x464 contains 2 entries:

Offset Info Type Sym.Value Sym. Name

08049ff0 00000206 R_386_GLOB_DAT 00000000 __gmon_start__

0804a028 00000d05 R_386_COPY 0804a028 stderr



Relocation section '.rel.plt' at offset 0x474 contains 8 entries:

Offset Info Type Sym.Value Sym. Name

0804a000 00000107 R_386_JUMP_SLOT 00000000 libtest2

0804a004 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__

0804a008 00000407 R_386_JUMP_SLOT 00000000 dlclose

0804a00c 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main

0804a010 00000607 R_386_JUMP_SLOT 00000000 libtest1

0804a014 00000707 R_386_JUMP_SLOT 00000000 dlopen

0804a018 00000807 R_386_JUMP_SLOT 00000000 fprintf

0804a01c 00000907 R_386_JUMP_SLOT 00000000 puts







関数の動的リンクに関して、これらのテーブルの違いは何ですか? これは次のセクションのトピックです。



共有ELFライブラリはどのようにコンパイルされますか?



libtest1.soおよびlibtest2.soライブラリのコンパイルはわずかに異なっていました。 -fPICスイッチを使用してコンパイルされたlibtest2.so(位置独立コードを生成)。 これがこれら2つのモジュールの動的シンボルテーブルにどのように影響したかを見てみましょう(readelfを使用します)。



Symbol table '.dynsym' contains 11 entries:

Num: Value Size Type Bind Vis Ndx Name

0: 00000000 0 NOTYPE LOCAL DEFAULT UND

1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__

2: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses

3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)

4: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)

5: 00002014 0 NOTYPE GLOBAL DEFAULT ABS _end

6: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS _edata

7: 0000043c 32 FUNC GLOBAL DEFAULT 11 libtest1

8: 0000200c 0 NOTYPE GLOBAL DEFAULT ABS __bss_start

9: 0000031c 0 FUNC GLOBAL DEFAULT 9 _init

10: 00000498 0 FUNC GLOBAL DEFAULT 12 _fini



Symbol table '.dynsym' contains 11 entries:

Num: Value Size Type Bind Vis Ndx Name

0: 00000000 0 NOTYPE LOCAL DEFAULT UND

1: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__

2: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses

3: 00000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.0 (2)

4: 00000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.1.3 (3)

5: 00002018 0 NOTYPE GLOBAL DEFAULT ABS _end

6: 00002010 0 NOTYPE GLOBAL DEFAULT ABS _edata

7: 00002010 0 NOTYPE GLOBAL DEFAULT ABS __bss_start

8: 00000304 0 FUNC GLOBAL DEFAULT 9 _init

9: 0000043c 52 FUNC GLOBAL DEFAULT 11 libtest2

10: 000004a8 0 FUNC GLOBAL DEFAULT 12 _fini







そのため、両方のライブラリの動的シンボルテーブルは、シンボル自体のシーケンスのみが異なります。 どちらも未定義の関数puts()を使用していますが、libtest1()またはlibtest2()を提供していることがわかります。 再配置テーブルはどのように変更されましたか?



Relocation section '.rel.dyn' at offset 0x2cc contains 8 entries:

Offset Info Type Sym.Value Sym. Name

00000445 00000008 R_386_RELATIVE

00000451 00000008 R_386_RELATIVE

00002008 00000008 R_386_RELATIVE

0000044a 00000302 R_386_PC32 00000000 puts

00000456 00000302 R_386_PC32 00000000 puts

00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__

00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses

00001ff0 00000406 R_386_GLOB_DAT 00000000 __cxa_finalize



Relocation section '.rel.plt' at offset 0x30c contains 2 entries:

Offset Info Type Sym.Value Sym. Name

00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__

00002004 00000407 R_386_JUMP_SLOT 00000000 __cxa_finalize







libtest1.soの場合、puts()関数の再配置は「.rel.dyn」セクションで2回発生します。 逆アセンブラを使用して、モジュール内のこれらの場所を直接見てみましょう。 double puts()呼び出しが発生するlibtest1()関数を見つける必要があります。 objdump –Dを使用します。



 0000043c <libtest1>: 43c: 55 push %ebp 43d: 89 e5 mov %esp,%ebp 43f: 83 ec 08 sub $0x8,%esp 442: c7 04 24 b4 04 00 00 movl $0x4b4,(%esp) 449: e8 fc ff ff ff call 44a <libtest1+0xe> 44e: c7 04 24 e0 04 00 00 movl $0x4e0,(%esp) 455: e8 fc ff ff ff call 456 <libtest1+0x1a> 45a: c9 leave 45b: c3 ret 45c: 90 nop 45d: 90 nop 45e: 90 nop 45f: 90 nop
      
      





オペランドが0xFFFFFFFCの2つの相対CALL命令(コードE8)があります。 このようなオペランドを持つ相対CALLは、本質的に、CALL命令のアドレスに対して1バイト前方に制御を転送するため、意味がありません。 「.rel.dyn」セクションのputs()の再配置のオフセットを見ると、CALL命令のオペランドにのみ適用されることがわかります。 したがって、puts()の呼び出しのどちらの場合でも、ローダーは0xFFFFFFFCを単純に上書きするため、CALLはputs()関数の正しいアドレスにジャンプします。

これは、タイプR_386_PC32の再配置の仕組みです。



次に、libtest2.soに注目しましょう。



Relocation section '.rel.dyn' at offset 0x2cc contains 4 entries:

Offset Info Type Sym.Value Sym. Name

0000200c 00000008 R_386_RELATIVE

00001fe8 00000106 R_386_GLOB_DAT 00000000 __gmon_start__

00001fec 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses

00001ff0 00000406 R_386_GLOB_DAT 00000000 __cxa_finalize



Relocation section '.rel.plt' at offset 0x2ec contains 3 entries:

Offset Info Type Sym.Value Sym. Name

00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__

00002004 00000307 R_386_JUMP_SLOT 00000000 puts

00002008 00000407 R_386_JUMP_SLOT 00000000 __cxa_finalize







puts()への参照は1回だけ言及されており、さらに「.rel.plt」セクションでも言及されています。 アセンブラを見て、デバッグを行います。



 0000043c <libtest2>: 43c: 55 push %ebp 43d: 89 e5 mov %esp,%ebp 43f: 53 push %ebx 440: 83 ec 04 sub $0x4,%esp 443: e8 ef ff ff ff call 437 <__i686.get_pc_thunk.bx> 448: 81 c3 ac 1b 00 00 add $0x1bac,%ebx 44e: 8d 83 d0 e4 ff ff lea -0x1b30(%ebx),%eax 454: 89 04 24 mov %eax,(%esp) 457: e8 f8 fe ff ff call 354 <puts@plt> 45c: 8d 83 fc e4 ff ff lea -0x1b04(%ebx),%eax 462: 89 04 24 mov %eax,(%esp) 465: e8 ea fe ff ff call 354 <puts@plt> 46a: 83 c4 04 add $0x4,%esp 46d: 5b pop %ebx 46e: 5d pop %ebp 46f: c3 ret
      
      





CALL命令のオペランドはすでに異なっており、意味があります。つまり、それらは何かを指しているということです。 これはもはや単なるパディングではありません。 puts()呼び出し自体の前に、0x1FF4(0x1BAC + 0x448)がEBXレジスタに書き込まれることに注意することも役立ちます。 デバッガーは、初期EBX値0x448の認識に役立ちます。 そのため、さらにどこかで便利になります。 アドレス0x354は、非常に興味深いセクション「.plt」につながります。これは、「。text」のように、実行可能としてマークされています。 ここにあります:



 Disassembly of section .plt: 00000334 <__gmon_start__@plt-0x10>: 334: ff b3 04 00 00 00 pushl 0x4(%ebx) 33a: ff a3 08 00 00 00 jmp *0x8(%ebx) 340: 00 00 add %al,(%eax) ... 00000344 <__gmon_start__@plt>: 344: ff a3 0c 00 00 00 jmp *0xc(%ebx) 34a: 68 00 00 00 00 push $0x0 34f: e9 e0 ff ff ff jmp 334 <_init+0x30> 00000354 <puts@plt>: 354: ff a3 10 00 00 00 jmp *0x10(%ebx) 35a: 68 08 00 00 00 push $0x8 35f: e9 d0 ff ff ff jmp 334 <_init+0x30> 00000364 <__cxa_finalize@plt>: 364: ff a3 14 00 00 00 jmp *0x14(%ebx) 36a: 68 10 00 00 00 push $0x10 36f: e9 c0 ff ff ff jmp 334 <_init+0x30>
      
      





興味のあるアドレス0x354には、3つの指示があります。 最初の例では、EBX(0x1FF4)と0x10が指すアドレスへの無条件ジャンプが発生します。 簡単な計算の後、ポインター0x2004の値を取得します。 これらのアドレスは「.got.plt」セクションに分類されます。



 Disassembly of section .got.plt: 00001ff4 <.got.plt>: 1ff4: 20 1f and %bl,(%edi) ... 1ffe: 00 00 add %al,(%eax) 2000: 4a dec %edx 2001: 03 00 add (%eax),%eax 2003: 00 5a 03 add %bl,0x3(%edx) 2006: 00 00 add %al,(%eax) 2008: 6a 03 push $0x3 ...
      
      





このポインターを逆参照し、最終的に0x35Aに等しい無条件ジャンプアドレスを取得すると、最も興味深いことが明らかになります。 しかし、これは実際には次の指示です! なぜこのような複雑な操作を実行し、単に「.got.plt」セクションを参照して次の指示に進む必要があったのですか? PLTとGOTとは何ですか?



PLT(プロシージャリンクテーブル)は、プロシージャリンクテーブルです。 実行可能モジュールおよび共有モジュールに存在します。 これはスタブの配列で、インポートされた関数ごとに1つです。



 PLT[n+1]: jmp *GOT[n+3] push #n @push n as a signal to the resolver jmp PLT[0]
      
      





PLT [n + 1]で関数呼び出しを行うと、GOT [n + 3]で間接的な制御遷移が発生します。 最初の呼び出しで、GOT [n + 3]はPLT [n + 1] + 6を指します。これは、PLT [0]のPUSH \ JMPシーケンスです。 PLT [0]を通過すると、リンカーは格納されたスタック引数を使用して「n」を指定し、文字「n」を解決します。 次に、リンカはターゲットルーチンを直接指すようにGOT [n + 3]の値を修正し、最終的にそれを呼び出します。 PLT [n + 1]への後続の各呼び出しは、JMP命令によるアドレスの同様の解決なしに、ターゲットルーチンに向けられます。



最初のPLT要素は特別で、動的アドレス解決コードに切り替えるために使用されます。



 PLT[0]: push &GOT[1] jmp GOT[2] @points to resolver()
      
      





制御はリンカーコードに渡されます。 「n」はすでにスタック上にあり、アドレスGOT [1]がそこに追加されます。 この方法で、リンカー(/lib/ld-linux.so.2にあります)は、どのライブラリーがサービスを必要とするかを決定できます。



GOT(Global Offset Table)は、グローバルオフセットテーブルです。 最初の3つの要素は予約されています。 GOTが初めて初期化されると、PLTのアドレス解決に関連するすべての要素は、PLT [0]に戻ります。



これらは特別な要素です:

各ライブラリと実行可能ファイルには、独自のPLTとGOTがあります。







これが、R_386_JUMP_SLOT型の再配置の仕組みです。これは、libtest2.soライブラリで使用されていました。 残りのタイプの再配置は静的レイアウトに関連しているため、役に立たないでしょう。



メモリへのロードの位置に依存するコードと、PICに依存しないコードの違いは、インポートされた関数の呼び出しを解決する方法にあります。



重要な調査結果



いくつかの有用な結論を下しましょう。

  1. インポートおよびエクスポートされた関数に関するすべての情報は、「。dynsym」セクションにあります。
  2. モジュールがPICモード(-fPICスイッチ)でコンパイルされた場合、インポートされた関数はPLTおよびGOTを介して呼び出され、再配置は各関数に対して1回だけ行われ、PLTの特定の要素の最初の命令に適用されます。 実際の再配置に関する情報は、「。rel.plt」セクションにあります。
  3. ライブラリのコンパイル時に-fPICスイッチを使用しなかった場合、コード内にインポートされた関数の呼び出しがある限り、各相対CALL命令のオペランドで再配置が実行されます。 実際の再配置に関する情報は、「。rel.dyn」セクションにあります。


注: -fPICコンパイルキーは、64ビットアーキテクチャに必要です。 つまり、64ビットライブラリでは、インポートされた関数の呼び出しは常にPLT \ GOTによって解決されます。 さらに、このようなアーキテクチャでは、再配置を伴うセクションは「.rela.plt」および「.rela.dyn」と呼ばれます。



待望の決定



いくつかのダイナミックリンクライブラリでインポートされた関数をリダイレクトするには、次のことを知る必要があります。

  1. このライブラリへのファイルシステム内のパス
  2. ダウンロードした仮想アドレス
  3. 置換する関数の名前。
  4. 代替機能アドレス
また、元の関数のアドレスを取得して、逆リダイレクトを実行し、すべてを元の場所に戻す必要があります。



Cリダイレクト関数のプロトタイプは次のとおりです。



 void *elf_hook(char const *library_filename, void const *library_address, char const *function_name, void const *substitution_address);
      
      







リダイレクトアルゴリズム



リダイレクト関数のアルゴリズムは次のとおりです。

  1. ライブラリファイルを開きます。
  2. 「.dynsym」セクションのシンボルのインデックスを覚えています。その名前は目的の関数の名前に対応しています。
  3. 「.rel.plt」セクションを見て、指定されたインデックスを持つシンボルの再配置を探します。
  4. そのようなシンボルが見つかった場合、元のアドレスを保存し、関数からそれを返すために、代替関数のアドレスを再配置で示された場所に書き留めます。 この場所は、メモリ内のライブラリ読み込みアドレスと再配置のオフセットの合計として計算されます。 それだけです 関数のアドレスは偽装されています。 ライブラリがこの関数を呼び出すたびにリダイレクトが発生します。 関数を終了し、元のアドレスを返します。
  5. そのようなシンボルが「.rel.plt」セクションで見つからない場合、同じ原理に従って「rel.dyn」セクションでそれを探しています。 ただし、rel.dyn再配置セクションでは、目的のインデックスを持つシンボルが複数回出現する可能性があることに注意する必要があります。 したがって、最初のリダイレクト後に検索サイクルを完了することはできません。 ただし、元のアドレスはインデックスの最初の一致時に記憶され、計算できなくなります-まだ変更されていません。
  6. 元の関数のアドレスを返すか、目的の名前の関数が見つからなかった場合はNULLを返します。


以下は、Cでのこの関数のコードです。



 void *elf_hook(char const *module_filename, void const *module_address, char const *name, void const *substitution) { static size_t pagesize; int descriptor; //file descriptor of shared module Elf_Shdr *dynsym = NULL, // ".dynsym" section header *rel_plt = NULL, // ".rel.plt" section header *rel_dyn = NULL; // ".rel.dyn" section header Elf_Sym *symbol = NULL; //symbol table entry for symbol named "name" Elf_Rel *rel_plt_table = NULL, //array with ".rel.plt" entries *rel_dyn_table = NULL; //array with ".rel.dyn" entries size_t i, name_index = 0, //index of symbol named "name" in ".dyn.sym" rel_plt_amount = 0, // amount of ".rel.plt" entries rel_dyn_amount = 0, // amount of ".rel.dyn" entries *name_address = NULL; //address of relocation for symbol named "name" void *original = NULL; //address of the symbol being substituted if (NULL == module_address || NULL == name || NULL == substitution) return original; if (!pagesize) pagesize = sysconf(_SC_PAGESIZE); descriptor = open(module_filename, O_RDONLY); if (descriptor < 0) return original; if ( section_by_type(descriptor, SHT_DYNSYM, &dynsym) || //get ".dynsym" section symbol_by_name(descriptor, dynsym, name, &symbol, &name_index) || //actually, we need only the index of symbol named "name" in the ".dynsym" table section_by_name(descriptor, REL_PLT, &rel_plt) || //get ".rel.plt" (for 32-bit) or ".rela.plt" (for 64-bit) section section_by_name(descriptor, REL_DYN, &rel_dyn) //get ".rel.dyn" (for 32-bit) or ".rela.dyn" (for 64-bit) section ) { //if something went wrong free(dynsym); free(rel_plt); free(rel_dyn); free(symbol); close(descriptor); return original; } //release the data used free(dynsym); free(symbol); rel_plt_table = (Elf_Rel *)(((size_t)module_address) + rel_plt->sh_addr); //init the ".rel.plt" array rel_plt_amount = rel_plt->sh_size / sizeof(Elf_Rel); //and get its size rel_dyn_table = (Elf_Rel *)(((size_t)module_address) + rel_dyn->sh_addr); //init the ".rel.dyn" array rel_dyn_amount = rel_dyn->sh_size / sizeof(Elf_Rel); //and get its size //release the data used free(rel_plt); free(rel_dyn); //and descriptor close(descriptor); //now we've got ".rel.plt" (needed for PIC) table and ".rel.dyn" (for non-PIC) table and the symbol's index for (i = 0; i < rel_plt_amount; ++i) //lookup the ".rel.plt" table if (ELF_R_SYM(rel_plt_table[i].r_info) == name_index) //if we found the symbol to substitute in ".rel.plt" { original = (void *)*(size_t *)(((size_t)module_address) + rel_plt_table[i].r_offset); //save the original function address *(size_t *)(((size_t)module_address) + rel_plt_table[i].r_offset) = (size_t)substitution; //and replace it with the substitutional break; //the target symbol appears in ".rel.plt" only once } if (original) return original; //we will get here only with 32-bit non-PIC module for (i = 0; i < rel_dyn_amount; ++i) //lookup the ".rel.dyn" table if (ELF_R_SYM(rel_dyn_table[i].r_info) == name_index) //if we found the symbol to substitute in ".rel.dyn" { name_address = (size_t *)(((size_t)module_address) + rel_dyn_table[i].r_offset); //get the relocation address (address of a relative CALL (0xE8) instruction's argument) if (!original) original = (void *)(*name_address + (size_t)name_address + sizeof(size_t)); //calculate an address of the original function by a relative CALL (0xE8) instruction's argument mprotect((void *)(((size_t)name_address) & (((size_t)-1) ^ (pagesize - 1))), pagesize, PROT_READ | PROT_WRITE); //mark a memory page that contains the relocation as writable if (errno) return NULL; *name_address = (size_t)substitution - (size_t)name_address - sizeof(size_t); //calculate a new relative CALL (0xE8) instruction's argument for the substitutional function and write it down mprotect((void *)(((size_t)name_address) & (((size_t)-1) ^ (pagesize - 1))), pagesize, PROT_READ | PROT_EXEC); //mark a memory page that contains the relocation back as executable if (errno) //if something went wrong { *name_address = (size_t)original - (size_t)name_address - sizeof(size_t); //then restore the original function address return NULL; } } return original; }
      
      





テストケースを備えたこの機能の完全な実装はダウンロード可能です。



テストプログラムを書き換えます。



 #include <stdio.h> #include <dlfcn.h> #include "elf_hook.h" #define LIBTEST1_PATH "libtest1.so" //position dependent code (for 32 bit only) #define LIBTEST2_PATH "libtest2.so" //position independent code void libtest1(); //from libtest1.so void libtest2(); //from libtest2.so int hooked_puts(char const *s) { puts(s); //calls the original puts() from libc.so because our main executable module called "test" is intact by hook puts("is HOOKED!"); } int main() { void *handle1 = dlopen(LIBTEST1_PATH, RTLD_LAZY); void *handle2 = dlopen(LIBTEST2_PATH, RTLD_LAZY); void *original1, *original2; if (NULL == handle1 || NULL == handle2) fprintf(stderr, "Failed to open \"%s\" or \"%s\"!\n", LIBTEST1_PATH, LIBTEST2_PATH); libtest1(); //calls puts() from libc.so twice libtest2(); //calls puts() from libc.so twice puts("-----------------------------"); original1 = elf_hook(LIBTEST1_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle1), "puts", hooked_puts); original2 = elf_hook(LIBTEST2_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle2), "puts", hooked_puts); if (NULL == original1 || NULL == original2) fprintf(stderr, "Redirection failed!\n"); libtest1(); //calls hooked_puts() twice libtest2(); //calls hooked_puts() twice puts("-----------------------------"); original1 = elf_hook(LIBTEST1_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle1), "puts", original1); original2 = elf_hook(LIBTEST2_PATH, LIBRARY_ADDRESS_BY_HANDLE(handle2), "puts", original2); if (NULL == original1 || original1 != original2) //both pointers should contain hooked_puts() address now fprintf(stderr, "Restoration failed!\n"); libtest1(); //again calls puts() from libc.so twice libtest2(); //again calls puts() from libc.so twice dlclose(handle1); dlclose(handle2); return 0; }
      
      





コンパイルします:



 gcc -g3 -m32 -shared -o libtest1.so libtest1.c gcc -g3 -m32 -fPIC -shared -o libtest2.so libtest2.c gcc -g3 -m32 -L$PWD -o test test.c elf_hook.c -ltest1 -ltest2 -ldl
      
      





そして実行:



 export LD_LIBRARY_PATH=$PWD:$LD_LIBRARY_PATH ./test
      
      







結論は次のとおりです。



libtest1: 1st call to the original puts()

libtest1: 2nd call to the original puts()

libtest2: 1st call to the original puts()

libtest2: 2nd call to the original puts()

-----------------------------

libtest1: 1st call to the original puts()

is HOOKED!

libtest1: 2nd call to the original puts()

is HOOKED!

libtest2: 1st call to the original puts()

is HOOKED!

libtest2: 2nd call to the original puts()

is HOOKED!

-----------------------------

libtest1: 1st call to the original puts()

libtest1: 2nd call to the original puts()

libtest2: 1st call to the original puts()

libtest2: 2nd call to the original puts()







これは、タスクセットが最初に完全に完了したことを示します。 やった!



共有ライブラリがロードされたアドレスを見つける方法は?


この非常に興味深い質問は、リダイレクト関数のプロトタイプを慎重に検討するときに発生します。 いくつかの調査の後、dlopen()関数が返す記述子からライブラリの読み込みアドレスを決定する方法を見つけることができました。 これは次のマクロで実行されます。



#define LIBRARY_ADDRESS_BY_HANDLE(dlhandle) ((NULL == dlhandle) ? NULL : (void*)*(size_t const*)(dlhandle))









新しい関数のアドレスを記録および復元する方法は?


「.rel.plt」セクションからの再配置が示すアドレスの書き換えに問題はありません。 基本的に、「。plt」セクションの対応する要素のJMP命令のオペランドが上書きされます。 そして、そのような命令のオペランドは単なるアドレスです。



さらに興味深いのは、相対CALL命令のオペランドに再配置を適用する状況です(コードE8)。 それらの遷移のアドレスは、式によって計算されます:



address_of_a_function = CALL_argument + address_of_the_next_instruction







したがって、元の関数のアドレスを見つけることができます。前の式から、必要な関数を呼び出すために相対CALLの引数として書き込む必要がある値を取得します。「。rel.dyn」セクションは「RE」とマークされたセグメントに入ります。つまり、アドレスを書き込むだけではうまくいきません。 。再配置する必要があるページの書き込み権限を追加する必要があり、リダイレクト後にすべてをリダイレクトすることを忘れないでください。これは、mprotect()関数を使用して行われます。この関数の最初のパラメーターは、再配置を含むページのアドレスです。常にページサイズの倍数にする必要があります。計算するのは難しくありません。ページサイズに応じて、再配置アドレスの最下位バイトをリセットするだけです。



CALL_argument = address_of_a_function - address_of_the_next_instruction











page_address = (size_t)relocation_address & ( ((size_t) -1) ^ (pagesize - 1) );







たとえば、32ビットシステムのサイズが4096(0x1000)バイトのページの場合、上記の式は次のように変換されます。1ページサイズは、sysconf(_SC_PAGESIZE)を呼び出すことで確認できます。



page_address = (size_t)relocation_address & (0xFFFFFFFF ^ 0xFFF) = (size_t)relocation_address & 0xFFFFF000;











使用例


演習として、すべてのネットワークコールをリダイレクトするプラグイン(例:Adobe Flashプラグイン(libflashplayer.so))をFirefoxに書き込むことができます。したがって、Firefoxプロセスからインターネット上のすべてのAdobe Flashトラフィックを制御でき、ブラウザー自体および他のプラグインのネットワーク呼び出しに影響を与えることはありません。



頑張って



関連リンク


ここで英語読みたい人は、ロシア語で初めて公開します。



All Articles