├── Active Tool ├── README.MD ├── codesysv3_protocol │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── channel.cpython-39.pyc │ │ ├── constants.cpython-39.pyc │ │ ├── device.cpython-39.pyc │ │ ├── encryption.cpython-39.pyc │ │ ├── exceptions.cpython-39.pyc │ │ ├── files_transfer.cpython-39.pyc │ │ ├── plcshell.cpython-39.pyc │ │ ├── protocol.cpython-39.pyc │ │ └── structures.cpython-39.pyc │ ├── channel.py │ ├── constants.py │ ├── device.py │ ├── encryption.py │ ├── exceptions.py │ ├── files_transfer.py │ ├── plcshell.py │ ├── protocol.py │ └── structures.py ├── log.py ├── main.py └── requirements.txt ├── CODE_OF_CONDUCT.md ├── IDA Python script └── ida_python_utils.py ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── Vulnerabilities-in-CODESYS-V3-SDK-could-lead-to-RCE-or-DoS.pdf └── Wireshark Dissector ├── CodeSysV3.lua └── example.pcap /Active Tool/README.MD: -------------------------------------------------------------------------------- 1 | CODESYS Version Extractor 2 | ========= 3 | 4 | CODESYS stands for controller development system. It’s a development environment for programming controller applications in line with the IEC 61131-3 standard. It was developed and is still maintained by the 3S (Smart Software Solutions) Company in Germany. The platform-independent development environment is compatible with approximately 1,000 different device types from over 500 manufacturers and several million devices. 5 | 6 | Our product aimed at extracting the CODESYS version via bunch of different ways. 7 | In light of recent events (which includes numerous CODESYS vulnerabilities) we would like to present a tool which will extract CODESYS version and allow different SOCs and IT admins to take counter measures and protect their organisation form malicious attacks. 8 | 9 | 10 | ## Getting Started 11 | 12 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. 13 | ``` 14 | git clone https://github.com/microsoft/CoDe16.git 15 | ``` 16 | 17 | ### Prerequisites 18 | 19 | - Install Python >= 3.9: https://www.python.org/downloads 20 | 21 | ### Installing 22 | 23 | - Install python requirements 24 | 25 | ``` 26 | pip install -r requirements.txt 27 | ``` 28 | 29 | 30 | ## Usage 31 | 32 | This project runs on python3.9. 33 | ```commandline 34 | python3.9 main.py -h 35 | ``` 36 | This will display basic manual the explains the usage of the tool: 37 | ```commandline 38 | usage: main.py [-h] [--username USERNAME] [--password PASSWORD] --dst_ip 39 | DST_IP --src_ip SRC_IP 40 | 41 | Welcome to RTS Version extractor. 42 | 43 | optional arguments: 44 | -h, --help show this help message and exit 45 | --username USERNAME The username that required to log into the plc. 46 | --password PASSWORD The password that required to log into the plc. 47 | --dst_ip DST_IP The IP address of the remote plc. 48 | --src_ip SRC_IP The IP address of the machine that will run this 49 | script. 50 | 51 | ``` 52 | 53 | ### General application arguments: 54 | | Args | Description | Required / Optional | 55 | |:----------------------:|:-------------------------------------------------------------------:|:-------------------:| 56 | | `-h`, `--help` | show this help message and exit | Optional | 57 | | `--dst_ip` | The IP address of the remote PLC | Required | 58 | | `--src_ip` |The local IP address of the machine that run this script | Required | 59 | | `--username` |Username to connected with to the PLC, required to run PLCShell command| Optional | 60 | | `--password` |Password to connected with to the PLC, required to run PLCShell command| Optional | 61 | 62 | ## Explanation of the methods 63 | 64 | ### Name Service Resolution 65 | Using the Name Service of CODESYS to extract information on the PLC, including firmware version, vendor, device name and more. 66 | No username and password required 67 | 68 | 69 | ### PLC Shell Service 70 | The CODESYS PLCShell services allows query for different information from CODESYS device. 71 | more information can be found at: 72 | [Codesys PLC Shell Service Doc](https://help.codesys.com/api-content/2/codesys/3.5.14.0/en/_cds_edt_device_plc_shell/) 73 | 74 | Required username and password, but it is not present on every CODESYS systems. 75 | We are using this service via ``rtsinfo`` command that returns the current CODESYS version. 76 | 77 | ## Contributing 78 | 79 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 80 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 81 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 82 | 83 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 84 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 85 | provided by the bot. You will only need to do this once across all repos using our CLA. 86 | 87 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 88 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 89 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 90 | 91 | ## Trademarks 92 | 93 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 94 | trademarks or logos is subject to and must follow 95 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 96 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 97 | Any use of third-party trademarks or logos are subject to those third-party's policies. 98 | 99 | ## Legal Disclaimer 100 | 101 | Copyright (c) 2018 Microsoft Corporation. All rights reserved. 102 | 103 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 104 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 105 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 106 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 107 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 108 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 109 | 110 | -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import * 2 | from .constants import * 3 | from .protocol import * 4 | from .structures import * 5 | from .device import * 6 | from .channel import * 7 | from .plcshell import * 8 | from .files_transfer import * -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/channel.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/channel.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/constants.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/constants.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/device.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/device.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/encryption.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/encryption.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/exceptions.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/exceptions.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/files_transfer.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/files_transfer.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/plcshell.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/plcshell.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/protocol.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/protocol.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/__pycache__/structures.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Active Tool/codesysv3_protocol/__pycache__/structures.cpython-39.pyc -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/channel.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | from .device import * 4 | from .encryption import CodeSysV3Encryption 5 | 6 | 7 | class CodeSysV3Channel: 8 | 9 | def __init__(self, channel_id, device): 10 | self._device = device 11 | self._channel_id = channel_id 12 | self._ack_id = 0 13 | self._blk_id = 1 14 | self._session_id = 0 15 | self.logger = logging.getLogger(self.__class__.__name__) 16 | 17 | @property 18 | def is_login(self): 19 | return self._session_id != 0 20 | 21 | @property 22 | def channel_id(self): 23 | return self._channel_id 24 | 25 | @property 26 | def session_id(self): 27 | return self._session_id 28 | 29 | def send(self, cmd_group: CmdGroup, smb_cmd: int, *tags: Tag): 30 | if tags is None: 31 | tags = [] 32 | self._blk_id += self._device.send_over_channel(self._channel_id, cmd_group, smb_cmd, self._session_id, self._blk_id, 33 | self._ack_id, tags) 34 | 35 | def read(self) -> typing.Tuple[ServiceLayer, typing.Dict[int, Tag]]: 36 | service_layer, tags, ack_id = self._device.read_over_channel(self._channel_id) 37 | if service_layer is None: 38 | raise CodeSysProtocolV3Exception("Fail to read from channel") 39 | self._device.send_channel_ack(self._channel_id, ack_id) 40 | self._ack_id = ack_id 41 | 42 | return service_layer, tags 43 | 44 | def close(self): 45 | self.logger.info(f"Closing the channel {self._channel_id:04X}") 46 | self._device.close_channel(self._channel_id) 47 | 48 | def login(self, username: str = "", password: str = "") -> bool: 49 | self.logger.info(f"Trying to login over the channel {self._channel_id: 04X}") 50 | try: 51 | tags = [] 52 | if len(username) % 2 != 0: 53 | username += '\0' 54 | tags.append(Tag(0x22, b"\x01\x00\x00\x00")) 55 | if len(password) > 0: 56 | password_hash = CodeSysV3Encryption.hash_password(CodeSysV3Encryption.CHALLENGE, password) 57 | tags.append(Tag(0x23, CodeSysV3Encryption.CHALLENGE.to_bytes(4, "little"))) 58 | username_pass_tag = Tag(0x81) 59 | tags.append(username_pass_tag) 60 | username_pass_tag.add_tag(Tag(0x10, bytes(username, "ascii"), align=0x42)) 61 | username_pass_tag.add_tag(Tag(0x11, password_hash)) 62 | else: 63 | username_pass_tag = Tag(0x81) 64 | tags.append(username_pass_tag) 65 | username_pass_tag.add_tag(Tag(0x10, bytes(username, "ascii"), align=0x40)) 66 | self.send(CmdGroup.CmpDevice, 2, *tags) 67 | service_layer, tags = self.read() 68 | if service_layer is not None and len(tags) > 0 and service_layer.cmd_group == CmdGroup.CmpDevice.value and service_layer.subcmd == 2: 69 | session_id = tags[0x82].get_tag(0x21) 70 | if session_id is not None: 71 | self._session_id = session_id.dword_le 72 | self.logger.info(f"Login successfully to the device, session id: {self._session_id: 08X}") 73 | return True 74 | 75 | else: 76 | self.logger.error(f"Failed to login to the device, error_code: {tags[0x82][0x20].word: 04X}") 77 | except Exception as ex: 78 | self.logger.error(f"Failed to login to the device, error: {ex}") 79 | return False 80 | 81 | def __enter__(self): 82 | return self 83 | 84 | def __exit__(self, exc_type, exc_val, exc_tb): 85 | self.close() -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/constants.py: -------------------------------------------------------------------------------- 1 | TCP_MAGIC = 0xe8170100 2 | DATAGRAM_LAYER_MAGIC = 0xc5 3 | CODESYS_TCP_MIN_PORT = 11740 4 | CODESYS_TCP_MAX_PORT = 11743 5 | CODESYS_UDP_MIN_PORT = 1740 6 | CODESYS_UDP_MAX_PORT = 1743 7 | MAX_PDU_SIZE = 512 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/device.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import socket 3 | import logging 4 | from .constants import * 5 | from .exceptions import * 6 | from .structures import * 7 | from .protocol import * 8 | from .channel import CodeSysV3Channel 9 | 10 | class DatagramLayerType(enum.Enum): 11 | TCP = 1 12 | UDP = 2 13 | 14 | 15 | class CodeSysV3Device: 16 | TCP_PORT = 11740 17 | UDP_PORT = 1740 18 | 19 | def __init__(self, dst_device_ip, interface_ip): 20 | self._src_ip = interface_ip 21 | self._dst_ip = dst_device_ip 22 | self.logger = logging.getLogger(self.__class__.__name__) 23 | self._datagram_type = None 24 | self._dst_address = b"" 25 | 26 | def _prepared_socket(self): 27 | if self._datagram_type == DatagramLayerType.TCP: 28 | self._socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP) 29 | self._socket.bind(('', 0)) 30 | self._socket.settimeout(5) 31 | self._src_port = self._socket.getsockname()[1] 32 | self._dst_port = CodeSysV3Device.TCP_PORT 33 | else: 34 | self._socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP) 35 | self._socket.bind(('', CodeSysV3Device.UDP_PORT)) 36 | self._socket.settimeout(5) 37 | self._src_port = CodeSysV3Device.UDP_PORT 38 | self._dst_port = CodeSysV3Device.UDP_PORT 39 | 40 | def connect(self): 41 | self.logger.info(f"Starting to connect to the device {self._dst_ip}") 42 | if self._datagram_type is None: 43 | self._datagram_type = self._determine_datagram_layer_type() 44 | self._prepared_socket() 45 | 46 | if self._datagram_type == DatagramLayerType.TCP: 47 | self._socket.connect((self._dst_ip, self._dst_port)) 48 | self.logger.info("Connected to the device") 49 | 50 | def is_connected(self): 51 | return self._datagram_type is not None 52 | 53 | def close(self): 54 | if self.is_connected(): 55 | self.logger.info("Closing the connection") 56 | self._socket.close() 57 | self._datagram_type = None 58 | 59 | def __enter__(self): 60 | self.connect() 61 | return self 62 | 63 | def __exit__(self, exc_type, exc_val, exc_tb): 64 | self.close() 65 | 66 | def send_pdu(self, service: DatagramLayerServices, pkt: bytes): 67 | pkt = self._prepared_datagram_layer(service, pkt) 68 | if self._datagram_type == DatagramLayerType.TCP: 69 | self._socket.sendall(pkt) 70 | else: 71 | self._socket.sendto(pkt, (self._dst_ip, self._dst_port)) 72 | 73 | def recv_pdu(self) -> list: 74 | if self._datagram_type == DatagramLayerType.TCP: 75 | pdu = self._socket.recv(ctypes.sizeof(BlockDriverLayerTcp)) 76 | tcp_header = BlockDriverLayerTcp.from_buffer_copy(pdu) 77 | tcp_length = tcp_header.tcp_length - ctypes.sizeof(BlockDriverLayerTcp) 78 | layers = CodeSysV3Protocol.parse_CodeSysTCPBlockDriverLayer(pdu + self._socket.recv(tcp_length)) 79 | else: 80 | layers = CodeSysV3Protocol.parse_CodeSysDatagramLayer(self._socket.recvfrom(MAX_PDU_SIZE)[0]) 81 | 82 | # Ignore Keep alive 83 | if len(layers) > 0 and isinstance(layers[-1], KeepLive): 84 | return self.recv_pdu() 85 | return layers 86 | 87 | def open_channel(self) -> CodeSysV3Channel: 88 | if not self.is_connected(): 89 | raise CodeSysProtocolV3Exception("Device is not connected") 90 | self.logger.info("Opening a new channel with the device") 91 | self.send_pdu(DatagramLayerServices.ChannelManager, 92 | CodeSysV3Protocol.build_CodeSysChannelLayerOpenRequest()) 93 | layers = self.recv_pdu() 94 | if len(layers) > 0 and isinstance(layers[-1], OpenChannelResponse): 95 | return CodeSysV3Channel(layers[-1].channel_id, self) 96 | raise CodeSysProtocolV3Exception("Failed to open channel") 97 | 98 | def close_channel(self, channel_id): 99 | if not self.is_connected(): 100 | raise CodeSysProtocolV3Exception("Device is not connected") 101 | self.logger.info(f"Closing a channel {channel_id: 04X}") 102 | self.send_pdu(DatagramLayerServices.ChannelManager, 103 | CodeSysV3Protocol.build_CodeSysChannelLayerCloseChannel(channel_id)) 104 | 105 | 106 | 107 | def _prepared_datagram_layer(self, service: DatagramLayerServices, payload: bytes): 108 | if self._datagram_type == DatagramLayerType.TCP: 109 | return CodeSysV3Protocol.build_DatagramLayerRequestOverTCP(self._src_ip, self._src_port, 110 | self._dst_ip, self._src_port, service, payload) 111 | elif self._datagram_type == DatagramLayerType.UDP: 112 | return CodeSysV3Protocol.build_DatagramLayerRequestOverUDP(self._src_ip, self._src_port, 113 | service, payload, 114 | dst_address=self._dst_address) 115 | 116 | def _get_dst_address(self): 117 | self.logger.info(f"Trying to receive the target device address: {self._dst_ip}") 118 | dst_address = None 119 | sock = None 120 | try: 121 | ns_server = CodeSysV3Protocol.build_NSServerDeviceInfo() 122 | sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP) 123 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 124 | datagram = CodeSysV3Protocol.build_DatagramLayerRequestOverUDP(self._src_ip, CodeSysV3Device.UDP_PORT, 125 | DatagramLayerServices.NSServer, ns_server) 126 | sock.bind(('', CodeSysV3Device.UDP_PORT)) 127 | sock.settimeout(5) 128 | 129 | sock.sendto(datagram, (self._dst_ip, CodeSysV3Device.UDP_PORT)) 130 | response = sock.recvfrom(MAX_PDU_SIZE)[0] 131 | layers = CodeSysV3Protocol.parse_CodeSysDatagramLayer(response) 132 | if len(layers) > 0 and isinstance(layers[0], DatagramLayer): 133 | dst_address = layers[0].sender_address 134 | except Exception as ex: 135 | self.logger.error(f"Fail to get the target device address: {self._dst_ip}, Error: {ex}") 136 | finally: 137 | if sock is not None: 138 | sock.close() 139 | return dst_address 140 | 141 | def _determine_datagram_layer_type(self): 142 | self.logger.info(f"Trying to determine the datagram layer type to connect to {self._dst_ip}") 143 | self._dst_address = self._get_dst_address() 144 | if self._dst_address is not None: 145 | return DatagramLayerType.UDP 146 | elif self._check_for_datagram_layer_tcp(): 147 | return DatagramLayerType.TCP 148 | 149 | raise CodeSysProtocolV3Exception("Couldn't figure out the Datagram layer type") 150 | 151 | def _check_for_datagram_layer_tcp(self): 152 | 153 | test_socket = None 154 | try: 155 | self.logger.info("Trying datagram layer TCP") 156 | ns_server = CodeSysV3Protocol.build_NSServerDeviceInfo() 157 | test_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=socket.IPPROTO_TCP) 158 | test_socket.bind(('', 0)) 159 | test_socket.settimeout(5) 160 | src_port = test_socket.getsockname()[1] 161 | datagram = CodeSysV3Protocol.build_DatagramLayerRequestOverTCP(self._src_ip, src_port, self._dst_ip, 162 | CodeSysV3Device.TCP_PORT, 163 | DatagramLayerServices.NSServer, ns_server) 164 | test_socket.connect((self._dst_ip, CodeSysV3Device.TCP_PORT)) 165 | test_socket.sendall(datagram) 166 | response = test_socket.recv(MAX_PDU_SIZE) 167 | layers = CodeSysV3Protocol.parse_CodeSysTCPBlockDriverLayer(response) 168 | if len(layers) > 0 and isinstance(layers[-1], NsClientDeviceInfo): 169 | self.logger.info("Succeeded to connect with TCP") 170 | return True 171 | except: 172 | pass 173 | finally: 174 | if test_socket: 175 | test_socket.close() 176 | self.logger.info("Failed to connect with TCP") 177 | return False 178 | 179 | def get_device_name_server_info(self) -> dict: 180 | if not self.is_connected(): 181 | raise CodeSysProtocolV3Exception("Device is not connected") 182 | self.logger.info("Trying to read the device name server info") 183 | ns_server = CodeSysV3Protocol.build_NSServerDeviceInfo() 184 | self.send_pdu(DatagramLayerServices.NSServer, ns_server) 185 | pdu = self.recv_pdu() 186 | if len(pdu) > 0 and isinstance(pdu[-1], NsClientDeviceInfo): 187 | ns_client_info = pdu[-1] 188 | 189 | return { 190 | "node_name": ns_client_info.node_name, 191 | "device_name": ns_client_info.device_name, 192 | "vendor_name": ns_client_info.vendor_name, 193 | "firmware_str": ns_client_info.firmware_str, 194 | } 195 | 196 | return {} 197 | 198 | def send_channel_ack(self, channel_id: int, blk_id: int): 199 | if not self.is_connected(): 200 | raise CodeSysProtocolV3Exception("Device is not connected") 201 | self.send_pdu(DatagramLayerServices.ChannelManager, 202 | CodeSysV3Protocol.build_CodeSysChannelLayerAck(channel_id, blk_id)) 203 | 204 | def read_over_channel(self, channel_id: int) -> typing.Tuple[ApplicationBlockFirst, typing.List[Tag], int]: 205 | if not self.is_connected(): 206 | raise CodeSysProtocolV3Exception("Device is not connected") 207 | pdu = self.recv_pdu() 208 | while len(pdu) > 0 and isinstance(pdu[-1], ApplicationAck): 209 | pdu = self.recv_pdu() 210 | tags = [] 211 | last_blk_id = 0 212 | service_layer = None 213 | if len(pdu) > 1 and isinstance(pdu[-2], ApplicationBlockFirst): 214 | channel_layer = pdu[-2] 215 | service_layer_data = pdu[-1] 216 | last_blk_id = channel_layer.blk_id 217 | if channel_layer.channel_id == channel_id: 218 | # Check if it needs to be defragmented 219 | if channel_layer.remaining_data_size > len(service_layer_data): 220 | self.send_channel_ack(channel_id, channel_layer.blk_id) 221 | while len(service_layer_data) < channel_layer.remaining_data_size: 222 | pdu = self.recv_pdu() 223 | if len(pdu) > 1 and isinstance(pdu[-2], ApplicationBlock) and \ 224 | pdu[-2].channel_id == channel_id and pdu[-2].ack_id == channel_layer.ack_id \ 225 | and pdu[-2].blk_id > last_blk_id: 226 | self.send_channel_ack(channel_id, pdu[-2].blk_id) 227 | service_layer_data += pdu[-1] 228 | last_blk_id = pdu[-2].blk_id 229 | else: 230 | continue 231 | service_layer, tags = CodeSysV3Protocol.parse_CodeSysServiceLayer(service_layer_data) 232 | return service_layer, tags, last_blk_id 233 | 234 | def send_over_channel(self, channel_id: int, cmd_group: CmdGroup, smb_cmd: int, session_id: int, 235 | blk_id: int, ack_id: int, 236 | tags: typing.List[Tag] = None): 237 | if tags is None: 238 | tags = [] 239 | if not self.is_connected(): 240 | raise CodeSysProtocolV3Exception("Device is not connected") 241 | service_layer = CodeSysV3Protocol.build_CodeSysServicesLayer(cmd_group, smb_cmd, session_id, tags) 242 | channel_layer_pkts = CodeSysV3Protocol.build_CodeSysChannelLayerAppBlk(channel_id, blk_id, ack_id, service_layer) 243 | for pkt in channel_layer_pkts: 244 | self.send_pdu(DatagramLayerServices.ChannelManager, pkt) 245 | return len(channel_layer_pkts) 246 | -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/encryption.py: -------------------------------------------------------------------------------- 1 | class CodeSysV3Encryption: 2 | ENC_ARRAY = [0x7A, 0x65, 0x44, 0x52, 0x39, 0x36, 0x45, 0x66, 0x55, 0x23, 0x32, 0x37, 0x76, 0x75, 0x70, 0x68, 3 | 0x37, 0x54, 0x68, 0x75, 0x62, 0x3F, 0x70, 0x68, 0x61, 0x44, 0x72, 0x2A, 0x72, 0x55, 0x62, 0x52] 4 | PASSWORD_MIN_SIZE = 32 5 | CHALLENGE = 0x12345678 6 | 7 | @staticmethod 8 | def hash_password(challenge:int, password: str): 9 | password_len = len(password) 10 | full_password_len = password_len 11 | if CodeSysV3Encryption.PASSWORD_MIN_SIZE <= password_len + 1: 12 | if full_password_len & 3: 13 | full_password_len += 4 - (full_password_len & 3) 14 | else: 15 | full_password_len = CodeSysV3Encryption.PASSWORD_MIN_SIZE 16 | 17 | challenge_array = bytearray(challenge.to_bytes(4, "little")) 18 | challenge_array[1] = 0 19 | challenge_array[2] = 0 20 | challenge_array[3] = 0 21 | crypto_password_char_index = 0 22 | loop_index = 0 23 | 24 | new_hashed_password = bytearray(32) 25 | 26 | while loop_index < full_password_len: 27 | current_crypto_hash_char = CodeSysV3Encryption.ENC_ARRAY[crypto_password_char_index] 28 | 29 | if loop_index >= password_len: 30 | current_password_char = 0 31 | else: 32 | current_password_char = ord(password[loop_index]) 33 | 34 | crypto_password_char_index = (crypto_password_char_index + 1) % 33 35 | 36 | challenge_char = (challenge_array[loop_index % 4] + current_crypto_hash_char) & 0xff 37 | new_hashed_password[loop_index] = current_password_char ^ challenge_char 38 | loop_index += 1 39 | 40 | return new_hashed_password -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/exceptions.py: -------------------------------------------------------------------------------- 1 | class CodeSysProtocolV3Exception(Exception): 2 | pass -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/files_transfer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pathlib 3 | import typing 4 | from .structures import * 5 | from .channel import CodeSysV3Channel 6 | 7 | 8 | class FilesTransfer: 9 | def __init__(self, channel: CodeSysV3Channel): 10 | self._channel = channel 11 | self.logger = logging.getLogger(self.__class__.__name__) 12 | 13 | def download(self, src_file: str, dst_file: pathlib.Path): 14 | 15 | if not self._channel.is_login: 16 | raise CodeSysProtocolV3Exception("Device is not connected") 17 | try: 18 | self.logger.info(f"Trying to download the file {src_file} over the channel {self._channel.channel_id: 04X}") 19 | src_file = bytes(src_file, "utf-8") + b"\x00" 20 | if len(src_file) < 14: 21 | src_file += b"\x00" * (len(src_file) - 14) 22 | 23 | if len(src_file) % 2 != 0: 24 | src_file += b"\x00" 25 | 26 | # Create Start download request 27 | self._channel.send(CmdGroup.CmpFileTransfer, 5, Tag(0x01, src_file), Tag(0x02, bytearray(8))) 28 | resp, tags = self._channel.read() 29 | 30 | error_code = tags[0x84][0x08].word 31 | if error_code == 0: 32 | full_file_path = str(tags[0x84][0x01].data, "utf-8") 33 | session_id = tags[0x84][0x03].dword 34 | file_size = int.from_bytes(tags[0x84][0x02].data[4:], "little") 35 | file_size_data = bytearray(12) 36 | file_size_data[:8] = tags[0x84][0x02].data 37 | file_size_data[0] = 1 38 | self.logger.info(f"Successfully open the file {full_file_path} over the channel {self._channel.channel_id: 04X}" 39 | f" to download, session ID: {session_id: 08X}, file size: {file_size} bytes") 40 | session_id = tags[0x84][0x03].data + b"\x00" * 4 41 | with dst_file.open("wb") as dst_fp: 42 | written = 0 43 | while written < file_size: 44 | self._channel.send(CmdGroup.CmpFileTransfer, 7, Tag(0x05, session_id)) 45 | resp, tags = self._channel.read() 46 | tag_data = tags.get(0x05, tags.get(0x07)) 47 | tag_size = tags[0x06].dword_le 48 | if tag_size == 0: 49 | break 50 | if tag_data is None: 51 | self.logger.error(f"Failed to download the file {src_file}, fail to get file's data") 52 | dst_fp.write(tag_data.data) 53 | written += len(tag_data.data) 54 | # Close the file 55 | self._channel.send(CmdGroup.CmpFileTransfer, 8, Tag(0x07, session_id + src_file), 56 | Tag(0x02, file_size_data)) 57 | layer, tags = self._channel.read() 58 | else: 59 | self.logger.error(f"Successfully open the file {src_file}, error_code:{error_code}") 60 | except Exception as ex: 61 | self.logger.error(f"Failed to download the file {src_file}, error: {ex}") 62 | 63 | def dir(self, folder: str): 64 | 65 | if not self._channel.is_login: 66 | raise CodeSysProtocolV3Exception("Device is not connected") 67 | try: 68 | self.logger.info(f"Trying to get folder list: {folder} over the channel {self._channel.channel_id: 04X}") 69 | folder = bytes(folder, "utf-8") + b"\x00" 70 | if len(folder) < 14: 71 | folder += b"\x00" * (len(folder) - 14) 72 | 73 | if len(folder) % 2 != 0: 74 | folder += b"\x00" 75 | 76 | self._channel.send(CmdGroup.CmpFileTransfer, 12, Tag(0x0b, folder)) 77 | resp, tags = self._channel.read() 78 | files = tags[0x8d][0x90] 79 | dir_files = [] 80 | for f in files: 81 | if f[0x0e] is not None: 82 | dir_files.append(str(f[0x0e].data, "utf-8").replace("\0", "")) 83 | return dir_files 84 | except Exception as ex: 85 | self.logger.error(f"Failed to get folder list {folder}, error: {ex}") 86 | 87 | 88 | -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/plcshell.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .structures import * 3 | from .channel import CodeSysV3Channel 4 | 5 | 6 | class PLCShell: 7 | def __init__(self, channel: CodeSysV3Channel): 8 | self._channel = channel 9 | self.logger = logging.getLogger(self.__class__.__name__) 10 | 11 | def run(self, command: str) -> str: 12 | if not self._channel.is_login: 13 | raise CodeSysProtocolV3Exception("Device is not connected") 14 | try: 15 | self.logger.info(f"Trying to run the shell command: {command} over the channel {self._channel.channel_id: 04X}") 16 | 17 | self._channel.send(CmdGroup.PlcShell, 0x01, Tag(0x11, self._channel.session_id.to_bytes(4, "little")), 18 | Tag(0x13, b"\x00" * 4), 19 | Tag(0x10, bytes(command, "ascii") + b"\x00" * 3, align=0x42)) 20 | resp, tags = self._channel.read() 21 | response_shell = str(tags[0x82][0x20].data, "utf-8") 22 | return response_shell 23 | except Exception as ex: 24 | self.logger.error(f"Failed to get the PLCShell response, error: {ex}") 25 | return "" 26 | 27 | -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/protocol.py: -------------------------------------------------------------------------------- 1 | import netifaces 2 | import struct 3 | from .structures import * 4 | from .constants import * 5 | from .exceptions import CodeSysProtocolV3Exception 6 | 7 | 8 | class CodeSysV3Protocol: 9 | PKT_COUNTER = 0 10 | @staticmethod 11 | def _build_DatagramLayer(header_size: int, src_address: bytes, dst_address, dg_service: DatagramLayerServices, payload: bytes = b"") -> bytes: 12 | dg_layer = bytes(DatagramLayer(dg_service, len(dst_address), len(src_address))) + \ 13 | dst_address + src_address 14 | padding_len = (len(dg_layer) + header_size) % 4 15 | if padding_len != 0: 16 | dg_layer += b"\x00" * padding_len 17 | return dg_layer + payload 18 | 19 | @staticmethod 20 | def build_DatagramLayerRequestOverTCP(src_ip: str, src_port:int, dst_ip: str, dst_port:int, 21 | dg_service: DatagramLayerServices, payload: bytes = b"") -> bytes: 22 | src_address = bytes(NetworkAddressTCP(src_ip, src_port)) 23 | dst_address = bytes(NetworkAddressTCP(dst_ip, dst_port)) 24 | dg_layer = CodeSysV3Protocol._build_DatagramLayer(ctypes.sizeof(BlockDriverLayerTcp), src_address, 25 | dst_address, 26 | dg_service, 27 | payload) 28 | return bytes(BlockDriverLayerTcp(len(dg_layer))) + dg_layer 29 | 30 | @staticmethod 31 | def _get_net_mask(src_ip: str) -> str: 32 | for interface in netifaces.interfaces(): 33 | try: 34 | addrs = netifaces.ifaddresses(interface)[netifaces.AF_INET] 35 | for addr in addrs: 36 | if addr['addr'] == src_ip: 37 | return addr['netmask'] 38 | except: 39 | pass 40 | raise Exception(f"Don't find netmask of the src_ip: {src_ip}") 41 | 42 | 43 | @staticmethod 44 | def _get_codesys_address_format(src_ip: str, port: int, netmask: str) -> int: 45 | netmask_n = int.from_bytes(socket.inet_aton(netmask), "big") 46 | address = (~netmask_n) & \ 47 | (int.from_bytes(socket.inet_aton(src_ip), "big")) 48 | port_index = port - CODESYS_UDP_MIN_PORT 49 | 50 | return address | (port_index << address.bit_length()) 51 | 52 | @staticmethod 53 | def build_NSServerDeviceInfo() -> bytes: 54 | pkt = bytes(NsHeader(subcmd=NSSubCmd.NameResolve.value, version=0x400, msg_id=CodeSysV3Protocol.PKT_COUNTER)) 55 | CodeSysV3Protocol.PKT_COUNTER += 1 56 | return pkt 57 | 58 | @staticmethod 59 | def build_DatagramLayerRequestOverUDP(src_ip: str, src_port: int, 60 | dg_service: DatagramLayerServices, payload: bytes = b"", dst_address: bytes = b"",) -> bytes: 61 | netmask = CodeSysV3Protocol._get_net_mask(src_ip) 62 | src_ip_format = CodeSysV3Protocol._get_codesys_address_format(src_ip, src_port, netmask) 63 | if src_ip_format.bit_length() > 14: 64 | src_address = src_ip_format.to_bytes(4, "big") 65 | else: 66 | src_address = src_ip_format.to_bytes(2, "big") 67 | 68 | return CodeSysV3Protocol._build_DatagramLayer(0, src_address, 69 | dst_address, 70 | dg_service, 71 | payload) 72 | 73 | @staticmethod 74 | def build_CodeSysChannelLayerOpenRequest() -> bytes: 75 | pkt = OpenChannelRequest(0x8321d481) 76 | CodeSysV3Protocol.PKT_COUNTER += 1 77 | 78 | return bytes(pkt) 79 | 80 | @staticmethod 81 | def build_CodeSysChannelLayerCloseChannel(channel_id: int) -> bytes: 82 | pkt = CloseChannel(channel_id) 83 | 84 | return bytes(pkt) 85 | 86 | @staticmethod 87 | def build_CodeSysChannelLayerAck(channel_id, blk_id) -> bytes: 88 | pkt = ApplicationAck(channel_id=channel_id, blk_id=blk_id) 89 | return bytes(pkt) 90 | 91 | @staticmethod 92 | def build_CodeSysChannelLayerAppBlk(channel_id: int, blk_id: int, ack_id: int, payload: bytes, header_size: int = 12)\ 93 | -> typing.List[bytes]: 94 | first_max_pkt_size = MAX_PDU_SIZE - header_size - ctypes.sizeof(ApplicationBlockFirst) 95 | second_max_pkt_size = MAX_PDU_SIZE - header_size - ctypes.sizeof(ApplicationBlock) 96 | first_pkt = bytes(ApplicationBlockFirst(payload, blk_id=blk_id, ack_id=ack_id, channel_id=channel_id)) + \ 97 | payload[0: first_max_pkt_size] 98 | pkts = [first_pkt] 99 | payload_offset = first_max_pkt_size 100 | while payload_offset < len(payload): 101 | blk_id += 1 102 | pkt = bytes(ApplicationBlock(blk_id=blk_id, ack_id=ack_id, channel_id=channel_id)) + \ 103 | payload[payload_offset: payload_offset + second_max_pkt_size] 104 | payload_offset += second_max_pkt_size 105 | pkts.append(pkt) 106 | return pkts 107 | 108 | @staticmethod 109 | def build_CodeSysServicesLayer(cmd_group: CmdGroup, subcmd:int, session_id: int, 110 | tags: typing.List[Tag]) -> bytes: 111 | tags_layer = b"" 112 | for t in tags: 113 | tags_layer += t.to_stream() 114 | service_layer = ServiceLayer(cmd_group=cmd_group.value, subcmd=subcmd, session_id=session_id, 115 | content_size=len(tags_layer)) 116 | return bytes(service_layer) + tags_layer 117 | 118 | @staticmethod 119 | def parse_CodeSysTCPBlockDriverLayer(pkt: bytes) -> list: 120 | layers = [] 121 | if len(pkt) >= ctypes.sizeof(BlockDriverLayerTcp): 122 | block_layer = BlockDriverLayerTcp.from_buffer_copy(pkt) 123 | if block_layer.tcp_magic != TCP_MAGIC: 124 | raise CodeSysProtocolV3Exception("Not Valid TCP CodeSys magic") 125 | elif block_layer.tcp_length != len(pkt): 126 | raise CodeSysProtocolV3Exception(f"Missing pkt bytes, total size:{block_layer.tcp_length}, " 127 | f"received: {len(pkt)}") 128 | layers.append(block_layer) 129 | layers += CodeSysV3Protocol.parse_CodeSysDatagramLayer(pkt, ctypes.sizeof(BlockDriverLayerTcp)) 130 | return layers 131 | 132 | @staticmethod 133 | def parse_CodeSysDatagramLayer(pkt: bytes, offset: int = 0) -> list: 134 | layers = [] 135 | if len(pkt) >= ctypes.sizeof(DatagramLayer) + offset: 136 | dg_layer = DatagramLayer.from_buffer_copy(pkt, offset) 137 | if dg_layer.dg_magic != DATAGRAM_LAYER_MAGIC: 138 | raise CodeSysProtocolV3Exception("Not Valid Datagram layer magic") 139 | total_address_len = 2 * (dg_layer.receiver_len + dg_layer.sender_len) 140 | padding = (offset + total_address_len + dg_layer.header_length * 2) % 4 141 | offset += dg_layer.header_length * 2 142 | dg_layer.receiver_address = pkt[offset: offset + 2 * dg_layer.receiver_len] 143 | offset += 2 * dg_layer.receiver_len 144 | dg_layer.sender_address = pkt[offset: offset + 2 * dg_layer.sender_len] 145 | offset += 2 * dg_layer.sender_len + padding 146 | layers.append(dg_layer) 147 | if dg_layer.service_id == DatagramLayerServices.NSClient.value: 148 | layers += CodeSysV3Protocol.parse_CodeSysNSClient(pkt, offset) 149 | if dg_layer.service_id == DatagramLayerServices.ChannelManager.value: 150 | layers += CodeSysV3Protocol.parse_CodeSysChannelLayer(pkt, offset) 151 | return layers 152 | 153 | 154 | @staticmethod 155 | def parse_CodeSysNSClient(pkt: bytes, offset: int) -> list: 156 | layers = [] 157 | if len(pkt) >= ctypes.sizeof(NsHeader) + offset: 158 | nsclient_layer = NsHeader.from_buffer_copy(pkt, offset) 159 | if nsclient_layer.subcmd == NSSubCmd.DeviceInfo.value and nsclient_layer.version in (0x103, 0x400): 160 | if len(pkt) >= ctypes.sizeof(NsClientDeviceInfo) + offset: 161 | nsclient_device_info_layer = NsClientDeviceInfo.from_buffer_copy(pkt, offset) 162 | layers.append(nsclient_device_info_layer) 163 | offset += nsclient_device_info_layer.node_name_offset + 48 164 | total_strings_name = 2 * (nsclient_device_info_layer.node_name_length + 165 | nsclient_device_info_layer.device_name_length + 166 | nsclient_device_info_layer.vendor_name_length + 3) +\ 167 | nsclient_device_info_layer.serial_length 168 | if len(pkt) >= offset + total_strings_name: 169 | nsclient_device_info_layer.node_name = pkt[offset:offset + 2*(nsclient_device_info_layer.node_name_length)].decode('UTF-16-LE') 170 | offset += 2*(nsclient_device_info_layer.node_name_length + 1) 171 | nsclient_device_info_layer.device_name = pkt[offset:offset + 2 * (nsclient_device_info_layer.device_name_length)].decode('UTF-16-LE') 172 | offset += 2 * (nsclient_device_info_layer.device_name_length + 1) 173 | nsclient_device_info_layer.vendor_name = pkt[offset:offset + 2 * (nsclient_device_info_layer.vendor_name_length)].decode('UTF-16-LE') 174 | offset += 2 * (nsclient_device_info_layer.vendor_name_length + 1) 175 | nsclient_device_info_layer.serial = pkt[offset:offset + nsclient_device_info_layer.serial_length].decode('ascii') 176 | nsclient_device_info_layer.firmware_str = f"{nsclient_device_info_layer.firmware[3]}.{nsclient_device_info_layer.firmware[2]}.{nsclient_device_info_layer.firmware[1]}.{nsclient_device_info_layer.firmware[0]}" 177 | 178 | return layers 179 | 180 | @staticmethod 181 | def parse_CodeSysChannelLayer(pkt: bytes, offset: int) -> list: 182 | if len(pkt) >= offset + 1: 183 | type = pkt[offset] 184 | if type == ChannelLayerType.ApplicationBlock.value: 185 | return CodeSysV3Protocol.parse_CodeSysChannelLayerAppBlk(pkt, offset) 186 | if type == ChannelLayerType.OpenChannelResponse.value: 187 | return CodeSysV3Protocol.parse_OpenChannelResponse(pkt, offset) 188 | elif type == ChannelLayerType.ApplicationAck.value: 189 | return CodeSysV3Protocol.parse_CodeSysChannelLayerAppAck(pkt, offset) 190 | elif type == ChannelLayerType.CloseChannel.value: 191 | return CodeSysV3Protocol.parse_CodeSysChannelLayerOpenChannelRes(pkt, offset) 192 | elif type == ChannelLayerType.KeepAlive.value: 193 | return CodeSysV3Protocol.parse_CodeSysChannelLayerKeepAlive(pkt, offset) 194 | return [] 195 | 196 | @staticmethod 197 | def parse_CodeSysChannelLayerKeepAlive(pkt: bytes, offset: int) -> list: 198 | if len(pkt) >= offset + ctypes.sizeof(KeepLive): 199 | return [KeepLive.from_buffer_copy(pkt, offset)] 200 | return [] 201 | 202 | @staticmethod 203 | def parse_CodeSysChannelLayerAppBlk(pkt: bytes, offset: int) -> list: 204 | if len(pkt) >= offset + 2: 205 | flags = pkt[offset + 1] 206 | if flags & 0x01 and len(pkt) >= offset + ctypes.sizeof(ApplicationBlockFirst): 207 | return [ApplicationBlockFirst.from_buffer_copy(pkt, offset), 208 | pkt[offset + ctypes.sizeof(ApplicationBlockFirst):]] 209 | elif len(pkt) >= offset + ctypes.sizeof(ApplicationBlock): 210 | return [ApplicationBlock.from_buffer_copy(pkt, offset), 211 | pkt[offset + ctypes.sizeof(ApplicationBlock):]] 212 | return [] 213 | 214 | @staticmethod 215 | def parse_CodeSysChannelLayerAppAck(pkt: bytes, offset: int) -> list: 216 | if len(pkt) >= offset + ctypes.sizeof(ApplicationAck): 217 | return [ApplicationAck.from_buffer_copy(pkt, offset)] 218 | return [] 219 | 220 | @staticmethod 221 | def parse_OpenChannelResponse(pkt: bytes, offset: int) -> list: 222 | if len(pkt) >= offset + ctypes.sizeof(OpenChannelResponse): 223 | return [OpenChannelResponse.from_buffer_copy(pkt, offset)] 224 | return [] 225 | 226 | @staticmethod 227 | def parse_CodeSysChannelLayerOpenChannelRes(pkt: bytes, offset: int) -> list: 228 | if len(pkt) >= offset + ctypes.sizeof(OpenChannelResponse): 229 | return [OpenChannelResponse.from_buffer_copy(pkt, offset)] 230 | return [] 231 | 232 | @staticmethod 233 | def parse_CodeSysServiceLayer(pkt: bytes) -> typing.Tuple[ServiceLayer, typing.Dict[int, Tag]]: 234 | if len(pkt) >= ctypes.sizeof(ServiceLayer): 235 | service_layer = ServiceLayer.from_buffer_copy(pkt) 236 | if service_layer.protocol_id == ProtocolID.Normal.value: 237 | offset = service_layer.header_size + 4 238 | tags = CodeSysV3Protocol.parse_TagsLayer(pkt, offset) 239 | return [service_layer, tags] 240 | return [service_layer, None] 241 | return [None, None] 242 | 243 | @staticmethod 244 | def parse_TagsLayer(pkt: bytes, offset: int) -> typing.Dict[int, Tag]: 245 | tags = {} 246 | while offset < len(pkt): 247 | tag, offset = Tag.from_stream(pkt, offset) 248 | if tag.id in tags and isinstance(tags[tag.id], Tag): 249 | tags[tag.id] = [] 250 | if tag.id in tags: 251 | tags[tag.id].append(tag) 252 | else: 253 | tags[tag.id] = tag 254 | return tags -------------------------------------------------------------------------------- /Active Tool/codesysv3_protocol/structures.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import enum 3 | import socket,struct 4 | import zlib, typing 5 | from .exceptions import CodeSysProtocolV3Exception 6 | from .constants import * 7 | 8 | class BaseLittleEndianStructure(ctypes.LittleEndianStructure): 9 | _defaults_ = {} 10 | _pack_ = 1 11 | def __init__(self, **kwargs): 12 | values = type(self)._defaults_.copy() 13 | values.update(kwargs) 14 | super().__init__(**values) 15 | 16 | 17 | class BlockDriverLayerTcp(BaseLittleEndianStructure): 18 | _fields_ = [ 19 | ("tcp_magic", ctypes.c_uint32), 20 | ("tcp_length", ctypes.c_uint32) 21 | ] 22 | _defaults_ = { 23 | "tcp_magic": TCP_MAGIC 24 | } 25 | 26 | def __init__(self, payload_len: int): 27 | super(BlockDriverLayerTcp, self).__init__( 28 | tcp_length=ctypes.sizeof(BlockDriverLayerTcp) + payload_len 29 | ) 30 | 31 | 32 | class DatagramLayerServices(enum.Enum): 33 | AddressNotificationRequest = 1 34 | AddressNotificationResponse = 2 35 | NSServer = 3 36 | NSClient = 4 37 | ChannelManager = 64 38 | 39 | 40 | class ChannelLayerType(enum.Enum): 41 | ApplicationBlock = 0x01 42 | ApplicationAck = 0x02 43 | KeepAlive = 0x03 44 | GetInfo = 0xc2 45 | OpenChannelRequest = 0xc3 46 | CloseChannel = 0xc4 47 | OpenChannelResponse = 0x83 48 | 49 | class Priority(enum.Enum): 50 | Low = 0 51 | Normal = 1 52 | High = 2 53 | Emergency = 3 54 | 55 | 56 | class AddressType(enum.Enum): 57 | Full = 0 58 | Relative = 1 59 | 60 | 61 | class Boolean(enum.Enum): 62 | TRUE = 1 63 | FALSE = 0 64 | 65 | 66 | class NSSubCmd(enum.Enum): 67 | DeviceInfo = 0xc280 68 | NameResolve = 0xc202 69 | AddressResolve = 0xc201 70 | 71 | 72 | class ProtocolID(enum.Enum): 73 | Normal = 0xcd55 74 | Secure = 0x7557 75 | 76 | 77 | class CmdGroup(enum.Enum): 78 | CmpAlarmManager = 0x18 79 | CmpApp = 0x02 80 | CmpAppBP = 0x12 81 | CmpAppForce = 0x13 82 | CmpCodeMeter = 0x1d 83 | CmpCoreDump = 0x1f 84 | CmpDevice = 0x01 85 | CmpFileTransfer = 0x08 86 | CmpIecVarAccess = 0x09 87 | CmpIoMgr = 0x0b 88 | CmpLog = 0x05 89 | CmpMonitor = 0x1b 90 | CmpOpenSSL = 0x22 91 | CmpSettings = 0x06 92 | CmpTraceMgr = 0x0f 93 | CmpUserMgr = 0x0c 94 | CmpVisuServer = 0x04 95 | PlcShell = 0x11 96 | SysEthernet = 0x07 97 | 98 | 99 | 100 | class DatagramLayer(BaseLittleEndianStructure): 101 | _fields_ = [ 102 | ("dg_magic", ctypes.c_uint8), 103 | 104 | ("header_length", ctypes.c_uint8, 3), 105 | ("hop_count", ctypes.c_uint8, 5), 106 | 107 | ("length_data_block", ctypes.c_uint8, 4), 108 | ("signal", ctypes.c_uint8, 1), 109 | ("type_address", ctypes.c_uint8, 1), 110 | ("priority", ctypes.c_uint8, 2), 111 | 112 | ("service_id", ctypes.c_uint8), 113 | ("message_id", ctypes.c_uint8), 114 | 115 | ("receiver_len", ctypes.c_uint8, 4), 116 | ("sender_len", ctypes.c_uint8, 4), 117 | ] 118 | 119 | _defaults_ = { 120 | "dg_magic": DATAGRAM_LAYER_MAGIC, 121 | "hop_count": 13, 122 | "header_length": 3, 123 | "priority": Priority.Normal.value, 124 | "signal": Boolean.FALSE.value, 125 | "type_address": AddressType.Full.value, 126 | "length_data_block": 0 127 | } 128 | 129 | def __init__(self, service: DatagramLayerServices, receiver_len: int, sender_len: int, message_id: int=0): 130 | super(DatagramLayer, self).__init__( 131 | service_id=service.value, 132 | message_id=message_id, 133 | receiver_len=int(receiver_len / 2), 134 | sender_len=int(sender_len / 2), 135 | ) 136 | 137 | class NetworkAddressTCP(ctypes.BigEndianStructure): 138 | _fields_ = [ 139 | ("port", ctypes.c_uint16), 140 | ("address", ctypes.c_ubyte * 4), 141 | ] 142 | 143 | def __init__(self, ip: str, port: int): 144 | super(NetworkAddressTCP, self).__init__() 145 | ip_bytes = (ctypes.c_ubyte * 4)() 146 | ip_bytes[:] = socket.inet_aton(ip) 147 | self.port = port 148 | self.address = ip_bytes 149 | 150 | class NsHeader(BaseLittleEndianStructure): 151 | _fields_ = [ 152 | ("subcmd", ctypes.c_uint16), 153 | ("version", ctypes.c_uint16), 154 | ("msg_id", ctypes.c_uint32), 155 | ] 156 | 157 | 158 | class NsClientDeviceInfo(BaseLittleEndianStructure): 159 | _fields_ = [ 160 | ("subcmd", ctypes.c_uint16), 161 | ("version", ctypes.c_uint16), 162 | ("msg_id", ctypes.c_uint32), 163 | ("max_channels", ctypes.c_uint16), 164 | ("byte_order", ctypes.c_ubyte), 165 | ("unk1", ctypes.c_ubyte), 166 | ("node_name_offset", ctypes.c_uint16), 167 | ("node_name_length", ctypes.c_uint16), 168 | ("device_name_length", ctypes.c_uint16), 169 | ("vendor_name_length", ctypes.c_uint16), 170 | ("target_type", ctypes.c_uint16), 171 | ("target_id", ctypes.c_uint16), 172 | ("unk2", ctypes.c_uint32), 173 | ("firmware", 4*ctypes.c_ubyte), 174 | ("unk3", ctypes.c_uint32), 175 | ("serial_length", ctypes.c_uint8), 176 | ] 177 | 178 | 179 | class OpenChannelRequest(BaseLittleEndianStructure): 180 | _fields_ = [ 181 | ("type", ctypes.c_ubyte), 182 | ("flags", ctypes.c_ubyte), 183 | ("version", ctypes.c_uint16), 184 | ("checksum", ctypes.c_uint32), 185 | ("msg_id", ctypes.c_uint32), 186 | ("receiver_buffer_size", ctypes.c_uint32), 187 | ("unk1", ctypes.c_uint32) 188 | ] 189 | 190 | _defaults_ = { 191 | "type": ChannelLayerType.OpenChannelRequest.value, 192 | "flags": 0x00, 193 | "version": 0x0101, 194 | "checksum": 0, 195 | "receiver_buffer_size": 0x001f4000, 196 | "unk1": 0x05 197 | } 198 | 199 | def __init__(self, msg_id: int): 200 | super(OpenChannelRequest, self).__init__() 201 | self.msg_id = msg_id 202 | self.update_checksum() 203 | 204 | def update_checksum(self): 205 | self.checksum = zlib.crc32(bytes(self)) 206 | 207 | 208 | class CloseChannel(BaseLittleEndianStructure): 209 | _fields_ = [ 210 | ("type", ctypes.c_ubyte), 211 | ("flags", ctypes.c_ubyte), 212 | ("version", ctypes.c_uint16), 213 | ("checksum", ctypes.c_uint32), 214 | ("channel_id", ctypes.c_uint16), 215 | ("reason", ctypes.c_uint16), 216 | ] 217 | 218 | _defaults_ = { 219 | "type": ChannelLayerType.CloseChannel.value, 220 | "flags": 0x00, 221 | "version": 0x0101, 222 | "checksum": 0, 223 | "reason": 0 224 | } 225 | 226 | def __init__(self, channel_id: int): 227 | super(CloseChannel, self).__init__() 228 | self.channel_id = channel_id 229 | self.update_checksum() 230 | 231 | def update_checksum(self): 232 | self.checksum = zlib.crc32(bytes(self)) 233 | 234 | 235 | class OpenChannelResponse(BaseLittleEndianStructure): 236 | _fields_ = [ 237 | ("type", ctypes.c_ubyte), 238 | ("flags", ctypes.c_ubyte), 239 | ("version", ctypes.c_uint16), 240 | ("checksum", ctypes.c_uint32), 241 | ("msg_id", ctypes.c_uint32), 242 | ("reason", ctypes.c_uint16), 243 | ("channel_id", ctypes.c_uint16), 244 | ("receiver_buffer_size", ctypes.c_uint32), 245 | ("unk1", ctypes.c_uint32) 246 | ] 247 | 248 | _defaults_ = { 249 | "type": ChannelLayerType.OpenChannelResponse.value, 250 | "flags": 0x00, 251 | "version": 0x0101, 252 | "checksum": 0, 253 | "reason": 0, 254 | "receiver_buffer_size": 0x001f4000, 255 | "unk1": 0x04, 256 | 257 | } 258 | 259 | def __init__(self, msg_id: int, channel_id: int): 260 | super(OpenChannelResponse, self).__init__() 261 | self.msg_id = msg_id 262 | self.channel_id = channel_id 263 | self.update_checksum() 264 | 265 | def update_checksum(self): 266 | self.checksum = zlib.crc32(bytes(self)) 267 | 268 | 269 | class ApplicationAck(BaseLittleEndianStructure): 270 | _fields_ = [ 271 | ("type", ctypes.c_ubyte), 272 | ("flags", ctypes.c_ubyte), 273 | ("channel_id", ctypes.c_uint16), 274 | ("blk_id", ctypes.c_uint32) 275 | ] 276 | 277 | _defaults_ = { 278 | "flags": 0x80, 279 | "type": ChannelLayerType.ApplicationAck.value 280 | } 281 | 282 | 283 | class KeepLive(BaseLittleEndianStructure): 284 | _fields_ = [ 285 | ("type", ctypes.c_ubyte), 286 | ("flags", ctypes.c_ubyte), 287 | ("channel_id", ctypes.c_uint16), 288 | ] 289 | 290 | _defaults_ = { 291 | "flags": 0x00, 292 | "type": ChannelLayerType.KeepAlive.value 293 | } 294 | 295 | 296 | class ApplicationBlockFirst(BaseLittleEndianStructure): 297 | _fields_ = [ 298 | ("type", ctypes.c_ubyte), 299 | ("is_first_payload", ctypes.c_ubyte, 7), 300 | ("is_request", ctypes.c_ubyte, 1), 301 | ("channel_id", ctypes.c_uint16), 302 | ("blk_id", ctypes.c_uint32), 303 | ("ack_id", ctypes.c_uint32), 304 | ("remaining_data_size", ctypes.c_uint32), 305 | ("checksum", ctypes.c_uint32), 306 | ] 307 | 308 | _defaults_ = { 309 | "is_first_payload": 1, 310 | "is_request": 1, 311 | "checksum": 0, 312 | "type": ChannelLayerType.ApplicationBlock.value 313 | } 314 | 315 | def __init__(self, payload, *args, **kwargs): 316 | super(ApplicationBlockFirst, self).__init__(*args, **kwargs) 317 | self.remaining_data_size = len(payload) 318 | self.checksum = zlib.crc32(bytes(payload)) 319 | 320 | 321 | class ApplicationBlock(BaseLittleEndianStructure): 322 | _fields_ = [ 323 | ("type", ctypes.c_ubyte), 324 | ("is_first_payload", ctypes.c_ubyte, 7), 325 | ("is_request", ctypes.c_ubyte, 1), 326 | ("channel_id", ctypes.c_uint16), 327 | ("blk_id", ctypes.c_uint32), 328 | ("ack_id", ctypes.c_uint32), 329 | ] 330 | 331 | _defaults_ = { 332 | "is_first_payload": 0, 333 | "is_request": 1, 334 | "type": ChannelLayerType.ApplicationBlock.value 335 | } 336 | 337 | 338 | class ServiceLayer(BaseLittleEndianStructure): 339 | _fields_ = [ 340 | ("protocol_id", ctypes.c_uint16), 341 | ("header_size", ctypes.c_uint16), 342 | ("cmd_group", ctypes.c_uint16, 7), 343 | ("is_response", ctypes.c_uint16, 1), 344 | ("subcmd", ctypes.c_uint16), 345 | ("session_id", ctypes.c_uint32), 346 | ("content_size", ctypes.c_uint32) 347 | ] 348 | 349 | _defaults_ = { 350 | "protocol_id": ProtocolID.Normal.value, 351 | "header_size": 12, 352 | "is_response": 0, 353 | "additional_data": 0 354 | } 355 | 356 | 357 | class Tag: 358 | 359 | DATA_FORMAT = { 360 | "dword": ">I", 361 | "word": ">H", 362 | "byte": ">B", 363 | "char": ">c", 364 | "long": ">Q", 365 | "dword_le": "= 0x80 378 | 379 | def __getitem__(self, tag_id): 380 | return self._sub_tags.get(tag_id) 381 | 382 | @staticmethod 383 | def _read_tag_number(stream: bytes, offset: int) -> typing.Tuple[int, int]: 384 | if len(stream) <= offset: 385 | raise CodeSysProtocolV3Exception("Not enough data for tag") 386 | t = stream[offset] 387 | n = t & 0x7f 388 | shift = 7 389 | while (t & 0x80) != 0: 390 | offset += 1 391 | if len(stream) <= offset: 392 | raise CodeSysProtocolV3Exception("Not enough data for tag") 393 | t = stream[offset] 394 | n |= ((t & 0x7f) << shift) 395 | shift += 7 396 | 397 | return n, offset + 1 398 | 399 | @staticmethod 400 | def _write_tag_number(v: int) -> bytes: 401 | b = b"" 402 | while v > 0: 403 | t = v & 0x7f 404 | v >>= 7 405 | if v > 0: 406 | t |= 0x80 407 | b += bytes([t]) 408 | 409 | return b 410 | 411 | def add_tag(self, subtag): 412 | if subtag.id in self._sub_tags and isinstance(self._sub_tags[subtag.id], Tag): 413 | self._sub_tags[subtag.id] = [] 414 | if subtag.id in self._sub_tags: 415 | self._sub_tags[subtag.id].append(subtag) 416 | else: 417 | self._sub_tags[subtag.id] = subtag 418 | 419 | def get_tag(self, id:int): 420 | return self._sub_tags.get(id) 421 | 422 | @staticmethod 423 | def from_stream(stream: bytes, offset: int = 0): 424 | tag_id, offset = Tag._read_tag_number(stream, offset) 425 | tag_size, offset = Tag._read_tag_number(stream, offset) 426 | if len(stream) < offset + tag_size: 427 | raise CodeSysProtocolV3Exception("Not enough data for tag") 428 | data = stream[offset: offset + tag_size] 429 | tag = Tag(tag_id, data) 430 | if tag.is_parent: 431 | toffset = 0 432 | while tag_size > toffset: 433 | sub_tag, toffset = Tag.from_stream(data, toffset) 434 | tag.add_tag(sub_tag) 435 | return tag, offset + tag_size 436 | 437 | def _add_align_to_size(self, tag_id_size: int, tag_size_length: int): 438 | align_modulus = (self._align & 0xF0) >> 4 439 | align_remainder = (self._align & 0x0F) 440 | 441 | total_header_size = tag_id_size + tag_size_length 442 | total_header_size_mod = total_header_size % align_modulus 443 | 444 | if total_header_size_mod < align_remainder: 445 | total_header_size += align_remainder - total_header_size_mod 446 | elif total_header_size_mod > align_remainder: 447 | total_header_size += align_modulus - (total_header_size_mod - align_remainder) 448 | 449 | return total_header_size 450 | 451 | def to_stream(self) -> bytes: 452 | tag_id = Tag._write_tag_number(self.id) 453 | data = self.data 454 | if self.is_parent: 455 | sub_tags_data = b"" 456 | for t in self._sub_tags.values(): 457 | sub_tags_data += t.to_stream() 458 | data = sub_tags_data 459 | tag_size = Tag._write_tag_number(len(data)) 460 | header = bytearray(tag_id + tag_size) 461 | total_size = self._add_align_to_size(len(tag_id), len(tag_size)) 462 | for i in range(total_size - len(header)): 463 | header[len(header) - 1] |= 0x80 464 | header.append(0) 465 | return header + data 466 | 467 | def __getattr__(self, item): 468 | if item in Tag.DATA_FORMAT: 469 | return struct.unpack(Tag.DATA_FORMAT[item], self.data)[0] 470 | 471 | -------------------------------------------------------------------------------- /Active Tool/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | def init_logger(): 4 | logger = logging.getLogger('Codesys') 5 | logger.setLevel(logging.DEBUG) 6 | 7 | stream_handler = logging.StreamHandler() 8 | stream_handler.setLevel(logging.INFO) 9 | 10 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 11 | stream_handler.setFormatter(formatter) 12 | -------------------------------------------------------------------------------- /Active Tool/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from codesysv3_protocol import * 3 | from log import init_logger 4 | from ipaddress import ip_address, IPv4Address 5 | 6 | 7 | def is_legal_ip_address(ip: str) -> bool: 8 | try: 9 | return isinstance(ip_address(ip), IPv4Address) 10 | except: 11 | return False 12 | 13 | 14 | def print_device_info(device_info: dict): 15 | print("Device Info:") 16 | print("\tNode Name:", device_info['node_name']) 17 | print("\tDevice Name:", device_info['device_name']) 18 | print("\tVendor Name:", device_info['vendor_name']) 19 | print("\tFirmware:", device_info['firmware_str']) 20 | 21 | 22 | def main(): 23 | init_logger() 24 | logger = logging.getLogger("Codesys") 25 | parser = argparse.ArgumentParser(description='Welcome to RTS Version extractor.') 26 | parser.add_argument('--username', type=str, default="", required=False, 27 | help='The username that required to log into the plc.') 28 | parser.add_argument('--password', type=str, default="", required=False, 29 | help='The password that required to log into the plc.') 30 | parser.add_argument('--dst_ip', type=str, default=None, required=True, 31 | help='The IP address of the remote plc.') 32 | parser.add_argument('--src_ip', type=str, default=None, required=True, 33 | help='The IP address of the machine that will run this script.') 34 | args = parser.parse_args() 35 | 36 | if False in (is_legal_ip_address(args.dst_ip), is_legal_ip_address(args.src_ip)): 37 | parser.error("Illegal IP address") 38 | 39 | try: 40 | rtsversion = None 41 | with CodeSysV3Device(args.dst_ip, args.src_ip) as device: 42 | device_info = device.get_device_name_server_info() 43 | print_device_info(device_info) 44 | if "codesys" in device_info['device_name'].lower() or "3s - smart software" in device_info['vendor_name'].lower(): 45 | rtsversion = device_info['firmware_str'] 46 | else: 47 | logger.info("Failed to get info from the device NSServer, trying to run PLCShell command") 48 | with device.open_channel() as channel: 49 | if not channel.login(args.username, args.password): 50 | logger.error("Failed to login into the PLC") 51 | else: 52 | try: 53 | shell = PLCShell(channel) 54 | version = shell.run("rtsinfo") 55 | rtsversion = version.split(":")[1].strip() 56 | except Exception as ex: 57 | logger.error("Failed to run PLCShell command") 58 | if rtsversion is not None: 59 | print(f"CodeSysV3 version: {rtsversion}") 60 | else: 61 | print("Failed to find the CodeSysV3 version") 62 | 63 | except Exception as ex: 64 | logger.error(f"Failed to extract the Codesys V3 Version from the PLC: {ex}") 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | 70 | -------------------------------------------------------------------------------- /Active Tool/requirements.txt: -------------------------------------------------------------------------------- 1 | # This contains the needed packages for this project 2 | 3 | netifaces==0.11.0 -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /IDA Python script/ida_python_utils.py: -------------------------------------------------------------------------------- 1 | import idautils 2 | import ida_bytes 3 | 4 | 5 | def restring(start_addr, end_addr): 6 | curr_addr = start_addr 7 | print("started renaming strings.") 8 | while curr_addr < end_addr: 9 | ida_bytes.create_strlit(curr_addr, 0, ida_nalt.STRTYPE_TERMCHR) 10 | curr_addr += 4 11 | 12 | print("Finshed restring.") 13 | 14 | 15 | def smart_restring(start_addr, end_addr, min_str_len=3): 16 | curr_addr = start_addr 17 | print("started smart renaming strings.") 18 | while curr_addr < end_addr: 19 | 20 | potential_len = ida_bytes.get_max_strlit_length(curr_addr, 0) 21 | if potential_len > min_str_len: 22 | ida_bytes.create_strlit(curr_addr, 0, ida_nalt.STRTYPE_TERMCHR) 23 | 24 | curr_addr += 1 25 | 26 | print("Finished smart renaimng strings") 27 | 28 | 29 | def redefine_methods(start_addr, end_addr): 30 | curr_addr = start_addr 31 | print("started redefine methods.") 32 | while curr_addr < end_addr: 33 | if idc.get_func_name(curr_addr) == '': 34 | idc.add_func(curr_addr) 35 | 36 | curr_addr += 4 37 | 38 | print("Finshed redefine methods.") 39 | 40 | 41 | def redefine_references(start_addr, end_addr): 42 | curr_addr = start_addr 43 | print("started redefine references.") 44 | while curr_addr < end_addr: 45 | ida_offset.op_offset(curr_addr, 0, idc.REF_OFF32) 46 | ida_offset.op_offset(curr_addr, 1, idc.REF_OFF32) 47 | ida_offset.op_offset(curr_addr, 2, idc.REF_OFF32) 48 | curr_addr += 4 49 | 50 | print("Finshed redefine references.") 51 | 52 | 53 | def rename_methods_based_on_ref_table_v1(start_addr, end_addr): 54 | curr_addr = start_addr 55 | next_addr = curr_addr + 4 56 | while next_addr < end_addr: 57 | curr_addr_content = idc.Dword(curr_addr) 58 | curr_addr_string = idc.GetString(curr_addr_content + 2) # NOTE: the +2 is for current firmware 59 | 60 | if curr_addr_string is None or curr_addr_string == '': 61 | curr_addr += 4 62 | next_addr += 4 63 | continue 64 | 65 | next_addr_content = idc.Dword(next_addr) 66 | next_addr_method_name = idc.GetFunctionName(next_addr_content) 67 | 68 | if next_addr_method_name is None or next_addr_method_name == '' or not next_addr_method_name.startswith("sub_"): 69 | curr_addr += 4 70 | next_addr += 4 71 | continue 72 | print("Renaming {src}->{dst} ".format(src=next_addr_method_name, dst=curr_addr_string)) 73 | idc.MakeNameEx(next_addr_content, curr_addr_string, idc.SN_NOWARN) 74 | 75 | curr_addr += 4 76 | next_addr += 4 77 | 78 | 79 | def rename_methods_based_on_ref_table_v2(start_addr, end_addr): 80 | curr_addr = start_addr 81 | next_addr = curr_addr + 4 82 | while next_addr < end_addr: 83 | curr_addr_content = ida_bytes.get_dword(curr_addr) 84 | print(curr_addr_content) 85 | curr_addr_string = str(ida_bytes.get_strlit_contents(curr_addr_content, -1, 86 | STRTYPE_TERMCHR)) # NOTE: the +2 is for current firmware 87 | 88 | if curr_addr_string is None or curr_addr_string == '': 89 | curr_addr += 4 90 | next_addr += 4 91 | continue 92 | 93 | next_addr_content = ida_bytes.get_dword(next_addr) 94 | next_addr_method_name = idc.get_func_name(next_addr_content) 95 | 96 | if next_addr_method_name is None or next_addr_method_name == '' or not next_addr_method_name.startswith("sub_"): 97 | curr_addr += 4 98 | next_addr += 4 99 | continue 100 | 101 | print("Renaming {src}->{dst} ".format(src=next_addr_method_name, dst=curr_addr_string)) 102 | idc.set_name(next_addr_content, curr_addr_string, idc.SN_NOWARN) 103 | 104 | curr_addr += 4 105 | next_addr += 4 106 | 107 | 108 | def rename_methods_by_references(start_addr, end_addr): 109 | """ 110 | NOTE: in case of number of segments you should use get_func_name instead of the string one 111 | NOTE: Tis good for 32 systems for 64 we should change get_dword to get_qword 112 | """ 113 | current_address = start_addr 114 | next_address = current_address + 4 115 | 116 | while next_address < end_addr: 117 | 118 | current_address_content = ida_bytes.get_dword(current_address) 119 | new_name = ida_bytes.get_strlit_contents(current_address_content, -1, STRTYPE_TERMCHR) 120 | 121 | if new_name is None or new_name == '': 122 | current_address += 4 123 | next_address += 4 124 | continue 125 | 126 | next_address_content = ida_bytes.get_dword(next_address) 127 | old_name = idc.get_func_name(next_address_content) 128 | 129 | if old_name is not None and type(old_name) is not str: 130 | old_name = old_name.decode('ascii') 131 | 132 | if new_name is not None and type(new_name) is not str: 133 | new_name = new_name.decode('ascii') 134 | 135 | if old_name is None or ' ' in old_name: 136 | current_address += 4 137 | next_address += 4 138 | continue 139 | 140 | print(hex(current_address_content), new_name, old_name) 141 | 142 | print("Renaming {src}->{dst} ".format(src=old_name, dst=new_name)) 143 | idc.set_name(next_address_content, str(new_name), 0x800) # 0x800 SN_FORCE 144 | 145 | current_address += 4 146 | next_address += 4 147 | 148 | 149 | 150 | def rename_methods_based_on_ref_for_codesys_emulator(start_addr, end_addr): 151 | """ 152 | NOTE: in case of number of segments you should yuse get_func_name instead of the string one 153 | NOTE: this is for x64 for x32 do + 4 154 | :param start_addr: 155 | :param end_addr: 156 | :return: 157 | """ 158 | 159 | curr_addr = start_addr 160 | next_addr = curr_addr 161 | next_addr = next_addr + 4 162 | while next_addr < end_addr: 163 | 164 | curr_addr_content = idc.get_wide_dword(curr_addr) 165 | old_name = str(ida_funcs.get_func_name(curr_addr_content)) 166 | 167 | if old_name is None or old_name == '': 168 | curr_addr += 4 169 | next_addr += 4 170 | continue 171 | 172 | next_addr_content = idc.get_qword(next_addr) 173 | new_name = idc.get_strlit_contents(next_addr_content, -1, STRTYPE_TERMCHR) 174 | 175 | if new_name is None: 176 | curr_addr += 4 177 | next_addr += 4 178 | continue 179 | 180 | new_name = new_name.decode('ascii') 181 | 182 | if len(new_name) < 3: 183 | curr_addr += 4 184 | next_addr += 4 185 | continue 186 | 187 | print("Renaming {src}->{dst} ".format(src=old_name, dst=new_name)) 188 | ret = idc.set_name(curr_addr_content, new_name, idc.SN_NOWARN) 189 | 190 | curr_addr += 4 191 | next_addr += 4 192 | 193 | 194 | 195 | def find_all_refs(addr, arg_val=None, operand=0xE3): 196 | """ 197 | Searches for x ref to given function address 198 | then for each xref gets last five opcodes 199 | and searches for 0x21 opcode which is the setting of R1 200 | checking the value if its 0x22 201 | then print the location 202 | """ 203 | map = {} 204 | for xref in idautils.XrefsTo(addr): 205 | # print(xref.type, XrefTypeName(xref.type), 'from', hex(xref.frm), 'to', hex(xref.to)) 206 | tar_addr = xref.frm 207 | 208 | for i in range(5): 209 | opcode = idc.GetManyBytes(PrevHead(tar_addr), ItemSize(PrevHead(tar_addr))) 210 | array_opcode = [int(ord(i)) for i in opcode] 211 | if array_opcode[len(array_opcode) - 1] == operand: # LDR = 0xE5,mov = 0xE3 212 | if arg_val is None: 213 | map[tar_addr] = array_opcode 214 | else: 215 | if array_opcode[0] == arg_val: 216 | map[tar_addr] = array_opcode 217 | tar_addr = PrevHead(tar_addr) 218 | 219 | # args = idaapi.get_arg_addrs(xref.frm) 220 | # print(xref.frm, args) 221 | 222 | return map 223 | 224 | 225 | def rename_if_name_contains(start_addr, end_addr): 226 | curr_addr = start_addr 227 | while curr_addr < end_addr: 228 | curr_addr += 1 229 | 230 | 231 | def create_sturct_with_fields(struct_name, amount_of_qdwords): 232 | id = add_struc(-1, struct_name, 0) 233 | for i in range(amount_of_qdwords): 234 | print("added field field_%x" % i + " to struct " + struct_name) 235 | add_struc_member(id, "field_%x" % i, i, FF_DATA | FF_QWORD, -1, 8) 236 | print("Finished adding structs") 237 | 238 | 239 | def rename_based_on_inheritance_strings(): 240 | """ 241 | Rename methods that have those kind of strings: ipnet_nat_proxy_dns_parse_questions() :: could not add transaction to list 242 | """ 243 | import idautils 244 | sc = idautils.Strings() 245 | for s in sc: 246 | if "::" in str(s): 247 | prev_address = s.ea - 4 248 | ref_to_prev_address = get_first_dref_to(prev_address) 249 | old_method_name = ida_funcs.get_func_name(ref_to_prev_address) 250 | 251 | if old_method_name is None: 252 | prev_address = s.ea - 8 253 | ref_to_prev_address = get_first_dref_to(prev_address) 254 | old_method_name = ida_funcs.get_func_name(ref_to_prev_address) 255 | if old_method_name is None: 256 | continue 257 | 258 | parts = str(s).split("::") 259 | relevant_part = parts[0] 260 | new_method_name = relevant_part.replace("(", "").replace(")", "") 261 | 262 | if "%" in new_method_name: 263 | new_method_name = new_method_name.split("%")[0] 264 | if "~" in new_method_name: 265 | new_method_name = new_method_name.replace("~", "") 266 | if ":" in new_method_name: 267 | new_method_name = new_method_name.replace(":", "") 268 | 269 | if new_method_name is not None and old_method_name is not None and old_method_name.startswith("sub_"): 270 | old_method_address = get_name_ea(0, old_method_name) 271 | if old_method_address is not None: 272 | print("Renaming {src}->{dst} ".format(src=old_method_name, dst=new_method_name)) 273 | idc.set_name(old_method_address, new_method_name, idc.SN_NOWARN) 274 | 275 | 276 | def is_camel_case(s): 277 | return s != s.lower() and s != s.upper() and "_" not in s 278 | 279 | 280 | def get_all_camel_case_words_in_image(): 281 | """ 282 | So the idea is that lets say we have a log that looks like 283 | ClassA::SendData() failed with error code %d .... 284 | and we have ref for this location we want to rename all the relevant methods 285 | with those names 286 | """ 287 | print("Searching for all camel case worlds") 288 | import idautils 289 | sc = idautils.Strings() 290 | for s in sc: 291 | parts = str(s).split(" ") 292 | if 2 < len(parts) and is_camel_case(parts[0]) and parts[1] == "not": 293 | print(parts[0]) 294 | 295 | 296 | def rename_based_on_logs(): 297 | """ 298 | The idea is to rename methods that got strings that looks like: 299 | 'ProcessEventRequestState(Device:%d) action %p max pending actions' 300 | or 301 | 'ServerInit: invalid parameter' 302 | """ 303 | import idautils 304 | sc = idautils.Strings() 305 | new_method_name = None 306 | for s in sc: 307 | s_as_str = str(s) 308 | prev_address = s.ea 309 | ref_to_prev_address = get_first_dref_to(prev_address) 310 | old_method_name = ida_funcs.get_func_name(ref_to_prev_address) 311 | old_method_address = get_name_ea(0, old_method_name) 312 | 313 | if "(" in s_as_str and ":" in s_as_str and s_as_str.find("c") < s_as_str.find(":"): 314 | parts = s_as_str.split(" ") 315 | name_with_extra_data = parts[0] 316 | name_parts = name_with_extra_data.split("(") 317 | parts[0] = parts[0].replace(":", "") 318 | new_method_name = name_parts[0] if len(name_parts[0]) > 4 else None 319 | 320 | elif ":" in s_as_str: 321 | parts = s_as_str.split(":") 322 | parts[0] = parts[0].replace(":", "") 323 | name_parts = parts[0].split(" ") 324 | 325 | if len(name_parts) > 1: 326 | continue 327 | new_method_name = parts[0] if len(parts[0]) > 4 else None 328 | 329 | if new_method_name is not None and new_method_name.startswith(" "): 330 | continue 331 | 332 | if new_method_name is not None and old_method_name is not None and old_method_name.startswith("sub_"): 333 | print("Renaming {src}->{dst} ".format(src=old_method_name, dst=new_method_name)) 334 | ret_code = idc.set_name(old_method_address, new_method_name, idc.SN_NOWARN) 335 | 336 | new_method_name = None 337 | 338 | 339 | def rename_based_on_particular_suffix(suffix_to_renamed_based_on, prefix): 340 | """ 341 | Rename method based on specific suffix 342 | """ 343 | import idautils 344 | sc = idautils.Strings() 345 | 346 | for s in sc: 347 | s_as_str = str(s) 348 | prev_address = s.ea 349 | ref_to_prev_address = get_first_dref_to(prev_address) 350 | old_method_name = ida_funcs.get_func_name(ref_to_prev_address) 351 | old_method_address = get_name_ea(0, old_method_name) 352 | 353 | if s_as_str.endswith(suffix_to_renamed_based_on) and old_method_name.startswith("sub"): 354 | s_as_str = s_as_str.replace(" ", "") 355 | new_method_name = "{prefix}{name}".format(prefix=prefix, name=s_as_str) 356 | print("Renaming {src}->{dst} ".format(src=old_method_name, dst=new_method_name)) 357 | ret_code = idc.set_name(old_method_address, new_method_name, idc.SN_NOWARN) 358 | print("returned", ret_code) 359 | 360 | 361 | def find_blx_gadgets(): 362 | data = "" 363 | for function_ea in idautils.Functions(): 364 | instructions = [] 365 | 366 | for ins in idautils.FuncItems(function_ea): 367 | if idaapi.is_code(idaapi.get_full_flags(ins)): 368 | cmd = idc.GetDisasm(ins) 369 | instructions.append([cmd, ins]) 370 | 371 | for i in range(len(instructions) - 5): 372 | print(instructions) 373 | if "BLX" in instructions[i + 4][0]: 374 | 375 | data += str(hex(instructions[i][1])) + \ 376 | "-> [" + "\n" + \ 377 | instructions[i][0] + "\n" + \ 378 | instructions[i + 1][0] + "\n" + \ 379 | instructions[i + 2][0] + "\n" + \ 380 | instructions[i + 3][0] + "\n" + \ 381 | instructions[i + 4][0] + "\n" 382 | 383 | with open(r"GadgetsWithBLX.txt", "w") as f: 384 | f.write(data) 385 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoDe16 2 | ## Intro 3 | Microsoft’s cyber physical system researchers recently identified multiple high-severity vulnerabilities in the CODESYS V3 software development kit (SDK) 4 | a software development environment widely used to program and engineer programmable logic controllers (PLCs). 5 | Exploitation of the discovered vulnerabilities, which affect all versions of CODESYS V3 prior to version 3.5.19.0, 6 | could put operational technology (OT) infrastructure at risk of attacks, such as remote code execution (RCE) and denial of service (DoS). 7 | The discovery of these vulnerabilities highlights the critical importance of ensuring the security of industrial control systems and underscores the need for continuous monitoring and protection of these environments. 8 | 9 | 10 | CODESYS is [compatible](https://www.codesys.com/the-system/codesys-inside.html) with approximately 1,000 different device types from over 500 manufacturers and several million devices that use the solution to implement the international industrial standard IEC 61131-3. A DoS attack against a device using a vulnerable version of CODESYS could enable threat actors to shut down a power plant, while remote code execution could create a backdoor for devices and let attackers tamper with operations, cause a PLC to run in an unusual way, or steal critical information. Exploiting the discovered vulnerabilities, however, requires user authentication, as well as deep knowledge of the proprietary protocol of CODESYS V3 and the structure of the different services that the protocol uses. 11 | 12 | Microsoft researchers reported the discovery to CODESYS in September 2022 and worked closely with CODESYS to ensure that the vulnerabilities are patched. Information on the patch released by CODESYS to address these vulnerabilities can be found here: [Security update for CODESYS Control V3](https://customers.codesys.com/index.php?eID=dumpFile&t=f&f=17554&token=5444f53b4c90fe37043671a100dffa75305d1825&download=). We strongly urge CODESYS users to apply these security updates as soon as possible. We also thank CODESYS for their collaboration and recognizing the urgency in addressing these vulnerabilities. 13 | 14 | Below is a list of the discovered vulnerabilities discussed in this blog: 15 | 16 | | CVE | CODESYS component | Impact | CVSS score | 17 | | --- | --- | --- | --- | 18 | | [CVE-2022-47379](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47379) | CMPapp | DoS, RCE | 8.8 | 19 | | [CVE-2022-47380](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47380) | CMPapp | DoS, RCE | 8.8 | 20 | | [CVE-2022-47381](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47381) | CMPapp | DoS, RCE | 8.8 | 21 | | [CVE-2022-47382](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47382) | CmpTraceMgr | DoS, RCE | 8.8 | 22 | | [CVE-2022-47383](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47383) | CmpTraceMgr | DoS, RCE | 8.8 | 23 | | [CVE-2022-47384](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47384) | CmpTraceMgr | DoS, RCE | 8.8 | 24 | | [CVE-2022-47385](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47385) | CmpAppForce | DoS, RCE | 8.8 | 25 | | [CVE-2022-47386](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47386) | CmpTraceMgr | DoS, RCE | 8.8 | 26 | | [CVE-2022-47387](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47387) | CmpTraceMgr | DoS, RCE | 8.8 | 27 | | [CVE-2022-47388](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47388) | CmpTraceMgr | DoS, RCE | 8.8 | 28 | | [CVE-2022-47389](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47389) | CmpTraceMgr | DoS, RCE | 8.8 | 29 | | [CVE-2022-47390](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47390) | CmpTraceMgr | DoS, RCE | 8.8 | 30 | | [CVE-2022-47391](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47391) | CMPDevice | DoS | 7.5 | 31 | | [CVE-2022-47392](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47392) | CmpApp/ CmpAppBP/ CmpAppForce | DoS | 8.8 | 32 | | [CVE-2022-47393](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-47393) | CmpFiletransfer | DoS | 8.8 | 33 | 34 | 35 | The security blog can be found in the following address:[https://aka.ms/codesys-v3-sdk-vulnerabilities](https://aka.ms/codesys-v3-sdk-vulnerabilities) 36 | 37 | ## What this repo contains? 38 | * [Full white paper of the research](/Vulnerabilities-in-CODESYS-V3-SDK-could-lead-to-RCE-or-DoS.pdf) 39 | * [Active tool to extract the CODESYS V3 runtime version of devices](/Active%20Tool/) 40 | * [Wireshark Dissector for CODESYS V3 proprietary protocol](/Wireshark%20Dissector/) 41 | * [IDA Python scripts that we wrote during our research](/IDA%20Python%20script/) 42 | 43 | 44 | ## Researcher 45 | * **Vladimir Tokarev, Microsoft Threat Intelligence Community** 46 | 47 | 48 | ## Contributing 49 | 50 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 51 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 52 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 53 | 54 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 55 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 56 | provided by the bot. You will only need to do this once across all repos using our CLA. 57 | 58 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 59 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 60 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 61 | 62 | ## Trademarks 63 | 64 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 65 | trademarks or logos is subject to and must follow 66 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 67 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 68 | Any use of third-party trademarks or logos are subject to those third-party's policies. 69 | 70 | ## Legal Disclaimer 71 | 72 | Copyright (c) 2018 Microsoft Corporation. All rights reserved. 73 | 74 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 75 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 76 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 77 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 78 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 79 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 80 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # TODO: The maintainer of this repo has not yet edited this file 2 | 3 | **REPO OWNER**: Do you want Customer Service & Support (CSS) support for this product/project? 4 | 5 | - **No CSS support:** Fill out this template with information about how to file issues and get help. 6 | - **Yes CSS support:** Fill out an intake form at [aka.ms/onboardsupport](https://aka.ms/onboardsupport). CSS will work with/help you to determine next steps. 7 | - **Not sure?** Fill out an intake as though the answer were "Yes". CSS will help you decide. 8 | 9 | *Then remove this first heading from this SUPPORT.MD file before publishing your repo.* 10 | 11 | # Support 12 | 13 | ## How to file issues and get help 14 | 15 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 16 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 17 | feature request as a new Issue. 18 | 19 | For help and questions about using this project, please **REPO MAINTAINER: INSERT INSTRUCTIONS HERE 20 | FOR HOW TO ENGAGE REPO OWNERS OR COMMUNITY FOR HELP. COULD BE A STACK OVERFLOW TAG OR OTHER 21 | CHANNEL. WHERE WILL YOU HELP PEOPLE?**. 22 | 23 | ## Microsoft Support Policy 24 | 25 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 26 | -------------------------------------------------------------------------------- /Vulnerabilities-in-CODESYS-V3-SDK-could-lead-to-RCE-or-DoS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Vulnerabilities-in-CODESYS-V3-SDK-could-lead-to-RCE-or-DoS.pdf -------------------------------------------------------------------------------- /Wireshark Dissector/CodeSysV3.lua: -------------------------------------------------------------------------------- 1 | codesysv3_protocol = Proto("codesysv3", "CodeSys V3") 2 | 3 | tcp_magic = ProtoField.uint32("codesysv3.tcp_header.magic", "TCP Magic", base.HEX) 4 | tcp_length = ProtoField.uint32("codesysv3.tcp_header.length", "TCP Length", base.DEC) 5 | 6 | 7 | magic = ProtoField.uint8("codesysv3.datagram.magic", "Magic", base.HEX) 8 | hop_count = ProtoField.uint8("codesysv3.datagram.hop_info.hop_count", "Hop Count", base.DEC, nil, 0xF8) 9 | header_length = ProtoField.uint8("codesysv3.datagram.hop_info.length", "Header Length", base.DEC, nil, 0x07) 10 | 11 | priority = ProtoField.uint8("codesysv3.datagram.packet_info.priority", "Priority", base.DEC, { 12 | [0] = "Low", 13 | [1] = "Normal", 14 | [2] = "High", 15 | [3] = "Emergency", 16 | }, 0xC0) 17 | signal = ProtoField.uint8("codesysv3.datagram.packet_info.signal", "Signal", base.DEC, {[0] = "False", [1] = "True"}, 0x20) 18 | type_address = ProtoField.uint8("codesysv3.datagram.packet_info.type_address", "Type Address", base.HEX, { 19 | [0] = "Full Address", 20 | [1] = "Relative Address" 21 | }, 0x10) 22 | length_data_block = ProtoField.uint8("codesysv3.datagram.packet_info.length_data_block", "Length Data Block", base.HEX, nil, 0x0F) 23 | 24 | local datagram_layer_services = { 25 | [1] = "AddressNotification Request", 26 | [2] = "AddressNotification Response", 27 | [3] = "NS Server", 28 | [4] = "NS Client", 29 | [64] = "Channel Manager", 30 | } 31 | 32 | service_id = ProtoField.uint8("codesysv3.datagram.service_id", "Service ID", base.HEX, datagram_layer_services) 33 | message_id = ProtoField.uint8("codesysv3.datagram.message_id", "Message ID", base.HEX) 34 | 35 | receiver_length = ProtoField.uint8("codesysv3.datagram.lengths.receiver_length", "Receiver Length", base.DEC) 36 | sender_length = ProtoField.uint8("codesysv3.datagram.lengths.sender_length", "Sender Length", base.DEC) 37 | 38 | 39 | receiver_address = ProtoField.bytes("codesysv3.datagram.receiver.tcp_address", "Receiver Address") 40 | sender_address = ProtoField.bytes("codesysv3.datagram.sender.tcp_address", "Sender Address") 41 | 42 | 43 | receiver_udp_address = ProtoField.string("codesysv3.datagram.receiver.udp_address", "Receiver UDP Address") 44 | receiver_udp_port = ProtoField.uint16("codesysv3.datagram.receiver.udp_port", "Receiver UDP Port", base.DEC) 45 | sender_udp_port = ProtoField.uint16("codesysv3.datagram.sender.udp_port", "Sender UDP Port", base.DEC) 46 | sender_udp_address = ProtoField.string("codesysv3.datagram.sender.udp_address", "Sender UDP Address") 47 | 48 | ns_server_subcmd = ProtoField.uint8("codesysv3.nsserver.subcmd", "subcmd", base.HEX, { 49 | [0xc201] = "Resolve Address Request", 50 | [0xc202] = "Resolve Name Request", 51 | [0xc280] = "DeviceInfo" 52 | }) 53 | ns_server_version = ProtoField.uint8("codesysv3.nsserver.version", "Version", base.HEX) 54 | ns_server_msg_id = ProtoField.uint8("codesysv3.nsserver.msg_id", "Message ID", base.HEX) 55 | ns_server_msg_data = ProtoField.bytes("codesysv3.nsserver.msg_data", "Message Data") 56 | 57 | ns_client_subcmd = ProtoField.uint8("codesysv3.nsserver.subcmd", "subcmd", base.HEX, { 58 | [0xc280] = "DeviceInfo", 59 | }) 60 | ns_client_version = ProtoField.uint8("codesysv3.nsclient.version", "Version", base.HEX) 61 | ns_client_msg_id = ProtoField.uint8("codesysv3.nsclient.msg_id", "Message ID", base.HEX) 62 | 63 | ns_client_max_channels = ProtoField.uint16("codesysv3.nsclient.max_channels", "Max Channels", base.DEC) 64 | ns_client_byte_order = ProtoField.uint8("codesysv3.nsclient.byte_order", "Byte Order", base.DEC, 65 | { 66 | [0] = "Big Endianness", 67 | [1] = "Little Endianness" 68 | }) 69 | ns_client_node_name_length = ProtoField.uint16("codesysv3.nsclient.node_name_length", "Node Name Length", base.DEC) 70 | ns_client_device_name_length = ProtoField.uint16("codesysv3.nsclient.device_name_length", "Device Name Length", base.DEC) 71 | ns_client_vendor_name_length = ProtoField.uint16("codesysv3.nsclient.vendor_name_length", "Vendor Name Length", base.DEC) 72 | ns_client_serial_length = ProtoField.uint16("codesysv3.nsclient.serial_length", "Serial Length", base.DEC) 73 | ns_client_target_type = ProtoField.uint32("codesysv3.nsclient.target_type", "Target Type", base.DEC) 74 | ns_client_target_id = ProtoField.uint32("codesysv3.nsclient.target_id", "Target ID", base.DEC) 75 | ns_client_target_version = ProtoField.string("codesysv3.nsclient.target_version", "Firmware Version") 76 | ns_client_node_name = ProtoField.string("codesysv3.nsclient.node_name", "Node Name", base.UNICODE) 77 | ns_client_device_name = ProtoField.string("codesysv3.nsclient.device_name", "Device Name", base.UNICODE) 78 | ns_client_vendor_name = ProtoField.string("codesysv3.nsclient.vendor_name", "Vendor Name", base.UNICODE) 79 | ns_client_serial = ProtoField.string("codesysv3.nsclient.plc_serial", "Serial") 80 | padding = ProtoField.bytes("codesysv3.padding", "Padding") 81 | 82 | local channel_layer_types = { 83 | [0x01] = "Application Block", 84 | [0x02] = "Application Ack", 85 | [0x03] = "Keep Alive", 86 | [0xc2] = "GetInfo", 87 | [0xc3] = "OpenChannel Request", 88 | [0xc4] = "CloseChannel", 89 | [0x83] = "OpenChannel Response", 90 | [0x84] = "Channel Error", 91 | } 92 | 93 | channel_type = ProtoField.uint8("codesysv3.channel.type", "Type", base.HEX, channel_layer_types) 94 | channel_flags = ProtoField.uint8("codesysv3.channel.flags", "Flags", base.HEX) 95 | channel_error = ProtoField.uint16("codesysv3.channel.error", "Error", base.HEX, { 96 | [0x182] = "Timeout Error", 97 | [0x183] = "Service Layer Error", 98 | [0x184] = "Checksum Error", 99 | }) 100 | channel_version = ProtoField.uint16("codesysv3.channel.version", "Version", base.HEX) 101 | channel_reason = ProtoField.uint16("codesysv3.channel.channel_reason", "Reason", base.HEX, { 102 | [0] = "OK" 103 | }) 104 | channel_channel_id = ProtoField.uint16("codesysv3.channel.channel_id", "Channel ID", base.HEX) 105 | channel_checksum = ProtoField.uint32("codesysv3.channel.checksum", "Checksum(CRC32)", base.HEX) 106 | channel_msg_id = ProtoField.uint32("codesysv3.channel.msg_id", "Msg ID", base.HEX) 107 | channel_max_channels = ProtoField.uint32("codesysv3.channel.max_channels", "Max Channels", base.DEC) 108 | channel_receiver_buffer_size = ProtoField.uint32("codesysv3.channel.receiver_buf_size", "Receiver Buffer Size", base.HEX) 109 | 110 | channel_blk_id = ProtoField.uint32("codesysv3.channel.blk_id", "BLK ID", base.HEX) 111 | channel_ack_id = ProtoField.uint32("codesysv3.channel.ack_id", "ACK ID", base.HEX) 112 | channel_remaining_data_size = ProtoField.uint32("codesysv3.channel.remain_data_size", "Remaining Data Size", base.DEC) 113 | channel_flags_is_request = ProtoField.uint8("codesysv3.channel.flags.is_request", "Is Request", base.HEX, {[1] = "True", [0] = "False"}, 0x80) 114 | channel_flags_is_first_payload = ProtoField.uint8("codesysv3.channel.flags.is_first_payload", "Is First Payload", base.HEX, {[1] = "True", [0] = "False"}, 0x01) 115 | 116 | 117 | service_protocol_id = ProtoField.uint16("codesysv3.service.protocol_id", "Protocol ID", base.HEX, { 118 | [0xcd55] = "Normal", 119 | [0x7557] = "Secure" 120 | }) 121 | 122 | local service_cmd_values = { 123 | [0x18] = "CmpAlarmManager", 124 | [0x02] = "CmpApp", 125 | [0x12] = "CmpAppBP", 126 | [0x13] = "CmpAppForce", 127 | [0x1d] = "CmpCodeMeter", 128 | [0x1f] = "CmpCoreDump", 129 | [0x01] = "CmpDevice", 130 | [0x08] = "CmpFileTransfer", 131 | [0x09] = "CmpIecVarAccess", 132 | [0x0b] = "CmpIoMgr", 133 | [0x05] = "CmpLog", 134 | [0x1b] = "CmpMonitor", 135 | [0x22] = "CmpOpenSSL", 136 | [0x06] = "CmpSettings", 137 | [0x0f] = "CmpTraceMgr", 138 | [0x0c] = "CmpUserMgr", 139 | [0x04] = "CmpVisuServer", 140 | [0x11] = "PlcShell", 141 | [0x07] = "SysEthernet", 142 | 143 | } 144 | 145 | service_header_size = ProtoField.uint16("codesysv3.service.header_size", "Header Size", base.DEC) 146 | service_cmd_group = ProtoField.uint16("codesysv3.service.cmd_group", "Cmd Group", base.HEX, 147 | service_cmd_values , 0x7f) 148 | 149 | local subcmd_device = { 150 | [1] = "Info", 151 | [2] = "Login", 152 | [3] = "Logout", 153 | [4] = "Reset Origin Device", 154 | [5] = "Echo", 155 | [6] = "Set Operating Mode", 156 | [7] = "Get Operating Mode", 157 | [8] = "Interactive Login", 158 | [9] = "Rename Node", 159 | [10] = "Session Create" 160 | } 161 | 162 | local subcmd_app = { 163 | [1] = "Login", 164 | [2] = "Logout", 165 | [3] = "Create Application", 166 | [4] = "Delete", 167 | [5] = "Download", 168 | [6] = "Online Change", 169 | [7] = "Device Download", 170 | [8] = "Create Dev App", 171 | [16] = "Start", 172 | [17] = "Stop", 173 | [18] = "Reset", 174 | [19] = "SetBreakPoint", 175 | [20] = "GetStatus", 176 | [21] = "Delete BP", 177 | [22] = "Read Call Stack", 178 | [23] = "Get Area Offset", 179 | [24] = "Application List", 180 | [25] = "Set Next Statement", 181 | [32] = "Release Force List", 182 | [33] = "Upload Force List", 183 | [34] = "Single Cycle", 184 | [35] = "Create Boot Project", 185 | [36] = "Reinit Application", 186 | [37] = "Application State List", 187 | [38] = "Load Boot App", 188 | [39] = "Register Boot Application", 189 | [40] = "Check FIle Consistency", 190 | [41] = "Read Application Info", 191 | [48] = "Download Compact", 192 | [49] = "Read Project Info", 193 | [50] = "Define Flow", 194 | [51] = "Read Flow Values", 195 | [52] = "Download Encrypted", 196 | [53] = "Read Application Content", 197 | [54] = "Save Retains", 198 | [55] = "Resotre Retains", 199 | [56] = "Get Area Address", 200 | [57] = "Leace Execpoints Active", 201 | [64] = "Claim Execpoints for Application", 202 | 203 | } 204 | 205 | local subcmd_files = { 206 | [0x12] = "Rename Directory", 207 | [0x11] = "Delete Directory", 208 | [0x10] = "Create Directory", 209 | [0x0f] = "Rename File", 210 | [0x0e] = "Delete File", 211 | [0x01] = "FileExist", 212 | [0x03] = "Open File For Write", 213 | [0x02] = "Open File For Write", 214 | [0x04] = "File Write", 215 | [0x05] = "Open File For Read", 216 | [0x06] = "Open File For Read", 217 | [0x07] = "File Read", 218 | [0x08] = "File Close", 219 | [0x09] = "File Close", 220 | [0x0a] = "File Read", 221 | [0x0b] = "File Read", 222 | [0x0c] = "Files List", 223 | [0x0d] = "File Read", 224 | } 225 | 226 | local subcmd_certificates = { 227 | [0x01] = "Import", 228 | [0x02] = "Export", 229 | [0x03] = "Delete", 230 | [0x04] = "Move", 231 | [0x05] = "List", 232 | [0x06] = "ListUseCases", 233 | [0x07] = "CreateSelfSignedCertificate", 234 | [0x09] = "GetStatus", 235 | } 236 | 237 | local subcmd_log = { 238 | [0x01] = "GetEntries", 239 | [0x02] = "GetComponentNames", 240 | [0x03] = "LoggerList", 241 | } 242 | 243 | local subcmd_plc_shell = { 244 | [0x01] = "Execute" 245 | } 246 | 247 | 248 | local subcmd_monitor = { 249 | [1] = "Read", 250 | [2] = "Write" 251 | } 252 | 253 | service_is_response = ProtoField.uint16("codesysv3.service.is_response", "Is Response", base.HEX, {[1] = "True", [0] = "False"}, 0x80) 254 | service_subcmd = ProtoField.uint16("codesysv3.service.subcmd", "subcmd", base.HEX) 255 | service_session_id = ProtoField.uint32("codesysv3.service.session_id", "Session ID", base.HEX) 256 | service_content_size = ProtoField.uint32("codesysv3.service.content_size", "Content Size", base.DEC) 257 | service_additional_data = ProtoField.uint32("codesysv3.service.additional_data", "Additional Data", base.HEX) 258 | 259 | 260 | tag_id = ProtoField.uint32("codesysv3.tags.id", "ID", base.HEX) 261 | tag_type = ProtoField.uint16("codesysv3.tags.type", "Type", base.HEX, {[1] = "Parent", [0] = "Data"}, 0x80) 262 | tag_size = ProtoField.uint32("codesysv3.tags.size", "Size", base.DEC) 263 | tag_data = ProtoField.bytes("codesysv3.tags.data", "Data") 264 | 265 | 266 | codesysv3_protocol.fields = {tcp_magic, tcp_length, magic, hop_count, header_length, priority, signal, type_address, length_data_block, 267 | service_id, message_id, receiver_length, sender_length, receiver_address, sender_address, 268 | ns_server_subcmd, ns_server_version, ns_server_msg_id, ns_server_msg_data, 269 | ns_client_subcmd, ns_client_version, ns_client_msg_id, 270 | ns_client_max_channels, ns_client_byte_order, ns_client_node_name_length, ns_client_device_name_length, ns_client_vendor_name_length, 271 | ns_client_target_type, ns_client_target_id, ns_client_target_version, ns_client_node_name, ns_client_device_name, ns_client_vendor_name, 272 | ns_client_serial_length, ns_client_serial, padding, channel_type, channel_flags, channel_version, channel_msg_id, channel_receiver_buffer_size, channel_checksum, 273 | channel_channel_id, channel_reason, channel_max_channels, channel_blk_id, channel_ack_id, channel_remaining_data_size, channel_flags_is_request, channel_flags_is_first_payload 274 | ,service_protocol_id, service_additional_data, service_cmd_group, service_content_size, service_header_size, service_subcmd, service_session_id, service_is_response 275 | , tag_id, tag_type, tag_size, tag_data, channel_error 276 | } 277 | function codesysv3_protocol.init () 278 | fragments = {} 279 | end 280 | local function has_enough_data(buffer, offset, n) 281 | return buffer:len() - offset >= n 282 | end 283 | 284 | local function dissect_tag_val(buffer, offset) 285 | local toffset = offset 286 | local val = 0 287 | local shift = 0 288 | local finish = false 289 | while not finish and has_enough_data(buffer, toffset, 1) do 290 | local b = buffer(toffset, 1):uint() 291 | val = bit.bor(bit.lshift(bit.band(b, 0x7f), shift), val) 292 | toffset = toffset + 1 293 | shift = shift + 7 294 | if bit.band(b, 0x80) == 0 then 295 | finish = true 296 | end 297 | end 298 | return val, toffset - offset 299 | end 300 | 301 | local function dissect_tag_header(buffer, offset) 302 | local tag_id_val, tag_id_length = dissect_tag_val(buffer, offset) 303 | offset = offset + tag_id_length 304 | local tag_size_val, tag_size_length = dissect_tag_val(buffer, offset) 305 | return tag_id_val, tag_id_length, tag_size_val, tag_size_length 306 | end 307 | local dissect_tags_layer 308 | local function dissect_tag(buffer, offset, tree) 309 | if not has_enough_data(buffer, offset, 1) then 310 | return buffer:len() 311 | end 312 | local tag_id_val, tag_id_length, tag_size_val, tag_size_length = dissect_tag_header(buffer, offset) 313 | local is_parent = tag_id_val >= 0x80 314 | local tag_type_val = "Data" 315 | local header_size = tag_id_length + tag_size_length 316 | 317 | if is_parent then 318 | tag_type_val = "Parent" 319 | end 320 | 321 | if has_enough_data(buffer, offset + header_size, tag_size_val) then 322 | 323 | local subtree = tree:add(codesysv3_protocol, buffer(offset, header_size + tag_size_val), string.format("%s Tag ID(0x%04x)", tag_type_val, tag_id_val)) 324 | subtree:add_le(tag_id, buffer(offset, tag_id_length), tag_id_val) 325 | subtree:add_le(tag_type, buffer(offset, 1)) 326 | subtree:add_le(tag_size, buffer(offset + tag_id_length, tag_size_length), tag_size_val) 327 | offset = offset + header_size 328 | 329 | if tag_size_val > 0 then 330 | if is_parent then 331 | dissect_tags_layer(buffer, subtree, offset, tag_size_val) 332 | else 333 | subtree:add_le(tag_data, buffer(offset, tag_size_val)) 334 | end 335 | offset = offset + tag_size_val 336 | end 337 | else 338 | 339 | offset = buffer:len() 340 | end 341 | return offset 342 | end 343 | 344 | local function add_str_to_field(strs, val, field, unknown_str) 345 | name = strs[val] 346 | if(name ~= nil) then 347 | -- Supported command 348 | field:append_text("[".. name .. "]") 349 | else 350 | -- Command unknownנ 351 | field:append_text(unknown_str) 352 | end 353 | end 354 | 355 | function dissect_tags_layer(buffer, tree, offset, length) 356 | local soffset = offset 357 | while length > 0 and has_enough_data(buffer, soffset, length) do 358 | offset = dissect_tag(buffer, soffset, tree) 359 | length = length - (offset - soffset) 360 | soffset = offset 361 | end 362 | 363 | return soffset 364 | end 365 | 366 | 367 | local function add_info(pinfo, values, exists_format, non_exists_format, val) 368 | if values[val] ~= nil then 369 | pinfo.cols['info']:append(string.format(exists_format, values[val], val)) 370 | else 371 | pinfo.cols['info']:append(string.format(non_exists_format, val)) 372 | end 373 | end 374 | 375 | local function dissect_codesys_service(buffer, pinfo, tree, offset) 376 | if has_enough_data(buffer, offset, 20) then 377 | local subtree = tree:add(codesysv3_protocol, buffer(offset), "Service Layer") 378 | subtree:add_le(service_protocol_id, buffer(offset , 2)) 379 | subtree:add_le(service_header_size, buffer(offset + 2, 2)) 380 | local header_size = buffer(offset + 2, 2):le_uint() 381 | subtree:add_le(service_cmd_group, buffer(offset + 4, 2)) 382 | local cmd_group = bit.band(buffer(offset + 4, 2):le_uint(), 0x7f) 383 | 384 | add_info(pinfo, service_cmd_values, ", Service(CMP: %s(%d)", ", Service(CMP: %d", cmd_group) 385 | subtree:add_le(service_is_response, buffer(offset + 4, 2)) 386 | subcmd_field = subtree:add_le(service_subcmd, buffer(offset + 6, 2)) 387 | subcmd = buffer(offset + 6, 2):le_uint() 388 | subtree:add_le(service_session_id, buffer(offset + 8, 4)) 389 | subtree:add_le(service_content_size, buffer(offset + 12, 4)) 390 | local content_size = buffer(offset + 12, 4):le_uint() 391 | if header_size == 16 then 392 | subtree:add_le(service_additional_data, buffer(offset + 16, 4)) 393 | end 394 | 395 | if header_size == 16 or header_size == 12 then 396 | if cmd_group == 1 then 397 | add_str_to_field(subcmd_device, subcmd, subcmd_field, "[Unknown command]") 398 | add_info(pinfo, subcmd_device, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 399 | elseif cmd_group == 0x8 then 400 | add_str_to_field(subcmd_files, subcmd, subcmd_field, "[Unknown command]") 401 | add_info(pinfo, subcmd_files, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 402 | elseif cmd_group == 0x22 then 403 | add_str_to_field(subcmd_certificates, subcmd, subcmd_field, "[Unknown command]") 404 | add_info(pinfo, subcmd_certificates, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 405 | elseif cmd_group == 5 then 406 | add_str_to_field(subcmd_log, subcmd, subcmd_field, "[Unknown command]") 407 | add_info(pinfo, subcmd_log, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 408 | elseif cmd_group == 2 then 409 | add_str_to_field(subcmd_app, subcmd, subcmd_field, "[Unknown command]") 410 | add_info(pinfo, subcmd_app, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 411 | elseif cmd_group == 0x1b then 412 | add_str_to_field(subcmd_monitor, subcmd, subcmd_field, "[Unknown command]") 413 | add_info(pinfo, subcmd_monitor, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 414 | elseif cmd_group == 0x11 then 415 | add_str_to_field(subcmd_plc_shell, subcmd, subcmd_field, "[Unknown command]") 416 | add_info(pinfo, subcmd_plc_shell, ", cmd: %s(%d))", ", cmd: %d)", subcmd) 417 | end 418 | if bit.band(buffer(offset + 4, 2):le_uint(), 0x80) ~= 0 then 419 | pinfo.cols['info']:append(", Response") 420 | else 421 | pinfo.cols['info']:append(", Request") 422 | end 423 | 424 | 425 | local tagstree = tree:add(codesysv3_protocol, buffer(offset + header_size + 4), "Tags Layer") 426 | -- Add 4 to the offset(2 for the protocol id and 2 for the content length) 427 | return dissect_tags_layer(buffer, tagstree, offset + header_size + 4, content_size) 428 | end 429 | 430 | end 431 | 432 | return buffer:len() 433 | 434 | end 435 | 436 | local function dissect_codesys_channel(buffer, pinfo, tree, offset) 437 | if has_enough_data(buffer, offset, 2) then 438 | local subtree = tree:add(codesysv3_protocol, buffer(offset), "Channel Layer") 439 | 440 | subtree:add(channel_type, buffer(offset, 1)) 441 | type = buffer(offset, 1):uint() 442 | add_info(pinfo, channel_layer_types, ", Channel(%s(%d))", ", Channel(%d)", type) 443 | subtree:add(channel_flags, buffer(offset + 1, 1)) 444 | offset = offset + 2 445 | if bit.band(type, 0x80) ~= 0 and has_enough_data(buffer, offset, 6) then 446 | subtree:add_le(channel_version, buffer(offset, 2)) 447 | subtree:add_le(channel_checksum, buffer(offset + 2, 4)) 448 | 449 | if has_enough_data(buffer, offset + 6, 8) and type == 0xc3 then 450 | subtree:add_le(channel_msg_id, buffer(offset + 6, 4)) 451 | subtree:add_le(channel_receiver_buffer_size, buffer(offset + 10, 4)) 452 | elseif has_enough_data(buffer, offset + 6, 12) and type == 0x83 then 453 | subtree:add_le(channel_msg_id, buffer(offset + 6, 4)) 454 | subtree:add_le(channel_reason, buffer(offset + 10, 2)) 455 | subtree:add_le(channel_channel_id, buffer(offset + 12, 2)) 456 | subtree:add_le(channel_receiver_buffer_size, buffer(offset + 14, 4)) 457 | elseif has_enough_data(buffer, offset + 6, 4) and type == 0x84 then 458 | subtree:add_le(channel_channel_id, buffer(offset + 6, 2)) 459 | subtree:add_le(channel_error, buffer(offset + 8, 2)) 460 | pinfo.cols['info']:append(string.format(", (Channel: 0x%04x)", buffer(offset + 6, 2):le_uint())) 461 | elseif has_enough_data(buffer, offset + 6, 4) and type == 0xc4 then 462 | subtree:add_le(channel_channel_id, buffer(offset + 6, 2)) 463 | subtree:add_le(channel_reason, buffer(offset + 8, 2)) 464 | pinfo.cols['info']:append(string.format(", (Channel: 0x%04x)", buffer(offset + 6, 2):le_uint())) 465 | elseif has_enough_data(buffer, offset + 6, 4) and type == 0xc2 then 466 | subtree:add_le(channel_max_channels, buffer(offset + 6, 2)) 467 | end 468 | 469 | elseif type == 1 and has_enough_data(buffer, offset + 6, 18) then 470 | subtree:add(channel_flags_is_request, buffer(offset - 1, 1)) 471 | subtree:add(channel_flags_is_first_payload, buffer(offset - 1, 1)) 472 | subtree:add_le(channel_channel_id, buffer(offset, 2)) 473 | subtree:add_le(channel_blk_id, buffer(offset + 2, 4)) 474 | subtree:add_le(channel_ack_id, buffer(offset + 6, 4)) 475 | 476 | pinfo.cols['info']:append(string.format(", (Channel: 0x%04x, BLK ID:0x%08x, ACK ID: 0x%08x)", buffer(offset, 2):le_uint(), buffer(offset + 2, 4):le_uint(), buffer(offset + 6, 4):le_uint())) 477 | 478 | 479 | local is_first_packet = bit.band(buffer(offset - 1, 1):uint(), 0x01) ~= 0 480 | local next_layer_data = nil 481 | local segment_size = nil 482 | if is_first_packet then 483 | subtree:add_le(channel_remaining_data_size, buffer(offset + 10, 4)) 484 | subtree:add_le(channel_checksum, buffer(offset + 14, 4)) 485 | next_layer_data = offset + 18 486 | segment_size = buffer(offset + 10, 4):le_uint() 487 | else 488 | next_layer_data = offset + 10 489 | segment_size = buffer(offset + 10):len() 490 | end 491 | if is_first_packet and has_enough_data(buffer, offset + 18, segment_size) then 492 | return dissect_codesys_service(buffer, pinfo, tree, offset + 18) 493 | else 494 | local key = ("%s:%i:%s:%i:%i:%i"):format(pinfo.src, pinfo.src_port, pinfo.dst, pinfo.dst_port,buffer(offset, 2):le_uint(), buffer(offset + 6, 4):le_uint()) 495 | local blk_id = buffer(offset + 2, 4):le_uint() 496 | if fragments[key] == nil then 497 | fragments[key] = {["total_size"] = 0, ["segs"] = {}, ["blk_id"] = 0, ["collected_size"] = 0} 498 | end 499 | if is_first_packet then 500 | fragments[key]["total_size"] = segment_size 501 | fragments[key]["blk_id"] = blk_id 502 | end 503 | 504 | if fragments[key]["segs"][blk_id] == nil then 505 | fragments[key]["segs"][blk_id] = buffer(next_layer_data):bytes() 506 | fragments[key]["collected_size"] = fragments[key]["collected_size"] + buffer(next_layer_data):len() 507 | end 508 | if fragments[key]["collected_size"] == fragments[key]["total_size"] and fragments[key]["total_size"] >0 then 509 | local complete_service_layer = ByteArray.new() 510 | local count = 0 511 | for i = fragments[key]["blk_id"], blk_id, 1 do 512 | complete_service_layer = complete_service_layer..fragments[key]["segs"][i] 513 | count = count + 1 514 | end 515 | if fragments[key]["total_size"] == complete_service_layer:len() then 516 | local newtvb = ByteArray.tvb(complete_service_layer, "Defragmented Service Layer") 517 | return dissect_codesys_service(newtvb, pinfo, tree, 0) 518 | end 519 | end 520 | end 521 | 522 | 523 | 524 | 525 | elseif type == 2 and has_enough_data(buffer, offset, 6) then 526 | subtree:add_le(channel_channel_id, buffer(offset, 2)) 527 | subtree:add_le(channel_blk_id, buffer(offset + 2, 4)) 528 | pinfo.cols['info']:append(string.format(", (Channel: 0x%04x, BLK ID:0x%08x)", buffer(offset, 2):le_uint(), buffer(offset + 2, 4):le_uint())) 529 | elseif type == 3 and has_enough_data(buffer, offset, 2) then 530 | subtree:add_le(channel_channel_id, buffer(offset, 2)) 531 | pinfo.cols['info']:append(string.format(", (Channel: 0x%04x)", buffer(offset, 2):le_uint())) 532 | end 533 | end 534 | 535 | return buffer:len() 536 | end 537 | 538 | 539 | local function dissect_codesys_nsserver(buffer, pinfo, tree, offset) 540 | if has_enough_data(buffer, offset, 8) then 541 | local subtree = tree:add(codesysv3_protocol, buffer(offset), "NS Server") 542 | subtree:add_le(ns_server_subcmd, buffer(offset, 2)) 543 | subtree:add_le(ns_server_version, buffer(offset + 2, 2)) 544 | subtree:add_le(ns_server_msg_id, buffer(offset + 4, 4)) 545 | if has_enough_data(buffer, offset + 8, 1) then 546 | subtree:add(ns_server_msg_data, buffer(offset + 8)) 547 | end 548 | end 549 | 550 | return buffer:len() 551 | end 552 | 553 | local function dissect_codesys_nsclient(buffer, pinfo, tree, offset) 554 | if has_enough_data(buffer, offset, 8) then 555 | local subtree = tree:add(codesysv3_protocol, buffer(offset), "NS Client") 556 | subtree:add_le(ns_client_subcmd, buffer(offset, 2)) 557 | subtree:add_le(ns_client_version, buffer(offset + 2, 2)) 558 | subtree:add_le(ns_client_msg_id, buffer(offset + 4, 4)) 559 | local version = buffer(offset + 2, 2):le_uint() 560 | local ns_subcmd = buffer(offset, 2):le_uint() 561 | offset = offset + 8 562 | if has_enough_data(buffer, offset + 8, 24) and (version == 0x103 or version == 0x400) and ns_subcmd == 0xc280 then 563 | subtree:add_le(ns_client_max_channels, buffer(offset, 2)) 564 | subtree:add_le(ns_client_byte_order, buffer(offset + 2, 1)) 565 | local node_name_offset = buffer(offset + 4, 2):uint() 566 | subtree:add_le(ns_client_node_name_length, buffer(offset + 6, 2)) 567 | local node_name_length = buffer(offset + 6, 2):le_uint() * 2 + 2 568 | subtree:add_le(ns_client_device_name_length, buffer(offset + 8, 2)) 569 | local device_name_length = buffer(offset + 8, 2):le_uint() * 2 + 2 570 | subtree:add_le(ns_client_vendor_name_length, buffer(offset + 10, 2)) 571 | local vendor_name_length = buffer(offset + 10, 2):le_uint() * 2 + 2 572 | 573 | subtree:add_le(ns_client_target_type, buffer(offset + 12, 4)) 574 | subtree:add_le(ns_client_target_id, buffer(offset + 16, 4)) 575 | local firmware = string.format("V%d.%d.%d.%d", buffer(offset + 23, 1):uint(), 576 | buffer(offset + 22, 1):uint(), 577 | buffer(offset + 21, 1):uint(), 578 | buffer(offset + 20, 1):uint()) 579 | 580 | subtree:add(ns_client_target_version, firmware) 581 | subtree:add(ns_client_serial_length, buffer(offset + 28, 1)) 582 | local serial_length = buffer(offset + 28, 1):le_uint() 583 | offset = offset + 40 + node_name_offset 584 | if has_enough_data(buffer, offset, node_name_length) then 585 | subtree:add_le(ns_client_node_name, buffer(offset, node_name_length), buffer(offset, node_name_length):le_ustring()) 586 | end 587 | offset = offset + node_name_length 588 | if has_enough_data(buffer, offset, device_name_length) then 589 | subtree:add_le(ns_client_device_name, buffer(offset, device_name_length), buffer(offset, device_name_length):le_ustring()) 590 | end 591 | offset = offset + device_name_length 592 | if has_enough_data(buffer, offset, vendor_name_length) then 593 | subtree:add_le(ns_client_vendor_name, buffer(offset, vendor_name_length), buffer(offset, vendor_name_length):le_ustring()) 594 | end 595 | offset = offset + vendor_name_length 596 | if has_enough_data(buffer, offset, serial_length) then 597 | subtree:add(ns_client_serial, buffer(offset, serial_length)) 598 | end 599 | 600 | end 601 | end 602 | 603 | return buffer:len() 604 | end 605 | 606 | 607 | local function dissect_codesys_pdu(buffer, pinfo, tree, offset, is_udp) 608 | 609 | 610 | if has_enough_data(buffer, offset, 6) then 611 | local lengths_byte = buffer(offset + 5, 1):uint() 612 | local address_length = bit.rshift(lengths_byte, 4) * 2 + bit.band(lengths_byte, 0x0F) * 2 613 | local subtree = tree:add(codesysv3_protocol, buffer(offset, 6 + address_length), "Datagram Layer") 614 | 615 | 616 | subtree:add(magic, buffer(offset, 1)) 617 | 618 | local hopsubtree = subtree:add(codesysv3_protocol, buffer(offset + 1, 1), string.format("Hop Info Byte(0x%x)", buffer(offset + 1, 1):uint())) 619 | hopsubtree:add(hop_count, buffer(offset + 1, 1)) 620 | hopsubtree:add(header_length, buffer(offset + 1, 1)) 621 | local header_size = bit.band(buffer(offset + 1, 1):uint(), 0x07) * 2 622 | local packetinfosubtree = subtree:add(codesysv3_protocol, buffer(offset + 2, 1), string.format("Packet Info Byte(0x%x)", buffer(offset + 2, 1):uint())) 623 | packetinfosubtree:add(priority, buffer(offset + 2, 1)) 624 | packetinfosubtree:add(signal, buffer(offset + 2, 1)) 625 | packetinfosubtree:add(type_address, buffer(offset + 2, 1)) 626 | packetinfosubtree:add(length_data_block, buffer(offset + 2, 1)) 627 | 628 | subtree:add(service_id, buffer(offset + 3, 1)) 629 | local service = buffer(offset + 3, 1):uint() 630 | subtree:add(message_id, buffer(offset + 4, 1)) 631 | pinfo.cols['info']:clear() 632 | add_info(pinfo, datagram_layer_services, "Datagram(%s(%d))", "Datagram(%d)", service) 633 | local address_lengths = subtree:add(codesysv3_protocol, buffer(offset + 5, 1), string.format("Packet Info Byte(0x%x)", lengths_byte)) 634 | address_lengths:add(sender_length, bit.rshift(lengths_byte, 4) * 2) 635 | address_lengths:add(receiver_length, bit.band(lengths_byte, 0x0F) * 2) 636 | local sender_len = bit.rshift(lengths_byte, 4) * 2 637 | local receiver_len = bit.band(lengths_byte, 0x0F) * 2 638 | offset = offset + header_size 639 | address_length = sender_len + receiver_len 640 | local address_tree = subtree:add(codesysv3_protocol, buffer(offset, address_length), "Network Addresses") 641 | if receiver_len > 0 then 642 | address_tree:add(receiver_address, buffer(offset, receiver_len)) 643 | end 644 | if sender_len > 0 then 645 | address_tree:add(sender_address, buffer(offset + receiver_len, sender_len)) 646 | end 647 | 648 | 649 | offset = offset + address_length 650 | padding_len = math.fmod(offset, 4) 651 | if padding_len ~= 0 then 652 | subtree:add(padding, buffer(offset, padding_len)) 653 | offset = offset + padding_len 654 | end 655 | if service == 3 then 656 | return dissect_codesys_nsserver(buffer, pinfo, tree, offset) 657 | 658 | elseif service == 4 then 659 | return dissect_codesys_nsclient(buffer, pinfo, tree, offset) 660 | 661 | elseif service == 64 then 662 | return dissect_codesys_channel(buffer, pinfo, tree, offset) 663 | end 664 | 665 | end 666 | return buffer:len() 667 | end 668 | 669 | local function dissect_codesys_udp(buffer, pinfo, tree) 670 | pinfo.cols.protocol = codesysv3_protocol.name 671 | 672 | local subtree = tree:add(codesysv3_protocol, buffer(), "CodeSys V3 Protocol UDP") 673 | 674 | return dissect_codesys_pdu(buffer, pinfo, subtree, 0, true) 675 | end 676 | 677 | local function dissect_codesys_tcp(buffer, pinfo, tree) 678 | pinfo.cols.protocol = codesysv3_protocol.name 679 | 680 | local subtree = tree:add(codesysv3_protocol, buffer(), "CodeSys V3 Protocol TCP") 681 | local subtree_tcp = subtree:add(codesysv3_protocol, buffer(), "Block Driver Layer") 682 | if buffer:len() >= 8 then 683 | subtree_tcp:add(tcp_magic, buffer(0, 4)) 684 | subtree_tcp:add_le(tcp_length, buffer(4, 4)) 685 | return dissect_codesys_pdu(buffer, pinfo, subtree, 8, false) 686 | end 687 | return buffer:len() 688 | end 689 | 690 | local function get_codesysv3_length(tvbuf, pktinfo, offset) 691 | return tvbuf(4, 4):le_uint() 692 | end 693 | 694 | function codesysv3_protocol.dissector(buffer, pinfo, tree) 695 | if buffer:len() >= 8 and buffer(0, 4):uint() == 0x000117e8 then 696 | dissect_tcp_pdus(buffer, tree, 8, get_codesysv3_length, dissect_codesys_tcp) 697 | return buffer:len() 698 | elseif buffer:len() >= 5 and buffer(0, 1):uint() == 0xc5 then 699 | return dissect_codesys_udp(buffer, pinfo, tree) 700 | end 701 | 702 | return 0 703 | end 704 | 705 | local tcp_port = DissectorTable.get("tcp.port") 706 | tcp_port:add(11740, codesysv3_protocol) 707 | tcp_port:add(11741, codesysv3_protocol) 708 | tcp_port:add(11742, codesysv3_protocol) 709 | tcp_port:add(11743, codesysv3_protocol) 710 | 711 | 712 | local udp_port = DissectorTable.get("udp.port") 713 | udp_port:add(1740, codesysv3_protocol) 714 | udp_port:add(1741, codesysv3_protocol) 715 | udp_port:add(1742, codesysv3_protocol) 716 | udp_port:add(1743, codesysv3_protocol) -------------------------------------------------------------------------------- /Wireshark Dissector/example.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/CoDe16/de4c624517707213c1e353c5626798dea34e2445/Wireshark Dissector/example.pcap --------------------------------------------------------------------------------