patchelfを使って動的ライブラリ(libcとか)を変える
これはCTF tools/tips Advent Calendar 2022の七日目の記事です。
埋まりきっていないので、協力していただけると嬉しいです!
モチベージョン
- 上司から特定のlibcのバージョンでしか動かないバイナリを渡された
- CTFのpwnチャレンジでバイナリとlibc.so.6が配られたが、そのlibcを使って起動したい
ローダーと動的ライブラリの仕組み
動的リンクされたバイナリには、起動時に動的ライブラリをロードするローダーと実際に必要になる動的ライブラリに関する情報が入っています。
ローダーに関する情報は.interp
にあり、以下のように確認できます。
sh-5.1$ objdump --full-contents --section .interp ./main
./main: file format elf64-x86-64
Contents of section .interp:
400318 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
400328 7838362d 36342e73 6f2e3200 x86-64.so.2.
また、動的ライブラリは、readelf
を用いて、
readelf --dynamic ./main
Dynamic section at offset 0x2df8 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x401000
0x000000000000000d (FINI) 0x401140
( 中略 )
0x000000006ffffff0 (VERSYM) 0x4004ee
0x0000000000000000 (NULL) 0x0
とすることで確認できます。この場合、必要なライブラリは libc.so.6
だとわかります。
また、バイナリを直接見なくても、lld
コマンドを使って簡単に確認することができます。
$ ldd ./main
linux-vdso.so.1 (0x00007fffcd3e9000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f0961a51000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2(0x00007f0961c64000)
左側が要求している物で、右側はバイナリが実際に起動される時に使われる物です。
これらの情報は、誰がどのタイミングで参照しているのでしょうか。
まず、バイナリに対して execve が呼ばれた時、linuxカーネルが .interp
を読み取り、もし該当セクションがあるならそのパスの先にあるバイナリを起動します。以下は該当部分のカーネルのソースコードです。
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
if (elf_ppnt->p_type != PT_INTERP)
continue;
...
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
...
interpreter = open_exec(elf_interpreter);
...
}
この場合 /lib/ld-linux-x86-64.so.2
がカーネルにより起動されます。
起動した /lib/ld-linux-x86-64-so.2
は、ユーザーが起動したいバイナリとそれに必要な動的オブジェクトをメモリ上に配置します。
この時、libcなど動的オブジェクトは絶対パス指定で無いためどこからか探索する必要があり、rpath
がバイナリにセットされている場合はそこから探索されます。
rpath
に値がセットされている場合(例えば"this_is_rpath")、readelf
でデバッグ情報を見ると以下のような表示になります。
$ readelf -d ./main
Dynamic section at offset 0x1568 contains 25 entries:
Tag Type Name/Value
0x000000000000001d (RUNPATH) Library runpath: [this_is_rpath]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x401000
(中略)
0x000000006ffffff0 (VERSYM) 0x4004ee
0x0000000000000000 (NULL) 0x0
最後に、ローダーからユーザーが渡したバイナリへジャンプすることで、我々のバイナリが起動する、というわけです。ソースコード
それらを入れ替える
バイナリが動的ライブラリ過程を眺めていると、それらの情報がバイナリ内に詰め込まれているのがわかります(ローダーやlibcのパスなど)。
それらを入れ替えることでシステム標準でないライブラリを使うことができそうです。
しかしバイナリを人間が直接弄るのは、予期せぬ変更やミスをしてしまいバイナリが起動できなくなる可能性があります。
patchelf
はそのような作業を安全に行ってくれます。
patchelf
はいくつかのオプションがあり、それらを用いてバイナリを操作できます。
CTFで用いる場合、set-interpreter
, set-rpath
, replace-needed
を使えれば十分でしょう。
名前からおおよその使い方が予測できますが、それぞれ説明します。
-
set-interpreter
- ローダーを指定する(前のローダーの情報はなくなる)
- 例
patchelf --set-interpreter /path/to/ld.so ./binary
-
set-rpath
-
rpath
を指定する - 例
patchelf --set-rpath /path/to/library_path ./binary
-
-
replace-needed
- バイナリに必要なものを入れ替える
- 例
patchelf --replace-needed libc.so.6 /path/to/alternative/libc.so.6 ./binary
問題でlibcが配られたら、set-rpath
をするか、replace-needed
で配られたものを指定する良いです。しかしこれだけではうまく起動しません。
入れ替えたあとのバイナリが起動しない
上の方法でlibcを入れ替えるとエラーが発生すると思います。これはlibcとld.so(標準ローダー)の間にバージョンに不整合があるため発生しています。実はld.soはlibcに依存しており、同じバージョンでなければ動かない様になっているのです。
glibcをビルドすると一緒にld.soがコンパイルされるのはそのためです。
実際、システム標準のlibcとldのバージョンを確認すると一致することがわかります。
$ /lib/libc.so.6
GNU C Library (GNU libc) stable release version 2.36.
(中略)
$ /lib/ld-linux-x86-64.so.2 --version
ld.so (GNU libc) stable release version 2.36.
(中略)
これを解決するためには、配布されたlibcに合うようなローダーを見つける必要があります。
このアドカレの前日の記事 pwn用にlibcを調べる/用意する にlibcの情報からライブラリをまとめて得る方法をまとめました。
ライブラリが一定以上ある場合には、replace-needed
ではなくset-rpath
を使い、ライブラリの探索パスを変える方が楽です。
それでも動かない
以下のようなエラーが出て起動しないことがあります。
./binary: /path/to/libc.so.6: version `GLIBC_2.34' not found (required by ./binary)
これは、コンパイル時に使えると仮定した glibc のバージョンより古いバージョンが使われた時に出るエラーメッセージです。つまり「新しいlibcの機能を使っているかもしれないから古いライブラリだと動かないよ」ということを主張しています。
CTFでは実際に使用されているlibcが配られる関係上、このエラーが出る場合は作問者のミスである可能性が高いと思います。
実務等でこのエラーが出た場合は、要求されているlibcを用意することで解決できると思われます。
終わりに
libcを変更したあと、更にデバッグ情報を使用する方法は別の記事にします。アドカレ全然埋まってないので...
誤字・脱字・間違い等があればご指摘いただければ修正します。
コメントでもTwitter(@iwancof_ptr)でも
Discussion