前回はELFファイルの構造を示しながら、動的リンクに必要な処理を説明しました。
今回はPythonで実装した簡易ローダを見ながら、実際の処理を追っていきます。
32bit Windows上でELFファイルを読み込み、動的リンクを解決して実行します。Pythonに標準で付属するctypesモジュールを使えば簡単にバイナリ処理できます。
ファイル読み込み
指定されたファイルを読み込みます。省略した場合は a.out を対象とします。
aout = "a.out" if len(argv) < 2 else argv[1] with open(aout, "rb") as f: elf = f.read()
本来は必要な部分だけ読み込むべきですが、プログラムを単純にするため一気に全体を読んでいます。
ELFヘッダ
ファイルがELFヘッダのサイズ以上で、ファイルの先頭がELFシグネチャかどうかを確認します。
assert len(elf) >= 52, "not found: ELF32 header" assert elf[0:4] == "\x7fELF", "not found: ELF signature"
ELFファイルが32bitでリトルエンディアンを対象としているかを確認します。詳細は仕様書のe_identを参照してください。
assert ord(elf[4]) == 1, "not 32bit" assert ord(elf[5]) == 1, "not little endian"
ELFヘッダ全体を読みこみます。unpackはstructモジュールの関数で、文字列に格納されたバイナリから値を取り出します。
(e_type, e_machine, e_version, e_entry, e_phoff, e_shoff, e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx) = unpack( "<HHLLLLLHHHHHH", elf[16:52])
位置独立コードで、i386を対象としているかを確認します。
assert e_type == 3, "not PIE" assert e_machine == 3, "not i386"
今回のローダはWindows上で動きますが、アドレスを指定しても既に使用されていれば確保できないため、どこに読み込んでも良いように位置独立を要求しています。
プログラムヘッダ
ローダが使う情報はプログラムヘッダに書いてあります。
プログラムヘッダは複数あるため、クラスを定義して1つずつインスタンスとして読み込みます。
class Elf32_Phdr: def __init__(self, data, pos): (self.p_type, self.p_offset, self.p_vaddr, self.p_paddr, self.p_filesz, self.p_memsz, self.p_flags, self.p_align) = unpack( "<LLLLLLLL", data[pos : pos + 32])
ELFヘッダに指定された情報を基に、プログラムヘッダを読み込みます。
e_phoff | プログラムヘッダのファイル中の位置 |
e_phentsize | プログラムヘッダ1個のサイズ |
e_phnum | プログラムヘッダの個数 |
phs = [Elf32_Phdr(elf, e_phoff + i * e_phentsize) for i in range(e_phnum)]
メモリに読み込み
プログラムヘッダにはファイルからメモリに読み込むときのアドレス(p_vaddr)とサイズ(p_memsz)が書いてあります。位置独立コードでは開始アドレスは0となっているため、終端だけを見て最大値を求めます。
memlen = max([ph.p_vaddr + ph.p_memsz for ph in phs])
必要なサイズの実行可能ページを確保します。最初の引数でアドレスを指定できますが、必ずしも確保できないため、0を指定してWindowsに任せます。戻り値はアドレス(整数値)です。
mem = VirtualAlloc(0, memlen, MEM_COMMIT, PAGE_EXECUTE_READWRITE)
VirtualAllocについてはJITの記事も参照してください。
- PythonでJIT (Windows 32bit)
- PythonでJIT (Mac OS X 64bit)
プログラムヘッダのp_typeがPT_LOADとなっているものだけ、メモリに読み込みます。この単位をセグメントと呼びます。PT_LOAD以外は、どこかのセグメントの中に含まれています。確認のためアドレスを出力します。
pelf = cast(elf, c_void_p).value for ph in phs: addr = mem + ph.p_vaddr if ph.p_type == 1: # PT_LOAD o, sz = ph.p_offset, ph.p_memsz memmove(addr, pelf + o, sz) print "LOAD: %08x-%08x => %08x-%08x" % ( o, o + sz - 1, addr, addr + sz - 1)
.dynamicセクション
プログラムヘッダに.dynamicセクションが指定されていれば、その中を読みます。既にロードされているため、直接メモリから読み込んでいます。.dynamicは type = 0 になるまで読みます。
elif ph.p_type == 2: # PT_DYNAMIC while True: type = read32(addr) val = read32(addr + 4) if type == 0: break elif type == 5: strtab = mem + val elif type == 6: symtab = mem + val elif type == 11: syment = val elif type == 23: jmprel = mem + val elif type == 2: pltrelsz = val addr += 8
メモリから読み込むread32はctypesを使っています。書き込みのwrite32とセットで定義しています。
def write32(addr, val): cast(addr, POINTER(c_uint32))[0] = val def read32(addr): return cast(addr, POINTER(c_uint32))[0]
.rel.pltセクション
jmprelには値が入っていれば.rel.pltセクションが存在します。そこから情報を読み込みます。infoをシフトした値からシンボル情報を取り出します。
if jmprel != None: print print ".rel.plt(DT_JMPREL):" for reladdr in range(jmprel, jmprel + pltrelsz, 8): offset = read32(reladdr) info = read32(reladdr + 4) stroff = read32(symtab + (info >> 8) * syment) name = string_at(strtab + stroff) print "[%08x]offset: %08x, info: %08x; %s" % ( reladdr, offset, info, name)
シンボル名に合致する関数を探してアドレスを書き込みます。これが実際のリンク作業です。
assert libc.has_key(name), "undefined reference: " + name addr = mem + offset faddr = cast(libc[name], c_void_p).value print "linking: %s -> [%08x]%08x" % (name, addr, faddr) write32(addr, faddr)
本来、シンボル名が指す関数は動的ライブラリの中にあります。しかし今回は処理を単純化するため動的ライブラリは読み込まずに、Pythonで定義した関数を渡しています。
def putchar(ch): stdout.write(chr(ch)) return ch def puts(addr): s = string_at(addr) stdout.write(s) return len(s) libc = { "putchar": CFUNCTYPE(c_int, c_int)(putchar), "puts" : CFUNCTYPE(c_int, c_void_p)(puts) }
もし本当に動的ライブラリとリンクさせるのであれば、動的ライブラリもメモリに読み込む必要があります。実行可能ファイルと同じようにELFを解析して読み込みます。
テスト
C言語のプログラムをコンパイルします。
void test(char start, const char *str) { int i; for (i = 0; i < 26; i++) putchar(start + i); putchar('\n'); puts(str); } void _start() { test('A', "Hello, ELF!\n"); test('a', "done.\n"); }
これを読み込ませれば、無事に実行できました。
===== 00000000-00001397 => 00420000-00421397 LOAD: 00000000-000002fb => 00420000-004202fb LOAD: 000002fc-00000397 => 004212fc-00421397 .rel.plt(DT_JMPREL): [004201ac]offset: 00001390, info: 00000107; putchar linking: putchar -> [00421390]00370fdc [004201b4]offset: 00001394, info: 00000207; puts linking: puts -> [00421394]00370fc0 ABCDEFGHIJKLMNOPQRSTUVWXYZ Hello, ELF! abcdefghijklmnopqrstuvwxyz done.
遅延リンク
.pltには遅延リンク用のコードが入っています。今回は実行前にすべてリンクしてしまいましたが、最初に関数を呼び出すときにリンクすることができます。
libcの関数と同じようにPython側のリンク関数にコールバックさせることで実装できます。興味のある方はコードを掲載するので参照してください。
def interp(id, offset): print "delayed link: id=%08x, offset=%08x" % (id, offset) return link(jmprel + offset) thunk_interp = CFUNCTYPE(c_void_p, c_void_p, c_uint32)(interp) call_interp = JIT([ 0xff, 0x14, 0x24, # call [esp] 0x83, 0xc4, 8, # add esp, 8 0x85, 0xc0, # test eax, eax 0x74, 2, # jz 0f 0xff, 0xe0, # jmp eax 0xc3 ]) # 0: ret if pltgot != None: writeptr(pltgot + 4, thunk_interp) writeptr(pltgot + 8, call_interp)
64bit
64bitではポインタのサイズが異なりますが、ELFの構造自体は同じです。しかしWindowsとUNIX系ではABI(関数呼び出し時のレジスタの使い方など)が異なるため変換する必要があります。
ABI | 引数 | 保存するレジスタ |
---|---|---|
Windows | rcx, rdx, r8, r9 | rbx, r12-r15, rdi, rsi |
UNIX | rdi, rsi, rdx, rcx, r8, r9 | rbx, r12-r15 |
Windowsでは4つの引数分のスタックを空けておく必要がありますが、UNIXではその必要はありません。詳細は以下の資料を参照してください。
変換を実装してWindowsでSystem V ABIの64bitコードを読み込めるようにしたローダを掲載します。ABIを変換している部分を抜粋します。
def SYSV2WIN64(restype, *argtypes): assert len(argtypes) <= 4, "too long arguments" def init(f): ret = JIT([ 0x48, 0xb8] + [0]*8 + [ # movabs rax, addr 0x49, 0x89, 0xc9, # mov r9 , rcx 0x49, 0x89, 0xd0, # mov r8 , rdx 0x48, 0x89, 0xf2, # mov rdx, rsi 0x48, 0x89, 0xf9, # mov rcx, rdi 0x48, 0x83, 0xec, 0x28, # sub rsp, 40 0xff, 0xd0, # call rax 0x48, 0x83, 0xc4, 0x28, # add rsp, 40 0xc3 ]) # ret ret.f = CFUNCTYPE(restype, *argtypes)(f) writeptr(ret.addr + 2, ret.f) return ret return init