Try   HackMD

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を重点的に読んでいく。
具体的にはこの辺

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

各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はスタックから値を取ってレジスタに移行するため、スタックオーバーフローのようなもので実現できる)
また、同関数には以下のような命令が存在している

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_INITDT_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用の関数を定義する仕事をしているっぽい。
短いのでソースコードを

__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_INITDT_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をビルドして検証

検証する必要ないじゃん
(問題は新しい奴を古い環境で動かした場合なので)