The challenge was framed as a command-line browser, but basically boiled down to answering this question: if you control Go source code but can only import the fmt package, can you get enough code execution to execute xcalc?
In this blog post let's look at some of the exploits that teams came up with, either during the CTF or after.
Why allowing fmt?
Second, because it makes the challenge easier: as mentioned before (2013, 2015) it gives a nice addrof (address of) primitive with fmt.Sprintf("%p", x), just like what you often need in a browser exploit.
Why no PIE?
Go now supports compiling PIE executables, but it's not the default, you have to specify -buildmode=pie.
I left it off because it's the default and because it makes the challenge easier for the CTF.
The rest of the environment was also well-known to make it so there is no guessing involved, a Debian 10 amd64 VM with Go version 1.13.3, given to the teams on a USB stick.
@NetanelBenSimon from 5BC (2nd place) published a very nice writeup, in particular including their thought process, experimentation including dead ends, up to their exploit. It's a good read instead of just showing the end exploit.
After exploring that maps aren't safe for concurrent use, they found my 2015 article about data races and built upon my race-interface.go proof-of-concept.
They used the fmt.Sprintf("%p", x) addrof primitive, modified the proof-of-concept to match syscall.Syscall6() included in the binary, set the right parameters for an execve syscall, and raced to corrupt the function pointer to point to syscall.Syscall6. That meant hardcoding a bunch of addresses, which was fair for the challenge.
pasten and p4 have not published their writeups, but their exploits were roughly the same.
Exploiting the interface race is not what I had in mind when I created the challenge (more below) but it's legit!
TokyoWesterns didn't manage to solve Gomium during the CTF, but @hama7230 published a writeup afterwards.
Interestingly, it doesn't use the interface race but the slice race:
- it uses the fmt.Sprintf("%p", x) addrof primitive
- it overwrites the length of a slice so that it can write out of bounds
- it uses the "bring your own gadget" technique (which I also like!) to store a few instructions in a number which will compile to a mov reg, immediate and can jump in the middle of the instruction
var a uint64 = 0xc3050f585a5e5f58;becomes
$ cstool x64 '58 5f 5e 5a 58 0f 05 c3' 0 58 pop rax 1 5f pop rdi 2 5e pop rsi 3 5a pop rdx 4 58 pop rax 5 0f 05 syscall 7 c3 ret
Very cool exploit! That's what I had intended for the challenge: using the race to corrupt a slice to build a powerful read/write primitive and continue to gaining full code execution, not unlike what we see in browser exploits.
Jackyxty & Gengming Liu (@dmxcsnsbh) published their writeup: it's really brilliant! They went to the Golang bug tracker and found issue #29312 cmd/compile: depth limit reached in type formatting routine, then found the proof-of-concept in #29264 (https://play.golang.org/p/bbI3nbNprvi). It does not use data races but an issue in the Go compiler leading to a type confusion. From that they build an arbitrary read/write primitive, overwrite a function pointer to get RIP control, jump to an mprotect stub stored in mov immediates, just like in TokyoWesterns exploit, and finally run their shellcode. See the full exploit.
The end result is a reliable exploit because it doesn't need data races and 2 CPUs. Also, I'm happy they noticed multiple parallels with browser/JS exploitation. That's also what I found when building the exploit and what made me frame the challenge this way.
Oh and yes, you read right, it's not fixed: cl/154583 just increases the depth from 100 to 250.
Obviously, a reminder that this is not a very concerning bug, not part of Go's threat model. Few things take arbitrary Go code, compile and run it, and those who do should be careful to sandbox it, like play.golang.org.
If you tried the challenge I hope you liked it. I left the challenge intentionally open, exploiting data races with slice was one way, but teams showed other ways with interface or using a Go compiler bug - which is very cool! A bug in the Go parser and bypassing the "fmt" import restriction would also have been fair game.
Now that you see how much fun we can have with Go, how about we up the game and try to make an exploit that works on all Go versions (from 1.0 to latest 1.13), with PIE ASLR (-buildmode=pie), without any import, not even "fmt", just using builtins? Still completely useless, so obviously I'll present one in a following blog post.
Post a Comment