Quirks in the Go Lang Process Initialization
While cybersecurity experts at S2 were building the most invaluable Red Team Toolkit on the market, aka Voodoo, a fully cross-platform (macOS, Linux, Windows) cross-architecture (x86, ARM, ARM64) OPSEC savvy collaborative platform for Red Teamers, Penetration Testers, and anyone who wants to take their cybersecurity game to the next level, we discovered some interesting edge cases while loading Go binaries into memory-only.
For more details on Voodoo, check out our website.
Early on in the development of Voodoo and our in-memory executable loader, we noticed issues with Linux binaries written in Go. As it turns out Go does some none standard stuff during initialization that required some special setup.
In our testing, our code was crashing very early in the execution of the binary. Debugging revealed the code was walking off the end of the char* argv. My first inclination was that we were missing NULL terminator or an off by one error when copying the argv variables into the target process. Further, inspection revealed the memory was correct and this wasn’t the issue.
As I stepped through the crash, at the same time marring up the compiled binary with the Go programming language source code I was able to spot the issue. It’s here in the cpuinit() function.
This for loop walks the argv array but notice it starts at argc+1. That’s clearly passed the end of the array, so then what is it trying to do? Looking at the rest of the code and the related comments show it is trying to parse the env variables and expects them to be located in memory after the argv array.
Walking past the end of the array technically should lead to undefined behavior yet empirically the env variables are placed on the stack directly after the argv variables. This behavior happens to be consistent across many Linux-based kernels that I’ve inspected, this common memory layout was first (to my knowledge) outlined in the famous Smashing The Stack For Fun And Profit paper by Aleph One.
The Linux kernel source code for this can be found here. I’m not going to dive too deep into this but rather explain it at a high level.
The call to copy_strings copies the pointers to the stack working backward in memory. Thus the argv parameters are just before the env parameters by nature of these two calls to copy_strings(bprm->envc, envp, bprm) and copy_strings(bprm->argc, argv, bprm) being back to back. This behavior is consistent as far back as version 0.01 of the kernel source
This trick works most of the time but relying on this memory layout is a risky design process. I’ve been searching and found no documentation that guarantees the env parameters are to be placed directly after the argv parameters on the stack. There is a reason processes are given a global environ pointer. It is surprising to me that the Go programming language source uses this kind of hackery in something that so many applications depend on.
S2’ Red Teaming as a Service (RTaaS) leverages Voodoo to provide continuous Red Teaming and/or Penetration Testing of Internal Networks, Applications, and/or Information Systems. As a result, our experts have discovered thousands of vulnerable applications within our Client’s infrastructure and collaboratively helped our Clients reduce high and critical risks before breaches occurred!
For more details on RTaaS, check out our website.