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")
}
コンパイル
go build print_test.go

これでprint_testというバイナリが生成された。

シンボルをけずる
$ strip --strip-unneeded ./print_test

これでstripped binaryとなった。

gdb

goで書かれたプログラムをgdbデバッグするには.gdbinitに以下の行を追加する必要がある。

add-auto-load-safe-path /usr/local/go/src/runtime/runtime-gdb.py

バイナリを読んでいく

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が配置されるアドレスとなるので、最初にこのアドレスを見にいって、違っていたらエントリポイントから処理を追っていくというアプローチがよさそうである。