動的ライブラリ関数をhackしよう


背景

動的ライブラリ関数をフックして独自関数を呼び出してっていうのが面白そうなのでやってみた。
動的ライブラリ関数を独自関数に置き換えてhackしてみます。
対象関数はwrite(2)

どうやってやるの?

プログラムは通常同じ名前の関数は定義できないが、
動的リンクされたプログラムでは、同じ関数が複数のライブラリに存在することがあり得る。
その際は、最初に見つかった関数が利用される。

環境変数 LD_PRELOAD で指定した共有ライブラリは最優先で読み込まれるため、簡単にプログラムの挙動を変えることができます。

ちなみに何もせずgccでコンパイルすると、glibc(libc.so.6)が動的にリンクされます

動的ライブラリ?

プログラム実行中の任意の時点で読み込まれるライブラリ。

  • 必要と判断されるまで、読み込まれない。
  • プラグインやモジュールの実装に役立つ。
  • 動的ライブラリの作成は、共有ライブラリと同様の方法で作成する。

詳しくは下記でご確認を
http://archive.linux.or.jp/JF/JFdocs/Program-Library-HOWTO/dl-libraries.html

実行環境/必要なもの

  • gccとlddを使ってますのでこれら2点があれば大丈夫です。
$ gcc --version
gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-16)

$ ldd --version
ldd (GNU libc) 2.12

実践例

まずはhackされるプログラム
標準出力に「hello world」のみを出力するだけ。

main.c
#include <string.h>
#include <unistd.h>

int main()
{
    char s[] = "Hello World!\n";
    write(1, s, strlen(s));
    return 0;
}

次はライブラリ
mainで呼んでいるwrite(2)はこちらで定義されているwriteが優先されることを確認。
「hello world」の前に「hello hack」を出力する。

write.c(ライブラリ)
#include <unistd.h>
#include <dlfcn.h>
#include <stdio.h>

int g_first = 0;
void *g_h;
ssize_t (*g_f)(int, const void *, size_t);

//子プロセスに引き継がないよう設定
void __attribute__((constructor)) unset_ld_preload() {
    unsetenv("LD_PRELOAD");
}

ssize_t write(int fd, const void *buf, size_t count) {
    if(g_first == 0){
        g_first = 1;

        // 動的ライブラリへの内部ハンドルを取得
        g_h = dlopen("libc.so.6", RTLD_LAZY);

        // シンボルがロードされたメモリのアドレスを取得
        g_f = dlsym(g_h, "write");
    }

    fprintf(stdout, "Hello Hack\n");
    return (*g_f)(fd, buf, count);
}

上記で独自のwrite関数を定義しています。
attribute((constructor)) は GCC の拡張機能で
main関数の前に行いたい処理がある場合に使います。

# コンパイル(ライブラリ作成)
$ gcc -fPIC -shared -o write.o write.c -ldl

# コンパイル
$ gcc main.c -o main

# 実行
$ LD_PRELOAD=./write.o ./main
Hello Hack
Hello World!

結果はこの通り
main.cでは「Hello World」のみの出力のはずが
その直前に「Hello Hack」が出力されています。

# lddコマンドでライブラリの依存関係を確認
$ ldd main
        linux-vdso.so.1 =>  (0x00007fff619d1000)
        libc.so.6 => /lib64/libc.so.6 (0x0000003edae00000)
        /lib64/ld-linux-x86-64.so.2 (0x0000003edaa00000)

$ export LD_PRELOAD=./write.o
$ ldd main
Hello Hack
        linux-vdso.so.1 =>  (0x00007fff643a7000)
        ./write.o (0x00002b947289c000)
        libc.so.6 => /lib64/libc.so.6 (0x0000003edae00000)
        libdl.so.2 => /lib64/libdl.so.2 (0x0000003edb600000)
        /lib64/ld-linux-x86-64.so.2 (0x0000003edaa00000)

まとめ/感想

今回の方法を使えば独自printf等が作成できます。
完成されたプログラムが使用しているライブラリ関数を
プログラム本体を修正せずに手を加えることが出来るので
使う機会はあまり無いですが面白いのでおすすめです。

参考文献/リンク

LD_PRELOADで関数フックしてみよう!
dlopen - ライブラリコールの説明