Linuxでの関数フックの手法はよく知られており、インターネットで説明されています。 最も簡単な方法は、「クローン関数」を使用して動的ライブラリを作成し、LD_PRELOADメカニズムを使用して、プロセスのロード段階でインポートテーブルをオーバーライドすることです。
LD_PRELOADの欠点は、プロセスの開始を制御する必要があることです。 すでに実行中のプロセスの関数またはインポートテーブルにない関数をインターセプトするには、「スプライシング」を使用します。インターセプトされた関数の先頭でインターセプターに移動するコマンドを記録します。
Pythonには、C言語のデータや関数(つまり、Cインターフェースを備えた多数の動的ライブラリ)とやり取りできる
ctypes
モジュールが
ctypes
ことも知られています。 したがって、プロセスの機能をインターセプトし、
ctypes
を使用してCコールバックでラップされたPythonメソッドに送信することを妨げるものはありません。
制御をインターセプトし、ターゲットプロセスにコードをロードするには、Python(
https://sourceware.org/gdb/current/onlinedocs/gdb/Python-API.html )で拡張モジュールの作成をサポートするGDBデバッガーを使用すると便利です。
ニュアンスこの例のコードは記事の最後に完全に記載されており、2つのファイルで構成されています。
- pyinject.py-GDB拡張機能
- hook.py-フック関数を備えたモジュール
GDB側では、コードはユーザーコマンドとして便利にフォーマットされます。
gdb.Command
クラスから継承することにより、新しいコマンドを作成できます。 GDBでコマンドを使用する場合、
invoke(argument, from_tty)
メソッドが呼び出されます。
gdb.Parameter
継承するカスタムパラメータを作成することもできます。 サンプル記事では、インターセプション関数でファイル名を指定するために使用されます。
実行中の
PID
プロセスへの接続とモジュールのロードは、GDBを起動するとすぐに実行できます
gdb -ex 'attach PID' -ex 'source pyinject.py' -ex 'set hookfile hook.py'
このデバッグされたプロセスのフィールドが停止し、インタラクティブなGDBコマンドラインが起動します。ここで、新しいpyinjectコマンドが使用可能になります。
傍受は3つの段階に分けることができます。
- Pythonインタープリターをターゲットプロセスのアドレス空間に挿入する
- キャプチャされた機能に関する情報のキャプチャ
- 実際に傍受
節1と2はデバッガー側で簡単に実行でき、節3はすでにターゲットプロセス内にあります。
Pythonインタープリターインジェクション
Python GDBインターフェースのほとんどは、デバッグ機能を拡張するように設計されています。 他のすべてについては、
gdb.execute(command, from_tty, to_string)
があります。これにより、任意のGDBコマンドを実行し、その出力を文字列として取得できます。
例:
out = gdb.execute("info registers", False, True)
また、式を評価し、結果を
gdb.Value
として返す
gdb.parse_end_eval(expression)
も
gdb.Value
ます。
最初のステップは、Pythonライブラリをターゲットプロセスのアドレス空間にロードすることです。 これを行うには、ターゲットプロセスのコンテキストで
dlopen
を呼び出します。
gdb.execute
または
gdb.parse_and_eval
で
call
コマンドを使用できます。
その後、インタープリターを初期化できます
最初の呼び出しはGIL(グローバルインタープリターロック)を作成し、2番目の呼び出しはPython C-APIを使用するために準備します。
そしてインターセプション関数でモジュールをロードします
PyRun_AnyFileEx
は、
PyRun_AnyFileEx
モジュールのコンテキストでファイルからコードを実行します。
ニュアンス上記は、ターゲットプロセスがPythonを(メインまたはスクリプト言語として)使用しない場合にのみ機能します。 そうでない場合、すべてが非常に複雑です。 主な問題は、デバッグのためにランダムな場所で停止したプロセスでは、Python C-API関数を使用できないことです(おそらくPy_AddPendingCall
を除く)。
Hook.pyモジュール
hook.pyモジュールには、フック関数と、フック自体を実行するフッククラスが含まれています。
インターセプター関数は、デコレーターを使用して指定されます。 たとえば、標準ライブラリの
open
関数の場合、引数を出力し、
orig
フィールドに格納されている元の関数を呼び出した結果を返します
@hook
デコレーターは2つのパラメーターを受け入れます。
- symbol-インターセプトされるシンボルの名前(インポートテーブルまたはデバッグ情報からGDBでシンボルが利用可能であると想定されますが、シンボルではなくアドレスによる関数のインターセプトを妨げるものはありません)
- ctype-関数のタイプを指定する
ctypes
クラス
デコレータはHookクラスに関数を登録し、変更せずに戻ります。
register
メソッドは、クラスのインスタンスを作成し、
all_hooks
辞書に格納します。 したがって、ファイルの実行後、
Hook.all_hooks
のデコレーターのおかげで、インターセプターの利用可能な機能に関するすべての情報が得られます。
1つの関数を呼び出してGDBからインターセプトするには、インターセプトを担当する
Hook
クラスで静的メソッドを定義すると便利です
*args
は、フックされた関数に関する追加情報をここに提供します。 どれが傍受の方法に依存します。
スプライシングの傍受方法
スプライシングは、元の関数の呼び出し方法に応じて、2つの亜種にグローバルに分割されます。
単純なフックでは、元の関数の呼び出しはいくつかのステップで構成されます。
- 元の関数の先頭が保存されたコピーから復元されます
- 電話をかける
- 始まりは、インターセプターへの遷移命令によって再び上書きされます
ニュアンス欠点は明らかです。マルチスレッドプログラムでは、別のスレッドが関数の先頭を上書きするときに関数を呼び出さないことを保証できません。 これは、元の関数が呼び出されている間に他のスレッドを停止することで部分的に処理されます。 しかし、第一に、これを達成する標準的な方法はありません。第二に、mallocのような関数の呼び出しに失敗すると、デッドロックをキャッチできます。
トランポリンフックでは、元の関数の先頭が新しい場所にコピーされ、その後、元の関数の本体への遷移が記録されます。 このオプションでは、元の機能は常に新しいアドレスで使用できます。
トランポリンフックはマルチスレッドプログラムで動作しますが、インストールははるかに困難です。 整数個の命令を書き換える必要がありますが、一般的には逆アセンブラが使用されます。 x86_64アーキテクチャの出現により、
%rip
レジスタ(現在のコマンドアドレス)に対するメモリアドレス指定の偏在性により、さらに多くの問題が追加されました。
ニュアンスGDBの
open
関数の始まりを見てみましょう。
0x7f6cc8aa83e0 <open64+0>: 83 3d ed 33 2d 00 00 cmpl $0x0,0x2d33ed(%rip) 0x7f6cc8aa83e7 <open64+7>: 75 10 jne 0x7f6cc8aa83f9 <open64+25> 0x7f6cc8aa83e9 <__open_nocancel+0>: b8 02 00 00 00 mov $0x2,%eax 0x7f6cc8aa83ee <__open_nocancel+5>: 0f 05 syscall
最初のコマンド "
cmpl $0x0,0x2d33ed(%rip)
"を別のアドレスに
0x7f6cc8d7b7d4
、現在
0x7f6cc8d7b7d4
指している相対アドレス
0x2d33ed(%rip)
は別の場所(hi SIGSEGV)を指します。
この関数のトランポリンフックを作成するには、次のものが必要です。
- 関数の先頭でコマンドのサイズを決定します
- cmplコマンドの宛先アドレスから2 GB以下のメモリを割り当てます(オフセット
0x2d33ed(%rip)
署名付き32ビット) - 先頭を新しい場所にコピーし、
cmpl
%rip
に関連するメモリアクセスにパッチを当てます
さらに、jumpコマンドは9バイトより短くする必要があります。 2つのエントリポイントを持つ関数であり、アドレス
0x7f6cc8aa83e9
すでに
__open_nocancel
です。 これは、32ビットのトランジションを許可するために、
open
の開始からスプリングボードが2 GBを超えてはならないことを意味します(64ビットのトランジションはすべて9バイトより長い)。
原則として、GDBのすべての機能(
gdb.execute()
)の背後にあるため、トランポリンフックを正しく実装することを妨げるものはありませんが、簡単にするために、この記事では単純なフックを使用します。
単純なフックでは、唯一の制限はジャンプ命令の長さです。
2つのオプションがあります(メイン):
この記事では2番目の方法を使用します
get_indlongjmp
は、
srcaddr
アドレスから
proxyaddr
QWORDに格納されているアドレスにジャンプするためのコードを返します
これで、
Hook
クラスの欠落しているメソッドを最終的に記述できます。
install
メソッドは、元の関数
address
と
proxyaddr
補助ゾーンのアドレスを取得します。 その後、インターセプターに切り替えて、関数の先頭を書き換えます(以前は
self.code
保存してい
self.code
)
patchmem
は、
src
データで元の関数の先頭を上書きします
origfunc
は、インターセプターへの遷移を削除および設定するコードで関数呼び出しをラップします。
最後の仕上げ
Pythonはアドレス空間にロードされ、hook.pyファイルはPythonにロードされます。 GDBモジュールのPython側から
Hook.hook(symbol, address, proxyaddr)
を呼び出すことは残ります。
「
open
」機能のアドレスを見つける
line = gdb.execute('info address %s' % "open" False, True) m = re.match(r'.*?(0x[0-9a-f]+)', line) addr = int(m.group(1), 16)
ニュアンス一般に、停止したプロセスのコードを書き直すために実行する前に、このコードの途中で停止しないように(またはそれに戻るように)する必要があります。 これを行う最も簡単な方法は、 gdb.execute("thread apply all backtrace")
の出力を解析することgdb.execute("thread apply all backtrace")
addr
近くにメモリを割り当てます
prot = PROT_READ | PROT_WRITE | PROT_EXEC flags = MAP_PRIVATE | MAP_ANONYMOUS maddr = gdb.parse_and_eval('(void*)mmap(0x%x, %d, %d, %d, -1, 0)\n' % (addr | 0x7FFFFFFF, 4096, prot, flags)) maddr = (long(maddr) & 0x00000000FFFFFFFF) | (addr & 0xFFFFFFFF00000000)
ニュアンス最後の行は、結果の上位ビットを食い尽くすGDBのバグの回避策です。 引数(addr | 0x7FFFFFFF)
は、文書化されていないプロパティmmap
を使用して、目的のアドレスよりも小さいアドレスのメモリを(addr | 0x7FFFFFFF)
ます。
トリックなしでは、正しい方法で少し長くなりますgdb.execute('info proc mappings', False, True)
の出力を解析し、アドレス空間でaddrに最も近い穴を見つけ、 MAP_FIXED
mmapをフェッチする必要があります。 もちろん、インターセプトされた関数ごとにメモリのページ全体を割り当てる必要はありません。
元の関数の書き換えを許可(別名SIGSEGV)
gdb.parse_and_eval('mprotect(0x%x, %u, %d)' % (addr & -0x1000, 4096*2, prot))
PyRun_SimpleString
を介して
Hook.hook
を呼び出し
PyRun_SimpleString
pyret = gdb.parse_and_eval('PyRun_SimpleString("Hook.hook(\\"open\\", 0x%x, 0x%x)")' % (addr, maddr))
できた! これで、ターゲットプロセスでの「
open
」の呼び出しがインターセプトされ、
python_open
からpython_openにルーティングされます。
サンプルファイル
完全なサンプルファイル(もう少し確認しますが、多くのニュアンスを考慮しません)
例を実行する(絶対パスで改善する)
gdb -ex 'attach PID' -ex 'source /path/pyinject.py' -ex 'set hookfile /path/hook.py' (gdb) pyinject hook open (gdb) continue