ELFとELFローダについて
ROPというハッキング手法がある。これを勉強していたら、共有ライブラリがメモリ上のどこに配置されるのか、それはどのタイミングなのか気になり始めた。そして、色々と文献を漁ってぼんやりとしたことは分かってきたが、知識が定着したとは言えないので、自分の理解をより深めるため、知識を定着させるため以下に、ELF周りの話を記す。
$uname -a Linux VM 3.13.0-24-generic #47-Ubuntu SMP Fri May 2 23:30:00 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.5 LTS Release: 14.04 Codename: trusty $ gcc --version gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4 // ASLR無効化 $ sudo sysctl -w kernel.randomize_va_space=0 kernel.randomize_va_space = 0
hello.c
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
ELF(Executable and Linking Format)
ELFとは、実行可能バイナリやオブジェクトファイルなどのフォーマットを規定したもの。ELFフォーマットのファイルは、ELFヘッダが先頭にあり、プログラムヘッダテーブル及びセクションテーブルがその後にあります。これらのヘッダ構造はelf.hに記述されている。
// http://lxr.free-electrons.com/source/tools/objtool/elf.h#L69 struct elf { Elf *elf; GElf_Ehdr ehdr; int fd; char *name; struct list_head sections; DECLARE_HASHTABLE(rela_hash, 16); };
ELFヘッダ
ELFヘッダはELFファイルの先頭に必ず存在し、そのファイルがELFファイルであることを示す。
ELFヘッダはelf->ehdrのことで以下のように定義されている。
// http://lxr.free-electrons.com/source/include/uapi/linux/elf.h#L235 typedef struct elf64_hdr { unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */ Elf64_Half e_type; Elf64_Half e_machine; Elf64_Word e_version; Elf64_Addr e_entry; /* Entry point virtual address */ Elf64_Off e_phoff; /* Program header table file offset */ Elf64_Off e_shoff; /* Section header table file offset */ Elf64_Word e_flags; Elf64_Half e_ehsize; Elf64_Half e_phentsize; Elf64_Half e_phnum; Elf64_Half e_shentsize; Elf64_Half e_shnum; Elf64_Half e_shstrndx; } Elf64_Ehdr;
$ readelf -h a.out ELF ヘッダ: マジック: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 クラス: ELF64 データ: 2 の補数、リトルエンディアン バージョン: 1 (current) OS/ABI: UNIX - System V ABI バージョン: 0 型: EXEC (実行可能ファイル) マシン: Advanced Micro Devices X86-64 バージョン: 0x1 エントリポイントアドレス: 0x400440 プログラムの開始ヘッダ: 64 (バイト) セクションヘッダ始点: 4472 (バイト) フラグ: 0x0 このヘッダのサイズ: 64 (バイト) プログラムヘッダサイズ: 56 (バイト) プログラムヘッダ数: 9 セクションヘッダ: 64 (バイト) セクションヘッダサイズ: 30 セクションヘッダ文字列表索引: 27
プログラムヘッダ
プログラムヘッダテーブルはELFヘッダのe_phoffで指定されるオフセットから始まり、e_phentsizeとe_phnumで決まる大きさのテーブルからなります。e_phentsizeがテーブルの中のプログラムヘッダのサイズを表し、e_phnumがそのテーブルの中にいくつセッションヘッダがあるか示しています。
$ readelf -l a.out Elf ファイルタイプは EXEC (実行可能ファイル) です エントリポイント 0x400440 9 個のプログラムヘッダ、始点オフセット 64 プログラムヘッダ: タイプ オフセット 仮想Addr 物理Addr ファイルサイズ メモリサイズ フラグ 整列 PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 R E 8 INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2] LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x000000000000070c 0x000000000000070c R E 200000 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000230 0x0000000000000238 RW 200000 DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x00000000000005e4 0x00000000004005e4 0x00000000004005e4 0x0000000000000034 0x0000000000000034 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0 R 1 セグメントマッピングへのセクション: セグメントセクション... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
ここでなぜ0x0000000000400000にELFファイルが配置されているか知りたい場合は
ELF実行ファイルのメモリ配置はどのように決まるのか - ももいろテクノロジー
を参照すると良い。
INTERPとは動的リンクを実際に処理するインタプリタ(リンカ)のことである。
http://lxr.free-electrons.com/source/fs/binfmt_elf.c?v=3.2#L559にELFファイルをメモリにロードする部分がある(多分)。
そこを読み解く前に、linux_binprm 型という、構造体の説明。これは、ユーザー空間にある引数をカーネル側で保持・加工する為のデータ構造らしい。
// http://lxr.free-electrons.com/source/include/linux/binfmts.h?v=3.2#L28 /* * This structure is used to hold the arguments that are used when loading binaries. */ struct linux_binprm { char buf[BINPRM_BUF_SIZE]; #ifdef CONFIG_MMU struct vm_area_struct *vma; unsigned long vma_pages; #else # define MAX_ARG_PAGES 32 struct page *page[MAX_ARG_PAGES]; #endif struct mm_struct *mm; unsigned long p; /* current top of mem */ unsigned int cred_prepared:1,/* true if creds already prepared (multiple * preps happen for interpreters) */ cap_effective:1;/* true if has elevated effective capabilities, * false if not; except for init which inherits * its parent's caps anyway */ #ifdef __alpha__ unsigned int taso:1; #endif unsigned int recursion_depth; struct file * file; struct cred *cred; /* new credentials * / int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*) */ unsigned int per_clear; /* bits to clear in current->personality */ int argc, envc; const char * filename; /* Name of binary as seen by procps */ const char * interp; /* Name of the binary really executed. Most of the time same as filename, but could be different for binfmt_{misc,script} */ unsigned interp_flags; unsigned interp_data; unsigned long loader, exec; };
はじめに、ELFヘッダのサイズ分をmallocして、ELFヘッダを読み込み、各要素をチェックする。次に、全てのプログラムヘッダのサイズ分をmallocして、全てプログラムヘッダを読み込む。そして、それを順にチェックしていき、p_type(セグメンタイプ)がINTERPの場合、その内容を読み取る。つまり、elf_interpreter="/lib64/ld-linux-x86-64.so.2"。ここでは、open_exec()した後、interpreterのBINPRM_BUF_SIZE分(128byte)をloc->interp_elf_exに代入。つまりはinterpreterの情報。
次にp_typeがPT_GNU_STACKセクションの場合、ELFヘッダのp_flagsをチェックしてスタック上でのコード実行の可否を決める。(ここはDEPと関わってくる)。
次に、INTERPの妥当性(マジックナンバーと、アーキテクチャの適合性)をチェック。
flush_old_exec()で現在のprogramの情報を消し、新しいprogramのための情報にcurrentの情報をいれかえていく。current は、カーネル中で現在実行中のプロセスの task_struct 構造体 を保持する変数。(プロセス、リンク、task_struct構造体)
SET_PERSONALITY()で親のpersonalityを継承する。その後、currentの情報を更新していく。(ここはlinuxのプロセス管理のお話なので、いつか勉強したい)
次はELFをmmapingする。p_typeがPT_LOADの場合にp_flagsを調べ、パーミッションを設定する。ヒープセクションがbssセクションより下位アドレスにあった場合、対象領域をクリアする?
その後、 MAP_PRIVATEとe_typeがET_EXECであった場合ET_MAP_FIXEDをelf_flagsに設定。(https://linuxjm.osdn.jp/html/LDP_man-pages/man2/mmap.2.html/)
共有オブジェクトならランダムフラグを見てアドレス計算。
オフセットとか計算したら、elf_map(おそらく中身はmmap()、セマフォとかしてるし、後々解析する予定)する。
mmap()した先のアドレス、セクションのサイズ等をチェックする。
その後、ヒープセクション、bssセクションの調整?(よくわかっていない)
load_elf_interp()でinterpreterを読み込む。読み込もうとしているinterpreterのe_typeがET_EXECかET_DYN(共有ライブラリ)なのか、アーキテクチャは合っているか、interpreterのfile operation等をチェック。e_phentsizeとe_phnumでサイズチェック。
interpreterのサイズ分のメモリを確保して、kernel_read()する。その後、elf_map()で共有ライブラリをmappingする。
その後、読み込んだアドレス等をチェック。そして、current情報を更新。
453 map_addr = elf_map(interpreter, load_addr + vaddr, 454 eppnt, elf_prot, elf_type, total_size); 462 if (!load_addr_set && 463 interp_elf_ex->e_type == ET_DYN) { 464 load_addr = map_addr - ELF_PAGESTART(vaddr); 465 load_addr_set = 1; 466 } 523 error = load_addr; 528 return error;
そして、start_thread(regs, elf_entry, bprm->p)で先ほどのerrorがeipに設定される。
この呼び出し元に戻ると、どうやらeipから実行するらしく、すなわち共有ライブラリが実行されることになる。
感想
OSのメモリマッピングの知識が欠けていたので、mmap()周辺が正しく解析できていない。よって、共有ライブラリの読み込み先アドレス等の計算が未だに分かっていないため、目的は達成できていない(参考文献読んだら理解できた)。メモリマッピングの勉強をしたら、また読んでみよう。
linux kernelのソースコードを初めて読んだ。面白くて仕方なかった。しかし、特有のデータ構造や頻繁に使われる関数等を一々追っていたため、記事の内容もまとまらず、知識も断片的なものとなった。これから他のところを読んだ時に、今回の知識は役に立つのであろうか?
もしそうでない場合は、また色々と追っているうちに闇に吸い込まれていくわけであるが、いつかは塵も積もり山となるであろう。
参考文献
Linux/ - Linux Cross Reference - Free Electrons
Linux 動的ライブラリーの徹底調査
プログラムはどう動くのか? 〜 ELFの黒魔術をかいまみる
linuxの話 (ELF ローダ) + 他:俺のインターフェイス:So-netブログ
ELF Format
ELFの動的リンク(1) - 七誌の開発日記
ELF実行ファイルのメモリ配置はどのように決まるのか - ももいろテクノロジー
プロセス、リンク、task_struct構造体
メモリ管理、アドレス空間、ページテーブル
ld.so - システム管理コマンドの説明 - Linux コマンド集 一覧表
http://www.skyfree.org/linux/references/ELF_Format.pdf
Binary Hacks ―ハッカー秘伝のテクニック100選