リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5
私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。
さらっとELF
前回までは、逆アセンブラについて学んでいました。前回メモリエディタを学ぼうと言っていましたが・・・やめます!笑
逆アセンブラを学んでいるうちになんとなーくメモリエディタの基本的なところも理解できましたので(恐らく)。
なので今回は逆アセンブラでも少し必要になった「ELF」について学びます。ドキュメント( https://refspecs.linuxfoundation.org/elf/elf.pdf )、実際のELFヘッダファイル(elf.h)、Webサイトを参考に学んでいきます。
ELFとは
ドキュメントによると、ELF(The Executable and Linking Format)は元々、UNIX System Laboratories(USL)がApplication Binary Interface(ABI)の一部として開発・公開されました。ABIは、同じプロセッサ環境内でアプリケーションが実行できるようにするインタフェースのことらしいです。The Tool Interface Standards committee(TIS)は、様々なOSの32bitインテルアーキテクチャ環境で動作するポートブルオブジェクトファイルフォーマットとして、進化したELF標準を選択しました。
ELF標準は複数のOSにまたがって拡張するバイナリインタフェース定義のセットを開発者に提供することで、ソフトウェア開発を簡素化することを目的としました。これにより、異なるインタフェース実装の数が減少し、コードの記録と再コンパイルの必要性が軽減されます。
つまり、たまたまABIとして作ったELFが一般に広まって、標準となっていったということでしょうかね。
あと、このドキュメントは32bit環境に焦点を置いているようです。
オブジェクトファイル
ELFには主に3つのオブジェクトファイルが存在するようです。
- relocatable file(再配置可能ファイル)
他のオブジェクトファイルでリンクするために適切なコードとデータを保持し、executable fileまたはshared object fileを作成します。 - executable file(実行可能ファイル)
実行するための適切なプログラムを保持します。 - shared object file(共有オブジェクトファイル)
2つのコンテキストでリンクするために適切なコードとデータを保持します。一つ目のリンクエディタは他のrelocatable fileとshared object fileを使って、他のオブジェクトファイルを作成します。二つ目の動的リンカはexecutable fileと他のshared object fileを結合し、プロセスイメージを作成します。
リンクプログラムと実行プログラムのファイルフォーマットは以下です。
[ELFドキュメントより]
ELFヘッダはファイルの最初に存在し、ファイル構成を示す「ロードマップ」を保持しています。
セクションはリンクするために命令、データ、シンボルテーブル、再配置情報などの大部分のオブジェクトファイル情報を保持しています。
プログラムヘッダテーブルはプロセスイメージがどのように作られたかをシステムに通知します。プロセスイメージをビルドするために使用するファイルはプログラムヘッダテーブルを持つ必要があります。relocatable fileはこれを必要としません。
セクションヘッダテーブルはファイルのセクション情報を含んでいます。
各セクションはテーブルのエントリを持っています。
各エントリはセクション名、セクションサイズなどの情報を提供します。リンクするときに使用されるファイルはセクションヘッダテーブルを持つ必要があります。
ELFヘッダ
ELFファイルに実際のサイズが含まれているため、一部のオブジェクトファイル制御構造を大きくすることが出来ます。オブジェクトファイルフォーマットが変更された場合、プログラムは制御構造が予想よりも大きいまたは小さくてもかまいません。
ここで私の環境(Ubuntu 16.04LTS)でのelf.h内のELFヘッダファイルを見てみます。
sudo file / -name elf.h
で検索するといっぱいelf.hが出てきました。とりあえず、/usr/include/elf.hが位置的にリーダーっぽそうなので(リーダーってなんや笑)、これを参考にしていきます。
#define EI_NIDENT (16)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file 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; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
上が32bit用で下が64bit用ですね。両方にサイズ以外の違いはなさそうです。
- e_ident
固定値といくつかの情報が示されているようです。- 0:EI_MAG0
固定値。ELFMAG0:0x7f - 1:EI_MAG1
固定値。ELFMAG1:'E' - 2:EI_MAG2
固定値。ELFMAG2:'L' - 3:EI_MAG3
固定値。ELFMAG3:'F' - 4:EI_CLASS
ファイルクラス。
無効クラス。ELFCLASSNONE:0
32bitオブジェクト。ELFCLASS32:1
64bitオブジェクト。ELFCLASS64:2 - 5:EI_DATA
データエンコーディング。
無効なデータエンコーディング。ELFDATANONE:0
2の補数リトルエンディアン。ELFDATA2LSB:1
2の補数ビッグエンディアン。ELFDATA2MSB:2 - 6:EI_VERSION
ELFヘッダバージョン。現在は下で説明するEV_CURRENTの値を持ちます。 - 7:EI_OSABI
OS ABI識別情報。
Unix System V ABI。ELFOSABI_NONE,ELFOSABI_SYSV:0
HP-UX。ELFOSABI_HPUX:1
NetBSD。ELFOSABI_NETBSD:2
GNU拡張ELF。ELFOSABI_GNU, ELFOSABI_LINUX:3
Sun Solaris。ELFOSABI_SOLARIS:6
IBM AIX。ELFOSABI_AIX:7
SGI Irix。ELFOSABI_IRIX:8
FreeBSD。ELFOSABI_FREEBSD:9
Compaq TRU64 Unix。ELFOSABI_TRU64:10
Novell Modesto。ELFOSABI_MODESTO:11
OpenBSD。ELFOSABI_OPENBSD:12
ARM EABI。ELFOSABI_ARM_AEABI:64
ARM。ELFOSABI_ARM:64
スタンドアロン組み込みアプリケーション。ELFOSABI_STANDALONE:255 - 8:EI_ABIVERSION
ABIバージョン。 - 9:EI_PAD
これはe_identで使用しないByteの開始位置を示します。これらのByteは予約済みで、0にセットされます。
- 0:EI_MAG0
- e_type
オブジェクトファイルタイプ。
ファイルタイプなし。ET_NONE:0
relocatable file。ET_REL:1
executable file。ET_EXEC:2
shared object file。ET_DYN:3
core file。ET_CORE:4 - e_machine
個々のファイルに必要なアーキテクチャを示します。
マシンなし。EM_NONE:0
AT&T WE 32100。EM_M32:1
SUN SPARC。EM_SPARC:2
Intel 80386。EM_386:3
Motorola m68k family。EM_68K:4
Motorola m88k family。EM_88K:5
Intel 80860。EM_860:6
MIPS R3000 big-endian。EM_MIPS:7
IBM System 370。EM_S370:8
...
Tilera TILE-Gx。EM_TILEGX:191 - e_version
オブジェクトファイルバージョン。
無効なELFバージョン。EV_NONE:0
現在のバージョン。EV_CURRENT:1 - e_entry
仮想アドレスのエントリーポイント。こんなところに書いてあったんですね!笑 - e_phoff
プログラムヘッダテーブルのファイルオフセット。 - e_shoff
セクションヘッダテーブルのファイルオフセット。 - e_flags
プロセッサ指定フラグ。
PowerPC embedded flag。EF_PPC_EMB:0x80000000
PowerPC -mrelocatable flag。EF_PPC_RELOCATABLE:0x00010000
PowerPC -mrelocatable-lib。EF_PPC_RELOCATABLE_LIB:0x00008000 - e_ehsize
ELFヘッダサイズ。 - e_phentsize
プログラムヘッダテーブルエントリサイズ。 - e_phnum
プログラムヘッダテーブルエントリカウント。 - e_shentsize
セクションヘッダテーブルエントリサイズ。 - e_shnum
セクションヘッダテーブルエントリカウント。 - e_shstrndx
セクション名文字列テーブルのインデックス。
では実際にELFヘッダを表示させてみたいと思います。
#include <stdio.h>
#include <elf.h>
int main(int argc, char *argv[]) {
int i = 0;
Elf64_Ehdr elf_header;
FILE *fd = fopen(argv[1], "rb");
if (fd) {
fread(&elf_header, 1, sizeof(elf_header), fd);
for (i=0; i<EI_NIDENT; i++) {
printf("e_ident[%d]=0x%x\n", i, elf_header.e_ident[i]);
}
printf("e_type=0x%x\n", elf_header.e_type);
printf("e_machine=0x%x\n", elf_header.e_machine);
printf("e_version=0x%x\n", elf_header.e_version);
printf("e_entry=0x%x\n", elf_header.e_entry);
printf("e_phoff=0x%x\n", elf_header.e_phoff);
printf("e_shoff=0x%x\n", elf_header.e_shoff);
printf("e_flags=0x%x\n", elf_header.e_flags);
printf("e_ehsize=0x%x\n", elf_header.e_ehsize);
printf("e_phnum=0x%x\n", elf_header.e_phnum);
printf("e_shentsize=0x%x\n", elf_header.e_shentsize);
printf("e_shnum=0x%x\n", elf_header.e_shnum);
printf("e_shstrndx=0x%x\n", elf_header.e_shstrndx);
fclose(fd);
}
return 0;
}
C言語は久しぶりなので、合っていますでしょうか。
試しにこのプログラム自身で試すと、とりあえず表示はできました。
共有オブジェクトファイルなんですね。実行ファイルだと思っていました。
あと、e_flagsは値が入っていないですね。
次はSectionを見ていきます。
Section
オブジェクトファイルのセクションヘッダテーブルを使用すると、すべてのファイルセクションを見つけることができます。ELFヘッダのe_shoffはセクションヘッダテーブルの始点からのオフセットです。e_shnumはセクションヘッダテーブルに含まれるエントリの数です。e_shentsizeは各エントリのByteサイズです。
一部のセクションヘッダテーブルインデックスは予約されています。
特別セクションインデックス
- SHN_UNDEF
未定義セクション:0 - SHN_LORESERVE
予約インデックスの始点:0xff00 - SHN_LOPROC
プロセッサ固有の始点:0xff00 - SHN_BEFORE
Order section before all others (Solaris):0xff00 (分かりません) - SHN_AFTER
Order section before all others (Solaris):0xff01 (分かりません) - SHN_HIPROC
プロセッサ固有の終点:0xff1f - SHN_LOOS
OS固有の始点:0xff20 - SHN_HIOS
OS固有の終点:0xff3f - SHN_ABS
シンボルなどの対応する参照の絶対値を指定:0xfff1 - SHN_COMMON
共通シンボルの指定??:0xfff2 - SHN_XINDEX
余分のテーブルインデックス:0xffff - SHN_HIRESERVE
予約インデックスの終点:0xffff
SectionはEFLヘッダ、プログラムヘッダテーブル、セクションヘッダテーブルを除くオブジェクトファイルのすべてのアクションの記載を含みます。
Section Header
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
- sh_name
セクション名(文字列テーブルインデックス)。 - sh_type
- SHT_NULL
未使用セクションヘッダテーブル始点:0 - SHT_PROGBITS
プログラムデータ:1 - SHT_SYMTAB
シンボルテーブル:2 - SHT_STRTAB
文字列テーブル:3 - SHT_RELA
再配置エントリ:4 - SHT_HASH
シンボルハッシュテーブル:5 - SHT_DYNAMIC
ダイナミックリンク情報:6 - SHT_NOTE
ノート:7 - SHT_NOBITS
データなしのプログラムスペース(bss):8 - SHT_REL
再配置エントリ:9 - SHT_SHLIB
予約済み:10 - SHT_DYNSYM
ダイナミックリンクシンボルテーブル:11 - SHT_INIT_ARRAY
コンストラクタの配列:14 - SHT_FINI_ARRAY
デストラクタの配列:15 - SHT_PREINIT_ARRAY
プリコンストラクタの配列:16 - SHT_GROUP
Sectionグループ:17 - SHT_SYMTAB_SHNDX
拡張Sectionインデックス:18 - SHT_NUM
定義された型の番号
(以下省略)
- SHT_NULL
- sh_flags
セクションの属性のフラグ。- SHF_WRITE
書き込み可能:0x1(1<<0) - SHF_ALLOC
プロセス実行中にメモリを占有:0x2(1<<1) - SHF_EXECINSTR
実行可能:0x4(0<<2) - SHF_MERGE
マージされる可能性がある:0x8(0<<3) - SHF_STRINGS
NULL文字を含む文字列:0x16(0<<4)
(以下省略)
- SHF_WRITE
- sh_addr
実行時のSection仮想アドレス - sh_offset
Sectionのオフセット - sh_size
SectionのByteサイズ - sh_link
他のSectionへのリンク - sh_info
追加Section情報 - sh_addralign
Sectionアライメント - sh_entsize
Sectionテーブルを持つ場合のエントリサイズ
では、セクションヘッダを実際に見てみます。
#include <stdio.h>
#include <elf.h>
int main(int argc, char *argv[]) {
int i = 0;
Elf64_Ehdr elf_header;
Elf64_Shdr sh_header;
FILE *fd = fopen(argv[1], "rb");
if (fd) {
// elf header
fread(&elf_header, 1, sizeof(elf_header), fd);
printf("e_shoff=0x%x\n", elf_header.e_shoff);
printf("e_shentsize=0x%x\n", elf_header.e_shentsize);
printf("e_shnum=0x%x\n", elf_header.e_shnum);
printf("e_shstrndx=0x%x\n", elf_header.e_shstrndx);
// section header
fseek(fd, elf_header.e_shoff, SEEK_SET);
/* for (i=0; i<elf_header.e_shnum; i++) { */
for (i=0; i<2; i++) {
fread(&sh_header, 1, sizeof(sh_header), fd);
printf("---------------------\n");
printf("sh_name=0x%x\n", sh_header.sh_name);
printf("sh_type=0x%x\n", sh_header.sh_type);
printf("sh_flags=0x%x\n", sh_header.sh_flags);
printf("sh_addr=0x%x\n", sh_header.sh_addr);
printf("sh_offset=0x%x\n", sh_header.sh_offset);
printf("sh_size=0x%x\n", sh_header.sh_size);
printf("sh_link=0x%x\n", sh_header.sh_link);
printf("sh_info=0x%x\n", sh_header.sh_info);
printf("sh_addralign=0x%x\n", sh_header.sh_addralign);
printf("sh_entsize=0x%x\n", sh_header.sh_entsize);
printf("---------------------\n");
}
fclose(fd);
}
return 0;
}
すべて出力すると多かったので、2番目まで出力するようにしました。
elfヘッダにはセクションヘッダテーブルの始点とセクション数、セクションエントリサイズなどが格納されているので、それを利用しました。
一番初めのセクションヘッダテーブルには0しか格納されていませんでした。
セクション名(sh_name)には文字列テーブルへのインデックス番号が格納されていて、参照するとセクション名が得られるようです。
この他にもプログラムヘッダテーブルや文字列テーブル、シンボルテーブルなどが存在しますが、先へ進みたいのでここまでにします。必要になったら適宜参照していきます。
まとめ
- ELFには3つのオブジェクトファイルが存在する
- ELFヘッダにはファイルの全体的な情報が格納されている
- Sectionヘッダには各セクション情報が格納されている