既存プログラムの関数を書き換える、強力で危険なLinux環境変数LD_PRELOAD


はじめに

前回のProxyパターン、ライブラリの差し替え部分でLD_PRELOADを使って色々なproxyパターン的な例を考えてみたのですが、調べているうちに
凄さと恐ろしさを感じたため、別件としてまとめさせていただきました。

基本的なLinuxの動的ライブラリリンクに関する仕組み

何よりもまず、Linuxの動的ライブラリがリンクされる際の仕組みについて整理しましょう。
(一旦LD_PRELOADは忘れてください。)

まずライブラリリンクの検索は、ld.soというライブラリによって検索されるそうです。
manpageよりld.soの仕様を確認すると、検索順は

  1. DT_RUNPATH, DT_RPATH(非推奨) (ビルド時の-rpathで指定されたパス)
  2. 環境変数LD_LIBRARY_PATH
  3. /etc/ld.so.cacheファイル内のパス   (基本は/etc/ld.so.conf, /etc/ld.so.conf.d/*, デフォルトのパス) となるとのこと。(1の解釈が怪しいかもです)

デフォルトは/lib、 次いで /usr/lib。
ビルド時に-z nodeflibを指定すると、ここからデフォルトのパスが除かれます。

rpathの指定には癖があり、Xlinkerでくくらないと効果が出ないようです。

-Xlinker -rpath -Xlinker $(libdir)

リンクされているライブラリについては、lddコマンドで確認できます。
こんな感じに

$ ldd /usr/bin/curl
        linux-vdso.so.1 (0x00007ffe307ac000)
        libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 (0x00007f85cce4e000)
...
        libssl.so.1.1 => /usr/lib/x86_64-linux-gnu/libssl.so.1.1 (0x00007f85cbb4b000)
        libcrypto.so.1.1 => /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 (0x00007f85cb6d3000)
...
        libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f85c6c95000)

色々な設定に合わせて優先度を変えてリンクされるライブラリですが、
この順番をぶっ飛ばして最優先に使用されるのがこのLD_PRELOADです!

LD_PRELOADの凄まじさ

最近Qiitaをはじめて一番最初に衝撃を受けた記事が、@koara-localさんが紹介されていた記事でした。
再コンパイルなしでprintfが差し替えられるだって!

自作ならまだしも標準関数のprintfがLD_PRELOADを使って差し替えるというのです。
このLD_PRELOADは環境変数で、ここにライブラリのパスを設定すると、そのパスがライブラリのリンク先検索最上位になります。
しかもstaticな関数をフックする方法を紹介している方までいる。なんでもありです。

こりゃすごいと、この機能で色々proxyパターンを表現しようと思ったところ、既に@koara-localさんがここで紹介されてました。そっか残念と思いつつ使い方を思い返していると、徐々にこのLD_PRELOADに対して怖さを感じ始めます。

LD_PRELOADの恐ろしさ

先ほどの話を整理しましょう。
- LD_PRELOADは環境変数でパスを指定するだけで好きな関数をラップ出来る
- 再コンパイルの必要がないので既存動作しているプログラムにも適用される。

さて、どんなことが出来ますかね

試してみた

OpenSSLはいわずと知れたSSLに関するライブラリを持っています。
色々なHTTPS通信に用いられており、ユーザープログラムとしては意識せず、ライブラリが暗号化を行ってくれます。

私の知っている使い方は、なんやかんや初期処理を行いクライアントサーバー間で認証が通った後は、SSL_read, SSL_writeを使えばライブラリ側で勝手に暗号化/復号をしてくれます。全部OpenSSLが暗号化処理をしてくれるなんて楽ちんですね。
…read/writeは全部これなんですよね。

試しにHTTP系通信に使用する定番のクライアントコマンド、curlのソースを確認。
やっぱり使ってますね、両方。サンプルに倣い、それぞれデコード前後にログを出力。
やり方は参考元を参照ください。

export LD_PRELOAD=XXX/libssl_dummy.so

でライブラリのパスを通します。ライブラリの置き場はどこでもいいようです。(root権限のない所でも)

試しにyahooにアクセスしてみました。
普通はこんな感じ

$ curl --url "https://yahoo.co.jp" -o /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  3191  100  3191    0     0   2818      0  0:00:01  0:00:01 --:--:--  2818

出力しなければ中身も見えないし、パケットログをみてみてもhttpのやり取りは見えません。
curlの引数で通信ログを出力しないようにすれば、その内容は確認できないはず。

ですが、LD_PRELOADの力を借りてdlsymを使うと…

$ curl --url "https://yahoo.co.jp" -o /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
#############Show write buffer before encrypt!!!
GET / HTTP/1.1
Host: yahoo.co.jp
User-Agent: curl/7.58.0
Accept: */*

...

#############Show read buffer after decrypt!!!
<!doctype html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head><title>Yahoo - 301 Moved Permanently</title><style>
...
<p class="more">Please try <strong><a href="http://us.rd.yahoo.com/301/*http://help.yahoo.com">Yahoo Help Central</a></strong> if you need more assistance.</p>
</div><div id="ft"><p>Copyright &copy; 2018 Yahoo Inc. All rights reserved. <a href="http://us.rd.yahoo.com/301/*http://privacy.yahoo.com">Privacy Policy</a> - <a href="http://us.rd.yahoo.com/301/*http://info.yahoo.com/legal/us/yahoo/utos/terms/">Terms of Service</a></p></div>
</div></body></html>

送信も受信も完全に平文が覗けちゃっています。
これはただprintfしただけですけど、例えば誰かのアドレスへのメール送信コードが埋め込まれていて、気付かずクレジットカード登録したなんて日には大変なことに。

しかもこれproxyパターンで作ってるからちゃんとcurlの機能を満たしているのでばれにくい気がします。

大事なファイルは全部root権限のある場所にあるからと安心して、
特定のパスは開けっ放しにしているなんて方、いらっしゃいませんか?
万が一にでもいらっしゃったら、なにか危険なライブラリが組み込まれているかもしれませんよ?

環境変数はどこまで影響があるか

じゃあLD_PRELOADの力がどこまで及ぶのか、環境変数の仕様についても調べてみました。

環境変数は、以下の順で設定ファイルが読み込まれ、設定されます。

1, /etc/profile
2, /etc/profile/profile.d
3, ~/.bash_profile
4, ~/.bash_login
5, ~/.profile
6, ~/.bashrc
7, /etc/bashrc

その他には実行時に変数を設定する方法もあります。

なので、仕込まれるとしたら上記設定ファイルか利用スクリプトくらい
/etcや利用スクリプトがちゃんとroot権限のままになっていれば、環境変数はそのユーザーのホームディレクトリのもの、つまりそのユーザーにしか影響はないようです。

外から環境変数を設定する手段については流石にないと思いますが、用意されている機能は計り知れないので油断はできませんね。

最後に

最初にLD_PRELOADの記事を見た時は、なんて凄い機能だと感心していましたが、dlsymという別の強力機能と組み合わせて悪用すれば、恐ろしいハック道具にもなってしまいます
気になるサーバー管理者の方は各環境変数の設定ファイルをご確認ください。(私も確認しなきゃ)

参考

ld.so manpage
https://linuxjm.osdn.jp/html/LDP_man-pages/man8/ld.so.8.html

LD_PRELOAD参考元
https://qiita.com/koara-local/items/d5205f94decade3ffbf1
https://qiita.com/koara-local/items/221508d4691c4502adce

LD_PRELOADでstaticな関数もフック出来るという話
http://neocat.hatenablog.com/entry/20111225/1324823705

環境変数の件
https://qiita.com/bokotomo/items/674e06da84c921f5407a