読者です 読者をやめる 読者になる 読者になる

goのバイナリを簡単に読みたい

go reversing

先日の goのバイナリをちょっとまじめに読んでみるstrippedなgoのバイナリを読み解くでは気合いでgoのバイナリを読んでいった。goのバイナリの解析の難しいところは、strippedだった場合に静的リンクされたライブラリの1000個以上の関数がどの関数なのか分かりづらいとか、メインの処理がどこからなのか分かりづらいとかそういうところである。

idaapiから生成した静的リンクされたライブラリのFLIRT(Fast Library Acquisition for Identification and Recognition) signatureとバイナリを照らし合わせれば、楽にシンボルを特定できそうだと考えたが、FireEyeがすでにgoバイナリを想定した似たようなツールを作っていた。

FLARE IDA Pro Script Series: Generating FLAIR function patterns using IDAPython « Threat Research Blog | FireEye Inc

これを試そうと思ったが、研究室のIDAがstarterライセンスであることに気づき完全にやる気をなくし1ヶ月経った。

また、Labeling Library Functions in Stripped Binariesでは関数ごとにCFG(Control Flow Graph)を作ることで、FLIRTよりも高精度でシンボルを回復できると論じているが、正直そこまでやる気がでない。

誰か知見を教えてほしい。

gccでlibc抜きでコンパイルを通す

c言語 asm

Hello from a libc-free world! (Part 1) (Ksplice Blog) がおもしろかったのでやっていく。

libcありのとき

コード

よくあるhello worldのコード

$ cat hello_with_libc.c
# include <stdio.h>

int main()
{
  printf("Hello World\n");
  return 0;
}

コンパイル

$ gcc hello_with_libc.c -o hello_with_libc
$ ./hello_with_libc 
Hello World
$ wc -c ./hello_with_libc 
8561 ./hello_with_libc

もちろん問題なくコンパイルできる

libcなしのとき

コード

libcを使わないコードを書いた。

$ cat hello_no_libc.c
int main()
{
  char *str = "Hello World";
  return 0;
}

コンパイル

$ gcc -nostdlib hello_no_libc.c -o hello_no_libc
/usr/bin/ld: 警告: エントリシンボル _start が見つかりません。デフォルトとして 0000000000400144 を使用します
$ ./hello_no_libc
Segmentation fault (コアダンプ)

-nostdlibをつけてlibcを含まずに、コンパイルするとエントリポイントが見つからないと怒られる。 ELF形式のエントリポイントはcrt1.oで定義されているのでリンクしてみる。

$ gcc -Os -c hello_no_libc.c
$ ld /usr/lib/x86_64-linux-gnu/crt1.o -o hello_no_libc hello_no_libc.o
ld: /usr/lib/debug/usr/lib/x86_64-linux-gnu/crt1.o(.debug_info): 再配置 0 が無効なシンボル索引 11 を持っています
ld: /usr/lib/debug/usr/lib/x86_64-linux-gnu/crt1.o(.debug_info): 再配置 1 が無効なシンボル索引 12 を持っています
...
ld: /usr/lib/debug/usr/lib/x86_64-linux-gnu/crt1.o(.debug_info): 再配置 18 が無効なシンボル索引 13 を持っています
ld: /usr/lib/debug/usr/lib/x86_64-linux-gnu/crt1.o(.debug_info): 再配置 19 が無効なシンボル索引 21 を持っています
ld: /usr/lib/debug/usr/lib/x86_64-linux-gnu/crt1.o(.debug_line): 再配置 0 が無効なシンボル索引 2 を持っています
/usr/lib/x86_64-linux-gnu/crt1.o: 関数 `_start' 内:
(.text+0x12): `__libc_csu_fini' に対する定義されていない参照です
/usr/lib/x86_64-linux-gnu/crt1.o: 関数 `_start' 内:
(.text+0x19): `__libc_csu_init' に対する定義されていない参照です
/usr/lib/x86_64-linux-gnu/crt1.o: 関数 `_start' 内:
(.text+0x25): `__libc_start_main' に対する定義されていない参照です

libc_csu_fini、libc_csu_init、libc_start_mainがないと怒られた。これらはmain関数が呼ばれる前に呼ばれる関数でlibcのセットアップを行う。crt1.oに頼らず、main関数を呼ぶエントリポイントを書く必要がある。

$ cat stubstart.s 
.globl _start

_start:
    call main
    mov  $60, %eax # exit
    xor  %rdi, %rdi   
    syscall

stubstart.sはmain関数を呼んだ後、exitのシステムコールを呼び出し終了する_start関数を書いたものである。 これがエントリポイントとなる。

$ gcc -nostdlib stubstart.s hello_no_libc.c -o hello_no_libc
$ ./hello_no_libc
$ wc -c ./hello_no_libc 
1613 ./hello_no_libc

無事にコンパイルが通り実行できるようになった。冒頭でコンパイルしたHello worldの8561byteに比べ、libcがない分、1613byteとサイズが小さくなったことも確認できる。

Unbreakable Enterprise Product Activationをz3pyを使って解いてみる

CTF reversing

はじめに

z3pyにkatagaitai勉強会#6 - 関西|easyで入門したので、練習がてらGoogle CTF 2016のUnbreakable Enterprise Product Activationを解いた。 この問題を人力で解くのは難しく、競技期間中は解けなかったが、z3pyを使えば楽に解くことができた。

実行してみる

$ ./unbreakable-enterprise-product-activation 
unbreakable-enterprise-product-activation: ./unbreakable_enterprise_product_activation product-key
$ ./unbreakable-enterprise-product-activation aaaa
Product activation failure 255

正しいproduct-keyがflagになりそうと分かる。

静的解析

0x4005aaから0x4005b8までの処理では入力したproduct-keyをコピーしている。0x4005bfから0x40071bまでつづくcallによって、0x4027f0から0x403440にproduct-keyをチェックする処理を呼んでいる。

4005aa         mov        rsi, qword [ds:rsi+8]             ; src address
4005ae         mov        edx, 0x43                         ; size
4005b3         mov        edi, 0x6042c0                     ; dist address
4005b8         call       j_strncpy
4005bd         xor        eax, eax
4005bf         call       0x4027f0
4005c4         xor        eax, eax
4005c6         call       0x402830
4005cb         xor        eax, eax
4005cd         call       0x402870
4005d2         xor        eax, eax
4005d4         call       0x4028c0

以下のようなproduct-keyをチェックする処理が50個ほどあり、人力で解くのは難しいと分かる。

4027f0         movzx      edx, byte [ds:0x6042e6]                     ; XREF=sub_400590+47
4027f7         movzx      eax, byte [ds:0x6042de]
4027fe         movzx      ecx, byte [ds:0x6042c6]
402805         movzx      esi, byte [ds:0x6042c8]
40280c         movzx      edi, byte [ds:0x6042c0]
402813         xor        eax, edx
402815         sub        eax, esi
402817         add        eax, ecx
402819         cmp        dil, al

また、strcpyのコピー先でXREFがついているアドレスは0x6042c0から0x6042f2だったので、flagの長さは51だと分かった。

6042c0         db  0x00 ; '.'                               ; XREF=sub_400590+35, sub_400870+9, sub_400870+30, sub_400870+2932, sub_400870+4029, sub_400870+4644, sub_400870+4934, sub_400870+6356, sub_400870+7477, sub_400870+8092, sub_400870+9189, …
6042c1         db  0x00 ; '.'                               ; XREF=sub_400870+47, sub_400870+65, sub_400870+2954, sub_400870+3889, sub_400870+4666, sub_400870+5244, sub_400870+6378, sub_400870+6980, sub_400870+8156, sub_400870+8528, sub_400870+9902, …
6042c2         db  0x00 ; '.'                               ; XREF=sub_400870+83, sub_400870+103, sub_400870+2977, sub_400870+4007, sub_400870+4651, sub_400870+4689, sub_400870+6401, sub_400870+6527, sub_400870+8227, sub_400870+9088
6042c3         db  0x00 ; '.'                                ; XREF=sub_400870+122, sub_400870+141, sub_400870+3001, sub_400870+3675, sub_400870+4713, sub_400870+5824, sub_400870+6425, sub_400870+7099, sub_400870+8213, sub_400870+8307, sub_400870+9182, …
6042c4         db  0x00 ; '.'                               ; XREF=sub_400870+161, sub_400870+179, sub_400870+3026, sub_400870+3034, sub_400870+4738, sub_400870+5100, sub_400870+6450, sub_400870+6598, sub_400870+8366, sub_400870+8414, sub_400870+8421, …
6042c5         db  0x00 ; '.'                               ; XREF=sub_400870+197, sub_400870+215, sub_400870+3050, sub_400870+4074, sub_400870+4762, sub_400870+5078, sub_400870+6474, sub_400870+6956, sub_400870+8428, sub_400870+8592, sub_400870+8599, …
...

z3pyを使う

コード

#!/usr/bin/env python2.7
# coding: UTF-8

from z3 import *

FLAG_LENGTH = 0x6042f2 - 0x6042c0 + 1

s = Solver()
text = []

for i in xrange(FLAG_LENGTH):
    text.append(BitVec(i, 8))
    s.add(And(text[i] >= 0x20, text[i] < 0x7f)) # in printable ascii

s.add(text[0] == text[6] + (text[38] ^ text[30]) - text[8])
s.add(text[1] == (text[42] ^ (text[38] ^ text[20] ^ text[19])))
s.add(text[2] == text[35] + text[36] - text[19] - text[3] - text[44])
s.add(text[3] == text[19] + (text[17] ^ (text[41] - text[10] - text[10])))
s.add(text[4] == text[33] - text[21])
s.add(text[5] == (text[4] ^ (text[4] ^ text[8] ^ text[39])))
s.add(text[6] == (text[14] ^ (text[10] + text[25] - text[39])))
s.add(text[7] == text[32] + (text[15] ^ text[1]))
s.add(text[8] == text[8])
s.add(text[9] == (text[24] ^ text[7]))
s.add(text[10] == text[32] + (text[49] ^ text[17]) - text[4])
s.add(text[11] == (text[42] ^ text[38]) - text[17] - text[8])
s.add(text[12] == text[14] + text[8])
s.add(text[13] == text[45] + text[20])
s.add(text[14] == text[9] + (text[20] ^ (text[25] - text[48])))
s.add(text[15] == text[18] - text[31])
s.add(text[16] == (text[24] ^ text[46]))
s.add(text[17] == ((text[13] + text[2] + text[47]) ^ (text[14] ^ text[50])))
s.add(text[18] == text[0] + text[36] + text[44] - text[3])
s.add(text[19] == (text[41] ^ text[30]) - text[25] - text[28])
s.add(text[20] == (text[25] ^ text[44]))
s.add(text[21] == text[25] + ((text[28] + text[22]) ^ (text[39] ^ text[21])))
s.add(text[22] == (text[31] ^ (text[44] - text[4] - text[12])) - text[30])
s.add(text[23] == (text[39] ^ (text[32] - text[14])))
s.add(text[24] == (text[21] ^ (text[0] ^ text[18] ^ text[21])))
s.add(text[25] == text[18] + text[4] + (text[12] ^ text[17]) - text[11])
s.add(text[26] == (text[32] ^ text[46]) + text[49] + text[20])
s.add(text[27] == text[36] + text[25] + text[39] - text[48])
s.add(text[28] == (text[14] ^ text[15]))
s.add(text[29] == text[1] + text[35] - text[42])
s.add(text[30] == text[8] - text[31] - text[30] - text[24])
s.add(text[31] == (text[42] ^ (text[15] + text[18] - text[29])))
s.add(text[32] == text[14] + text[5] + text[15] - text[44])
s.add(text[33] == (text[20] ^ (text[45] - text[15])) - text[32])
s.add(text[34] == (text[3] ^ text[33]) - text[20] - text[10])
s.add(text[35] == (text[44] ^ (text[6] - text[43])) + text[1] - text[44])
s.add(text[36] == (text[49] ^ (text[31] + text[25] - text[28])))
s.add(text[37] == text[11] + (text[34] ^ text[31]) - text[34])
s.add(text[38] == text[42] + (text[27] ^ text[36]) - text[5])
s.add(text[39] == (text[37] ^ text[8]))
s.add(text[40] == (text[44] ^ (text[7] + text[28])) - text[10])
s.add(text[41] == (text[20] ^ (text[7] ^ text[17] ^ text[26])))
s.add(text[42] == text[50] + text[1] - text[28])
s.add(text[43] == text[46] + text[33] - text[15])
s.add(text[44] == ((text[24] + text[42] + text[16]) ^ (text[45] ^ text[21])))
s.add(text[45] == text[22] - text[40])
s.add(text[46] == text[12] - text[46] - text[7] - text[35])
s.add(text[47] == (text[39] ^ (text[15] + text[26])) - text[12])
s.add(text[48] == (text[11] ^ (text[15] - text[8])))
s.add(text[49] == (text[27] ^ text[37]))
s.add(text[50] == ((text[13] + text[8] + text[17]) ^ (text[24] ^ text[15])))

if s.check() == sat:
    m = s.model()
    flag = ''
    for i in xrange(FLAG_LENGTH):
        flag += chr(int(str(m[text[i]])))

    print('RESULT : {0}'.format(flag))

結果

$ python unbrekable.py 
RESULT : CTF{0The1Quick2Brown3Fox4Jumped5Over6The7Lazy8Fox9}
$ ./unbreakable-enterprise-product-activation CTF{0The1Quick2Brown3Fox4Jumped5Over6The7Lazy8Fox9}
Thank you - product activated!

参考記事

IoTセキュリティハッキングコンテスト神戸で大洗女子学園は廃校になりました

イベント CTF

IoTセキュリティハッキングコンテスト神戸2016 | Kobe Digital Labo 神戸デジタル・ラボ に大洗女子学園を代表して参加してきました。IoTセキュリティハッキングコンテストというのは、要はIoTに関係がある問題が出るCTFですね。

様子





結果

高校の存続をかけて大洗女子学園代表として参加しましたが4位で惜しくも入賞できず、大洗女子学園は廃校になりました。今後は継続高校で精進していこうと思います。

strippedなgoのバイナリを読み解く

go reversing

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

医療セキュリティハッキングコンテスト神戸2016に行ってきた

医療セキュリティハッキングコンテストとは

実際に運用されている医療系ソフトウェアを合法的に叩き潰せて、賞金も出るナイスなイベントです。学校の先輩達とチームで出場してました。

医療セキュリティハッキングコンテスト神戸2016 | Kobe Digital Labo 神戸デジタル・ラボ

結果

3位入賞しました。

f:id:TAKEmaru:20160529143539j:plain

感想

野生の脆弱性を見つけたことがなく、参加前は不安だったのですが、蓋を開けてみると攻略対象は非常に雑なアプリケーションで自明な脆弱性が数多くあり安心?して取り組めました。

また、脆弱性を報告した経験もなかったので、報告文の書き方や脆弱性の分類の仕方など勉強になりました。雑に報告した脆弱性が、3つの脆弱性に小分けされて3回ポイントが得られたときは最高でしたが、今度はちゃんと自分で分類して報告したいと思います。

命に関わることもありそうな病院のシステムはもっと堅牢だと思っていたのですが、内部のネットワーク、人間からしか使用されないと思っているのか、CTFよりも攻略が非常に簡単な脆弱なシステムが運用されていることが分かり、もう病院にはいけないなと思いました。

前の週はdefconでブチギレていたので、いいストレス発散になって良かったですね。

goのバイナリをちょっとまじめに読んでみる

reversing go

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というバイナリが生成された。

gdb

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

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

バイナリを読んでいく

エントリポイント

まずは、gdbでエントリポイントを確認する。

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$ i symbol 0x4562a0
_rt0_amd64_linux in section .text

エントリポイントは0x4562a0の_rt0_amd64_linux だと分かる。
つづいて_rt0_amd64_linuxの中身を見る。

gdb-peda$ disas _rt0_amd64_linux
Dump of assembler code for function _rt0_amd64_linux:
   0x00000000004562a0 <+0>:	lea    rsi,[rsp+0x8]
   0x00000000004562a5 <+5>:	mov    rdi,QWORD PTR [rsp]
   0x00000000004562a9 <+9>:	lea    rax,[rip+0x10]        # 0x4562c0 <main>
   0x00000000004562b0 <+16>:	jmp    rax
   0x00000000004562b2 <+18>:	int3   
   0x00000000004562b3 <+19>:	int3   
   0x00000000004562b4 <+20>:	int3   
   0x00000000004562b5 <+21>:	int3   
   0x00000000004562b6 <+22>:	int3   
   0x00000000004562b7 <+23>:	int3   
   0x00000000004562b8 <+24>:	int3   
   0x00000000004562b9 <+25>:	int3   
   0x00000000004562ba <+26>:	int3   
   0x00000000004562bb <+27>:	int3   
   0x00000000004562bc <+28>:	int3   
   0x00000000004562bd <+29>:	int3   
   0x00000000004562be <+30>:	int3   
   0x00000000004562bf <+31>:	int3   
End of assembler dump.

main関数に飛ばすだけだった。

lib.goアーキテクチャからエントリポイントに置く関数を決定したあと、この場合だと rt0_linux_amd64.s に書かれた関数の処理内容を、elf.go でELFのエントリポイントに書き込むようだ。

main関数

つづいてmain関数を読んでいく。

gdb-peda$ disas main
Dump of assembler code for function main:
   0x00000000004562c0 <+0>:	lea    rax,[rip+0xffffffffffffcb19]        # 0x452de0 <runtime.rt0_go>
   0x00000000004562c7 <+7>:	jmp    rax
   0x00000000004562c9 <+9>:	int3   
   0x00000000004562ca <+10>:	int3   
   0x00000000004562cb <+11>:	int3   
   0x00000000004562cc <+12>:	int3   
   0x00000000004562cd <+13>:	int3   
   0x00000000004562ce <+14>:	int3   
   0x00000000004562cf <+15>:	int3   
End of assembler dump.

main関数はruntime.rt0_goに飛ばすだけで、ここで文字列の出力等のメインの処理を行っているわけではないようだ。

これは rt0_linux_amd64.s で定義されていた。

runtime.rt0_go

runtime.rt0_goを読む。長いので4つに分けて読んでいく。
なお、この関数は asm_amd64.s で定義されていた。

gdb-peda$ disas 0x452de0
Dump of assembler code for function runtime.rt0_go:
   0x0000000000452de0 <+0>:	mov    rax,rdi
   0x0000000000452de3 <+3>:	mov    rbx,rsi
   0x0000000000452de6 <+6>:	sub    rsp,0x27
   0x0000000000452dea <+10>:	and    rsp,0xfffffffffffffff0
   0x0000000000452dee <+14>:	mov    QWORD PTR [rsp+0x10],rax
   0x0000000000452df3 <+19>:	mov    QWORD PTR [rsp+0x18],rbx
   0x0000000000452df8 <+24>:	lea    rdi,[rip+0x131d61]        # 0x584b60 <runtime.g0>
   0x0000000000452dff <+31>:	lea    rbx,[rsp-0xff98]
   0x0000000000452e07 <+39>:	mov    QWORD PTR [rdi+0x10],rbx
   0x0000000000452e0b <+43>:	mov    QWORD PTR [rdi+0x18],rbx
   0x0000000000452e0f <+47>:	mov    QWORD PTR [rdi],rbx
   0x0000000000452e12 <+50>:	mov    QWORD PTR [rdi+0x8],rsp
   0x0000000000452e16 <+54>:	xor    eax,eax
   0x0000000000452e18 <+56>:	cpuid  
   0x0000000000452e1a <+58>:	cmp    rax,0x0
   0x0000000000452e1e <+62>:	je     0x452e9a <runtime.rt0_go+186>
   0x0000000000452e20 <+64>:	cmp    ebx,0x756e6547
   0x0000000000452e26 <+70>:	jne    0x452e3f <runtime.rt0_go+95>
   0x0000000000452e28 <+72>:	cmp    edx,0x49656e69
   0x0000000000452e2e <+78>:	jne    0x452e3f <runtime.rt0_go+95>
   0x0000000000452e30 <+80>:	cmp    ecx,0x6c65746e
   0x0000000000452e36 <+86>:	jne    0x452e3f <runtime.rt0_go+95>
   0x0000000000452e38 <+88>:	mov    BYTE PTR [rip+0x14bc95],0x1        # 0x59ead4 <runtime.lfenceBeforeRdtsc>
   0x0000000000452e3f <+95>:	mov    rax,0x1
   0x0000000000452e46 <+102>:	cpuid  
   0x0000000000452e48 <+104>:	mov    DWORD PTR [rip+0x14bc9a],ecx        # 0x59eae8 <runtime.cpuid_ecx>
   0x0000000000452e4e <+110>:	mov    DWORD PTR [rip+0x14bc98],edx        # 0x59eaec <runtime.cpuid_edx>
   0x0000000000452e54 <+116>:	and    ecx,0x18000000
   0x0000000000452e5a <+122>:	cmp    ecx,0x18000000
   0x0000000000452e60 <+128>:	jne    0x452f78 <runtime.rt0_go+408>
   0x0000000000452e66 <+134>:	xor    ecx,ecx
   0x0000000000452e68 <+136>:	xgetbv 
   0x0000000000452e6b <+139>:	and    eax,0x6
   0x0000000000452e6e <+142>:	cmp    eax,0x6
   0x0000000000452e71 <+145>:	jne    0x452f78 <runtime.rt0_go+408>
   0x0000000000452e77 <+151>:	mov    BYTE PTR [rip+0x14bc58],0x1        # 0x59ead6 <runtime.support_avx>
   0x0000000000452e7e <+158>:	mov    eax,0x7
   0x0000000000452e83 <+163>:	xor    ecx,ecx
   0x0000000000452e85 <+165>:	cpuid  
   0x0000000000452e87 <+167>:	and    ebx,0x20
   0x0000000000452e8a <+170>:	cmp    ebx,0x20
   0x0000000000452e8d <+173>:	jne    0x452f6c <runtime.rt0_go+396>
   0x0000000000452e93 <+179>:	mov    BYTE PTR [rip+0x14bc3d],0x1        # 0x59ead7 <runtime.support_avx2>

0x452e18から0x452e93まででは、Intel CPUを使用しているか確認し、それがAVX、AVX2拡張命令セットをサポートしているCPUであればそれに対応するための処理を実行しているようだ。

   0x0000000000452e9a <+186>:	mov    rax,QWORD PTR [rip+0x1313c7]        # 0x584268 <_cgo_init>
   0x0000000000452ea1 <+193>:	test   rax,rax
   0x0000000000452ea4 <+196>:	je     0x452f34 <runtime.rt0_go+340>
   0x0000000000452eaa <+202>:	mov    rcx,rdi
   0x0000000000452ead <+205>:	lea    rsi,[rip+0x1dbc]        # 0x454c70 <setg_gcc>
   0x0000000000452eb4 <+212>:	call   rax
   0x0000000000452eb6 <+214>:	lea    rcx,[rip+0x131ca3]        # 0x584b60 <runtime.g0>
   0x0000000000452ebd <+221>:	mov    rax,QWORD PTR [rcx]
   0x0000000000452ec0 <+224>:	add    rax,0x2d0
   0x0000000000452ec6 <+230>:	mov    QWORD PTR [rcx+0x10],rax
   0x0000000000452eca <+234>:	mov    QWORD PTR [rcx+0x18],rax

0x452e9aから0x452ea4までで、_cgo_initとシンボルが付いているアドレスの値がゼロか確認している。goには、cgoというc言語のコードを呼べるようにする機能があり、それが有効であった場合のみ、cgoを有効にするための処理をするようだ。今回、cgoは使っていないので 0x452f34にジャンプする。0x452eb6から0x452ecaまでではStackGuardの更新をしている。

   0x0000000000452f34 <+340>:	lea    rdi,[rip+0x131fc5]        # 0x584f00 <runtime.m0+96>
   0x0000000000452f3b <+347>:	call   0x456790 <runtime.settls>
   0x0000000000452f40 <+352>:	mov    QWORD PTR fs:0xfffffffffffffff8,0x123
   0x0000000000452f4d <+365>:	mov    rax,QWORD PTR [rip+0x131fac]        # 0x584f00 <runtime.m0+96>
   0x0000000000452f54 <+372>:	cmp    rax,0x123
   0x0000000000452f5a <+378>:	je     0x452ece <runtime.rt0_go+238>
   0x0000000000452f60 <+384>:	mov    DWORD PTR ds:0x0,eax
   0x0000000000452f67 <+391>:	jmp    0x452ece <runtime.rt0_go+238>
   0x0000000000452f6c <+396>:	mov    BYTE PTR [rip+0x14bb64],0x0        # 0x59ead7 <runtime.support_avx2>
   0x0000000000452f73 <+403>:	jmp    0x452e9a <runtime.rt0_go+186>
   0x0000000000452f78 <+408>:	mov    BYTE PTR [rip+0x14bb57],0x0        # 0x59ead6 <runtime.support_avx>
   0x0000000000452f7f <+415>:	jmp    0x452f6c <runtime.rt0_go+396>
   0x0000000000452f81 <+417>:	int3   
   0x0000000000452f82 <+418>:	int3   
   0x0000000000452f83 <+419>:	int3   
   0x0000000000452f84 <+420>:	int3   
   0x0000000000452f85 <+421>:	int3   
   0x0000000000452f86 <+422>:	int3   
   0x0000000000452f87 <+423>:	int3   
   0x0000000000452f88 <+424>:	int3   
   0x0000000000452f89 <+425>:	int3   
   0x0000000000452f8a <+426>:	int3   
   0x0000000000452f8b <+427>:	int3   
   0x0000000000452f8c <+428>:	int3   
   0x0000000000452f8d <+429>:	int3   
   0x0000000000452f8e <+430>:	int3   
   0x0000000000452f8f <+431>:	int3   
End of assembler dump.

0x452f3bでruntime.settlsという関数を呼び、TLSのセットアップをしている。TLS (Thread Local Storage)は他のスレッドからは参照されない、スレッド固有のデータを保持できるようにするものである。この関数は sys_linux_amd64.s で定義されていた。
TLSのセットアップが終われば0x452eceにジャンプする。

   0x0000000000452ece <+238>:	lea    rcx,[rip+0x131c8b]        # 0x584b60 <runtime.g0>
   0x0000000000452ed5 <+245>:	mov    QWORD PTR fs:0xfffffffffffffff8,rcx
   0x0000000000452ede <+254>:	lea    rax,[rip+0x131fbb]        # 0x584ea0 <runtime.m0>
   0x0000000000452ee5 <+261>:	mov    QWORD PTR [rax],rcx
   0x0000000000452ee8 <+264>:	mov    QWORD PTR [rcx+0x30],rax
   0x0000000000452eec <+268>:	cld
   0x0000000000452eed <+269>:	call   0x4364c0 <runtime.check>
   0x0000000000452ef2 <+274>:	mov    eax,DWORD PTR [rsp+0x10]
   0x0000000000452ef6 <+278>:	mov    DWORD PTR [rsp],eax
   0x0000000000452ef9 <+281>:	mov    rax,QWORD PTR [rsp+0x18]
   0x0000000000452efe <+286>:	mov    QWORD PTR [rsp+0x8],rax
   0x0000000000452f03 <+291>:	call   0x435e20 <runtime.args>
   0x0000000000452f08 <+296>:	call   0x425770 <runtime.osinit>
   0x0000000000452f0d <+301>:	call   0x42ab00 <runtime.schedinit>
   0x0000000000452f12 <+306>:	lea    rax,[rip+0xd80ff]        # 0x52b018 <runtime.mainPC>
   0x0000000000452f19 <+313>:	push   rax
   0x0000000000452f1a <+314>:	push   0x0
   0x0000000000452f1c <+316>:	call   0x430b30 <runtime.newproc>
   0x0000000000452f21 <+321>:	pop    rax
   0x0000000000452f22 <+322>:	pop    rax
   0x0000000000452f23 <+323>:	call   0x42c7b0 <runtime.mstart>
   0x0000000000452f28 <+328>:	mov    DWORD PTR ds:0xf1,0xf1
   0x0000000000452f33 <+339>:	ret

0x452eedでruntime.checkという関数を呼んでいる。これは型のサイズ等を確認する。この関数は runtime1.go で定義されていた。

0x452f03でruntime.argsという関数を呼んでいる。ここではコマンドライン引数とその個数を保存している。この関数も runtime1.go で定義されていた。


0x452f08から0x452f23ではメインの処理を行うgoroutineをスタートする処理を行っている。
0x452f08でruntime.osinitという関数を呼んでいる。ncpuという変数にCPUのコア数を入れている。
os1_linux.go で定義されていた。

0x452f0dでruntime.schedinitという関数を呼んでいる。proc.go で定義されていた。
これはスケジューリングを行い、race conditionを防ぐ。


0x452f1cでruntime.newprocを呼ぶことで、新しいgoroutineを作り、0x452f23でruntime.mstartを呼ぶことで、goroutineを開始している。どんなgoroutineを開始したのか。

0x452f12でruntime.mainPCというシンボルのついたメモリアドレスが指す値をlea命令によってraxに入れているのが気になる。
runtime.mainPCをgdbで確認するとruntime.mainという関数へのポインタになっていた。

gdb-peda$ x/x 0x52b018
0x52b018 <runtime.mainPC>:	0x0000000000429800

gdb-peda$ i symbol 0x429800
runtime.main in section .text of /mnt/hgfs/code/go_study/print_test

つづく0x452f19でこのポインタをpushしている。runtime.newproc、runtime.mstartで使うためのようだ。runtime.main関数がgoroutineとしてスタートされるようだ。

runtime.newprocは proc.go で、runtime.mstartは proc.go で定義されていた。

runtime.rt0_goがCPUの確認やcgoを使う準備、runtime.mainの起動を行う関数だと分かった。

runtime.main

この関数は長いのでmain.initとmain.mainという関数を呼んでいることに着目し、これらを読んでいく。

gdb-peda$ disas 0x429800
Dump of assembler code for function runtime.main:
...
   0x0000000000429a7a <+634>:	call   0x401100 <main.init>
...
   0x0000000000429aab <+683>:	call   0x401000 <main.main>
main.init
gdb-peda$ disas 0x401100
Dump of assembler code for function main.init:
   0x0000000000401100 <+0>:	mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x0000000000401109 <+9>:	cmp    rsp,QWORD PTR [rcx+0x10]
   0x000000000040110d <+13>:	jbe    0x401143 <main.init+67>
   0x000000000040110f <+15>:	movzx  ebx,BYTE PTR [rip+0x19d9ac]        # 0x59eac2 <main.initdone.>
   0x0000000000401116 <+22>:	cmp    bl,0x0
   0x0000000000401119 <+25>:	je     0x40112f <main.init+47>
   0x000000000040111b <+27>:	movzx  ebx,BYTE PTR [rip+0x19d9a0]        # 0x59eac2 <main.initdone.>
   0x0000000000401122 <+34>:	cmp    bl,0x2
   0x0000000000401125 <+37>:	jne    0x401128 <main.init+40>
   0x0000000000401127 <+39>:	ret    
   0x0000000000401128 <+40>:	call   0x426660 <runtime.throwinit>
   0x000000000040112d <+45>:	ud2    
   0x000000000040112f <+47>:	mov    BYTE PTR [rip+0x19d98c],0x1        # 0x59eac2 <main.initdone.>
   0x0000000000401136 <+54>:	call   0x466f70 <fmt.init>
   0x000000000040113b <+59>:	mov    BYTE PTR [rip+0x19d980],0x2        # 0x59eac2 <main.initdone.>
   0x0000000000401142 <+66>:	ret    
   0x0000000000401143 <+67>:	call   0x4531f0 <runtime.morestack_noctxt>
   0x0000000000401148 <+72>:	jmp    0x401100 <main.init>
   0x000000000040114a <+74>:	int3   
   0x000000000040114b <+75>:	int3   
   0x000000000040114c <+76>:	int3   
   0x000000000040114d <+77>:	int3   
   0x000000000040114e <+78>:	int3   
   0x000000000040114f <+79>:	int3   
End of assembler dump.

0x401136でfmt.init関数を呼び、importしたfmtパッケージの初期化を行っている。main.initはパッケージの初期化を行う関数のようだ。

main.main
$ disas 0x401000
Dump of assembler code for function main.main:
   0x0000000000401000 <+0>:	mov    rcx,QWORD PTR fs:0xfffffffffffffff8
   0x0000000000401009 <+9>:	cmp    rsp,QWORD PTR [rcx+0x10]
   0x000000000040100d <+13>:	jbe    0x4010ec <main.main+236>
   0x0000000000401013 <+19>:	sub    rsp,0x78
   0x0000000000401017 <+23>:	lea    rbx,[rip+0x100252]        # 0x501270
   0x000000000040101e <+30>:	mov    QWORD PTR [rsp+0x50],rbx
   0x0000000000401023 <+35>:	mov    QWORD PTR [rsp+0x58],0xc
   0x000000000040102c <+44>:	xor    ebx,ebx
   0x000000000040102e <+46>:	mov    QWORD PTR [rsp+0x40],rbx
   0x0000000000401033 <+51>:	mov    QWORD PTR [rsp+0x48],rbx
   0x0000000000401038 <+56>:	lea    rbx,[rsp+0x40]
   0x000000000040103d <+61>:	cmp    rbx,0x0
   0x0000000000401041 <+65>:	je     0x4010e5 <main.main+229>
   0x0000000000401047 <+71>:	mov    QWORD PTR [rsp+0x68],0x1
   0x0000000000401050 <+80>:	mov    QWORD PTR [rsp+0x70],0x1
   0x0000000000401059 <+89>:	mov    QWORD PTR [rsp+0x60],rbx
   0x000000000040105e <+94>:	lea    rbx,[rip+0xb7d9b]        # 0x4b8e00
   0x0000000000401065 <+101>:	mov    QWORD PTR [rsp],rbx
   0x0000000000401069 <+105>:	lea    rbx,[rsp+0x50]
   0x000000000040106e <+110>:	mov    QWORD PTR [rsp+0x8],rbx
   0x0000000000401073 <+115>:	mov    QWORD PTR [rsp+0x10],0x0
   0x000000000040107c <+124>:	call   0x40b9f0 <runtime.convT2E>
   0x0000000000401081 <+129>:	mov    rcx,QWORD PTR [rsp+0x18]
   0x0000000000401086 <+134>:	mov    rax,QWORD PTR [rsp+0x20]
   0x000000000040108b <+139>:	mov    rbx,QWORD PTR [rsp+0x60]
   0x0000000000401090 <+144>:	mov    QWORD PTR [rsp+0x30],rcx
   0x0000000000401095 <+149>:	mov    QWORD PTR [rbx],rcx
   0x0000000000401098 <+152>:	mov    QWORD PTR [rsp+0x38],rax
   0x000000000040109d <+157>:	cmp    BYTE PTR [rip+0x19da3c],0x0        # 0x59eae0 <runtime.writeBarrier>
   0x00000000004010a4 <+164>:	jne    0x4010d1 <main.main+209>
   0x00000000004010a6 <+166>:	mov    QWORD PTR [rbx+0x8],rax
   0x00000000004010aa <+170>:	mov    rbx,QWORD PTR [rsp+0x60]
   0x00000000004010af <+175>:	mov    QWORD PTR [rsp],rbx
   0x00000000004010b3 <+179>:	mov    rbx,QWORD PTR [rsp+0x68]
   0x00000000004010b8 <+184>:	mov    QWORD PTR [rsp+0x8],rbx
   0x00000000004010bd <+189>:	mov    rbx,QWORD PTR [rsp+0x70]
   0x00000000004010c2 <+194>:	mov    QWORD PTR [rsp+0x10],rbx
   0x00000000004010c7 <+199>:	call   0x45a680 <fmt.Println>
   0x00000000004010cc <+204>:	add    rsp,0x78
   0x00000000004010d0 <+208>:	ret    
   0x00000000004010d1 <+209>:	lea    r8,[rbx+0x8]
   0x00000000004010d5 <+213>:	mov    QWORD PTR [rsp],r8
   0x00000000004010d9 <+217>:	mov    QWORD PTR [rsp+0x8],rax
   0x00000000004010de <+222>:	call   0x40eef0 <runtime.writebarrierptr>
   0x00000000004010e3 <+227>:	jmp    0x4010aa <main.main+170>
   0x00000000004010e5 <+229>:	mov    DWORD PTR [rbx],eax
   0x00000000004010e7 <+231>:	jmp    0x401047 <main.main+71>
   0x00000000004010ec <+236>:	call   0x4531f0 <runtime.morestack_noctxt>
   0x00000000004010f1 <+241>:	jmp    0x401000 <main.main>
   0x00000000004010f6 <+246>:	int3   
   0x00000000004010f7 <+247>:	int3   
   0x00000000004010f8 <+248>:	int3   
   0x00000000004010f9 <+249>:	int3   
   0x00000000004010fa <+250>:	int3   
   0x00000000004010fb <+251>:	int3   
   0x00000000004010fc <+252>:	int3   
   0x00000000004010fd <+253>:	int3   
   0x00000000004010fe <+254>:	int3   
   0x00000000004010ff <+255>:	int3   
End of assembler dump.

0x4010c7でfmt.Println関数を呼び出し、"hello, world"を出力している。main.mainはメインとなる処理を行う、c言語のmain関数のようなものだと分かった。

まとめ

goのバイナリにおいてメインの処理を行うのはmain.mainであり、これは_rt0_amd64_linux、main、runtime.rt0_go、runtime.mainを経て呼び出されると分かった。

感想

処理系のコードと照らし合わせながら、バイナリを読んでいくことで、新たな楽しさを見つけられた。この記事がgoの開発やリバース・エンジニアリングの助けとなればうれしい。