# glibc code reading 〜なぜ俺達のglibcは後方互換を捨てたのか〜 ## 概要 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を重点的に読んでいく。 具体的にはこの辺 ![](https://i.imgur.com/uYIdacQ.png) 各commitの日時毎にまとめていく。 ## 2021-02-25 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はスタックから値を取ってレジスタに移行するため、スタックオーバーフローのようなもので実現できる) また、同関数には以下のような命令が存在している ``` asm mov rdx, r13 mov rsi, r14 mov edi, r15d call qword ptr [r12 + rbx * 8] ``` x86_64の関数呼び出し規約では * 第一引数 : RDI * 第二引数 : RSI * 第三引数 : RDX となっているため、事実上、**任意の引数を設定した上で任意の関数を呼び出せる**という事になる。 これが`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用の関数を定義する仕事をしているっぽい。 短いのでソースコードを... ```c __libc_csu_init (int argc, char **argv, char **envp) { /* For dynamically linked executables the preinit array is executed by the dynamic linker (before initializing any shared object). */ #ifndef LIBC_NONSHARED /* For static executables, preinit happens right before init. */ { const size_t size = __preinit_array_end - __preinit_array_start; size_t i; for (i = 0; i < size; i++) (*__preinit_array_start [i]) (argc, argv, envp); } #endif #if ELF_INITFINI _init (); #endif const size_t size = __init_array_end - __init_array_start; for (size_t i = 0; i < size; i++) (*__init_array_start [i]) (argc, argv, envp); } ``` `__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をビルドして検証...~~ 検証する必要ないじゃん... (問題は新しい奴を古い環境で動かした場合なので)