共有ライブラリのリロケーションに使われるセクションとreturn_to_dl_resolve攻撃
ELFバイナリにおいて共有ライブラリの関数を使用する場合、通常はプログラム実行時に動的なリロケーション(再配置)が行われる。
この時は.pltや.got.pltといったセクションが使われ、Pwnではしばしば.got.pltセクションを任意のアドレスに書き換える攻撃が行われる。
今回は一度目のリロケーション時に使われるセクションとその構造について調べてみた。
実行環境とGlibcのバージョンは以下の通り。
└──╼ $uname -a
Linux parrot-b660mprors 5.15.0-15parrot1-amd64 #1 SMP Debian 5.15.15-15parrot2 (2022-02-15) x86_64 GNU/Linux
└──╼ $/lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Debian GLIBC 2.31-13+deb11u2) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
今回使用するサンプルプログラムは以下の通り。
// test.c
#include <stdio.h>
int main() {
int a = 1;
char s[16];
puts("a:");
gets(s);
printf("s: %s\n", s);
return 0;
}
動作自体は全く無意味であるが、puts(), gets(), printf()の3つの関数を呼び出していることがわかる。これらはいずれもリロケーションが行われる。
コンパイルしたサンプルプログラムのセクションは上の通り。この中でリロケーションに関わるのは、.plt, .got.plt, .rela.plt, .dynsym, .dynstrといったセクション。
セクションとデータ構造体
.rela.plt, .dynsymはelf.hで構造体として定義されている。
.rela.plt は以下の通り。 64ビットで使用されるElf64_Rela構造体のサイズは24バイトである。このうちr_infoにはリロケーションタイプとシンボルのインデックスが格納されており、後者は後述する.dynsymのインデックスを指定している。
/* Relocation table entry with addend (in section of type SHT_RELA). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
Elf32_Sword r_addend; /* Addend */
} Elf32_Rela;
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
.dynsymは以下の通り定義されている。このElf64_Symのサイズも24バイト。Elf64_Symのst_nameには.dynstrの先頭からのオフセットが格納されており、.dynstrセクションにはリロケーションを行う関数名が格納されている。
/* Symbol table entry. */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
各セクションの関連
大まかな流れは以下の通り。
1. 共有ライブラリから関数が呼び出され、.pltセクションに飛んでくる。この.pltでは初めに対応する.got.pltにジャンプするが、関数が再配置されていない場合は.pltのジャンプ命令の次に戻ってくる。
例えばputs()であればこのあとpush 0x0している。この0x0は.rela.pltのインデックスであり、printf()では0x1, gets()は0x2となっていることがわかる。その後.pltの先頭にジャンプし、.got.pltの先頭から8バイトの部分(link_mapと呼ばれる)をpushしてからglibcの_dl_runtime_resolve関数へjmpしている。
2. .rela.pltは前述の構造体で定義されており、glibcのプログラム中ではrelocとして扱われている。例えばputs()であれば.pltでインデックス0がpushされ、reloc[0]が対応するエントリとなる。reloc[0]->r_infoはエンディアンの都合で"07000000 01000000"と表示されているが0x100000007であり、上位32ビットが対応する.dynsymのシンボルインデックスである。puts()では0x1, printf()は0x2, gets()は0x5がそれぞれ.dynsymのインデックスとなる。
3. .dynsymはx64であればElf64_Sym構造体で表現されている。このうちst_nameが、.dynstrの先頭からのオフセットを示しており、ここに再配置すべき関数名が格納されている。オフセットはputs()では0x6, printf()では0xb, gets()は0x1となっている。
4. .dynstrには関数名が詰め込まれており、.dynsymで指定されたオフセットの場所を見ることで対応する関数名を入手できる。
以上より複数セクションにわたってデータ構造が関連しており、これらをたどることでリロケーションする関数名を手に入れることが出来る。
return_to_dl_resolve攻撃について
上述のようにして解決するべき関数名を手に入れていることがわかった。更に構造体のインデックスやオフセットの値によってデータ構造を辿っていることもわかった。
そこで偽の.rela.pltや.dynsym、.dynstrを用意して、.pltから偽データ構造に飛ぶように任意のインデックスをpushすればsystem()などユーザの望む関数を呼び出せるのではないか、というのがreturn_to_dl_resolve攻撃である。
参考サイト
https://warabanshi.hatenablog.com/entry/2013/05/18/231628