Introduction
Let’s take a look at gdb (the debugger) Enhanced Features (GEF) checksec command to examine our go binary to see if go programs uses modern self-protection techniques. Spoiler alert, they don’t by default but they can be improved with very little work.
Many thanks to my day job employer Confluera for encouraging me to share some of my knowledge with the broader community. I joined them because I thought all the detection and response (EDR/XDR/CIM) products out there were pretty terrible and we could build something better. Some time has passed and I feel like we did build something better, you should check it out.
Associated Code Open Source
The example/template code below has been open sourced at https://github.com/joelschopp/hello-secure-golang with a permissive MIT license, please feel free to follow along or copy into your own projects. Pull requests accepted, espeically if you find any mistakes.
Future posts in this series will be added to that project as well as we layer up our defense in depth.
Default ‘go build’ vs GEF
Let’s start out by compiling a simple hello world golang program with default settings and examine what GEF checksec tells us about it.
go build -o hello
gdb -batch -ex "checksec ./hello"
Ouch! 4 out of 5 are red x instead of green checkmarks. Let’s dive deeper on what each of these mean. Then we’ll turn those all into green checkmarks.
Stack Canary
From Wikipedia: Stack canaries, named for their analogy to a canary in a coal mine, are used to detect a stack buffer overflow before execution of malicious code can occur. This method works by placing a small integer, the value of which is randomly chosen at program start, in memory just before the stack return pointer. Most buffer overflows overwrite memory from lower to higher memory addresses, so in order to overwrite the return pointer (and thus take control of the process) the canary value must also be overwritten. This value is checked to make sure it has not changed before a routine uses the return pointer on the stack.[2] This technique can greatly increase the difficulty of exploiting a stack buffer overflow because it forces the attacker to gain control of the instruction pointer by some non-traditional means such as corrupting other important variables on the stack.[2]
Thanks Wikipedia, couldn’t have said it better myself. We want that.
Non-Executable Segments
From Wikipedia: Another approach to preventing stack buffer overflow exploitation is to enforce a memory policy on the stack memory region that disallows execution from the stack (W^X, "Write XOR Execute"). This means that in order to execute shellcode from the stack an attacker must either find a way to disable the execution protection from memory, or find a way to put their shellcode payload in a non-protected region of memory. This method is becoming more popular now that hardware support for the no-execute flag is available in most desktop processors.
Luckily golang already does this. One win for defaults.
Position Independent Execution
This one is a little misleading, Position Indpendent Code (PIC) allows Position Independent Execution (PIE). But the real win is that Position Independent Exeuction enables Address Space Layout Randomization (ASLR).
Address Space Layout Randomization probably needs no introduction with many of my readers. But for the rest suffice it to say the job of a hacker exploiting a bug a lot harder.
Fortify
This is more important if you use cgo. There are a lot of C standard library functions that don’t bounds check like strcpy, memcy, or asprintf. Often, the compiler can actually determine the size and can replace the call with a bounds checked version of the call.
Relocation Read-Only
Binary headers including the Global Offset Table that need to be writable during program load are marked read-only after the linker finishes loading them. RelRO reduces the attackable surface of the application.
Turn It Green
Golang has support for PIE, we just have to pass the flag.
go build -buildmode=pie -o hello
Now we are up to 2.5 out of 5 green. Not bad. So why isn’t this default?
root@dd46b2466c25:/repo# go build -buildmode=pie -o hello
root@dd46b2466c25:/repo# du -h ./hello
2.3M ./hello
root@dd46b2466c25:/repo# go build -o hello
root@dd46b2466c25:/repo# du -h ./hello
2.0M ./hello
The binary size gets a little larger. Frankly, if you are considering less security for 300KB smaller binary size you are reading the wrong blog. Build with pie.
From here I got pretty stuck trying to turn the rest green. Then I tried building with pie on a program that uses cgo and everything turned green. Hmmm. Interesting. Use cgo to make your program safer isn’t the takeway I was looking for, or one that passes the sniff test.
After some digging I decided cgo forced the use of gcc linker instead of go’s linker. It makes sense that a linker mostly used to link a language like C to shared libraries might have some security features enabled that a linker used to link a modern garbage collected bounds checkd language like go into static binaries didn’t.
go build -buildmode=pie -ldflags '-linkmode=external' -o hello
This indeed turns everything green.
However, as is often a case when you do security as a checklist you can miss tradeoffs. For instance, using the external linker brings in two dynamic library dependencies.
objdump -p ./hello
...
Dynamic Section:
NEEDED libpthread.so.0
NEEDED libc.so.6
...
Version References:
required from libpthread.so.0:
0x09691972 0x00 05 GLIBC_2.3.2
0x09691a75 0x00 03 GLIBC_2.2.5
required from libc.so.6:
0x0d696914 0x00 06 GLIBC_2.4
0x09691974 0x00 04 GLIBC_2.3.4
0x09691a75 0x00 02 GLIBC_2.2.5
This open you up to modified libary attacks. That has to be weighed against the other security features you pick up.
Takeaways
Using -buildmode=pie with golang binaries is a slam dunk.
Using -ldflags ‘-linkmode=external’ brings some benefit but has some tradeoffs and isn’t as much of a slam dunk but is worth considering.
checksec and objdump are useful tools to check security features and dependencies