CVE-2020-10611 - RCE in a flawed DNP3 Implementation

Adam Crain September 01, 2020
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.

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.