2024.10.01
自社の社内情報を未来の“ゴミ”にしないための備え 「情報量が多すぎる」時代がもたらす課題とは?
バイナリを書き換えてシステムコールをフックする(全1記事)
リンクをコピー
記事をブックマーク
yasukata氏(以下、yasukata):yasukataといいます。発表を始めます。
今回は、「Zpoline」という、バイナリを書き換えることでシステムコールをフックする仕組みを紹介します。ここではx84-64のCPUで動作するLinuxを想定しています。(スライドを示して)ソースコードはこちらにURLがあるので、よろしければ見てみてください。あとでスライドも公開するので、そちらも併せてご覧ください。
まず、なぜシステムコールをフックしたくなったのかですが、個人的にカーネルに実装されている機能を、アプリケーションを変えないでユーザー空間で置き換えたいと思ったからです。
ネットワークスタックをユーザー空間に持っていきたいと思った時、システムコールをフックして、適宜ユーザー空間実装を実行すればよさそうだと考えて、作り始めました。その時に「もしかすると、この用途に合ったシステムコールをフックする仕組みはないのでは?」と気づきました。
システムコールをフックするには具体的に4つの要件があります。1つ目はフックの適用後にアプリケーションの性能劣化が小さいこと。2つ目は、フック適用の確度が高い、つまりフックし損ねないこと。3つ目は、ユーザー空間プログラムの再コンパイルが不要であること。4つ目は、カーネルを変える必要がなく、カーネルモジュールもいらないことです。
調べてみた結果、代表的なところを3つ上げると、ptraceのような既存のカーネル機能や、LD_PRELOADを使ったライブラリ関数の置き換え、既存のバイナリ書き換え手法などがありました。これらを一見したところ、既存の仕組みでは、「性能」と「フックをし損ねないこと」の両立が難しそうでした。
今回のモチベーションは、それらを両立する仕組みを作りたいということです。ここで紹介するZpolineという仕組みは、バイナリ書き換えでシステムコールをフックします。バイナリ書き換えの特性上、性能の劣化は抑えやすいのですが、一方で、既存の仕組みでも起こるように、フックをし損ねてしまうことがあります。
なので、どうすればバイナリ書き換えでフックし損ねないようにできるかが今回のチャレンジで、なぜそれが起こるのかと、何が難しいのかをお話しします。
x86-64のCPUでシステムコールを発行しようとすると、基本的にsyscallもしくはsysenterというCPU命令が使われます。これらはそれぞれ2byteですが、具体的にやりたいのは、syscall、sysenter命令を置き換えて、任意のフック関数のアドレスへジャンプすることです。
この時難しいのは、任意のアドレスを指定するのに2byteでは小さくて、2byteを超えてしまうと、ほかの命令を壊してしまうということです。
このため、既存のバイナリ書き換えの仕組みでは、確実な置き換えの保証が難しいという問題があります。
今回のZpolineの考え方ですが、2byteでジャンプ先のアドレスを指定するのは難しそうなので、代わりにシステムコールの呼出規約を利用した書き換えを行って、かつ、適切にトランポリンコードを用意する方向性でやっていきます。
では、どのようにバイナリを書き換えるのかですが、Zpolineでは、syscall、sysenter命令を「callq *%rax」へ置き換えます。読みにくいので「コールアールエーエックス」と呼びます。
ポイントは、callq *%raxはオペコードが「0xff 0xd0」の2byteなので、syscall、sysenterをそのまま置き換えられることです。「置き換えた後はどうなるの?」と思われるかもしれませんが、callq *%raxを実行すると、raxレジスタに入っている値を宛先アドレスと解釈してジャンプします。
さらに「それどうなるの?」と思われることについては、ここでシステムコールの呼出規約が利いてきます。x86-64のCPUで動いているLinux上では、ユーザー空間プログラムは利用したいシステムコールの番号をraxレジスタに入れた後に、syscall、sysenter命令を実行することが決められています。
システムコール番号は、カーネルが中でシステムコールを識別するために定義している番号で、例えばreadシステムコールだったら0、writeシステムコールだったら1、と決まっています。システムコールは合計4〜500個あるので、システムコール番号は400から500ぐらいまでとなっています。
syscall、sysenter命令が実行される時には、raxレジスタにシステムコール番号が入っています。syscall、sysenterをcallq *%raxで置き換えると、アドレス0から400、500程度までジャンプするところがポイントです。
なのでここでは、callq *%raxでジャンプしてくるアドレス0から500程度までを含む領域に、トランポリンコードを用意していきます。(スライドを示して)Linuxでは、こちらのコマンドにあるように、procfsから設定するとmmapでアドレス0にメモリを確保できるようになります。
ちなみに、Zpolineの名前は、アドレス0に置かれるトランポリンコードというところから来ています。
(スライドを示して)具体的にトランポリンコードの中身はこのようになっています。まず、システムコール番号の数だけ先頭をnopで埋めます。その直後に任意のフックへのジャンプのコードを置きます。
これによって、callq *%raxを実行すると、最初に置いたnopのどれかに着地して、その後は、下に続いているnopを辿ってフックへジャンプする処理まで行きます。そして、任意のフックに飛んでくれます。これで任意のフックへジャンプする処理が書けました。
今回は、初期化する部分をLD_PRELOADで最初にロードされることを想定した共有ライブラリとして実装して、この例のように実行します。すると、トランポリンコードの用意とバイナリの書き換えを、a.out内のmain関数が開始する前に実行します。
バイナリ書き換え自体は、メモリにロードされたプログラムに対して行うので、プログラムファイル自体の変更は必要ありません。
フックのオーバーヘッドがどのようなものかを知るために、実際にシステムコールのフックを適用した後に、getpidという軽いシステムコールを1回実行するために必要なCPUサイクルを計測しました。
2パターン試しました。フック関数の中でgetpidシステムコールを実際に発行して結果を返すパターンと、メモリ上にキャッシュしたpidの値を返すパターンです。テーブルには、pidキャッシュなし、あり、という区別で書いてあります。
今回は、ユーザー空間でシステムコールを置き換えて自分で実装したかったので、pidキャッシュありのほうが、今回のケースでのオーバーヘッドがより見えるようになっていると思っています。
この環境で計った感じでは、Zpolineでシステムコールをフックした場合、ptraceで実装する場合に比べて、getpidが100倍以上高速にエミュレートできる結果になりました。
まとめです。今回はZpolineという、バイナリを書き換えることでシステムコールをフックする仕組みを紹介しました。ソースコードは(スライドを示して)こちらのURLにあるので、よろしければ試してみてください。
それから、今回は時間の関係で省いたのですが、具体的なフック関数のプログラミングの方法については、新しく記事を用意したので、よろしければこちらもご覧ください。以上です。ご清聴ありがとうございました。
司会者:ありがとうございます。けっこう質問が出ますね。「純粋なフックなしに比べれば、逆にどれくらいのオーバーヘッドなんだろう?」「ほかより速いのはわかったけど、オーバーヘッドはどれくらい?」という質問があります。
yasukata:(スライドを示して)pidキャッシュありのこの数字は、メモリ上にキャッシュしたpidの値を返しているだけなので、これはかなり純粋なオーバーヘッドに近いと思っています。
関連タグ:
2024.10.29
5〜10万円の低単価案件の受注をやめたら労働生産性が劇的に向上 相見積もり案件には提案書を出さないことで見えた“意外な効果”
2024.10.24
パワポ資料の「手戻り」が多すぎる問題の解消法 資料作成のプロが語る、修正の無限ループから抜け出す4つのコツ
2024.10.28
スキル重視の採用を続けた結果、早期離職が増え社員が1人に… 下半期の退職者ゼロを達成した「関係の質」向上の取り組み
2024.10.22
気づかぬうちに評価を下げる「ダメな口癖」3選 デキる人はやっている、上司の指摘に対する上手な返し方
2024.10.24
リスクを取らない人が多い日本は、むしろ稼ぐチャンス? 日本のGDP4位転落の今、個人に必要なマインドとは
2024.10.23
「初任給40万円時代」が、比較的早いうちにやってくる? これから淘汰される会社・生き残る会社の分かれ目
2024.10.23
「どうしてもあなたから買いたい」と言われる営業になるには 『無敗営業』著者が教える、納得感を高める商談の進め方
2024.10.28
“力を抜くこと”がリーダーにとって重要な理由 「人間の達人」タモリさんから学んだ自然体の大切さ
2024.10.29
「テスラの何がすごいのか」がわからない学生たち 起業率2年連続日本一の大学で「Appleのフレームワーク」を教えるわけ
2024.10.30
職場にいる「困った部下」への対処法 上司・部下間で生まれる“常識のズレ”を解消するには