CVE-2020-10611: Achieving Code Execution on the Triangle MicroWorks SCADA Data Gateway

August 25, 2020 | The ZDI Research Team

Back at Pwn2Own Miami earlier this year, the team of Tobias Scharnowski, Niklas Breitfeld, and Ali Abbasi demonstrated a couple of different bugs to get code execution – with continuation – on the Triangle MicroWorks SCADA Data Gateway. What might not have been clear is the specifically where the bugs resides within the Gateway system. This blog is taken from the researchers’ submission with some additional information added for context.


Before we begin, here’s a quick video describing the vulnerabilities and how they were exploited at Pwn2Own Miami.

A Brief Look at DNP3

If you aren’t a regular user of ICS/SCADA systems, you might not be familiar with the Distributed Network Protocol 3 (DNP3) set of communication protocols. DNP3 is primarily used for communications between SCADA Master Stations, Remote Terminal Units (RTUs), and Intelligent Electrical Devices (IEDs). Its adoption has been mainly in the utilities sector, including electric and water. It offers a rich feature set including data fragmentation, error checking, and generic data types which makes the protocol more robust than older ICS protocols. Relative to the OSI model for networks, DNP3 defines a Layer 2 (Data Link) protocol. The protocol additionally defines a Transport layer analogous to OSI Layer 4 and an Application layer similar to OSI Layer 7.

Figure 1 - Tobias Scharnowski (left) Prepares to Demonstrate the Exploit at Pwn2Own Miami

Figure 1 - Tobias Scharnowski (left) Prepares to Demonstrate the Exploit at Pwn2Own Miami

The Vulnerabilities

There are two bugs inside the GTWLib.dll library that are used to exploit the service.

Bug 1: Disclosure of uninitialized memory

When reporting the data for a DNP3 Data Set Descriptor element of a given type, first the value of the given element is added to the output buffer. The output buffer cursor is then incremented by a user-specified max-size value. The max-size value is not checked to make sure it is not too large for the given type. When setting the max-size field of an element of type UINT (type code: 2), the output value size is of constant size 4, while the output buffer cursor is still incremented by the user-defined max-size. As the full output buffer gets sent in the response, this leaks more data than was added to the output buffer to the requesting entity.

In combination with the fact that the output buffer does not get initialized to constant values (malloc is used), this leads to disclosure of the previous contents of the memory corresponding to the allocated buffer.

Bug 2: Type Confusion when updating Data Set Prototypes

Data Set Descriptors allow users to define custom data types. To allow for type reuse, the DNP3 standard allows Data Set Descriptors to incorporate Data Set Prototypes. A Data Set Prototype defines a sub-type that consists of a list of primitive data types. A Data Set Descriptor can include a reference to a data set prototype to incorporate an embedded data structure.

When modifying the contents of a Data Set Element, its contents are updated based on the type stored in the underlying data set. However, it is not checked to determine if the type of the element has been changed between the last modification and the current one. When a value is updated, its current value is treated as the one stored in the underlying data set prototype.

When a string (type OSTR with type code 5) gets updated, it is checked whether a buffer has already been allocated to hold the string’s contents. If a pointer to a buffer is present, realloc is used to modify the allocation to accommodate to the newly required size.

In the following series of interactions:
      1 - Creation of Data Set Prototype specifying a double value (FLT with type code 4 and size 8)
      2 - Data Set Descriptor using the prototype
      3 - Setting the Present Value of the Data Set Descriptor’s float element to 0x9090909090909090
      4 - Changing the prototype member’s type to OSTR
      5 - Modifying the Present Value

The user-controlled value is treated as a pointer, which is then re-allocated to the user-controlled size. This leads to the primitive of triggering an arbitrary realloc:

            realloc(controlled_ptr, controlled_size)

where 1 <= controlled_size <= 0x100.

Exploitation

Pointer Leak

The allocation size of the DNP3 output buffer is configurable with a default size of 0x800. If an attacker can control the data that is previously contained in an 0x800-sized buffer, the majority of bytes residing inside that buffer can be leaked using Bug 1 (as described above).

When Present Values from a data set descriptor get aggregated, an array of 0x28-sized items representing the current values is allocated. Depending on the type of the underlying Data Set Element, pointers can reside in the item.

By creating a Data Set Descriptor with 51 elements, a buffer of 0x33*0x28==0x7f8 gets allocated when serving the answer to the requested Present Value. If the allocation of the output buffer re-claims a buffer which previously contained this array of data items, pointers to user-controlled data get leaked.

Memory Corruption

Using the technique described for Bug 2 and with the knowledge of a controlled pointer from the pointer leak, an attacker can craft pointers and trigger re-allocs of these buffers to create a dangling pointer. This allows reading values from the now-freed buffer. After the buffer is reclaimed by another object, the buffer can be allocated again by the attacker using the same technique. The attacker can use the dangling pointer to repeatedly disclose the contents of the buffer’s memory.

Leaking data from known objects using the dangling pointers also reveals base addresses of DLLs through function pointers and vptrs.

First, an attacker frees the contents of one of the buffers they leaked in previous steps. Then, they trigger an allocation of the same size. Next, they fill the allocation with their fake object. At this point, they’ve reclaimed the object with their payload. Finally, trigger the type confusion condition to hijack control flow.

The victim object type that is used in this case is the Data Collection class holding Dataset Prototypes itself.

Exploit Mitigations

As for exploit mitigations, ASLR is bypassed by the pointer leak, and CFG is not enabled in this binary. Other mitigations are not relevant in this UAF scenario.

Conclusion

Triangle MicroWorks patched these bugs as CVE-2020-10611 and CVE-2020-10613. You can also reference these bugs on the ICS-CERT Advisory ICSA-20-105-03. Part of the fix included disabling Data Sets in the Gateway. Thanks again to Tobias Scharnowski, Niklas Breitfeld, and Ali Abbasi for providing much of the information included in this write-up. Their DNP3 exploit demonstration was certainly a highlight of Pwn2Own Miami, and we hope to see them at future competitions. 

Until then, follow the ZDI team for the latest in exploit techniques and security patches.