├── README.md ├── pcom2tcp.py ├── pcom_client.py └── requirements.txt /README.md: -------------------------------------------------------------------------------- 1 | # Unitronics Forensic Tools / by Claroty Team82 2 | 3 | ### TL;DR 4 | Unitronics Forensic Tools, including a modular PCOM client, as well as PCOM to TCP converter (and vice versa). 5 | 6 | Using these tools, we implemented a custom PCOM client (Unitronics Vision/Samba communication protocol), allowing users to connect to their PLC using either serial/TCP, and query information from it. 7 | These tools main goal is to enable users to extract forensic infromation from attacked Unitronics PLCs. 8 | 9 | ### Background 10 | Due to [recent attacks](https://claroty.com/team82/blog/opportunistic-hacktivists-target-plcs-at-us-water-facility) on Unitronics Vision/Samba PLCs, we research the communication protocol implemented by these PLC/HMI devices. The goal of our research was to develop a toolset enabling users to extract forensic information from attacked PLCs. 11 | 12 | At the end of our research, we developed two tools - 13 | - `PCOMClient`: a PCOM client, enabling users to connect to their Unitronics Vision/Samba series PLCs/HMIs. In this module, we support PCOM Serial, PCOM TCP, PCOM ASCII and PCOM Binary messages, as well as support a wide range of built-in function codes/procedures. This tool in specific allows users to extract forensic information from their PLC. 14 | 15 | - `PCOM2TCP`: This tool allows users to convert PCOM messages from PCOM TCP to PCOM Serial and vice versa. This allows users with only serial connection to the PLC to connect to it using PCOM TCP, and sniff packets. 16 | 17 | 18 | 19 | ### What's supported? 20 | - `PCOM Serial` 21 | - `PCOM\TCP` 22 | - `PCOM ASCII` 23 | - `PCOM Binary` 24 | - `Supported PCOM Function Codes` - 23 opcodes 25 | - `Supported Procedures` - read/write raw memory, upload project, read resources etc. 26 | - `Forensic Information Extraction` - read PLC infromation (version, name, UnitID etc.), dump and parse siganture table. 27 | 28 | 29 | ### Usage 30 | Basic Usage: `python pcom_client.py SERVER_IP` 31 | Main functionalities: 32 | 1. Setup PCOM Client - 33 | ``` 34 | ip_addr = sys.argv[1] 35 | pcom = PCOM_CLIENT(isTcp=True, debugMode=False) 36 | s = socket.socket() 37 | port = 20256 38 | s.connect((ip_addr, port)) 39 | 40 | # You are connected, now invoke any function you choose. 41 | print_plc_name(s, pcom) 42 | print_plc_firmware(s, pcom) 43 | ``` 44 | 2. Extract Information About PLC - 45 | ``` 46 | print_plc_name(s, pcom) 47 | print_plc_firmware(s, pcom) 48 | print_plc_unitid(s, pcom) 49 | ``` 50 | 51 | 3. Extract Signature Table - 52 | ``` 53 | print_signature_table(s, pcom) 54 | ``` 55 | 56 | 4. Read Project File: 57 | ``` 58 | read_and_save_project_zip(s, pcom) 59 | ``` 60 | 61 | 5. Read/Write RAM: 62 | ``` 63 | # Read memory: 64 | location_id = 1 # 1/4 - identifies memory type 65 | start_addr = 0 # Read from 66 | end_addr = 0x100 # Read until 67 | 68 | read_memory(s, pcom, location_id, start_addr, end_addr) 69 | 70 | # Write memory (use with caution): 71 | location_id = 1 # 1/4 - identifies memory type 72 | addr = 0 # Write to 73 | data = b'Claroty Team82' # Data to write 74 | 75 | write_memory(s, pcom, addr, data, location_id) 76 | ``` 77 | 78 | 79 | ### Function Codes 80 | ---------------------------------------------------- 81 | | Function Code (req/resp) | Description | 82 | |--------------------------|-----------------------| 83 | | 0x01 / 0x81 | Read Memory | 84 | | 0x02 / 0x82 | Check Password | 85 | | 0x0C / 0x8C | Get PLC Name | 86 | | 0x10 / 0x90 | Find resources | 87 | | 0x16 / 0x96 | Translate Resource Index to Address* | 88 | | 0x1A / 0x9A | Flush Memory Buffer | 89 | | 0x41 / 0xC1 | Write Memory | 90 | | 0x42 / 0xC2 | Reset Upload Password ([CVE-2024-38434](https://claroty.com/team82/disclosure-dashboard/cve-2024-38434)) | 91 | | 0x4D / 0xCD | Read Operand | 92 | | 0xFF | Error | 93 | | ID (ASCII) | Get PLC ID | 94 | | UG (ASCII) | Get PLC UnitID | 95 | | GF (ASCII) | Get PLC Version | 96 | 97 | 98 | ### Forensic Information 99 | 100 | Using our PCOMClient tool, users which their PLC was attacked can extract *TONS* of forensic information from their PLC, containig details about the attack itself, as well as on the attacker's computer and setup. 101 | Our tool allows users to extract forensic information using two methods: 102 | 103 | - Project Upload - using the `read_and_save_project_zip` function, it is possible to extract the project from the PLC (only available if the project was "burned" - downloaded using "Download & Burn"). The project is an encrypted zip file (with an hardcoded password), containing an Access DB file. In this Access DB file, there is a lot of information about the project creator PC. 104 | 105 | - Signature Table - in the Unitronics ecosystem, the Signature Table is a structure containg data about PLC connectsions, as well as the PC of the user connecting to it. using the `print_signature_table` function, it is possible to extract the Signature Table from the PLC. 106 | 107 | 108 | Here is a table showing all forensic evidence possible for extraction from the PLC: 109 | ------------------------------------------------------------------------------ 110 | | Forensic Evidence | Is Inside Siganture Table | Is Inside Project File | 111 | |-----------------------|---------------------------|------------------------| 112 | | Project Path | Yes | Yes | 113 | | PC Username | Yes | No (could be in path) | 114 | | Project Creation Date | No | Yes | 115 | | PLC Connection Date | Yes | Yes | 116 | | Computer Keyboards | Yes | Yes | 117 | | PLC Connection String | Yes | Yes | 118 | | Project Images | No | Yes | 119 | | Project Functions | No | Yes | 120 | 121 | 122 | 123 | 124 | ### How to use 125 | ``` 126 | git clone https://github.com/claroty/pcom-forensic-tools.git 127 | cd pcom-forensic-tools 128 | python3 -m venv venv 129 | source ./venv/bin/activate 130 | pip install -r requirements.txt 131 | ``` 132 | then for example, `python pcom_client.py 1.2.3.4` 133 | 134 | -------------------------------------------------------------------------------- /pcom2tcp.py: -------------------------------------------------------------------------------- 1 | import serial 2 | import struct 3 | import time 4 | import struct 5 | import socket 6 | import binascii 7 | import sys 8 | 9 | # User configable - change to your setup 10 | COM_PORT = "COM1" 11 | COM_RATE = 115200 # Old series - 57600 12 | PCOM_PORT = 20256 13 | 14 | 15 | # Consts 16 | BINARY_PREFIX = b"/_" 17 | ASCII_REQUEST = b"/0" 18 | ASCII_RESPONSE = b"/A" 19 | ASCII_TERMINATE = ord("\r") 20 | BINARY_TERMINATE = b"\\" 21 | 22 | 23 | # This function reads PCOM message (Binary/ASCII) and returns the 24 | # message, or 0 if encountered an error 25 | def read_until(reader): 26 | 27 | # Read magic 28 | magic = reader.read(size=2) 29 | 30 | # If no magic - return error 31 | if not magic: 32 | return 0 33 | 34 | # Match magic to correct packet type 35 | if magic == BINARY_PREFIX: 36 | rest_of_header = reader.read(size=22) 37 | if len(rest_of_header) != 22: 38 | return 0 39 | 40 | header = magic + rest_of_header 41 | packet_length = struct.unpack("PCOM: {data}") 81 | forwarder.write(com_data) 82 | pcom_response = read_until(forwarder) 83 | 84 | # If did not get data - try retransmittion 85 | if not pcom_response: 86 | continue 87 | 88 | tcp_response = transaction_id 89 | tcp_response += protocol_type 90 | tcp_response += b"\x00" 91 | tcp_response += struct.pack("TCP: {tcp_response}") 95 | conn.send(tcp_response) 96 | 97 | 98 | if __name__ == "__main__": 99 | main() -------------------------------------------------------------------------------- /pcom_client.py: -------------------------------------------------------------------------------- 1 | import time 2 | import zlib 3 | import socket 4 | import re 5 | import binascii 6 | import struct 7 | import sys 8 | import datetime 9 | import random 10 | from io import BytesIO 11 | 12 | # Helper functions 13 | def tx(d): 14 | return binascii.hexlify(d).decode() 15 | 16 | def up_I(r): 17 | return struct.unpack(" None: 54 | self.isTcp = isTcp 55 | self.debugMode = debugMode 56 | 57 | 58 | def print_pcom_binary(self, data): 59 | opcode_table = { 60 | b"\x01": "Read Memory Reqeust", 61 | b"\x81": "Read Memory Response", 62 | b"\x02": "Check Password", 63 | b"\x82": "Password OK", 64 | b"\x0c": "Get PLC Name Request", 65 | b"\x8C": "Get PLC Name Response", 66 | b"\x10": "Where Resource Reqeust", 67 | b"\x90": "Where Resource Response", 68 | b"\x16": "Translate Index to Address Request", 69 | b"\x96": "Translate Index to Address Response", 70 | b"\x1A": "Flash Memory Buffer Request", 71 | b"\x9A": "Flash Memory Buffer Response", 72 | b"\x41": "Write Memory Reqeust", 73 | b"\xC1": "Write Memory Response", 74 | b"\x4D": "Read Operand Request", 75 | b"\xCD": "Read Operand Response", 76 | b"\xFF": "Error", 77 | } 78 | opcode = data[12:13] 79 | opcode_message = opcode_table.get(opcode, "") 80 | if int.from_bytes(opcode, 'little') & 0b10000000: 81 | direction = "Client <--- Server" 82 | else: 83 | direction = "Client ---> Server" 84 | 85 | print(f"{direction}: Binary PCOM Command {opcode_message} ({hex(int.from_bytes(opcode, 'little'))})") 86 | 87 | 88 | def print_pcom_ascii(self, data): 89 | opcode_table = { 90 | b"ID": "Get PLC Version ", 91 | b"UG": "Get UnitID Command ", 92 | b"GF": "Read Integers ", 93 | b"CCS": "Stop PLC " 94 | } 95 | if data[0:2] == b"/A": 96 | direction = "Client (EWS) <--- Server (PLC)" 97 | type_of_msg = "Response" 98 | opcode = data[4:6] 99 | else: 100 | direction = "Client (EWS) ---> Server (PLC)" 101 | type_of_message = "Request" 102 | opcode = data[3:5] 103 | 104 | opcode_message = opcode_table.get(opcode, "") 105 | print(f"{direction}: ASCII PCOM Command {opcode_message} ({opcode.decode()})") 106 | 107 | 108 | def extract_pcom_data(self, data): 109 | # Move after the PCOM/TCP header 110 | if self.isTcp: 111 | data = data[self.PCOM_TCP_HEADER_SIZE:] 112 | if self.debugMode: 113 | self.print_pcom_binary(data) 114 | return data[self.PCOM_HEADER_SIZE:-self.PCOM_BINARY_FOOTER_SIZE] 115 | 116 | 117 | def make_tcp(self,data, isAscii=False): 118 | tcp = struct.pack(" 4.10.36 B (OS) 254 | # 002 002 53 P --> 2.2.53 P (BOOT) 255 | # 000 000 43 F --> 0.0.43 F (BinLib / Force) 256 | # 11100001 --> ????? 257 | 258 | data = data.decode() 259 | data = data[self.PCOM_TCP_HEADER_SIZE:-self.PCOM_BINARY_FOOTER_SIZE] 260 | REGEX_FW_SINGLE = "(([0-9x]{3})([0-9x]{3})([0-9x]{2})([A-Z]+))" 261 | res = {} 262 | model_len = 6 263 | 264 | while self.MODELS.get(data[:model_len]) is None: 265 | model_len -= 1 266 | if model_len == 2: 267 | return data 268 | 269 | res["model"] = data[:model_len] 270 | res["model_desc"] = self.MODELS.get(data[:model_len]) 271 | res["hw_rev"] = data[model_len:model_len+1] 272 | res["fw_ver"] = [] 273 | sub_fw = re.findall(REGEX_FW_SINGLE, data) 274 | 275 | for match in sub_fw: 276 | full_match, ver, ver_major, ver_minor, ver_type = match 277 | if ver_type == "FT": 278 | 279 | # 03100020 --> 0-3.10 (20) 280 | # 11100001 --> 1-1.10 (10) 281 | version = f"{full_match[0:1]}-{full_match[1:2]}.{full_match[2:4]} ({full_match[4:8]})" 282 | 283 | else: 284 | ver = int(ver) if ver.isdigit() else ver 285 | ver_major = int(ver_major) if ver_major.isdigit() else ver_major 286 | ver_minor = int(ver_minor) if ver_minor.isdigit() else ver_minor 287 | version = f"{ver}.{str(ver_major).zfill(3)} ({str(ver_minor).zfill(2)})" # 2.011 (02) 288 | 289 | res["fw_ver"].append({"version": version, "type": ver_type}) 290 | 291 | leftovers = data.split(full_match)[-1] 292 | res["leftover"] = leftovers 293 | return res 294 | 295 | 296 | # Binary Request 297 | def create_read_operand(self): 298 | command_opcode = 0x4d 299 | command_details = b"\x00\x00\x00\x00\x02\x00" # Const for read_operand 300 | command_data = b"\x01\x00\x02\xff\x8d\x00\x03\x00\x11\xff\x00\x00\x09\x00\x03\x00" # Const for read Operand 301 | return self.create_binary_request(command_opcode=command_opcode, command_details=command_details, command_data=command_data) 302 | 303 | 304 | # Binary Response 305 | def parse_read_operand(self,data): 306 | 307 | # Move after the PCOM/TCP header 308 | if self.isTcp: 309 | data = data[self.PCOM_TCP_HEADER_SIZE:] 310 | 311 | if self.debugMode: 312 | self.print_pcom_binary(data) 313 | 314 | # Not interesting data for us here 315 | pass 316 | 317 | 318 | # ASCII Requset 319 | def create_stop_command(self): 320 | request = self.ascii_header_magic # Magic (/) 321 | request += b"00" # Unit ID (00 const) 322 | request += b"CCS" # Opcode 323 | request += self.calc_ascii_crc(request) 324 | request += self.ascii_suffix 325 | 326 | if self.debugMode: 327 | self.print_pcom_ascii(request) 328 | 329 | if self.isTcp: 330 | return self.make_tcp(request, isAscii=True) 331 | 332 | return request 333 | 334 | 335 | # ASCII Requset 336 | def create_read_unitID(self): 337 | request = self.ascii_header_magic # Magic (/) 338 | request += b"00" # Unit ID (00 const) 339 | request += b"UG" # Opcode 340 | request += self.calc_ascii_crc(request) 341 | request += self.ascii_suffix 342 | 343 | if self.debugMode: 344 | self.print_pcom_ascii(request) 345 | 346 | if self.isTcp: 347 | return self.make_tcp(request, isAscii=True) 348 | 349 | return request 350 | 351 | 352 | # ASCII Response 353 | def parse_read_unitID(self,data): 354 | 355 | # Move after the PCOM/TCP header 356 | if self.isTcp: 357 | data = data[self.PCOM_TCP_HEADER_SIZE:] 358 | 359 | if self.debugMode: 360 | self.print_pcom_ascii(data) 361 | 362 | return data[self.PCOM_TCP_HEADER_SIZE:-self.PCOM_BINARY_FOOTER_SIZE] 363 | 364 | 365 | # Binary Request 366 | def create_where_resource(self, resource_id): 367 | opcode = 0x10 368 | res1 = b"\xfe" 369 | res2 = b"\x01" 370 | res3 = b"\x00\x00\x00" 371 | res4 = struct.pack("b", resource_id) 372 | command_details = b"\x00"*6 373 | return self.create_binary_request(command_opcode=opcode, res1=res1, res2=res2, res3=res3, res4=res4, command_details=command_details) 374 | 375 | 376 | # Binary Response 377 | def parse_where_resource(self, data): 378 | 379 | # Move after the PCOM/TCP header 380 | if self.isTcp: 381 | data = data[self.PCOM_TCP_HEADER_SIZE:] 382 | 383 | if self.debugMode: 384 | self.print_pcom_binary(data) 385 | 386 | addr, size, res_id = struct.unpack(" 24 + 9*4 418 | index = struct.unpack(" block_size > 200: 520 | body = reader.read(block_size-4-4-4-18-4) 521 | else: 522 | block_size = 1000 # default 523 | body = reader.read(block_size-4-4-4-18-4) 524 | 525 | if not (reader.tell() < len(data)): 526 | footer = int.from_bytes(body[-4:],"little") 527 | body = body[:-4] 528 | else: 529 | footer = up_I(reader) 530 | 531 | try: 532 | decompressed_body = zlib.decompress(body[11:],-15) 533 | print(f"\t\t[-] Decompressed Body: {len(decompressed_body)} bytes") 534 | self.parse_decompressed(decompressed_body) 535 | print(f"\t\t[-] Footer: {hex(footer)}") 536 | except Exception as e: 537 | pass 538 | 539 | 540 | def parse_decompressed(self, data): 541 | 542 | try: 543 | 544 | # data[0x4:0xc] - PC Date 545 | pc_date_double, = struct.unpack('Idx Downloaded: {page_idx_bit} (CRC={page_idx})', None) 621 | 622 | print_body_info(f'eDT_Reserved2 Downloaded: {eDT_Reserved2_bit} (CRC={eDT_Reserved2})', None) 623 | 624 | print_body_info(f'Variables Downloaded: {variables_bit} (CRC={variables})', None) 625 | 626 | print_body_info(f'eDT_Reserved3 Downloaded: {eDT_Reserved3_bit} (CRC={eDT_Reserved3})', None) 627 | 628 | print_body_info(f'Counters Downloaded: {counters_bit} (CRC={counters})', None) 629 | 630 | print_body_info(f'FunctionBlocks Downloaded: {functionBlocks_bit} (CRC={functionBlocks})', None) 631 | 632 | print_body_info(f'Function Blocks Instance Downloaded: {func_blocks_inst_bit} (CRC={func_blocks_inst})', None) 633 | 634 | print_body_info(f'Timers Downloaded: {timers_bit} (CRC={timers})', None) 635 | 636 | print_body_info(f'Data Tables Downloaded: {data_tables_bit} (CRC={data_tables})', None) 637 | 638 | print_body_info(f'HW Configuration Downloaded: {hw_config_bit} (CRC={hw_config})', None) 639 | 640 | # data[0xbc:0xcc] - Connection info 641 | conn_info = data[0xbc:0xcc] 642 | conn_info_type = conn_info[0] 643 | 644 | if conn_info_type == 0: 645 | conn_info_general = "Serial" 646 | conn_info_details = conn_info[1:] 647 | 648 | elif conn_info_type == 3: 649 | conn_info_general = "TCP/IP" 650 | conn_info_ip = socket.inet_ntoa(conn_info[1:5]) 651 | conn_info_port, = struct.unpack(" (B) Get Name Req (0c) 835 | get_name_req = pcom.create_read_plc_name() 836 | s.send(get_name_req) 837 | # <-- (B) Get Name Res (8c) 838 | resp = s.recv(1024*5) 839 | plc_name = pcom.parse_read_plc_name(resp) 840 | print(f"\t[-] PLC Name: {plc_name.decode()}") 841 | 842 | 843 | def print_plc_firmware(sock, pcom_client): 844 | s = sock 845 | pcom = pcom_client 846 | # --> (A) Get Versions Req (ID) 847 | get_versions_req = pcom.create_read_plc_versions() 848 | s.send(get_versions_req) 849 | # <-- (A) Get Versions Res (ID) 850 | resp = s.recv(1024*5) 851 | firmware_version_raw = pcom.parse_version(resp) 852 | print_firmware_version(firmware_version_raw) 853 | 854 | 855 | def print_plc_unitid(sock, pcom_client): 856 | s = sock 857 | pcom = pcom_client 858 | # --> (A) Get UnitID (UG) Req (UG) 859 | get_unitid_req = pcom.create_read_unitID() 860 | s.send(get_unitid_req) 861 | # <-- (A) Get UnitID (UG) Res (UG) 862 | resp = s.recv(1024*5) 863 | unitId = pcom.parse_read_unitID(resp) 864 | print(f"\t[-] UnitID: {unitId.decode()}") 865 | 866 | 867 | def print_signature_table(sock, pcom_client): 868 | # Directions: 869 | # Client (EWS) <--> Server (PLC) 870 | # Type of Connection: 871 | # (B)inary (A)scii 872 | # --> (B) Get Name Req (0c) 873 | # <-- (B) Get Name Res (8c) 874 | # --> (A) Get Versions Req (ID) 875 | # <-- (A) Get Versions Res (ID) 876 | # --> (B) Read Operands Req (4D) 877 | # <-- (B) Read Operands Res (CD) 878 | # --> (A) Get UnitID (UG) Req (UG) 879 | # <-- (A) Get UnitID (UG) Res (UG) 880 | # --> (B) Get Resource Table Address Req (16) 881 | # <-- (B) Get Resource Table Address Res (96) 882 | # --> (B) Opcode 0x1a Req (1A) 883 | # <-- (B) Opcode 0x1a Res (9A) 884 | # --> (B) Read Resource Table Memory Req (01) 885 | # <-- (B) Read Resource Table Memory Res (81) 886 | # --> (B) Get Signature Address Req (16) 887 | # <-- (B) Get Signature Address Res (96) 888 | # --> (B) Opcode 0x1a Req (1A) 889 | # <-- (B) Opcode 0x1a Res (9A) 890 | # --> (B) Read Signature Table Memory Req (01) 891 | # <-- (B) Read Signature Table Memory Res (81) 892 | 893 | s = sock 894 | pcom = pcom_client 895 | 896 | # --> (B) Read Operands Req (4D) 897 | read_operand_req = pcom.create_read_operand() 898 | s.send(read_operand_req) 899 | # <-- (B) Read Operands Res (CD) 900 | resp = s.recv(1024*5) 901 | 902 | # --> (B) Get Resource Table Address Req (16) 903 | get_resource_table_address_req = pcom.create_read_resource_table_address() 904 | s.send(get_resource_table_address_req) 905 | # <-- (B) Get Resource Table Address Res (96) 906 | resp = s.recv(1024*5) 907 | rTable_addr, rTable_size = pcom.parse_translate_index(resp) 908 | rTable_addr_hex = hex(struct.unpack(" (B) Opcode 0x1a Req (1A) 912 | flush_req = pcom.create_opcode_1a_request() 913 | s.send(flush_req) 914 | # <-- (B) Opcode 0x1a Res (9A) 915 | resp = s.recv(1024*5) 916 | pcom.parse_opcode_1a(resp) 917 | 918 | # --> (B) Read Resource Table Memory Req (01) 919 | read_rTable_memory = pcom.create_read_memory(rTable_addr, rTable_size, flag=4) 920 | s.send(read_rTable_memory) 921 | # <-- (B) Read Resource Table Memory Res (81) 922 | resp = s.recv(1024*5) 923 | sigTable_index = pcom.parse_read_resource_table(resp) 924 | print(f"\t[-] Signature Table Index: {sigTable_index}") 925 | 926 | # --> (B) Get Signature Address Req (16) 927 | get_signature_table_address_req = pcom.create_translate_resource_index_to_address(sigTable_index) 928 | s.send(get_signature_table_address_req) 929 | # <-- (B) Get Signature Address Res (96) 930 | resp = s.recv(1024*5) 931 | sigTable_address, sigTable_size = pcom.parse_translate_index(resp) 932 | sigTable_address_hex = hex(struct.unpack(" (B) Opcode 0x1a Req (1A) 936 | flush_req = pcom.create_opcode_1a_request() 937 | s.send(flush_req) 938 | # <-- (B) Opcode 0x1a Res (9A) 939 | resp = s.recv(1024*5) 940 | pcom.parse_opcode_1a(resp) 941 | 942 | # --> (B) Read Signature Table Memory Req (01) 943 | # <-- (B) Read Signature Table Memory Res (81) - Check 944 | addr = struct.unpack(" (B) Opcode 0x02 Req 990 | req_pass = pcom.create_check_password_request(password) 991 | s.send(req_pass) 992 | # <-- (B) Opcode 0x82 Res 993 | resp = s.recv(1024*5) 994 | is_ok = pcom.parse_check_password(resp) 995 | if is_ok: 996 | print(f"\t[-] Password: OK") 997 | return True 998 | else: 999 | print(f"\t[-] Password: Bad") 1000 | return False 1001 | 1002 | 1003 | def main(): 1004 | ip_addr = sys.argv[1] 1005 | pcom = PCOM_CLIENT(isTcp=True, debugMode=False) 1006 | s = socket.socket() 1007 | port = 20256 1008 | s.connect((ip_addr, port)) 1009 | 1010 | print_plc_name(s, pcom) 1011 | print_plc_firmware(s, pcom) 1012 | print_plc_unitid(s, pcom) 1013 | print_signature_table(s, pcom) 1014 | read_and_save_project_zip(s, pcom) 1015 | 1016 | 1017 | if __name__ == "__main__": 1018 | main() 1019 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyserial --------------------------------------------------------------------------------