Movatterモバイル変換


[0]ホーム

URL:


BLOGTIMES

cles::blog

平常心是道
« :: »
2020/08/14

LD_PRELOAD で標準ライブラリの関数の挙動を変更する

  c  linux  softwareengineering 
このエントリーをはてなブックマークに追加

Linux でLD_PRELOAD 環境変数と共有ライブラリを使うと、標準ライブラリの関数の前後に処理を挟んだり、処理を上書きしたりする簡易アスペクト指向のようなことができます。

簡単なターゲットプログラム

今回ターゲットにする関数はみんな大好きprintf()
今日はこんな感じのプログラムを用意しました。

helloworld.c

#include <stdio.h>int main(){ printf("Hello World!\n"); printf("%d\n", 2020); return 0;}

これをコンパイルして実行すると、こんな感じの実行結果になります。

$ gcc helloworld.c -o helloworld$ ./helloworldHello World!2020

今回の目的はこの helloworld をリコンパイルせずにprintf() の挙動を変更することにあります。

printf() は何者なのか?

今回はプログラムの挙動を変更するために、同じ名前で同じ引数(つまり同じシグネチャ)を持った関数を作って、プログラムの関数呼び出しを横取り(いわゆる、フック)するという方法を取ります。このため、あらかじめ対象となる関数のシグネチャを取得しておく必要があります。

今回の対象はprintf() ですから、Linux であれば、printf() の定義は/usr/include/stdio.h に入っているはずです。
ファイルの中からprintf() を探すと以下の行が見つかります。

・・・・/* Write formatted output to stdout. This function is a possible cancellation point and therefore not marked with __THROW. */extern int printf (const char *__restrict __format, ...);・・・・・

関数呼び出しをフックするとは

前述のとおり、今回は関数呼び出しをフックすることで関数の動作を変更します。
このため、ある程度の C 言語の知識と Linux の動的リンカー/ローダーであるld.so*1 の仕組みについての知識が必要になります。おまじないのように言われることが多いLD_PRELOAD 環境変数はld.so の動作を変更するものです。

動的リンカー/ローダーがどのライブラリをロードするのかについてはldd というコマンドで確かめることができます。
例えば、先ほどのhelloworldldd してみると以下のような出力が得られます。

$ ldd ./helloworld linux-vdso.so.1 => (0x00007ffe5d1f6000) libc.so.6 => /lib64/libc.so.6 (0x00007f9fe9b16000) /lib64/ld-linux-x86-64.so.2 (0x00007f9fe9ee4000)

以下のようにnm を使ってシンボルを取り出してみるとprintf()libc.so.6 の中に入っていることが確認できます。

$ nm -gD /lib64/libc.so.6 | grep -E " printf$"0000000000053410 T printf

また、ldd で出力されるライブラリの並びには意味があり、上に表示されているものの方が優先順位が高くなっています。つまり、複数のライブラリに同じ名前のシンボルがあった場合、上のライブラリで見つかったものが優先されることになります。

したがって、libc.so.6 よりも上の行に表示されるライブラリにprintf() を仕込むことができれば、自分のプログラムから呼出すprintf() の挙動が変更できるということになります。さらに、libc.so.6printf は上書きされて無くなってしまっているわけではなく、シンボルの検索順序の関係で見えなくなっているだけに過ぎないので、呼び出し方を工夫すればlibc.so.6printf() を呼び出すこともできるということになります。

逆に、今回のやり方でできるのは対象となる関数が.so に含まれている場合だけで、プログラムを静的リンクされている場合には適用できません。

関数を上書きするためのライブラリを作る

今回はprintf()(とputs()*2 )を上書きするための以下のプログラムを作成しました。

今回は単純に関数が呼出される前に「BEFORE 関数名」、関数が呼出された後に「AFTER 関数名」を出力するという単純な処理を追加するプログラムになっています。今回の printf は可変長引数 (...) を持っているので、オリジナルのprintf() ではなくva_list に対応したvprintf() を呼出す必要があり、ちょっとハマってしまいました。

hook.c

#include <dlfcn.h>#include <stdio.h>#include <stdarg.h>typedef int (*ORIGINAL_PUTS)(const char *__s);typedef int (*ORIGINAL_PRINTF)(const char *__restrict __format, ...);int puts(const char *__s){ ORIGINAL_PUTS original_puts = (ORIGINAL_PUTS)dlsym(RTLD_NEXT, "puts"); int ret; original_puts("BEFORE puts()"); ret = original_puts(__s); original_puts("AFTER puts()"); return ret;}int printf(const char *__restrict __format, ...){ ORIGINAL_PRINTF original_printf = (ORIGINAL_PRINTF)dlsym(RTLD_NEXT, "printf"); int ret; va_list args; va_start(args, __format); original_printf("BEFORE printf()\n"); ret = vprintf(__format, args); /* printfを呼ぶと動かないので注意 */ original_printf("AFTER printf()\n"); va_end(args); return ret;}

プログラムのポイントは見慣れないdlsym() という関数。

Man page of DLOPEN

関数 dlsym() は、 dlopen() が返した動的ライブラリの「ハンドル」と、 NULL 終端されたシンボル名の文字列を引き数に取り、 そのシンボルがロードされたメモリーのアドレスを返す。

シンボルがロードされたメモリーのアドレスという記述だとちょっとピンと来づらいですが、要は ld.so にロードされた関数への関数ポインタが取れるということです。これを使ってオリジナルのlibc.so 側のprintf() にアクセスすることになります。

hook.c は以下のコマンドでコンパイルすることができ、成功するとhook.so というファイルが得られます。

gcc -g -Wall -D_GNU_SOURCE -fPIC -shared -o hook.so hook.c -ldl

生成されたhook.sonm でダンプすると、自分で定義したprintfputs というシンボルが含まれていることが分かります。

$ nm -gD ./hook.so w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable w _Jv_RegisterClasses0000000000201038 B __bss_start w __cxa_finalize w __gmon_start__0000000000201038 D _edata0000000000201040 B _end0000000000000878 T _fini00000000000005d8 T _init U dlsym0000000000000787 T printf0000000000000735 T puts U vprintf

実際に動作させてみる

作成したhook.so と helloworld は以下のように起動させて、動作確認することができます。
printf()puts() の前後に処理が追加されているのが分かります。

$ LD_PRELOAD=./hook.so ./helloworldBEFORE puts()Hello World!AFTER puts()BEFORE printf()2020AFTER printf()

この状態のldd を確認すると、以下のようにlibc.so.6 よりも上に./hook.so が来るのが確認できます。
つまりLD_PRELOAD 環境変数によってライブラリの優先順位が変わり、こちらに入っているprintf() が優先的に呼び出されることになるわけです。

$ LD_PRELOAD=./hook.so ldd ./helloworld linux-vdso.so.1 => (0x00007ffc2bbe0000) ./hook.so (0x00007fed7ab10000) libc.so.6 => /lib64/libc.so.6 (0x00007fed7a742000) libdl.so.2 => /lib64/libdl.so.2 (0x00007fed7a53e000) /lib64/ld-linux-x86-64.so.2 (0x00007fed7ad12000)

ちょっと調べてみるだけという興味本位で始めたことでしたが、予想以上に勉強になりました。

参考


byhsur at 23:38[5年前][4年前][3年前][2年前][1年前][1年後][2年後][3年後][4年後] |
こんな記事もあります 「stdout stdio.h dlopen
gcc で配列の境界外アクセスをチェックする
scanf() で整数を 1 バイト分だけ読み取る
logspout を使って Docker のログを集約してみる
C 言語で cwd を取得する
int arr[n]; はいつからできるようになったのか
double 型の変数には -0.0 が代入できる
Visual Studio 2019 で Hello World するとマルウェア扱いされる?
clang-query を使って cpp の AST を解析する
vCSA を 6.7 にアップデート
Visual Studio 2017/2019 で scanf() がエラー(C4996)になるときは
トラックバックについて
Trackback URL:
お気軽にどうぞ。トラックバック前にポリシーをお読みください。[policy]
このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/11910
Trackbacks
このエントリにトラックバックはありません
Comments
愛のあるツッコミをお気軽にどうぞ。[policy]
古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
コメントはありません
Comments Form

コメントは承認後の表示となります。
OpenIDでログインすると、即時に公開されます。

OpenID を使ってログインすることができます。

Identity URL:Yahoo! JAPAN IDでログイン

« :: »
Copyright © 2004-2023 by CLES All Rights Reserved.
サイト内検索
検索ワードランキング
へぇが多いエントリ
閲覧数が多いエントリ
1 .アーロンチェアのポスチャーフィットを修理(99701)
2 .年次の人間ドックへ(99110)
3 .福岡銀がデマの投稿者への刑事告訴を検討中(99102)
4 .三菱鉛筆がラミーを買収(98708)
5 .2023 年分の確定申告完了!(1つめ)(98679)
最新のエントリ
cles::blogについて
誰が書いてる?
最近行った場所
サイトポリシー
タグ一覧
検索ワードランキング

Referrers

    Powered by CLES
    Nucleus CMS v3.31SP3/w memcached
    21376888(W:7513 Y:1720 T:0987)
    cles::blogのはてなブックマーク数
    benchmark


    [8]ページ先頭

    ©2009-2025 Movatter.jp