glibc 2.34よりプログラムの起動に関わる処理が大きく変更されたため、後方互換が死んだ!
(glibc 2.34以降の環境で作った動的リンクを行うELFバイナリをglibc 2.33以前の環境で実行しようとするとエラーが出て動かない)
https://twitter.com/t3mpwn/status/1553509946980851713
何が変更されたのか!?何故変更されなければならなかったのか!?
その謎を追うために我々はsourceware.orgの奥地へと足を踏み入れた…
(叙述的なので説明に最適化された形式ではないです)
起動時の処理の問題なのでglibcのソースコードのcsu
フォルダ以下の変更に注目する。
https://sourceware.org/git/?p=glibc.git;a=history;f=csu;hb=HEAD
glibc 2.33が2021-02-01、glibc 2.34が2021-08-01にリリースなので、その間のcommitdiffを重点的に読んでいく。
具体的にはこの辺
各commitの日時毎にまとめていく。
https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=035c012e32c11e84d64905efaf55e74f704d3668
commit説明より引用
It turns out the startup code in csu/elf-init.c has a perfect pair of
ROP gadgets (see Marco-Gisbert and Ripoll-Ripoll, "return-to-csu: A
New Method to Bypass 64-bit Linux ASLR").
どうやらret2csu
という攻撃手法についての防御策のようである。
以下が言及されている論文
https://i.blackhat.com/briefings/asia/2018/asia-18-Marco-return-to-csu-a-new-method-to-bypass-the-64-bit-Linux-ASLR-wp.pdf
(ROPやGadgetについてはCTFで飽きるほどやったので記述は省略します…)
このままだとわかりにくいので、実際にCTFの競技内で使われていたものの解説を貼る。
https://bitbucket.org/ptr-yudai/writeups/src/master/2019/Securinets_CTF_Quals_2019/baby_one/
概要としては、glibcに実装されている__libc_csu_init
関数にはrbx, rbp, r12, r13, r14, r15をpopする命令があるため、これらのレジスタに任意の値を設定できる場合がある。
(popはスタックから値を取ってレジスタに移行するため、スタックオーバーフローのようなもので実現できる)
また、同関数には以下のような命令が存在している
x86_64の関数呼び出し規約では
となっているため、事実上、任意の引数を設定した上で任意の関数を呼び出せるという事になる。
これがret2csu
というテクニックの概要なので、今回のcommitではこれらのgadget(機械語コードの断片)を削除するような実装になっていると思われる。
そこで件の__libc_csu_init
関数がどこに実装されちるのか調べてみると、以下の場所が出てくる
https://elixir.bootlin.com/glibc/glibc-2.33.9000/source/csu/elf-init.c#L68
どうやらcsu/elf-init.c
に実装されているらしい。
今回のcommitでelf-init.c
に対する変更を調べてみると…
https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=035c012e32c11e84d64905efaf55e74f704d3668#patch3
なんてこったい!消されてしまっている😇
そこまでしてCTFerを苦しめたいのか…
commitの説明の続きを読んでみると、
These functions are not needed in dynamically-linked binaries because DT_INIT/DT_INIT_ARRAY are already processed by the dynamic linker.
とのことらしく、どうやらこの辺の処理はリンカのDT_INIT
とDT_INIT_ARRAY
がやってくれるからいらね、との事らしい。
リンカ何もわからん…
追記: 08/26 https://android.googlesource.com/platform/bionic/+/android-4.2_r1/linker/README.TXT
https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html
サイコーのドキュメントがあったわ…
DT_INIT
DT_INIT
Points to the address of an initialization function
that must be called when the file is loaded.
これを関数に設定する事で一番最初に呼び出されるようになるらしい
初期化の処理用…?
http://www.yosbits.com/opensonar/rest/man/freebsd/man/ja/man1/ld.1.html?l=ja
初期だと_init
関数に設定されているっぽい。
DT_INIT_ARRAY
DT_INIT_ARRAY
Points to an array of function addresses that must be called, in-order, to perform initialization.
Some of the entries in the array can be 0 or -1, and should be ignored.
Note: this is generally stored in a .init_array section
DT_INIT
の後に実行する関数の配列。
これデフォルトだとELFのどのセクションになるんだろ…
リンカメモ終わり。
つまりこの関数はinitialization用の関数を定義する仕事をしているっぽい。
短いのでソースコードを…
__init_array_start
は以下で定義されていた
https://elixir.bootlin.com/glibc/glibc-2.33.9000/source/csu/elf-init.c#L45
/* These magic symbols are provided by the linker. */
らしい。そんな事できるのか。
リンカスクリプトに書かれてた。
https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=ld/scripttempl/elf.sc;h=149eec7ab3e650ddb2c9efd15467c8044a69d85c;hb=0aff3d5c72d5c090be861f34f8aa543fb6590e45#l238
でこいつが何してるのかgdbで追ってみると、frame_dummy
をcallしてた。
中身はregister_tm_clones
にジャンプするだけ。
この関数はglibcやldにもなく、なんかgccが作ってるらしい。ほえー。
https://github.com/gcc-mirror/gcc/blob/16e2427f50c208dfe07d07f18009969502c25dc8/libgcc/crtstuff.c#L299
話が逸れたが、たしかにこれらの処理は前述のDT_INIT
とDT_INIT_ARRAY
で済みそうである。
さて、commitの説明文を読み進めていくと、なんかstaticリンクは古いのが使われてて変更がないっぽい。
が、最後の方に
A new symbol version
__libc_start_main@@GLIBC_2.34
is introduced because new binaries running on an old libc would not run their ELF constructors, leading to difficult-to-debug issues.
と重要な事が書いてある。
古いglibcの環境だとELF constructorsが実行されないから新しく__libc_start_main
作ったよみたいな?
https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=035c012e32c11e84d64905efaf55e74f704d3668#patch4
変更量が思ってたよりも多いな…お腹すいた。
この文言だったら、なんか古い環境で作ったやつも動きそうじゃね?
→ このcommitでのglibcをビルドして検証…
検証する必要ないじゃん…
(問題は新しい奴を古い環境で動かした場合なので)