torutkのブログ

ソフトウェア・エンジニアのブログ

Linux glibcで、LD_AUDIT機能による関数トレース

Binary Hacks ―ハッカー秘伝のテクニック100選
この本の「#77 関数へのenter/exitをフックする」で、GCCコンパイルオプション-finstrument-functionsを使い、関数が呼び出された時、関数から復帰するときにフックを入れる方法が紹介されています。これはコンパイルオプションで埋め込むものですが、同書に「もう1つのフック方法(LD_AUDIT)」というものがあることだけ参考紹介されています。LD_AUDITの方法だと、PLT経由(動的リンク用に作られた)の関数ならばコンパイルし直すことなくフックすることができます。

このLD_AUDITの方法を少し実験してみます。

サンプルを探して

LD_AUDITを使う方法は、インターネット上にわずかしか情報を見つけることができませんでした。以下情報源とコメントです。

これは、書籍で引用された文献です。しかし、読んでも内容がピンとこないし、実例がなくこれだけでサンプルを作って動かすのは苦しいです。

関数をフックするサンプルがあります。64bit用Linuxのようですが、参考になります。

上記日記でも紹介されていたサンプルソースです。コードの書き方はここを一番参考にしました。

監視インタフェースを呼ぶ側のコードです。pltexitが最初呼ばれずなぜか調べるときに参照しました。

LA_AUDITを使ったトレース機能です。(straceのようなツール)このソースコードのaudit.cを参考にしました。

概要

glibc 2.4以降では、環境変数LD_AUDITに監査インタフェースを実装したライブラリファイルを指定していると、各プログラムを実行しているときに、監査インタフェースを介してLD_AUDITに指定したライブラリを呼び出してくれます。
この監査インタフェースには、関数のenter/exitもあるため、監査インタフェースの実装にいろいろ仕掛けを書きます。

監査インタフェース一覧

Linux glibc-2.5 (CentOS 5.2 32bit)での調査結果です。

関数名 内容 定義ヘッダ
la_version 監査インタフェースのハンドシェイク link.h
la_activity リンクマックアクティビティ通知 link.h
la_objsearch 検索実行を通知 link.h
la_opjopen リンカによるオブジェクトのロードを通知 link.h
la_preinit ロード後アプリケーションに処理を渡す前に通知 link.h
la_symbind32 2つのオブジェクト間の結合時に通知 link.h
la_symbind64 同上(64bit版?) link.h
la_objclose オブジェクト終了後アンロード前に通知 link.h
la_i86_gnu_pltenter 関数呼び出し時(PLT) bits/link.h
la_i86_gnu_pltexit 関数復帰時 bits/link.h
関数のフックに必要な処理は?
  • la_version
    • これがないと監査インタフェースが無効
  • la_objopen
    • 対象ライブラリがロードされたときに、以後監査インタフェースを有効にするかを指定する
  • la_i86_gnu_pltenter
    • フック処理を記述するのはここ。関数がPLT経由で呼ばれるときにこの関数が呼ばれる

サンプルコード

以下のソースコードコンパイルし、動的リンクライブラリを作成します。

#include <link.h>
#include <iostream>

// 監査インタフェースの初期ハンドシェイク
// link.hで定義されるLAV_CURRENTを返す
unsigned int la_version(unsigned int version) {
    return LAV_CURRENT;
}

// オブジェクト(ライブラリ)が実行時リンカによりロードされた通知
// 戻り値により以後の監査インタフェース呼び出しに影響あり
// plenterが呼ばれるには、LA_FLG_BINDTO|LA_FLG_BINDFROM を返す
unsigned int la_objopen (struct link_map* map, Lmid_t lmid,
                         uintptr_t* cookie) {
    std::cout << "[LD_AUDIT]" << map->l_name << " loaded" << std::endl;
    return LA_FLG_BINDTO | LA_FLG_BINDFROM;
}

// 関数呼び出しのフック
//
ElfW(Addr) la_i86_gnu_pltenter(
    ElfW(Sym)* sym, unsigned int ndx, uintptr_t* refcook,
    uintptr_t* defcook, La_i86_regs* regs, unsigned int* flags,
    const char* symname, long* framesizep
) {
    std::cout << "[LD_AUDIT]" << symname << " entering" << std::endl;
    return sym->st_value;
}

コンパイルします。

$ g++ -fPIC -shared audit.cpp -o libaudit.so -lpthread
$

関数フックしたいプログラムを、LD_AUDIT環境変数を設定して呼び出します。

$ LD_AUDIT=libaudit.so hoge
  :

トラブル

関数から復帰するときのフックla_i86_gnu_pltexitが呼ばれない

関数から復帰するときにフックされる関数la_i86_gnu_pltexitがあります。最初これが呼ばれないので調べてみると、la_86_gnu_pltenterの引数framesizepにスタックサイズを設定する必要があります。

// 関数呼び出しのフック
//
ElfW(Addr) la_i86_gnu_pltenter(
    ElfW(Sym)* sym, unsigned int ndx, uintptr_t* refcook,
    uintptr_t* defcook, La_i86_regs* regs, unsigned int* flags,
    const char* symname, long* framesizep
) {
    std::cout << "[LD_AUDIT]" << symname << " entering" << std::endl;
    *framesizep = 128;
    return sym->st_value;
}

128が適切かは状況によります。
なお、手元のマシン(CentOS 5.2/glibc 2.5-24/gcc 4.1.2)では、framesizepを設定した場合、la_i86_gnu_pltexitが呼ばれましたが、リターンした直後にセグメンテーションフォールトが発生してしまいます。

コマンド終了時にセグメンテーションフォールトが発生

上記監査用ライブラリを指定してコマンドを実行すると、コマンドによらず最後にセグメンテーションフォールトが発生します。原因不明。