「 elf 」一覧

elfバイナリでの関数フック. 強引編


今までのものは手間はかかっているもののきちんと手順を踏んで、わりと行儀よく(?)関数のフックを実現していました。今回は豪快な関数フックの方法を試してみたいと思います。

※ なおこの手順で問題が起こっても自己責任でお願いします。前提条件として 32bitのアプリケーションとしています。

その方法とは、対象関数の先頭に、自分のフック関数へのジャンプを埋め込んでしまえばいい!というものです。
具体的には、関数先頭のアセンブラを書き換えて、自分の用意したフック関数のアドレスへの無条件ジャンプに数値を書き換えてしまう、という流れになります。

多くの場合、関数の配置されているメモリは実行可能&リード可能なメモリ領域としてマークされています。そのため、まずはメモリ領域の属性を変更する必要があります。Linuxにおいては下記のプログラムのようにして属性を変更します。

ここで func が目的の関数となります。関数アドレスから、その関数が所属しているページを割り出して mprotect API で属性を変更します。

その後、関数のアドレスの場所に 5バイトほど値を書き込みます。

0xE9 [フック関数のアドレス] で5バイトです。これはアセンブラではアドレスへの無条件ジャンプとなります。

これらの手順で加工した元の関数を呼び出すと、見事に hookedFunc に実行が移ってきていると思います。今回はいきなり5バイト破壊してしまっているので、フック関数から、元の関数を呼び出すような実装となっていません。必要ならば、このあたり整合性を持つようにバイト列をコピー&加工する必要があると思います。元に戻す場合には書き換えた5バイトをバックアップから書き戻すようにすればできるでしょう。


セクションヘッダがない場合の関数フック(elf編)


前回は共有ライブラリ(.so)での関数フックの話でした。そしてこれはセクションヘッダがある前提で rel.plt セクションを見つけました。今回はセクションヘッダを参照しないでPLTの場所を特定し、関数フックを実現する案を記載してみようと思います。

そもそも elf実行体がメモリにロードされて実行されている状況では、elf実行体そのものにセクションヘッダがあったとしてもメモリにロードされていません。実行体そのもののファイルを読み込んでセクションヘッダを解析して特定する方法もあるかと思います。前回のsoの場合においては、そのようにして手元ではサンプルを組んでいました。しかし、メモリにロードされている情報からうまく特定できるのであれば無駄なメモリを使うことなく、ディスクIOも減らせて何かと都合がよいです。またストリップ化されているsoについてはセクションヘッダなしで何とか特定しなければなりません。

つまり、セクションヘッダなしで前回と同じようなことを実現するのを目的としたいと思います。今回は下記のような呼び出しをフックする方向を考えています。
elf-hook1

elf実行体からある共有ライブラリの関数を呼び出しており、その関数のフック先は別に自分自身の内部に用意してあるというものです。フック処理を適用して呼び出し先を変更します。

続きを読む


共有ライブラリ(.so) の関数呼び先をフック


前回の内容でとりあえずシンボル情報にアクセスする糸口がわかりました。
これを利用して、共有ライブラリの関数フックを考えてみたいと思います。

sample.app が共有ライブラリ libfunc.so と libbar.so を利用しているとします。そしてこれらが下記の図に示すような呼び出し関係を持っているとします。

original_call

しかし、このlibbar.so の関数にはバグがあるようで、しかも今すぐには修正できないような状況だったとします。自分ではなく他の人の作成したライブラリを使うような場合、こういうことってありますよね・・・。
 そこで、関数そのものを一時的であれ自分ハックをいれたようなものに置き換えて今をしのぐ、というようなことを考えます。ここで関数フックの登場となります。そして下記に示すような呼び出し関係を構築することを目標とします。

modified_call

前回の内容と対象のsoがロードされているベースアドレスから、soがもっているセクションヘッダを取得します。そしてそのセクションヘッダを解釈して、置き換えたい関数のPLTの場所を特定します。PLTの場所が判明し、そこの内容をフック先の関数アドレスを入れておけば、この図のような呼び出しのリダイレクトが実現できます。

※ ひとまず 32bit 実行環境であると想定して話を進めます。
また、共有.soについては PIC で生成されているものとします。

やり方としては、まず “.rel.plt”セクションのセクションヘッダを取得します。
このヘッダの sh_addr と 共有ライブラリのベースアドレスを加算したアドレスに、 Elf32_Rel構造体の情報が格納されています。個数は sh_size がバイトサイズなので、1要素単位で除算して求めておきます。
Elf32_Rel.r_info メンバを ELF32_R_SYM マクロを使って、シンボルのインデックスを取得することができます。シンボルのインデックスは Elf32_Sym 配列の序数として使えるので、関連するストリングテーブルを取得しておけば関数の名前が求まります。関数の名前を比較して、フックしたい該当関数かどうかを判定します。
 続いて、PLTの場所ですがこれは Elf32_Rel.r_offsetメンバを参照することでわかります。これもまたベースアドレスからのオフセットになっています。これでアドレスが特定できます。実際にそのアドレスの中身を見ると関数先頭のアドレスが格納されています。動的リンクではこのテーブルのアドレスを見て関数ジャンプという仕組みでおこなっているため、このアドレスを自分の関数をいれておけばOKという仕掛けでフックを実現します。

文章ではちょっとわかりにくさを感じたので、これらの位置関係を図示してみました。こんな感じです。
pos_rel_plt

通常PLT経由のジャンプにおいては、PLTの該当場所に記録されたアドレスを参照して jmp命令を実行するというコードになっています。初期状態ではこのPLTには jmp命令の次の行を示しており、そこから実際の関数はどこなのかを調査するコードが実行されます。このコードが実行されると、PLTの場所に見つけ出された関数のアドレスが記録されます。次回以降はこの調査コードが実行されずに関数を呼び出すことができるという仕掛けになっています。

このPLTジャンプの内容については、ここ(共有ライブラリーはどのように動作するか)がわかりやすいかも。

これが動作する秘密は、PLT(Procedure Linkage Table)と呼ばれるデータの塊、つまりプログラムが呼ぶ全てのファンクションをリストアップした、プログラム中のテーブルです。プログラムが開始する時に、ファンクションがロードされたアドレスをランタイム・リンカーに対して問い合わせるコードを、PLTは各ファンクションに対して持っています。そうするとプログラムは、テーブル中のそのエントリーを取り込み、そこにジャンプします。それぞれのファンクションが呼ばれるにつれ、それぞれのファンクションに対するPLTでのエントリーは、ロードされたファンクションへの直接ジャンプに単純化されます。

余談

関数フックについては、同じ関数名で実装した別の .so を作成し、これをLD_PRELOAD環境変数にセットして、プログラムを実行するという方法があります。Linuxでは割と定番の関数フックです。今回のような内容は、LD_PRELOADを使えば簡単にできるのですが、アプリを使う側への手間をかけたくない、コンパイル(&リンク)時に全ての解決をしてしまいたい、というポリシーのため調べてみたという経緯です。


Elfフォーマットのシンボル情報を取得したい


gnuのツールでは、objdump や readelf, nm といったツールで実行体の情報を出力することができます。これはElfバイナリにセクションやシンボルの情報がどのように格納されているかがセクションヘッダとして格納されているために表示できます。
そのため、stripコマンドでセクションヘッダを削除してしまうと、これらの情報が表示されなくなってしまいます。
またセクションヘッダはElfバイナリの末尾についているため、そもそも削除されることを想定しています。このセクションヘッダはツールによる解釈のために存在するようで、実行時には不要な物です。そのためローダーはこの部分をロードしません。(正確にはストリングテーブルからロードしないようだ…)

しかしここではこのセクションヘッダが存在するとして、話を進めたいと思います。
セクションヘッダは Elf32_Shdr の配列として存在しています。これを読み込んでいくと、sh_typeメンバの値が SHT_DYNSYM というものが見つかります。そしてこのセクションの内容(sh_addrで示される場所において)が、Elf32_Symの配列となっています。
このとき、セクションヘッダに示されている sh_linkのセクションヘッダを参照して文字列が格納されているテーブルも取得しておきます。名前はこのテーブル先頭からのオフセットで格納されているためです。

  1. セクションヘッダを読み込む
  2. 読み込んだセクションヘッダのリストから、SHT_DYNSYM, SHT_SYMTAB のものを抽出
  3. SHT_SYMTABのsh_addrによりストリングテーブルを取得
  4. SHT_DYNSYMの中身は Elf32_Symの配列.
  5. ストリングテーブルのアドレス+Elf32_Sym.sh_name で文字列を得る

このような手順で、含まれているシンボル情報を見ることができます。

冒頭で述べた、セクションヘッダを削除すると情報がみれない話ですが、バイナリエディタで見るとストリングテーブルは残っていたりします。実行にはこのテーブルもロードされないので削除してしまっても正常に動きます。実行体elfになるべく情報を残さないというポリシーならばこのストリングテーブルの削除も忘れずにやっておきたいところです。