Blog

A Totally Tubular Treatise on TRITON and TriStation

Steve Miller, Evan Reese
Jun 07, 2018
16 min read
|   Last updated: Apr 02, 2024
Threat Research

Introduction

In December 2017, FireEye's Mandiant discussed an incident response involving the TRITON framework. The TRITON attack and many of the publicly discussed ICS intrusions involved routine techniques where the threat actors used only what is necessary to succeed in their mission. For both INDUSTROYER and TRITON, the attackers moved from the IT network to the OT (operational technology) network through systems that were accessible to both environments. Traditional malware backdoors, Mimikatz distillates, remote desktop sessions, and other well-documented, easily-detected attack methods were used throughout these intrusions.

Despite the routine techniques employed to gain access to an OT environment, the threat actors behind the TRITON malware framework invested significant time learning about the Triconex Safety Instrumented System (SIS) controllers and TriStation, a proprietary network communications protocol. The investment and purpose of the Triconex SIS controllers leads Mandiant to assess the attacker's objective was likely to build the capability to cause physical consequences.

TriStation remains closed source and there is no official public information detailing the structure of the protocol, raising several questions about how the TRITON framework was developed. Did the actor have access to a Triconex controller and TriStation 1131 software suite? When did development first start? How did the threat actor reverse engineer the protocol, and to what extent? What is the protocol structure?

FireEye’s Advanced Practices Team was born to investigate adversary methodologies, and to answer these types of questions, so we started with a deeper look at the TRITON’s own Python scripts.

Glossary:

  • TRITON – Malware framework designed to operate Triconex SIS controllers via the TriStation protocol.
  • TriStation – UDP network protocol specific to Triconex controllers.
  • TRITON threat actor – The human beings who developed, deployed and/or operated TRITON.

Diving into TRITON's Implementation of TriStation

TriStation is a proprietary network protocol and there is no public documentation detailing its structure or how to create software applications that use TriStation. The current TriStation UDP/IP protocol is little understood, but natively implemented through the TriStation 1131 software suite. TriStation operates by UDP over port 1502 and allows for communications between designated masters (PCs with the software that are “engineering workstations”) and clients (Triconex controllers with special communications modules) over a network.

To us, the Triconex systems, software and associated terminology sound foreign and complicated, and the TriStation protocol is no different. Attempting to understand the protocol from ground zero would take a considerable amount of time and reverse engineering effort – so why not learn from TRITON itself? With the TRITON framework containing TriStation communication functionality, we pursued studying the framework to better understand this mysterious protocol. Work smarter, not harder, amirite?

The TRITON framework has a multitude of functionalities, but we started with the basic components:

  • TS_cnames.pyc # Compiled at: 2017-08-03 10:52:33
  • TsBase.pyc # Compiled at: 2017-08-03 10:52:33
  • TsHi.pyc # Compiled at: 2017-08-04 02:04:01
  • TsLow.pyc # Compiled at: 2017-08-03 10:46:51

TsLow.pyc (Figure 1) contains several pieces of code for error handling, but these also present some cues to the protocol structure.

TsLow.pyc function print_last_error()
TsLow.pyc function print_last_error()

In the TsLow.pyc’s function for print_last_error we see error handling for “TCM Error”. This compares the TriStation packet value at offset 0 with a value in a corresponding array from TS_cnames.pyc (Figure 2), which is largely used as a “dictionary” for the protocol.

TS_cnames.pyc TS_cst array
TS_cnames.pyc TS_cst array

From this we can infer that offset 0 of the TriStation protocol contains message types. This is supported by an additional function, tcm_result, which declares type, size = struct.unpack('<HH', data_received[0 : 4]), stating that the first two bytes should be handled as integer type and the second two bytes are integer size of the TriStation message. This is our first glimpse into what the threat actor(s) understood about the TriStation protocol.

Since there are only 11 defined message types, it really doesn't matter much if the type is one byte or two because the second byte will always be 0x00.

We also have indications that message type 5 is for all Execution Command Requests and Responses, so it is curious to observe that the TRITON developers called this “Command Reply.” (We won’t understand this naming convention until later.)

Next we examine TsLow.pyc’s print_last_error function (Figure 3) to look at “TS Error” and “TS_names.” We begin by looking at the ts_err variable and see that it references ts_result.

TsLow.pyc function print_last_error() with ts_err highlighted
TsLow.pyc function print_last_error() with ts_err highlighted

We follow that thread to ts_result, which defines a few variables in the next 10 bytes (Figure 4): dir, cid, cmd, cnt, unk, cks, siz = struct.unpack('<, ts_packet[0 : 10]). Now things are heating up. What fun. There’s a lot to unpack here, but the most interesting thing is how this piece script breaks down 10 bytes from ts_packet into different variables.

ts_result with ts_packet header variables highlighted
ts_result with ts_packet header variables highlighted
tcm_result
tcm_result

Referencing tcm_result (Figure 5) we see that it defines type and size as the first four bytes (offset 0 – 3) and tcm_result returns the packet bytes 4:-2 (offset 4 to the end minus 2, because the last two bytes are the CRC-16 checksum). Now that we know where tcm_result leaves off, we know that the ts_reply “cmd” is a single byte at offset 6, and corresponds to the values in the TS_cnames.pyc array and TS_names (Figure 6). The TRITON script also tells us that any integer value over 100 is a likely “command reply.” Sweet.

When looking back at the ts_result packet header definitions, we begin to see some gaps in the TRITON developer's knowledge: dir, cid, cmd, cnt, unk, cks, siz = struct.unpack('<, ts_packet[0 : 10]). We're clearly speculating based on naming conventions, but we get an impression that offsets 4, 5 and 6 could be "direction", "controller ID" and "command", respectively. Values such as "unk" show that the developer either did not know or did not care to identify this value. We suspect it is a constant, but this value is still unknown to us.

Excerpt TS_cnames.pyc TS_names array, which contain TRITON actor’s notes for execution command function codes
Excerpt TS_cnames.pyc TS_names array, which contain TRITON actor’s notes for execution command function codes

TriStation Protocol Packet Structure

The TRITON threat actor’s knowledge and reverse engineering effort provides us a better understanding of the protocol. From here we can start to form a more complete picture and document the basic functionality of TriStation. We are primarily interested in message type 5, Execution Command, which best illustrates the overall structure of the protocol. Other, smaller message types will have varying structure.

Figure 7: Sample TriStation "Allocate Program" Execution Command, with color annotation and protocol legend
Figure 7: Sample TriStation "Allocate Program" Execution Command, with color annotation and protocol legend

Corroborating the TriStation Analysis

Minute discrepancies aside, the TriStation structure detailed in Figure 7 is supported by other public analyses. Foremost, researchers from the Coordinated Science Laboratory (CSL) at University of Illinois at Urbana-Champaign published a 2017 paper titled "Attack Induced Common-Mode Failures on PLC-based Safety System in a Nuclear Power Plant". The CSL team mentions that they used the Triconex System Access Application (TSAA) protocol to reverse engineer elements of the TriStation protocol. TSAA is a protocol developed by the same company as TriStation. Unlike TriStation, the TSAA protocol structure is described within official documentation. CSL assessed similarities between the two protocols would exist and they leveraged TSAA to better understand TriStation. The team's overall research and analysis of the general packet structure aligns with our TRITON-sourced packet structure.

There are some awesome blog posts and whitepapers out there that support our findings in one way or another. Writeups by Midnight Blue Labs, Accenture, and US-CERT each explain how the TRITON framework relates to the TriStation protocol in superb detail.

TriStation's Reverse Engineering and TRITON's Development

When TRITON was discovered, we began to wonder how the TRITON actor reverse engineered TriStation and implemented it into the framework. We have a lot of theories, all of which seemed plausible: Did they build, buy, borrow, or steal? Or some combination thereof?

Our initial theory was that the threat actor purchased a Triconex controller and software for their own testing and reverse engineering from the "ground up", although if this was the case we do not believe they had a controller with the exact vulnerable firmware version, else they would have had fewer problems with TRITON in practice at the victim site. They may have bought or used a demo version of the TriStation 1131 software, allowing them to reverse engineer enough of TriStation for the framework. They may have stolen TriStation Python libraries from ICS companies, subsidiaries or system integrators and used the stolen material as a base for TriStation and TRITON development. But then again, it is possible that they borrowed TriStation software, Triconex hardware and Python connectors from government-owned utility that was using them legitimately.

Looking at the raw TRITON code, some of the comments may appear oddly phrased, but we do get a sense that the developer is clearly using many of the right vernacular and acronyms, showing smarts on PLC programming. The TS_cnames.pyc script contains interesting typos such as 'Set lable', 'Alocate network accepted', 'Symbol table ccepted' and 'Set program information reponse'. These appear to be normal human error and reflect neither poor written English nor laziness in coding. The significant amount of annotation, cascading logic, and robust error handling throughout the code suggests thoughtful development and testing of the framework. This complicates the theory of "ground up" development, so did they base their code on something else?

While learning from the TriStation functionality within TRITON, we continued to explore legitimate TriStation software. We began our search for "TS1131.exe" and hit dead ends sorting through TriStation DLLs until we came across a variety of TriStation utilities in MSI form. We ultimately stumbled across a juicy archive containing "Trilog v4." Upon further inspection, this file installed "TriLog.exe," which the original TRITON executable mimicked, and a couple of supporting DLLs, all of which were timestamped around August 2006.

When we saw the DLL file description "Tricon Communications Interface" and original file name "TricCom.DLL", we knew we were in the right place. With a simple look at the file strings, "BAZINGA!" We struck gold.

File Nametr1com40.dll
MD5069247DF527A96A0E048732CA57E7D3D
Size110592
Compile Date2006-08-23
File DescriptionTricon Communications Interface
Product NameTricCom Dynamic Link Library
File Version4.2.441
Original File NameTricCom.DLL
CopyrightCopyright © 1993-2006 Triconex Corporation

The tr1com40.DLL is exactly what you would expect to see in a custom application package. It is a library that helps support the communications for a Triconex controller. If you've pored over TRITON as much as we have, the moment you look at strings you can see the obvious overlaps between the legitimate DLL and TRITON's own TS_cnames.pyc.

Strings excerpt from tr1com40.DLL
Strings excerpt from tr1com40.DLL

Each of the execution command "error codes" from TS_cnames.pyc are in the strings of tr1com40.DLL (Figure 8). We see "An MP has re-educated" and "Invalid Tristation I command". Even misspelled command strings verbatim such as "Non-existant data item" and "Alocate network accepted". We also see many of the same unknown values. What is obvious from this discovery is that some of the strings in TRITON are likely based on code used in communications libraries for Trident and Tricon controllers.

In our brief survey of the legitimate Triconex Corporation binaries, we observed a few samples with related string tables.

Pe:dllnameCompile DateReference CPP Strings Code
Lagcom40.dll2004/11/19$Workfile:   LAGSTRS.CPP  $ $Modtime:   Jul 21 1999 17:17:26  $ $Revision:   1.0
Tr1com40.dll2006/08/23$Workfile:   TR1STRS.CPP  $ $Modtime:   May 16 2006 09:55:20  $ $Revision:   1.4
Tridcom.dll2008/07/23$Workfile:   LAGSTRS.CPP  $ $Modtime:   Jul 21 1999 17:17:26  $ $Revision:   1.0
Triccom.dll2008/07/23$Workfile:   TR1STRS.CPP  $ $Modtime:   May 16 2006 09:55:20  $ $Revision:   1.4
Tridcom.dll2010/09/29$Workfile:   LAGSTRS.CPP  $ $Modtime:   Jul 21 1999 17:17:26  $ $Revision:   1.0 
Tr1com.dll2011/04/27$Workfile:   TR1STRS.CPP  $ $Modtime:   May 16 2006 09:55:20  $ $Revision:   1.4
Lagcom.dll2011/04/27$Workfile:   LAGSTRS.CPP  $ $Modtime:   Jul 21 1999 17:17:26  $ $Revision:   1.0
Triccom.dll2011/04/27$Workfile:   TR1STRS.CPP  $ $Modtime:   May 16 2006 09:55:20  $ $Revision:   1.4

We extracted the CPP string tables in TR1STRS and LAGSTRS and the TS_cnames.pyc TS_names array from TRITON, and compared the 210, 204, and 212 relevant strings from each respective file.

TS_cnames.pyc TS_names and tr1com40.dll share 202 of 220 combined table strings. The remaining strings are unique to each, as seen here:

TS_cnames.TS_names (2017 pyc)Tr1com40.dll (2006 CPP)
Go to DOWNLOAD mode<200>
Not set<209>
Unk75Bad message from module
Unk76Bad message type
Unk77Bad TMI version number
Unk78Module did not respond
Unk79Open Connection: Invalid SAP %d
Unk81Unsupported message for this TMI version
Unk83 
Wrong command 

TS_cnames.pyc TS_names and Tridcom.dll (1999 CPP) shared only 151 of 268 combined table strings, showing a much smaller overlap with the seemingly older CPP library. This makes sense based on the context that Tridcom.dll is meant for a Trident controller, not a Tricon controller. It does seem as though Tr1com40.dll and TR1STRS.CPP code was based on older work.

We are not shocked to find that the threat actor reversed legitimate code to bolster development of the TRITON framework. They want to work smarter, not harder, too. But after reverse engineering legitimate software and implementing the basics of the TriStation, the threat actors still had an incomplete understanding of the protocol. In TRITON's TS_cnames.pyc we saw "Unk75", "Unk76", "Unk83" and other values that were not present in the tr1com40.DLL strings, indicating that the TRITON threat actor may have explored the protocol and annotated their findings beyond what they reverse engineered from the DLL. The gaps in TriStation implementation show us why the actors encountered problems interacting with the Triconex controllers when using TRITON in the wild.

You can see more of the Trilog and Triconex DLL files on VirusTotal.

Item NameMD5Description
Tr1com40.dll069247df527a96a0e048732ca57e7d3dTricom Communcations DLL
Data1.cabe6a3c93a6d433cbaf6f573b6c09d76c4Parent of Tr1com40.dll
Trilog v4.1.360R13a3b83ba2c4236ca59aba679941c8a5RAR Archive of TriLog
TridCom.dll5c2ed617fdec4779cb33c89082a43100Trident Communications DLL

Afterthoughts

Seeing Triconex systems targeted with malicious intent was new to the world six months ago. Moving forward it would be reasonable to anticipate additional frameworks, such as TRITON, designed for usage against other SIS controllers and associated technologies. If Triconex was within scope, we may see similar attacker methodologies affecting the dominant industrial safety technologies.

Basic security measures do little to thwart truly persistent threat actors and monitoring only IT networks is not an ideal situation. Visibility into both the IT and OT environments is critical for detecting the various stages of an ICS intrusion. Simple detection concepts such as baseline deviation can provide insight into abnormal activity.

While the TRITON framework was actively in use, how many traditional ICS “alarms” were set off while the actors tested their exploits and backdoors on the Triconex controller? How many times did the TriStation protocol, as implemented in their Python scripts, fail or cause errors because of non-standard traffic? How many TriStation UDP pings were sent and how many Connection Requests? How did these statistics compare to the baseline for TriStation traffic? There are no answers to these questions for now. We believe that we can identify these anomalies in the long run if we strive for increased visibility into ICS technologies.

We hope that by holding public discussions about ICS technologies, the Infosec community can cultivate closer relationships with ICS vendors and give the world better insight into how attackers move from the IT to the OT space. We want to foster more conversations like this and generally share good techniques for finding evil. Since most of all ICS attacks involve standard IT intrusions, we should probably come together to invent and improve any guidelines for how to monitor PCs and engineering workstations that bridge the IT and OT networks. We envision a world where attacking or disrupting ICS operations costs the threat actor their cover, their toolkits, their time, and their freedom. It's an ideal world, but something nice to shoot for.

Thanks and Future Work

There is still much to do for TRITON and TriStation. There are many more sub-message types and nuances for parsing out the nitty gritty details, which is hard to do without a controller of our own. And although we’ve published much of what we learned about the TriStation here on the blog, our work will continue as we continue our study of the protocol.

Thanks to everyone who did so much public research on TRITON and TriStation. We have cited a few individuals in this blog post, but there is a lot more community-sourced information that gave us clues and leads for our research and testing of the framework and protocol. We also have to acknowledge the research performed by the TRITON attackers. We borrowed a lot of your knowledge about TriStation from the TRITON framework itself.

Finally, remember that we're here to collaborate. We think most of our research is right, but if you notice any errors or omissions, or have ideas for improvements, please spear phish contact: smiller@fireeye.com.

Recommended Reading

Appendix A: TriStation Message Type Codes

The following table consists of hex values at offset 0 in the TriStation UDP packets and the associated dictionary definitions, extracted verbatim from the TRITON framework in library TS_cnames.pyc.

Value at 0x0Message Type
1Connection Request
2Connection Response
3Disconnect Request
4Disconnect Response
5Execution Command
6Ping Command
7Connection Limit Reached
8Not Connected
9MPS Are Dead
10Access Denied
11Connection Failed

Appendix B: TriStation Execution Command Function Codes

The following table consists of hex values at offset 6 in the TriStation UDP packets and the associated dictionary definitions, extracted verbatim from the TRITON framework in library TS_cnames.pyc.

Value at 0x6TS_cnames String
00: 'Start download all',
11: 'Start download change',
22: 'Update configuration',
33: 'Upload configuration',
44: 'Set I/O addresses',
55: 'Allocate network',
66: 'Load vector table',
77: 'Set calendar',
88: 'Get calendar',
99: 'Set scan time',
A10: 'End download all',
B11: 'End download change',
C12: 'Cancel download change',
D13: 'Attach TRICON',
E14: 'Set I/O address limits',
F15: 'Configure module',
1016: 'Set multiple point values',
1117: 'Enable all points',
1218: 'Upload vector table',
1319: 'Get CP status ',
1420: 'Run program',
1521: 'Halt program',
1622: 'Pause program',
1723: 'Do single scan',
1824: 'Get chassis status',
1925: 'Get minimum scan time',
1A26: 'Set node number',
1B27: 'Set I/O point values',
1C28: 'Get I/O point values',
1D29: 'Get MP status',
1E30: 'Set retentive values',
1F31: 'Adjust clock calendar',
2032: 'Clear module alarms',
2133: 'Get event log',
2234: 'Set SOE block',
2335: 'Record event log',
2436: 'Get SOE data',
2537: 'Enable OVD',
2638: 'Disable OVD',
2739: 'Enable all OVDs',
2840: 'Disable all OVDs',
2941: 'Process MODBUS',
2A42: 'Upload network',
2B43: 'Set lable',
2C44: 'Configure system variables',
2D45: 'Deconfigure module',
2E46: 'Get system variables',
2F47: 'Get module types',
3048: 'Begin conversion table download',
3149: 'Continue conversion table download',
3250: 'End conversion table download',
3351: 'Get conversion table',
3452: 'Set ICM status',
3553: 'Broadcast SOE data available',
3654: 'Get module versions',
3755: 'Allocate program',
3856: 'Allocate function',
3957: 'Clear retentives',
3A58: 'Set initial values',
3B59: 'Start TS2 program download',
3C60: 'Set TS2 data area',
3D61: 'Get TS2 data',
3E62: 'Set TS2 data',
3F63: 'Set program information',
4064: 'Get program information',
4165: 'Upload program',
4266: 'Upload function',
4367: 'Get point groups',
4468: 'Allocate symbol table',
4569: 'Get I/O address',
4670: 'Resend I/O address',
4771: 'Get program timing',
4872: 'Allocate multiple functions',
4973: 'Get node number',
4A74: 'Get symbol table',
4B75: 'Unk75',
4C76: 'Unk76',
4D77: 'Unk77',
4E78: 'Unk78',
4F79: 'Unk79',
5080: 'Go to DOWNLOAD mode',
5181: 'Unk81',
52 
5383: 'Unk83',
54 
55 
56 
57 
58 
59 
5A 
5B 
5C 
5D 
5E 
5F 
60 
61 
62 
63 
64100: 'Command rejected',
65101: 'Download all permitted',
66102: 'Download change permitted',
67103: 'Modification accepted',
68104: 'Download cancelled',
69105: 'Program accepted',
6A106: 'TRICON attached',
6B107: 'I/O addresses set',
6C108: 'Get CP status response',
6D109: 'Program is running',
6E110: 'Program is halted',
6F111: 'Program is paused',
70112: 'End of single scan',
71113: 'Get chassis configuration response',
72114: 'Scan period modified',
73115: '<115>',
74116: '<116>',
75117: 'Module configured',
76118: '<118>',
77119: 'Get chassis status response',
78120: 'Vectors response',
79121: 'Get I/O point values response',
7A122: 'Calendar changed',
7B123: 'Configuration updated',
7C124: 'Get minimum scan time response',
7D125: '<125>',
7E126: 'Node number set',
7F127: 'Get MP status response',
80128: 'Retentive values set',
81129: 'SOE block set',
82130: 'Module alarms cleared',
83131: 'Get event log response',
84132: 'Symbol table ccepted',
85133: 'OVD enable accepted',
86134: 'OVD disable accepted',
87135: 'Record event log response',
88136: 'Upload network response',
89137: 'Get SOE data response',
8A138: 'Alocate network accepted',
8B139: 'Load vector table accepted',
8C140: 'Get calendar response',
8D141: 'Label set',
8E142: 'Get module types response',
8F143: 'System variables configured',
90144: 'Module deconfigured',
91145: '<145>',
92146: '<146>',
93147: 'Get conversion table response',
94148: 'ICM print data sent',
95149: 'Set ICM status response',
96150: 'Get system variables response',
97151: 'Get module versions response',
98152: 'Process MODBUS response',
99153: 'Allocate program response',
9A154: 'Allocate function response',
9B155: 'Clear retentives response',
9C156: 'Set initial values response',
9D157: 'Set TS2 data area response',
9E158: 'Get TS2 data response',
9F159: 'Set TS2 data response',
A0160: 'Set program information reponse',
A1161: 'Get program information response',
A2162: 'Upload program response',
A3163: 'Upload function response',
A4164: 'Get point groups response',
A5165: 'Allocate symbol table response',
A6166: 'Program timing response',
A7167: 'Disable points full',
A8168: 'Allocate multiple functions response',
A9169: 'Get node number response',
AA170: 'Symbol table response',
AB 
AC 
AD 
AE 
AF 
B0 
B1 
B2 
B3 
B4 
B5 
B6 
B7 
B8 
B9 
BA 
BB 
BC 
BD 
BE 
BF 
C0 
C1 
C2 
C3 
C4 
C5 
C6 
C7 
C8200: 'Wrong command',
C9201: 'Load is in progress',
CA202: 'Bad clock calendar data',
CB203: 'Control program not halted',
CC204: 'Control program checksum error',
CD205: 'No memory available',
CE206: 'Control program not valid',
CF207: 'Not loading a control program',
D0208: 'Network is out of range',
D1209: 'Not enough arguments',
D2210: 'A Network is missing',
D3211: 'The download time mismatches',
D4212: 'Key setting prohibits this operation',
D5213: 'Bad control program version',
D6214: 'Command not in correct sequence',
D7215: '<215>',
D8216: 'Bad Index for a module',
D9217: 'Module address is invalid',
DA218: '<218>',
DB219: '<219>',
DC220: 'Bad offset for an I/O point',
DD221: 'Invalid point type',
DE222: 'Invalid Point Location',
DF223: 'Program name is invalid',
E0224: '<224>',
E1225: '<225>',
E2226: '<226>',
E3227: 'Invalid module type',
E4228: '<228>',
E5229: 'Invalid table type',
E6230: '<230>',
E7231: 'Invalid network continuation',
E8232: 'Invalid scan time',
E9233: 'Load is busy',
EA234: 'An MP has re-educated',
EB235: 'Invalid chassis or slot',
EC236: 'Invalid SOE number',
ED237: 'Invalid SOE type',
EE238: 'Invalid SOE state',
EF239: 'The variable is write protected',
F0240: 'Node number mismatch',
F1241: 'Command not allowed',
F2242: 'Invalid sequence number',
F3243: 'Time change on non-master TRICON',
F4244: 'No free Tristation ports',
F5245: 'Invalid Tristation I command',
F6246: 'Invalid TriStation 1131 command',
F7247: 'Only one chassis allowed',
F8248: 'Bad variable address',
F9249: 'Response overflow',
FA250: 'Invalid bus',
FB251: 'Disable is not allowed',
FC252: 'Invalid length',
FD253: 'Point cannot be disabled',
FE254: 'Too many retentive variables',
FF255: 'LOADER_CONNECT',
 256: 'Unknown reject code'