Golang and Rustlang Memory Safety
I recently read an excellent blog post by Scott Piper about a tool he has released called Serene. The tool analyzes a binary to see if it has been compiled with security mitigation techniques - essentially a sanity check for best practices. As I was reading the post I came across this quote:
Anything compiled with Golang will not have ASLR/PIE. This is a decision by the language creators as Golang is a secure language, but if the process imports a C library, it exposes itself to possible issues. As such, I didn't want to skip Golang binaries.
I was pretty shocked - this seems like a huge oversight. Scott referenced me to a quote by the author (and a quick rundown of Golang security that he’d written about) here:
"Address space randomization is an OS-level workaround for a language-level problem, namely that simple C programs tend to be full of exploitable buffer overflows. Go fixes this at the language level, with bounds-checked arrays and slices and no dangling pointers, which makes the OS-level workaround much less important. In return, we receive the incredible debuggability of deterministic address space layout. I would not give that up lightly."
EDIT: Note that Golang does in fact support ASLR/ PIE on Linux, though it is not enabled by default. See this snippet
Thanks to Shawn Webb (@lattera) for pointing this out.
Essentially, because Golang is “memory safe”, there is no need for a defense in depth approach involving mitigation techniques. The cost of the ASLR mitigation is cited as improved debugging experience.
But is Go even memory safe? That’s a bit of a sticky definition. Go-the-language is memory safe… I guess. But Go programs compiled with the standard compiler are not. Go has data races, a design choice made for performance reasons. This means that you can write code with security vulnerabilities in Go.
What’s worse is that the decision to exclude ASLR has doomed these vulnerabilities to be much more easily exploitable.
A great blog post by stalkr shows some proof of concepts for exploitable Go code here.
As stalkr mentions, he doesn’t know of a situation where such a vulnerabliity exists in the wild.
However, it is more likely that Golang code will make exploiting C/C++ code, loaded dynamically via CGO, easier. The improved debuggability hardly seems worth it to me - one could simply disable ASLR for debug builds, though I have rarely heard complains about ASLR for debugging.
Beyond that, I would expect more soundness issues and vulnerabilities to exist in Go code than we currently know about, which is exactly why defense in depth is so important. And, of course, one can drop into unsafe in Golang as well.
Rust, thankfully, takes what I would consider a much saner approach. While it still maintains a similar “The language is safe even if the implementation isn’t” attitude, it also makes use of defense in depth measures.
While looking into the Underhanded Rust Contest I had a look at the current soundness issues in rust. Unlike Go, these were very easy to find and they were all nicely labeled as soundness issues.
Within a few minutes I had found a few solid candidates for exploitability, and I narrowed it down to one that seemed particularly inconspicuous.
// U modified the code a bit while playing around for the contest.
// Issue with original code here: https://github.com/rust-lang/rust/issues/29723
fn main() {
let foo = String::from("FOO");
let foo = match 0 {
0 if {
some_func(foo) // foo is freed here
} => unreachable!(),
_ => {
// Use After Free - we return freed memory
foo
}
};
println!("{:#?}", foo); // And here we access the invalid memory
}
fn some_func(foo: String) -> bool {
drop(foo);
false
}
What we have here is a use after free vulnerability. This will print garbage, or panic. This is an issue with how rust’s current borrow semantics work with match statements.
The vulnerability is here:
0 if {
some_func(foo)
}
Effectively, this branch succeeds if some_func returns true. It does not, so the branch does not succeed. However, ‘foo’ is freed in some_func, leaving it invalid.
Despite that, we can use the value in the other branch, returning it, and then accessing it later.
This may seem a bit contrived, and I know of no place where this code exists, but I thought it was an ideal candidate for the undheranded contest because the ‘free’ is hidden elsewhere and it may not be obvious.
Of course, I then realized that rustc compiles rust programs with ‘the works’. Any rust program has full RELRO, NX, ASLR/ PIE, and (I believe) safestack.
These mitigations would make it considerably more difficult to make a reliable exploit against this or other vulnerabilities in rust code. For the contest, if I get around to writing an exploit for this, I will definitely not bother trying to get around them and instead I’ll take a hit to points and disable ASLR.
This defense in depth attitude is, in my opinion, exactly the right way to go. Rust programs don’t have to rely entirely on the memory safety guarantees of the language, which is critical since rust allows explicitly unsafe code, FFI, etc.
Security is about so much more than language level security, or even memory safety. It is fundamentally an ongoing process with moving goals. Saying “Well, we’ve solved those problems in one place, so we can stop there” is a dangerous attitude.
This isn’t to say that Go is better or worse than Rust. This isn’t to say that either languages are unlikely or likely to have vulnerable code out in the wild. I think it is much more about how the two languages approach security.
blog comments powered by Disqus