CVE-2020-10611 - RCE in a flawed DNP3 Implementation
S4x20 hosted the Pwn2Own Miami hacking competition this year, and one of the more interesting and impactful results was a bug chain leading to remote code execution (RCE) in the Triangle Microworks (TMW) SCADA Data Gateway.
The Zero Day Initiative who puts on these competitions recently released a detailed writeup (and video) of the bugs and the exploit.
Achieving code execution on the Triangle MicroWorks SCADA Data Gateway - details (and video!) on 2 CVEs used by @ScepticCtf, @brymko, & @bl4ckic3 during #Pwn2Own Miami to win $25K https://t.co/3KGLE2SEHO
— Zero Day Initiative (@thezdi) August 25, 2020
In this case, the attack vector was the outstation side of a DNP3 protocol implementation. TMW OEMs the underlying C library to 3rd parties, and it is unclear whether the full exploit chain applies only to their gateway product, or possibly also to 3rd parties that integrate their library.
Regardless of the impact, this bug chain serves as a nice reminder of some of the benefits of Rust over C. The rest of this post explores the bugs and how safe Rust avoids these pitfalls.
Bug #1: Disclosure of uninitialized memory
You cannot allocate a stack or heap buffer in safe Rust without initializing its contents. Much of Rust’s functionality
relies on the proper initialization of memory. Exhaustive pattern matching of Rust enums, for example, would not be safe
if the compiler couldn’t be sure that memory was initialized to be one of the enum variants. While it is possible to use
unsafe Rust to create uninitialized buffers, this kind of thing is super easy to preclude in code you write using a crate
level macro like #![forbid(unsafe_code)]
.
Bug #2: Type confusion
Type confusion occurs when one type is treated as if it is another type. This is easy to accidently do in C which lacks
any kind of type-safe support for variants. Idiomatic C code is full of casts from opaque pointers (void*
) to
assumed types. If you attempt to use a type in memory as the wrong type, undefined behavior occurs which can lead to
exploitable memory corruption as is the case with this bug.
Safe Rust prevents all sources of memory corruption. One of the mechanisms that it uses to accomplish this is a strong
type system. The enum
type and pattern matching are particularly powerful tools that allows the programmer to
safely express a set of variants of which only one will be valid at a time.
// A value of type Widget will either be a Value or a Name
enum Widget {
Value(u8),
Name(String),
}
Enums resemble tagged unions, but you can safely and exhaustively match on them:
fn what_is_it(w: Widget) {
match w {
Widget::Value(x) => println!("It's a value of {}", x),
Widget::Name(x) => println!("It's a name called {}", x),
}
}
These types of guarantees do not exist in C, which allows the programmer to confuse an opaque type as the wrong
type, either by casting a typed pointer from void*
or by using the wrong variant in a union.
While the ZDI writeup doesn’t say exactly how the type confusion occurs, it is likely due to one of these two unsafe mechanisms. The root cause of type confusion is a lack of type safety, and Rust prevents this class of bug with its strong type system.