strippedなgoのバイナリを読み解く
この前はふつーのgoのバイナリを読んだ。今回はstrippedなgoのバイナリを読んでいく。
環境
$ uname -a Linux ubuntu 3.19.0-58-generic #64~14.04.1-Ubuntu SMP Fri Mar 18 19:05:43 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.4 LTS Release: 14.04 Codename: trusty $ go version go version go1.6.2 linux/amd64
準備
バイナリを生成
"hello, world"をprintするだけのバイナリを生成する。
コード
package main import "fmt" func main() { fmt.Println("hello, world") }
シンボルをけずる
$ strip --strip-unneeded ./print_test
これでstripped binaryとなった。
バイナリを読んでいく
goのバイナリをちょっとまじめに読んでみる - 脱力系日記 でgoのバイナリにおいてメインの処理を行うのはmain.mainであり、これは_rt0_amd64_linux、main、runtime.rt0_go、runtime.mainを経て呼び出されると分かったので、これらを追って読んでいく。
シンボルの確認
シンボルはすべてなくなっていた。
gdb-peda$ symbol No symbol file now.
エントリポイント
エントリポイントを確認する。この環境でのgoのエントリポイントは_rt0_amd64_linuxである。
gdb-peda$ i file Symbols from "/mnt/hgfs/code/go_study/print_test". Local exec file: `/mnt/hgfs/code/go_study/print_test', file type elf64-x86-64. Entry point: 0x4562a0 0x0000000000401000 - 0x00000000004aa630 is .text 0x00000000004ab000 - 0x000000000052d7df is .rodata 0x000000000052d7e0 - 0x000000000052f898 is .typelink 0x000000000052f898 - 0x000000000052f898 is .gosymtab 0x000000000052f8a0 - 0x000000000057fb0b is .gopclntab 0x0000000000580000 - 0x0000000000581ce8 is .noptrdata 0x0000000000581d00 - 0x0000000000584250 is .data 0x0000000000584260 - 0x000000000059eab8 is .bss 0x000000000059eac0 - 0x00000000005a38c0 is .noptrbss 0x0000000000400fc8 - 0x0000000000401000 is .note.go.buildid gdb-peda$ x/10i 0x4562a0 => 0x4562a0: lea rsi,[rsp+0x8] 0x4562a5: mov rdi,QWORD PTR [rsp] 0x4562a9: lea rax,[rip+0x10] # 0x4562c0 0x4562b0: jmp rax 0x4562b2: int3 0x4562b3: int3 0x4562b4: int3 0x4562b5: int3 0x4562b6: int3 0x4562b7: int3
エントリポイントでは0x4562c0に飛ばしている。これは main関数 のアドレスである。
main
gdb-peda$ x/5i 0x4562c0 0x4562c0: lea rax,[rip+0xffffffffffffcb19] # 0x452de0 0x4562c7: jmp rax 0x4562c9: int3 0x4562ca: int3 0x4562cb: int3
main関数では0x452de0に飛ばしている。これは runtime.rt0_go のアドレスである。
runtime.rt0_go
この関数はメインの処理を行う runtime.main へ飛ばす役割を持っている。シンボルが削られていることもあり、まともに読むと死ぬので、ret 付近を見る。
gdb-peda$ x/100i 0x452de0 ... 0x452f12: lea rax,[rip+0xd80ff] # 0x52b018 0x452f19: push rax 0x452f1a: push 0x0 0x452f1c: call 0x430b30 0x452f21: pop rax 0x452f22: pop rax 0x452f23: call 0x42c7b0 0x452f28: mov DWORD PTR ds:0xf1,0xf1 0x452f33: ret
rax にアドレスの値を代入したあとpushして関数をcallしている。このraxにいれられた0x52b018が指す値がruntime.mainのアドレスのようだ。
gdb-peda$ x/4x 0x52b018 0x52b018: 0x00 0x98 0x42 0x00
0x429800がruntime.mainのアドレスとなっていた。
runtime.main
長いのでまともに読むのはめんどくさい。処理系のコードを見ると、runtime.mainのmain.mainを呼び出す場所は以下のようになっていた。
if isarchive || islibrary { // A program compiled with -buildmode=c-archive or c-shared // has a main, but it is not executed. return } main_main() if raceenabled { racefini() } // Make racy client program work: if panicking on // another goroutine at the same time as main returns, // let the other goroutine finish printing the panic trace. // Once it does, it will exit. See issue 3934. if panicking != 0 { gopark(nil, nil, "panicwait", traceEvGoStop, 1) } exit(0) for { var x *int32 *x = 0 } }
これを見ながら、ret より逆に命令を読んでいくとそれらしい命令が見つかった。0x429aabでmain.mainをcallしているようだ。0x40100がmain.mainのアドレスであった。
$ x/200i 0x429800 0x429aab: call 0x401000 # call main.main 0x429ab0: mov ebx,DWORD PTR [rip+0x17505e] # 0x59eb14 0x429ab6: cmp ebx,0x0 0x429ab9: je 0x429af4 0x429abb: mov QWORD PTR [rsp],0x0 0x429ac3: mov QWORD PTR [rsp+0x8],0x0 0x429acc: lea rbx,[rip+0xd80cd] # 0x501ba0 0x429ad3: mov QWORD PTR [rsp+0x10],rbx 0x429ad8: mov QWORD PTR [rsp+0x18],0x9 0x429ae1: mov BYTE PTR [rsp+0x20],0x10 0x429ae6: mov QWORD PTR [rsp+0x28],0x1 0x429aef: call 0x429d20 0x429af4: mov DWORD PTR [rsp],0x0 0x429afb: call 0x4562d0 0x429b00: xor eax,eax 0x429b02: mov DWORD PTR [rax],0x0 0x429b08: jmp 0x429b00 0x429b0a: nop 0x429b0b: call 0x427820 0x429b10: add rsp,0x48 0x429b14: ret
main.main
シンボルがないため、気合で読んでいく必要があるが、冒頭では初期化処理が行われているため、retから下から上に処理を見ていく方がよい。今回はretから2つ目の命令で fmt.Println を読んでいた。
gdb-peda$ x/100i 0x401000 0x401000: mov rcx,QWORD PTR fs:0xfffffffffffffff8 0x401009: cmp rsp,QWORD PTR [rcx+0x10] 0x40100d: jbe 0x4010ec 0x401013: sub rsp,0x78 0x401017: lea rbx,[rip+0x100252] # 0x501270 0x40101e: mov QWORD PTR [rsp+0x50],rbx 0x401023: mov QWORD PTR [rsp+0x58],0xc 0x40102c: xor ebx,ebx 0x40102e: mov QWORD PTR [rsp+0x40],rbx 0x401033: mov QWORD PTR [rsp+0x48],rbx 0x401038: lea rbx,[rsp+0x40] 0x40103d: cmp rbx,0x0 0x401041: je 0x4010e5 0x401047: mov QWORD PTR [rsp+0x68],0x1 0x401050: mov QWORD PTR [rsp+0x70],0x1 0x401059: mov QWORD PTR [rsp+0x60],rbx 0x40105e: lea rbx,[rip+0xb7d9b] # 0x4b8e00 0x401065: mov QWORD PTR [rsp],rbx 0x401069: lea rbx,[rsp+0x50] 0x40106e: mov QWORD PTR [rsp+0x8],rbx 0x401073: mov QWORD PTR [rsp+0x10],0x0 0x40107c: call 0x40b9f0 0x401081: mov rcx,QWORD PTR [rsp+0x18] 0x401086: mov rax,QWORD PTR [rsp+0x20] 0x40108b: mov rbx,QWORD PTR [rsp+0x60] 0x401090: mov QWORD PTR [rsp+0x30],rcx 0x401095: mov QWORD PTR [rbx],rcx 0x401098: mov QWORD PTR [rsp+0x38],rax 0x40109d: cmp BYTE PTR [rip+0x19da3c],0x0 # 0x59eae0 0x4010a4: jne 0x4010d1 0x4010a6: mov QWORD PTR [rbx+0x8],rax 0x4010aa: mov rbx,QWORD PTR [rsp+0x60] 0x4010af: mov QWORD PTR [rsp],rbx 0x4010b3: mov rbx,QWORD PTR [rsp+0x68] 0x4010b8: mov QWORD PTR [rsp+0x8],rbx 0x4010bd: mov rbx,QWORD PTR [rsp+0x70] 0x4010c2: mov QWORD PTR [rsp+0x10],rbx 0x4010c7: call 0x45a680 # call fmt.Println 0x4010cc: add rsp,0x78 0x4010d0: ret 0x4010d1: lea r8,[rbx+0x8] 0x4010d5: mov QWORD PTR [rsp],r8 0x4010d9: mov QWORD PTR [rsp+0x8],rax 0x4010de: call 0x40eef0 0x4010e3: jmp 0x4010aa 0x4010e5: mov DWORD PTR [rbx],eax 0x4010e7: jmp 0x401047 0x4010ec: call 0x4531f0 0x4010f1: jmp 0x401000
まとめ
strippedなgoのバイナリの読み解き方を確認した。メインの処理が呼ばれるまでの流れを把握していれば読める。
リンカがカスタムされてないかぎり、0x401000がmain.mainが配置されるアドレスとなるので、最初にこのアドレスを見にいって、違っていたらエントリポイントから処理を追っていくというアプローチがよさそうである。