LD_PRELOADを駆使してロードする共有ライブラリを動的に変える(dlopen対応)
概要
- 共有ライブラリの関数呼び出しをフックする方法について解説
-
LD_PRELOADを使用した共有ライブラリのフック方法を紹介 -
dlopenとdlsymを使用してロードされた関数をフックする方法 - 自身をロードして公開するテクニックにより、
dlopenもフック可能
LD_PRELOADによる共有ライブラリのフック
Linuxでは、LD_PRELOAD を用いて共有ライブラリの関数をフックすることが一般的です。
LD_PRELOADに共有ライブラリのパスを指定すると、プログラム起動時に動的リンカが対象のライブラリを優先的にリンクします。
これにより、指定ライブラリ内の関数が同名の既存関数を置き換える形で動作します。
以下のC++プログラムは math.h を介して libm.so の pow 関数と log 関数を呼び出します。
#include <iostream>
#include <math.h>
int main()
{
double result;
// Use variables to avoid compiler optimization
auto x = 8.0;
auto y = 2.0;
result = pow(x, y);
std::cout << "pow(8.0, 2.0) = " << result << std::endl;
auto e = 2.718282;
result = log(e);
std::cout << "log(e) = " << result << std::endl;
}
$ g++ -O0 -o main main.cpp -lm && ./main
pow(8.0, 2.0) = 64
log(e) = 1
ここで、フックしたい関数(この例ではpow)と同名、同シグネチャの関数を定義して共有ライブラリを作成します。
このライブラリでは、powに渡される引数を勝手に倍にしてしまう処理を挟んでいます。
また、オリジナルのpowを呼び出すためにdlsymのRTLD_NEXTフラグを使用しています。
#include <dlfcn.h>
#include <iostream>
static double (*original_pow)(double, double) = nullptr;
extern "C" double pow(double x, double y)
{
// Load original pow() if not loaded yet
if (!original_pow)
{
original_pow = reinterpret_cast<double (*)(double, double)>(dlsym(RTLD_NEXT, "pow"));
if (!original_pow)
{
std::cout << "Error loading original pow function." << std::endl;
std::exit(EXIT_FAILURE);
}
}
// Do some interception
x *= 2;
y *= 2;
std::cout << "\033[31m";
std::cout << "Indercepted! Args are doubled: (" << x << ", " << y << ")" << std::endl;
std::cout << "\033[0m";
// Call original pow()
return original_pow(x, y);
}
この共有ライブラリをビルド、LD_PRELOAD環墧変数に指定してプログラムを実行すると、pow関数の引数が2倍にされていることが確認できます。
$ g++ -O0 -o main main.cpp -lm
$ g++ -O0 -shared -fPIC -o intercept.so intercept.cpp -ldl
$ LD_PRELOAD=./intercept.so ./main
Indercepted! Args are doubled: (16, 4)
pow(8.0, 2.0) = 65536
log(e) = 1
これが、共有ライブラリのフックの基本的な方法です。
dlopenによる共有ライブラリのロード
先述のプログラム(main.cpp)では、実行環境の動的リンカがプログラム起動時にlibm.soをリンクしていました。
一方でdlopen関数を使用することで、プログラム実行中に任意の共有ライブラリを動的にロードできます。
以下のプログラムでは、dlopenを使用してlibm.soをロードし、dlsymでpowとlogのアドレスを取得して呼び出しています。
#include <iostream>
#include <stdlib.h> // for exit()
#include <dlfcn.h> // for dlopen(), dlsym()
// we don't need math.h anymore
int main()
{
char *error;
// load libm.so dynamically
auto handle = dlopen("libm.so", RTLD_LAZY);
if (!handle)
{
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
dlerror();
// get pow() address
auto dl_pow = (double (*)(double, double))dlsym(handle, "pow");
if ((error = dlerror()) != NULL)
{
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
// get log() address
auto dl_log = (double (*)(double))dlsym(handle, "log");
if ((error = dlerror()) != NULL)
{
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
// do same thing as main.cpp
double result;
auto x = 8.0;
auto y = 2.0;
result = dl_pow(x, y);
std::cout << "pow(8.0, 2.0) = " << result << std::endl;
auto e = 2.718282;
result = dl_log(e);
std::cout << "log(e) = " << result << std::endl;
// unload libm.so
dlclose(handle);
return 0;
}
このプログラムの実行結果は先述のmain.cppと同じです。
どちらの方法でも同じ関数が呼び出されていることが確認できます。
$ g++ -O0 -o main_via_dlopen main_via_dlopen.cpp -ldl
$ ./main_via_dlopen
pow(8.0, 2.0) = 64
log(e) = 1
さて、LD_PRELOADを使ってmain_via_dlopenの中で使われている関数をフックできるでしょうか?
残念ながら、LD_PRELOADによるフックはdlopenでロードしたライブラリには効果がありません。
これは、LD_PRELOADがあくまでプログラム起動時のリンクに対して働くためです。
$ LD_PRELOAD=./intercept.so ./main_via_dlopen
pow(8.0, 2.0) = 64
log(e) = 1
dlopenでロードしたライブラリをフックする
なんとかして、dlsymで検索される対象に自作のpow関数を含める必要があります。
そのための方法として、dlopenをフックして自作の共有ライブラリをロードする方法があります。
#include <dlfcn.h>
#include <string>
#include <iostream>
static void *(*original_dlopen)(const char *filename, int flags) = nullptr;
extern "C" void *dlopen(const char *filename, int flags)
{
if (!original_dlopen)
{
original_dlopen = reinterpret_cast<void *(*)(const char *, int)>(dlsym(RTLD_NEXT, "dlopen"));
if (!original_dlopen)
{
std::cout << "Error loading original dlopen function." << std::endl;
std::exit(EXIT_FAILURE);
}
}
if (std::string(filename) == "libm.so")
{
// Set dummy variable, it has static address.
static int dummy = 0xdeedbeef;
Dl_info info;
// Find .so info which contains dummy variable address (=ownself).
if (dladdr(&dummy, &info))
{
// Load ownself instead of libm.so
// This program have loaded libm.so implicitly (see ldd ./intercept_dl.so).
// Thus we can expose symbols of original libm.so.
// If you want to hook library which is not loaded yet, you should load it explicitly with RTLD_GLOBAL.
// ex. original_dlopen("libm.so", RTLD_LAZY | RTLD_GLOBAL);
return original_dlopen(info.dli_fname, flags);
}
}
return original_dlopen(filename, flags);
}
注目すべきはdladdr関数の利用です。
staticに定義された変数dummyのアドレスを検索することで、自分自身のパスを取得しています。
これをpowを公開しているintercept.cppと一緒に共有ライブラリとしてビルド、main_via_dlopen.cppと一緒に実行すると、pow関数がフックされていることが確認できます。
$ g++ -O0 -shared -fPIC -o intercept_dl.so intercept.cpp intercept_dl.cpp -ldl
$ LD_PRELOAD=./intercept_dl.so ./main_via_dlopen
Indercepted! Args are doubled: (16, 4)
pow(8.0, 2.0) = 65536
log(e) = 1
見事にdlopenでロードしたライブラリもフックすることに成功しました。
非フック対象関数の実行
さて、dlopenをフックすることでlibm.soの代わりにintercept_dl.soをロードできました。
しかし、libm.soの他の関数はどうなるでしょうか?
今回のプログラムでは、log関数をフックしていませんが、main_via_dlopen.cppから呼び出しています。
intercept_dl.soではlog関数を定義していないため、dlsymの結果libm.soのlog関数が呼び出されて欲しいところです。
実際のところ実行結果を見て分かるように、ちゃんとlibm.soのlog関数が呼び出されています。
これはintercept_dl.soが暗黙にlibm.soをロード、ロードしたシンボルを外部に公開しているためです。
lddコマンドでintercept_dl.soを調べるとlibm.soに依存していることが分かります。
$ ldd ./intercept_dl.so
linux-vdso.so.1 (0x0000ffff9d861000)
libdl.so.2 => /lib/aarch64-linux-gnu/libdl.so.2 (0x0000ffff9d80a000)
libstdc++.so.6 => /usr/lib/aarch64-linux-gnu/libstdc++.so.6 (0x0000ffff9d632000)
libgcc_s.so.1 => /lib/aarch64-linux-gnu/libgcc_s.so.1 (0x0000ffff9d60e000)
libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff9d49a000)
/lib/ld-linux-aarch64.so.1 (0x0000ffff9d831000)
libm.so.6 => /lib/aarch64-linux-gnu/libm.so.6 (0x0000ffff9d3ef000)
これはiostreamなどのライブラリがlibm.soをリンクしているためです。
intercept_dl.soのロード時にlibm.soも自動的にロードされ、intercept_dl.soの中で見つからなかったシンボルはlibm.soから探されるようになります。
しかし、libm.soがリンクされているのはたまたまであり、他のライブラリでは同じようなことが期待できません。
従って、明示的に非フック対象を含むライブラリをロード、公開しておくことが重要です。
その場合、dlopenのフラグにRTLD_GLOBALを指定することでロードしたライブラリのシンボルを他のライブラリに公開できます。
(dlopen("libm.so", RTLD_LAZY | RTLD_GLOBAL);)
まとめ
-
LD_PRELOADはプログラム起動時のリンクに対して働く -
dlopenでロードしたライブラリはLD_PRELOADによるフックの対象外 -
dlopenをフックして自分自身をロードすることで、dlsymで検索される対象に自作の関数を含めることができる - 非フック対象関数を実行するためには、明示的に非フック対象を含むライブラリをロード、公開しておく
-
RTLD_GLOBALフラグを指定することでロードしたライブラリのシンボルを他のライブラリに公開できる
今回の例はマルチメディア系のアプリ等、実行環境によって動的にロードするライブラリが変わるような場合に有用です。
実際、このアイデアはOpenGLのトレースツールであるapitraceのコード中に発見しました。
また、今回の実行プログラムと実行環境はこちらのリポジトリで試すことができます。
名古屋のAI企業「来栖川電算」です。LLM・機械学習の仕事をしています。 リモート中心の環境で、尖ったエンジニアがそれぞれの強みを活かして挑戦する会社です。 公式HP→ kurusugawa.jp/ |採用情報→ kurusugawa.jp/jobs/
Discussion