├── .gitignore ├── .gitmodules ├── Docs ├── Modbus_Application_Protocol_V1_1b3.pdf ├── Modbus_over_serial_line_V1_02.pdf └── PI_MBUS_300_Legacy.pdf ├── LICENSE ├── README.md ├── __init__.py ├── libs ├── __init__.py ├── crc16modbus.py ├── modbus_connection.py └── modbus_constants.py └── msak.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | .vscode/ 156 | 157 | #TEST 158 | Tests/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/payload_generator"] 2 | path = libs/payload_generator 3 | url = git@github.com:mindedsecurity/simple_payload_generator.git 4 | -------------------------------------------------------------------------------- /Docs/Modbus_Application_Protocol_V1_1b3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindedsecurity/msak/3d61964c72ea158ddef8e7ee64bdefad71adc24f/Docs/Modbus_Application_Protocol_V1_1b3.pdf -------------------------------------------------------------------------------- /Docs/Modbus_over_serial_line_V1_02.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindedsecurity/msak/3d61964c72ea158ddef8e7ee64bdefad71adc24f/Docs/Modbus_over_serial_line_V1_02.pdf -------------------------------------------------------------------------------- /Docs/PI_MBUS_300_Legacy.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindedsecurity/msak/3d61964c72ea158ddef8e7ee64bdefad71adc24f/Docs/PI_MBUS_300_Legacy.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stefano Di Paola 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 | # ModBus Swiss Army Knife 2 | 3 | MSAK is a tool written in Python to help discovering and testing exposed standard and custom services of ModBus Servers/Slaves over Serial or TCP/IP connections. 4 | It also offers a highly customizable payload generator that will help the tester to perform complex scans using a simple but powerful templating format. 5 | 6 | It was created to facilitate the security analysis of Modbus systems, as described on [Minded Security Blog](https://blog.mindedsecurity.com/2024/03/testing-security-of-modbus-services.html). 7 | # Cloning 8 | 9 | Use --recurse-submodules 10 | 11 | ```git clone --recurse-submodules https://github.com/mindedsecurity/msak``` 12 | 13 | # MSAK Tool 14 | 15 | ```python3 msak.py -h 16 | usage: msak.py [-h] [--tcp] [--host HOST] [--port PORT] [--serial] [-p PATH] [--speed BAUDS] [--parity PARITY] [--bytesize BYTESIZE] [--stopbits STOPBITS] [--no-crc] [-v VERBOSITY] 17 | [--timeout TIMEOUT] [--id SLAVE_ADDRESS] [-f FUNCTION_CODE] [-S] [-D] [-C] [-R] -d DATA_PAYLOAD 18 | 19 | ModBus Swiss Army Knife [M-SAK]. Based on Specification: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf 20 | 21 | options: 22 | -h, --help Show this help message and exit 23 | --tcp Connect through TCP 24 | --host HOST The hostname of the ModBus Server 25 | --port PORT The port of the ModBus Server 26 | --serial Connect through Serial (RTU) 27 | -p PATH Serial device path (defaults /dev/ttyUSB0) 28 | --speed BAUDS Serial device speed (defaults 19200) 29 | --parity PARITY Serial device parity (defaults NONE 'N') 30 | --bytesize BYTESIZE Serial device bytesize (defaults 8) 31 | --stopbits STOPBITS Serial device stopbits (defaults 1) 32 | --no-crc Do not append the CRC 33 | -v VERBOSITY Verbosity Level (0-4) 34 | --timeout TIMEOUT Timeout for Serial Responses (defaults 0.3s) 35 | --id SLAVE_ADDRESS Slave Address (defaults 1) 36 | -f FUNCTION_CODE Function code (int) 37 | -S Services Scan 38 | -D Diagnostic Scan (defaults Function = 7 ) 39 | -C Custom Scan 40 | -R Raw Packet 41 | -d DATA_PAYLOAD Data payload (hexdump style) OR see custom payload definition if using custom scan option -C 42 | 43 | Usage Examples: 44 | #Create ModBusPacket with PDU for slave 1 function code 3 data (PDU ) 45 | msak --tcp --host 127.0.0.1 --port 5020 --id 1 -f 3 -d '0001' # For TCP (will add the header +PDU > ) 46 | msak --serial -p /dev/ttyUSB0 --id 1 -f 3 -d '0001' # For RTU Serial (will add slaveId and CRC > 0) 47 | 48 | #Create Raw Packet 49 | msak --serial -p /dev/ttyUSB0 -R -d '010300013018' --no-crc # without CRC 50 | 51 | #Scan all function codes: 52 | msak --serial -p /dev/ttyUSB0 -S -d '01030' 53 | 54 | ``` 55 | 56 | E.g.: 57 | 58 | ### Service Scan: 59 | Scan all functions codes [1-127] using the given payload and then will print a summary grouped according to the responses: 60 | 61 | ``` 62 | python3 msak.py -S -d '0001' 63 | Requested Data \x01\x01\x00\x01\x91\xD8 64 | .. 65 | Requested Data \x01\x02\x00\x01\x91\xD8 66 | .. 67 | Requested Data \x01\x03\x00\x01\x91\xD8 68 | ... 69 | ``` 70 | 71 | ### Diagnostic Scan: 72 | Scan all diagnostic codes [1-255] using the given payload and then will print a summary grouped according to the responses: 73 | ``` 74 | $ python3 msak.py -D -d '0001' 75 | >>>>>>>>>>>>>>>>>> Data: b'\x00\x01' 76 | Requested Data \x01\x07\x00\x01\x00\x01\x24\x0A 77 | .. 78 | >>>>>>>>>>>>>>>>>> Data: b'\x00\x01' 79 | Requested Data \x01\x07\x00\x02\x00\x01\xD4\x0A 80 | .... 81 | ``` 82 | 83 | ### Custom Scan 84 | 85 | It's possible to create custom multiple payloads using a templating format. 86 | 87 | In particular it's possible to use the following special sequences: 88 | * RANGE: {R[min,max,pack_format]}: Creates a sequence of numbers from min to max and encodes it using the struct.pack format ('B','>H' etc). It will create (max-min) payloads. 89 | * RANDOM: {r{bytesequence_length, ar_len}}: Creates an array of 'ar_len' length where each element is a randome sequence of bytes of 'bytesequence_length' length 90 | * ARRAY: {[n1, n2, n3 ...]}: Adds to the payload the numbers and will create a set of payload according to the length of the array. 91 | * FROM FILE: {@/path/to/file}: using @ char the sequence will be taken from a file. 92 | * CONSTANT: 00-FF: will create a single byte. 93 | 94 | When completed it will print a summary grouped according to the responses. 95 | ``` 96 | python3 msak.py -C -d '0001{[0,3]}' 97 | 98 | will scan the slave using the following 2 payloads: 99 | b'\x00\x01\x00' 100 | b'\x00\x01\x03' 101 | 102 | python3 msak.py -C -d '0001{R[0,3,">H"]}FF{r[3,2]}00' 103 | 104 | will scan the slave using the following set of generated payloads: 105 | b'\x00\x01\x00\x00\xff\xa3\x91\xa7\x00' 106 | b'\x00\x01\x00\x00\xff6\x9fr\x00' 107 | b'\x00\x01\x00\x01\xff\xa3\x91\xa7\x00' 108 | b'\x00\x01\x00\x01\xff6\x9fr\x00' 109 | b'\x00\x01\x00\x02\xff\xa3\x91\xa7\x00' 110 | b'\x00\x01\x00\x02\xff6\x9fr\x00' 111 | b'\x00\x01\x00\x03\xff\xa3\x91\xa7\x00' 112 | b'\x00\x01\x00\x03\xff6\x9fr\x00' 113 | ``` 114 | 115 | # ModBus Specification 116 | 117 | ## Over Serial RTU 118 | 119 | Based on https://www.modbus.org/docs/PI_MBUS_300.pdf 120 | 121 | Big Endian (most significant byte first) 122 | 123 | ADU = | MasterID | PDU | CRC | = |1 Byte MasterID| PDU | 2 Bytes CRCH+CRCL | 124 | 125 | PDU = | Function ID | DATA | 126 | 127 | MasterID = 1 Byte = 0-255 ( 0 Broadcast / 1-247 Unicast / 248-155 Reserved) 128 | 129 | Function ID = 1 Byte = 1-255 / 127-255 for exception response codes) 130 | 131 | SubFunctionID = 1Byte (only for Diagnostics and custom functions) 132 | 133 | DATA = Depends on FunctionID 134 | 135 | ## Over TCP/IP 136 | 137 | ``` 138 | Modbus PDU packet: _PDU_ = 139 | Modbus TCP Packet: 0000<_PDU_> 140 | ``` 141 | 142 | ![](https://visitor-badge.laobi.icu/badge?page_id=wisec.msak) 143 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mindedsecurity/msak/3d61964c72ea158ddef8e7ee64bdefad71adc24f/__init__.py -------------------------------------------------------------------------------- /libs/__init__.py: -------------------------------------------------------------------------------- 1 | from .payload_generator.payload_generator import Payload_Generator as Payload_Generator -------------------------------------------------------------------------------- /libs/crc16modbus.py: -------------------------------------------------------------------------------- 1 | 2 | _CRC16TABLE = ( 3 | 0, 4 | 49345, 5 | 49537, 6 | 320, 7 | 49921, 8 | 960, 9 | 640, 10 | 49729, 11 | 50689, 12 | 1728, 13 | 1920, 14 | 51009, 15 | 1280, 16 | 50625, 17 | 50305, 18 | 1088, 19 | 52225, 20 | 3264, 21 | 3456, 22 | 52545, 23 | 3840, 24 | 53185, 25 | 52865, 26 | 3648, 27 | 2560, 28 | 51905, 29 | 52097, 30 | 2880, 31 | 51457, 32 | 2496, 33 | 2176, 34 | 51265, 35 | 55297, 36 | 6336, 37 | 6528, 38 | 55617, 39 | 6912, 40 | 56257, 41 | 55937, 42 | 6720, 43 | 7680, 44 | 57025, 45 | 57217, 46 | 8000, 47 | 56577, 48 | 7616, 49 | 7296, 50 | 56385, 51 | 5120, 52 | 54465, 53 | 54657, 54 | 5440, 55 | 55041, 56 | 6080, 57 | 5760, 58 | 54849, 59 | 53761, 60 | 4800, 61 | 4992, 62 | 54081, 63 | 4352, 64 | 53697, 65 | 53377, 66 | 4160, 67 | 61441, 68 | 12480, 69 | 12672, 70 | 61761, 71 | 13056, 72 | 62401, 73 | 62081, 74 | 12864, 75 | 13824, 76 | 63169, 77 | 63361, 78 | 14144, 79 | 62721, 80 | 13760, 81 | 13440, 82 | 62529, 83 | 15360, 84 | 64705, 85 | 64897, 86 | 15680, 87 | 65281, 88 | 16320, 89 | 16000, 90 | 65089, 91 | 64001, 92 | 15040, 93 | 15232, 94 | 64321, 95 | 14592, 96 | 63937, 97 | 63617, 98 | 14400, 99 | 10240, 100 | 59585, 101 | 59777, 102 | 10560, 103 | 60161, 104 | 11200, 105 | 10880, 106 | 59969, 107 | 60929, 108 | 11968, 109 | 12160, 110 | 61249, 111 | 11520, 112 | 60865, 113 | 60545, 114 | 11328, 115 | 58369, 116 | 9408, 117 | 9600, 118 | 58689, 119 | 9984, 120 | 59329, 121 | 59009, 122 | 9792, 123 | 8704, 124 | 58049, 125 | 58241, 126 | 9024, 127 | 57601, 128 | 8640, 129 | 8320, 130 | 57409, 131 | 40961, 132 | 24768, 133 | 24960, 134 | 41281, 135 | 25344, 136 | 41921, 137 | 41601, 138 | 25152, 139 | 26112, 140 | 42689, 141 | 42881, 142 | 26432, 143 | 42241, 144 | 26048, 145 | 25728, 146 | 42049, 147 | 27648, 148 | 44225, 149 | 44417, 150 | 27968, 151 | 44801, 152 | 28608, 153 | 28288, 154 | 44609, 155 | 43521, 156 | 27328, 157 | 27520, 158 | 43841, 159 | 26880, 160 | 43457, 161 | 43137, 162 | 26688, 163 | 30720, 164 | 47297, 165 | 47489, 166 | 31040, 167 | 47873, 168 | 31680, 169 | 31360, 170 | 47681, 171 | 48641, 172 | 32448, 173 | 32640, 174 | 48961, 175 | 32000, 176 | 48577, 177 | 48257, 178 | 31808, 179 | 46081, 180 | 29888, 181 | 30080, 182 | 46401, 183 | 30464, 184 | 47041, 185 | 46721, 186 | 30272, 187 | 29184, 188 | 45761, 189 | 45953, 190 | 29504, 191 | 45313, 192 | 29120, 193 | 28800, 194 | 45121, 195 | 20480, 196 | 37057, 197 | 37249, 198 | 20800, 199 | 37633, 200 | 21440, 201 | 21120, 202 | 37441, 203 | 38401, 204 | 22208, 205 | 22400, 206 | 38721, 207 | 21760, 208 | 38337, 209 | 38017, 210 | 21568, 211 | 39937, 212 | 23744, 213 | 23936, 214 | 40257, 215 | 24320, 216 | 40897, 217 | 40577, 218 | 24128, 219 | 23040, 220 | 39617, 221 | 39809, 222 | 23360, 223 | 39169, 224 | 22976, 225 | 22656, 226 | 38977, 227 | 34817, 228 | 18624, 229 | 18816, 230 | 35137, 231 | 19200, 232 | 35777, 233 | 35457, 234 | 19008, 235 | 19968, 236 | 36545, 237 | 36737, 238 | 20288, 239 | 36097, 240 | 19904, 241 | 19584, 242 | 35905, 243 | 17408, 244 | 33985, 245 | 34177, 246 | 17728, 247 | 34561, 248 | 18368, 249 | 18048, 250 | 34369, 251 | 33281, 252 | 17088, 253 | 17280, 254 | 33601, 255 | 16640, 256 | 33217, 257 | 32897, 258 | 16448, 259 | ) 260 | r"""CRC-16 lookup table with 256 elements.""" 261 | 262 | def _calculate_crc_string(inputstring): 263 | """Calculate CRC-16 for Modbus. 264 | 265 | Args: 266 | inputstring (str): An arbitrary-length message (without the CRC). 267 | 268 | Returns: 269 | A two-byte CRC string, where the least significant byte is first. 270 | 271 | """ 272 | #_check_string(inputstring, description="input CRC string") 273 | 274 | # Preload a 16-bit register with ones 275 | register = 0xFFFF 276 | 277 | for char in inputstring: 278 | register = (register >> 8) ^ _CRC16TABLE[(register ^ ord(char)) & 0xFF] 279 | 280 | return pack('>= 1 290 | crc ^= 0xA001 291 | else: 292 | crc >>= 1 293 | return crc.to_bytes(2, byteorder='little') 294 | -------------------------------------------------------------------------------- /libs/modbus_connection.py: -------------------------------------------------------------------------------- 1 | import struct 2 | from libs.crc16modbus import calculate_crc 3 | from pymodbus.client import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient, ModbusTlsClient 4 | from pymodbus.exceptions import ConnectionException 5 | 6 | # SETUP LOGGING 7 | import logging 8 | logger = logging.getLogger(__file__) 9 | logging.basicConfig( # filename="std.log", 10 | format='%(message)s', 11 | # filemode='w' 12 | ) 13 | # Let us Create an object 14 | logger = logging.getLogger() 15 | # Now we are going to Set the threshold of logger to DEBUG 16 | logger.setLevel(logging.DEBUG) 17 | 18 | 19 | class ModBus_Packet(): 20 | def __init__(self, packet_type, raw_packet): 21 | self.type = packet_type 22 | self.raw = raw_packet 23 | 24 | def get_raw_pdu(self): 25 | if len(self.raw) != 0: 26 | if self.type == 'tcp': 27 | return self.raw[7:] 28 | else: 29 | return self.raw[1:] 30 | 31 | def get_slave_id(self): 32 | if len(self.raw) != 0: 33 | if self.type == 'tcp': 34 | return self.raw[6] 35 | else: 36 | return self.raw[0] 37 | 38 | def get_raw(self): 39 | return self.raw 40 | 41 | def tohex(self): 42 | return ("\\x{}".format('\\x'.join(format(c, '02X') for c in self.get_raw()))) 43 | 44 | def __str__(self): 45 | return f"{(self.raw)}" 46 | 47 | 48 | class ModBus_Request_Packet(ModBus_Packet): 49 | def __init__(self, raw_packet, packet_type, function_code, slave_id=1, is_raw=False, use_crc=True, trans_id=1): 50 | self.type = packet_type 51 | self.function_code = function_code 52 | self.slave_id = slave_id 53 | self.raw = raw_packet 54 | self.trans_id = trans_id 55 | self.is_raw = is_raw 56 | if is_raw == False: 57 | self.pdu = struct.pack(">BB", slave_id, function_code)+self.raw 58 | else: 59 | self.pdu = self.raw 60 | self.is_finalized = False 61 | if packet_type == 'serial': 62 | self.use_crc = use_crc 63 | else: 64 | self.use_crc = False 65 | 66 | # b'\x00\x01\x00\x00\x00\x06|\x00\x01\x00\x01\x00\x01' 67 | # b'\x00\x01\x00\x00\x00\x06|\x01\x01\x00\x01\x00\x01' 68 | # ModBus Tcp is like RTU but with a header (MBAP) and no CRC 69 | # MBAP Header| PDU 70 | # >HHH|BBHH 71 | # Where Header is 72 | # 2Bytes TransID|2Bytes ProtoId (0000)|2Bytes Length of the following data 73 | # Response 74 | # b'\x00\x01\x00\x00\x00\x04|\x00\x01\x01\x0a' 75 | 76 | def finalize_packet(self): 77 | if self.is_finalized == True: 78 | return self.finalized_packet 79 | 80 | self.is_finalized = True 81 | if self.type == 'tcp': 82 | self.finalized_packet = struct.pack( 83 | '>HHH', self.trans_id, 0, len(self.pdu))+self.pdu 84 | else: 85 | self.finalized_packet = self.pdu + self.get_crc() 86 | return self.finalized_packet 87 | 88 | def get_crc(self): 89 | if self.use_crc: 90 | return calculate_crc(self.pdu) 91 | return b'' 92 | 93 | def set_use_crc(self, crc): 94 | self.use_crc = crc 95 | 96 | def get_use_crc(self): 97 | return self.use_crc 98 | 99 | 100 | class ModBus_Response_Packet(ModBus_Packet): 101 | 102 | def has_exception(self): 103 | pdu = self.get_raw_pdu() 104 | if pdu != None and len(pdu) > 1: 105 | return (pdu[0] & 0x80) != 0 106 | else: 107 | return False 108 | 109 | def get_exception_id(self): 110 | if self.has_exception(): 111 | return self.get_raw_pdu()[1] 112 | return None 113 | 114 | 115 | """_summary_ 116 | ModBusConnection(type, ...) 117 | 118 | Raises: 119 | Exception: _description_ 120 | Exception: _description_ 121 | Exception: _description_ 122 | Exception: _description_ 123 | 124 | Returns: 125 | _type_: _description_ 126 | """ 127 | 128 | 129 | class ModBusConnection(): 130 | # types serial/tcp/udp 131 | def __init__(self, type='serial', **kwargs): 132 | 133 | if type == None: 134 | raise Exception("No type specified! It should be serial/tcp/..") 135 | self.type = type 136 | self.create_client(**kwargs) 137 | self.connect() 138 | 139 | def is_tcp(self): 140 | return self.type == 'tcp' 141 | 142 | def is_serial(self): 143 | return self.type == 'serial' 144 | 145 | def create_client(self, **kwargs): 146 | 147 | port = kwargs.pop("port", None) 148 | if port is None: 149 | raise Exception("Error port must be set") 150 | 151 | if self.type == 'serial': 152 | bauds = kwargs.get("bauds", None) 153 | timeout = kwargs.get("timeout", None) 154 | bytesize = kwargs.get("bytesize", None) 155 | parity = kwargs.get("parity", None) 156 | stopbits = kwargs.get("stopbits", None) 157 | self.client = ModbusSerialClient(port=port, **kwargs) 158 | elif self.type == 'tcp': 159 | host = kwargs.pop("host", None) 160 | 161 | self.client = ModbusTcpClient( 162 | port=port, host=host, **kwargs) 163 | elif self.type == 'udp': 164 | raise Exception("Not Yet Implemented") 165 | host = kwargs.get("host", None) 166 | self.client = ModbusUdpClient(port=port, **kwargs) 167 | elif self.type == 'tls': 168 | raise Exception("Not Yet Implemented") 169 | self.client = ModbusTlsClient(port=port, **kwargs) 170 | 171 | return self.client 172 | 173 | def connect(self): 174 | return self.client.connect() 175 | 176 | def send_raw(self, pckt): 177 | return self.client.send(pckt) 178 | 179 | def send(self, pckt): 180 | if self.client.connected == False: 181 | raise Exception("Not Connected") 182 | 183 | if isinstance(pckt, ModBus_Packet): 184 | prepared_pckt = pckt.finalize_packet() 185 | else: 186 | prepared_pckt = pckt 187 | 188 | logger.debug(f'Sending: {prepared_pckt}') 189 | 190 | return self.send_raw(prepared_pckt) 191 | 192 | def send_and_recv(self,pckt, size = 2048): 193 | import socket 194 | import time 195 | try: 196 | self.send(pckt.finalize_packet()) 197 | return self.recv(size) 198 | except ConnectionException as exc: 199 | if self.is_tcp: 200 | connected = False 201 | print( "connection lost... reconnecting" ) 202 | while not connected: 203 | # attempt to reconnect, otherwise sleep for 2 seconds 204 | try: 205 | self.connect() 206 | connected = True 207 | print( "re-connection successful" ) 208 | return ModBus_Response_Packet(self.type,b'') 209 | except socket.error: 210 | time.sleep( 2 ) 211 | else: 212 | raise exc 213 | 214 | 215 | ### TODO Add managing abrupt disconnections from Server 216 | def recv_raw(self, size): 217 | return self.client.recv(size) 218 | 219 | def read_raw(self, size): 220 | return self.recv(size) 221 | 222 | def recv(self, size=2048): 223 | return self.read(size) 224 | 225 | def read(self, size=2048): 226 | pckt = self.recv_raw(size) 227 | return ModBus_Response_Packet(packet_type=self.type, raw_packet=pckt) 228 | 229 | def close(self): 230 | return self.client.close() 231 | 232 | def write(self, pckt, is_raw=False): 233 | return self.send(pckt, is_raw=is_raw) 234 | 235 | 236 | if __name__ == "__main__": 237 | import sys 238 | if len(sys.argv) < 3: 239 | print("Usage: modbus_connection.py type port [host]") 240 | sys.exit() 241 | type = sys.argv[1] 242 | port = sys.argv[2] 243 | if len(sys.argv) == 4: 244 | host = sys.argv[3] 245 | else: 246 | host = None 247 | mbdevice = ModBusConnection( 248 | type=type, port=port, host=host, timeout=0.1) 249 | # | tid | 00 | len |cid|fid|Addr |input #| 250 | req = b'\x00\x01\x00\x00\x00\x06\x01\x02\x00\x00\x00\x01' 251 | # \x00\x01\x00\x00\x00\x04\x01\x01\x01\x01' 252 | # \x01\x01\x00\x01\x00\x01' 253 | # \x00\x01\x00\x00\x00\x06\x01\x01\x00\x01\x00\x01 254 | print("Request:", req) 255 | mbdevice.send(req) 256 | 257 | # print(mbdevice.read_coils(2)) 258 | print("Response", mbdevice.read()) 259 | mbdevice.close() 260 | -------------------------------------------------------------------------------- /libs/modbus_constants.py: -------------------------------------------------------------------------------- 1 | ######################################### 2 | # MODBUS CONSTANTS 3 | FUNCTION_CODES = { 4 | # TOTAL NUMBER = 1 to 2000 (0x7D0) 5 | 1: '(0x01) Read Coils [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]', 6 | # TOTAL NUMBER = 1 to 2000 (0x7D0) 7 | 2: '(0x02) Read Discrete Inputs [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]', 8 | # TOTAL NUMBER = 1 to 125 (0x7D) 9 | 3: '(0x03) Read Holding Registers [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]', 10 | # TOTAL NUMBER = 0x0001 to 0x007D 11 | 4: '(0x04) Read Input Registers [FUN_ID|ADDRESS|TOTAL NUMBER| >BHH]', 12 | # Value can be ON=>0xFF00 or OFF=>0x0000 13 | 5: '(0x05) Write Single Coil [FUN_ID|ADDRESS|COIL VALUE| >BHH]', 14 | # Value = 0x0000 to 0xFFFF 15 | 6: '(0x06) Write Single Register [FUN_ID|ADDRESS|REG VALUE| >BHH]', 16 | # No data 17 | 7: '(0x07) Read Exception Status (Serial Line only) [FUN_ID >B]', 18 | # Needs Sub-function codes supported by the serial line devices 19 | 8: '(0x08) Diagnostics (Serial Line only) [|FUN_ID|SUB_FUN|VALUES| >BHN*H]', 20 | # 9: (0x09) RESERVED 21 | # 10: (0x0A) RESERVED 22 | 11: '(0x0B) Get Comm Event Counter (Serial Line only) [FUN_ID >B]', 23 | 12: '(0x0C) Get Comm Event Log (Serial Line only)[FUN_ID >B]', 24 | # 13 : (0x0D) RESERVED 25 | # 14 : (0x0E) RESERVED 26 | 15: '(0x0F) Write Multiple Coils [FUN_ID|ADDRESS|TOTAL NUM|BYTE COUNT|BYTE VALS >BHHBN*B]', 27 | 16: '(0x10) Write Multiple registers [FUN_ID|ADDRESS|TOTAL NUM|BYTE COUNT|VALS >BHHBN*H]', 28 | 17: '(0x11) Report Server ID (Serial Line only) [FUN_ID >B]', 29 | # 18 : (0x12) RESERVED 30 | # 19 : (0x13) RESERVED 31 | 20: '(0x14) Read File Record', # TODO Needs specific format 32 | 21: '(0x15) Write File Record', # TODO Needs specific format 33 | 22: '(0x16) Mask Write Register', # TODO 34 | 23: '(0x17) Read/Write Multiple registers', # TODO 35 | 24: '(0x18) Read FIFO Queue', # TODO 36 | # 24-42 RESERVED 37 | # TODO 43/13 43/14 Needs Specific format 38 | 43: '(0x2B) Encapsulated Interface Transport' # TODO 39 | # 25-127 RESERVED 40 | # 128-255 are reserved as RESPONSE Errors (0x80+REQUESTED FUNCTION CODE) 41 | } 42 | 43 | DIAGNOSTIC_SUB_FUNCTION_CODES = { 44 | 0: '0x00 Return Query Data', 45 | 1: '0x01 Restart Communications Option', 46 | 2: '0x02 Return Diagnostic Register', 47 | 3: '0x03 Change ASCII Input Delimiter', 48 | 4: '0x04 Force Listen Only Mode', 49 | # 05.. 09 RESERVED', 50 | 10: '0x0A Clear Counters and Diagnostic Register', 51 | 11: '0x0B Return Bus Message Count', 52 | 12: '0x0C Return Bus Communication Error Count', 53 | 13: '0x0D Return Bus Exception Error Count', 54 | 14: '0x0E Return Server Message Count', 55 | 15: '0x0F Return Server No Response Count', 56 | 16: '0x10 Return Server NAK Count', 57 | 17: '0x11 Return Server Busy Count', 58 | 18: '0x12 Return Bus Character Overrun Count', 59 | 19: '0x13 RESERVED', 60 | 20: '0x14 Clear Overrun Counter and Flag', 61 | # 21 ... 65535 RESERVED' 62 | } 63 | 64 | EXC_CODES = { 65 | 0x1: 'ILLEGAL FUNCTION', 66 | 0x2: 'ILLEGAL DATA ADDRESS', 67 | 0x3: 'ILLEGAL DATA VALUE', 68 | 0x4: 'SERVER DEVICE FAILURE', 69 | 0x5: 'ACKNOWLEDGE', 70 | 0x6: 'SERVER DEVICE BUSY', 71 | 0x7: 'NEGATIVE ACKNOWLEDGE', 72 | 0x8: 'MEMORY PARITY ERROR', 73 | # Missing 0x09?? 74 | 0xA: 'GATEWAY PATH UNAVAILABLE', 75 | 0xB: 'GATEWAY TARGET DEVICE FAILED TO RESPOND' 76 | } 77 | ######################################### -------------------------------------------------------------------------------- /msak.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | ######################################### 4 | # Local Libs 5 | from libs.crc16modbus import calculate_crc 6 | from libs.modbus_connection import ModBusConnection, ModBus_Packet, ModBus_Request_Packet 7 | from libs.modbus_constants import * 8 | from libs import Payload_Generator 9 | ######################################### 10 | 11 | import serial 12 | import struct 13 | import traceback 14 | import sys 15 | import argparse 16 | 17 | # SETUP LOGGING 18 | import logging 19 | logger = logging.getLogger("M-SAK") 20 | logging.basicConfig( # filename="std.log", 21 | format='%(message)s', 22 | # filemode='w' 23 | ) 24 | # Let us Create an object 25 | logger = logging.getLogger() 26 | # Now we are going to Set the threshold of logger to DEBUG 27 | logger.setLevel(logging.DEBUG) 28 | 29 | ######################################### 30 | # DEFAULTS 31 | DEFAULT_FUNCTION_CODE = 1 32 | DEFAULT_SLAVE_ADDRESS = 1 33 | ######################################### 34 | 35 | 36 | ######################################### 37 | # Functions 38 | 39 | 40 | def pretty_print_packet(packet): 41 | return packet 42 | 43 | 44 | def pretty_print_scan(scan_response, data_type='function'): 45 | for type, responses in scan_response.items(): 46 | print(f'{type}\t') 47 | for function_code in responses: 48 | # print(data_type,function_code) 49 | if data_type == 'function': 50 | print( 51 | f'\t{function_code} {FUNCTION_CODES.get(function_code,"CUSTOM")}') 52 | elif data_type == 'subfunction': 53 | print( 54 | f'\t{function_code} {DIAGNOSTIC_SUB_FUNCTION_CODES.get(function_code,"CUSTOM")}') 55 | elif data_type == 'packet': 56 | print(f'\t{function_code} {pretty_print_packet(function_code)}') 57 | 58 | 59 | def strtohex(str): 60 | return ("\\x{}".format('\\x'.join(format(c, '02X') for c in str))) 61 | 62 | 63 | #### 64 | # SlaveID = 1 Byte = 0-255 ( 0 Broadcast / 1-247 Unicast / 248-155 Reserved) 65 | # Function ID = 1 Byte = 1-255 / 127-255 for exception response codes) 66 | # SubFunctionID = 1Byte (only for Diagnostics and custom functions) 67 | # DATA = Depends on FunctionID 68 | # >BB 69 | ####### 70 | 71 | 72 | def payload_from_fuzzer(template): 73 | return Payload_Generator(template, True) 74 | 75 | 76 | ########################### 77 | 78 | ########################### 79 | # Function: 80 | # We don't need function codes since we're going to loop over them. 81 | 82 | def discover_services(modbus_client, slave_address, data): 83 | """ loops over function codes 1-127 84 | Args: 85 | modbus_client (_type_): connection client object 86 | slave_address (_type_): integer slave id 87 | data (_type_): data to be sent in the PDU 88 | """ 89 | results = dict() 90 | 91 | for function_code in range(1, 127): 92 | logger.info(">>>>>>>>>>>>>>>>>> ", function_code) 93 | 94 | pckt = ModBus_Request_Packet( 95 | raw_packet=data, packet_type=type, slave_id=slave_address, function_code=function_code, use_crc=args.use_crc) 96 | 97 | print(strtohex(pckt.finalize_packet())) 98 | response_data = modbus_client.send_and_recv(pckt) 99 | 100 | logger.info("Response Data:", response_data, 101 | response_data.tohex(), response_data.raw) 102 | 103 | response_exception = response_data.get_exception_id() 104 | if response_exception: 105 | results.setdefault(EXC_CODES.get( 106 | response_exception, response_exception), []).append(function_code) 107 | else: 108 | raw_pckt = response_data.raw 109 | if len(raw_pckt) == 0: 110 | results.setdefault('NO_RESPONSE', []).append(function_code) 111 | else: 112 | results.setdefault('ACCEPTED_WITH_RESPONSE', 113 | []).append(function_code) 114 | 115 | pretty_print_scan(results, data_type='function') 116 | 117 | 118 | 119 | def discover_diagnostic(modbus_client, slave_address, function_code=7, data=None): 120 | """_summary_ 121 | 122 | Args: 123 | modbus_client (_type_): _description_ 124 | slave_address (_type_): _description_ 125 | function_code (int, optional): _description_. Defaults to 7. 126 | data (_type_, optional): _description_. Defaults to None. 127 | """ 128 | results = dict() 129 | if function_code is None: 130 | function_code = 7 131 | 132 | for subfunction in range(1, 255): 133 | print(">>>>>>>>>>>>>>>>>> Data: ", data) 134 | pckt = ModBus_Request_Packet( 135 | raw_packet=struct.pack('>H', subfunction)+data, packet_type=type, slave_id=slave_address, function_code=function_code, use_crc=args.use_crc) 136 | 137 | print(strtohex(pckt.finalize_packet())) 138 | response_data = modbus_client.send_and_recv(pckt) 139 | 140 | logger.info("Response Data:", response_data, 141 | response_data.tohex(), response_data.raw) 142 | response_exception = response_data.get_exception_id() 143 | if response_exception: 144 | results.setdefault(EXC_CODES.get( 145 | response_exception, response_exception), []).append(subfunction) 146 | else: 147 | raw_pckt = response_data.raw 148 | if len(raw_pckt) == 0: 149 | results.setdefault('NO_RESPONSE', []).append(subfunction) 150 | else: 151 | results.setdefault('ACCEPTED_WITH_RESPONSE', 152 | []).append(subfunction) 153 | 154 | pretty_print_scan(results, data_type="subfunction") 155 | 156 | 157 | ################################################## 158 | # Scan with custom template 159 | # Eg. '01{R[0,1,"B"]}FF{R[1,2,">H"]}0E{[0,4]}DD' 160 | # 01 161 | ################################################# 162 | def discover_by_template(modbus_client, slave_address, function_code, template=None): 163 | """_summary_ 164 | 165 | Args: 166 | modbus_client (_type_): _description_ 167 | slave_address (_type_): _description_ 168 | function_code (_type_): _description_ 169 | template (_type_, optional): _description_. Defaults to None. 170 | 171 | Raises: 172 | Exception: _description_ 173 | """ 174 | results = dict() 175 | 176 | prefix = b'' 177 | if slave_address != None: 178 | prefix = struct.pack('B', slave_address) 179 | # If Slave address is not set, it is not expected to have 180 | if slave_address != None and function_code != None: 181 | prefix += struct.pack('B', function_code) 182 | elif function_code != None: 183 | raise Exception("Cannot Set Function Code without a Slave Address") 184 | 185 | payload = payload_from_fuzzer(template) 186 | 187 | for data in payload: 188 | 189 | print(">>>>>>>>>>>>>>>>>> ", data) 190 | if prefix != '': 191 | data = prefix + data 192 | 193 | ######################################## send_modbus_request #### 194 | pckt = ModBus_Request_Packet( 195 | raw_packet=data, packet_type=type, slave_id=slave_address, function_code=function_code, use_crc=args.use_crc, is_raw=True) 196 | 197 | print(strtohex(pckt.finalize_packet())) 198 | response_data = modbus_client.send_and_recv(pckt) 199 | 200 | 201 | ################################################################# 202 | # Check Response: 203 | if response_data: 204 | print("Response Data:", response_data, 205 | response_data.tohex(), response_data.raw) 206 | 207 | response_exception = response_data.get_exception_id() 208 | if response_exception: 209 | results.setdefault(EXC_CODES.get( 210 | response_exception, response_exception), []).append(data) 211 | else: 212 | raw_pckt = response_data.raw 213 | if len(raw_pckt) == 0: 214 | results.setdefault('NO_RESPONSE', []).append(data) 215 | else: 216 | results.setdefault('ACCEPTED_WITH_RESPONSE', []).append(data) 217 | 218 | print(results) 219 | 220 | 221 | ############################################################################ 222 | # M-SAK MAIN 223 | # Expecting: 224 | # SERIAL Options: --serial [-p /serial/path/to/com] [--speed SPEED] [--parity PARITY] [--bytesize BYTESIZE] [--stopbits STOPBITS] [--no-crc] [--timeout TIMEOUT] 225 | # TCP Options: --tcp --host HOST [--port PORT] [--timeout TIMEOUT] 226 | # GENERIC Options: [-v VERBOSITY] [--id SLAVE_ADDRESS] [-f FUNCTION_CODE] [-S] [-D] [-C] [-R] -d DATA_PAYLOAD 227 | # 228 | 229 | 230 | # if --tcp --host is required and --port is set as default 231 | tcpargs = argparse.ArgumentParser(add_help=False) 232 | tcpargs.add_argument('--tcp', action='store_true', 233 | dest='is_tcp', help='Connect through TCP/IP') 234 | tcpargs.add_argument('--host', dest='host', 235 | help='The hostname of the ModBus Server') 236 | tcpargs.add_argument('--port', dest='port', type=int, 237 | default=502, help='The port of the ModBus Server') 238 | 239 | # if type serial 240 | serialargs = argparse.ArgumentParser(add_help=False) 241 | serialargs.add_argument('--serial', action='store_true', 242 | dest='is_serial', default=True, help='Connect through Serial (RTU)') 243 | serialargs.add_argument('-p', dest='path', action='store', default='/dev/ttyUSB0', 244 | help='Serial device path (defaults /dev/ttyUSB0)') 245 | serialargs.add_argument('--speed', dest='bauds', action='store', default=19200, 246 | help='Serial device speed (defaults 19200)') 247 | serialargs.add_argument('--parity', dest='parity', action='store', default=serial.PARITY_NONE, 248 | help='Serial device parity (defaults NONE \x27N\x27)') 249 | serialargs.add_argument('--bytesize', dest='bytesize', type=int, action='store', default=serial.EIGHTBITS, 250 | help='Serial device bytesize (defaults 8)') 251 | serialargs.add_argument('--stopbits', dest='stopbits', type=float, action='store', default=1.0, 252 | help='Serial device stopbits (defaults 1)') 253 | serialargs.add_argument('--no-crc', dest='use_crc', action='store_false', 254 | help='Do not append the CRC') 255 | 256 | 257 | parser = argparse.ArgumentParser(parents=[tcpargs, serialargs], formatter_class=argparse.RawDescriptionHelpFormatter, 258 | description="ModBus Swiss Army Knife [M-SAK]. Based on Specification: https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf", 259 | epilog=""" 260 | Usage Examples: 261 | #Create ModBusPacket with PDU for slave 1 function code 3 data \x00\x01 (PDU \x03\x00\x01) 262 | msak --tcp --host 127.0.0.1 --port 5020 --id 1 -f 3 -d '0001' # For TCP (will add the header \x00\x01\x00\x00\x06+PDU > \x00\x01\x00\x00\x00\x04\x01\x03\x00\x01 ) 263 | msak --serial -p /dev/ttyUSB0 --id 1 -f 3 -d '0001' # For RTU Serial (will add slaveId and CRC > \x01\x03\x00\x01\x30\x18) 264 | 265 | #Create Raw Packet 266 | msak --serial -p /dev/ttyUSB0 -R -d '010300013018' --no-crc # without CRC 267 | 268 | #Scan all function codes: 269 | msak --serial -p /dev/ttyUSB0 -S -d '01030' 270 | 271 | === ModBus Packet Reminder Schema: 272 | 273 | Modbus PDU packet: *PDU* = 274 | Modbus Serial Packet: <*PDU*> 275 | Modbus TCP Packet: 0000<*PDU*> 276 | 277 | """) 278 | parser.add_argument('-v', dest='verbosity', action='store', type=int, default=1, 279 | help='Verbosity Level (0-4)') 280 | parser.add_argument('--timeout', dest='timeout', action='store', type=float, default=0.3, 281 | help='Timeout for Serial Responses (defaults 0.3s)') 282 | parser.add_argument('--id', dest='slave_address', action='store', 283 | help='Slave Address (defaults 1)') 284 | parser.add_argument('-f', dest='function_code', type=int, 285 | help='Function code (int)') 286 | parser.add_argument('-S', dest='serv_scan', action='store_true', 287 | help='Services Scan') 288 | parser.add_argument('-D', dest='diag_scan', action='store_true', 289 | help='Diagnostic Scan (defaults Function = 7 )') 290 | parser.add_argument('-C', dest='custom_scan', action='store_true', 291 | help='Custom Scan') 292 | parser.add_argument('-R', dest='send_raw', action='store_true', 293 | help='Raw Packet') 294 | parser.add_argument('-d', dest='data_payload', required=True, 295 | help='Data payload (hexdump style) OR see custom payload definition if using custom scan option -C') 296 | # TODO: 297 | # parser.add_argument('--print-functions', dest='print_functions', 298 | # help='print all modbus functions') 299 | args = parser.parse_args() 300 | 301 | print(args) 302 | if args.verbosity == 4: 303 | logger.setLevel(logging.DEBUG) 304 | elif args.verbosity == 3: 305 | logger.setLevel(logging.INFO) 306 | elif args.verbosity == 2: 307 | logger.setLevel(logging.WARN) 308 | elif args.verbosity == 1: 309 | logger.setLevel(logging.ERROR) 310 | 311 | logger.debug(args) 312 | 313 | modbus_args = {} 314 | if args.is_tcp: 315 | type = 'tcp' 316 | args.use_crc = False 317 | modbus_args = {"host": args.host, 318 | "port": args.port, "timeout": args.timeout} 319 | elif args.is_serial: 320 | type = 'serial' 321 | modbus_args = {"port": args.path, "bauds": args.bauds, "timeout": args.timeout, 322 | "bytesize": args.bytesize, 323 | "parity": args.parity, "stopbits": args.stopbits} 324 | else: 325 | type = None 326 | 327 | try: 328 | 329 | # Open serial port 330 | modbus_client = ModBusConnection(type=type, **modbus_args) 331 | 332 | logger.debug(modbus_client) 333 | 334 | # Modbus RTU read holding registers example 335 | if args.slave_address is not None: 336 | slave_address = int(args.slave_address) 337 | else: 338 | slave_address = None 339 | 340 | if args.function_code is not None: 341 | function_code = int(args.function_code) 342 | else: 343 | function_code = None 344 | 345 | if args.custom_scan is False: 346 | try: 347 | data = bytes.fromhex(args.data_payload.replace(r'\x','')) 348 | except: 349 | logger.error( 350 | "Got error in parsing the data payload. Please ensure using only hexadecimal text") 351 | sys.exit() 352 | else: # CUSTOM DATA PAYLOAD Expects a different type of data payload. 353 | data = args.data_payload 354 | 355 | if data == b'': 356 | logger.debug("DATA to NONE") 357 | data = None 358 | 359 | ############################### 360 | # Scan Features: 361 | 362 | ################################################################# 363 | # Scan by Service (FUNCTION_CODE [1-127]) 364 | if args.serv_scan is True: 365 | if slave_address is None: 366 | slave_address = 1 367 | 368 | print(modbus_args) 369 | discover_services(modbus_client, slave_address, data) 370 | 371 | ################################################################# 372 | # Scan by Diagnostic ( FUNCTION_CODE=7 , SUB_FUNCTION_CODE [1-255]) 373 | elif args.diag_scan is True: 374 | if slave_address is None: 375 | slave_address = 1 376 | 377 | discover_diagnostic(modbus_client, slave_address, 378 | function_code, data) 379 | 380 | ################################################################# 381 | # Scan by Template 382 | elif args.custom_scan is True: 383 | discover_by_template( 384 | modbus_client, slave_address, function_code, data) 385 | 386 | ################################################################# 387 | # Send Raw Packet from -d 388 | elif args.send_raw is True: 389 | # Create Raw packet 390 | pckt = ModBus_Request_Packet( 391 | raw_packet=data, packet_type=type, function_code=None, use_crc=args.use_crc, is_raw=True) 392 | print(pckt.finalize_packet()) 393 | response_data = modbus_client.send_and_recv(pckt) 394 | 395 | # response_data = modbus_request( 396 | # modbus_client, slave_address, function_code, data, check_response=True, is_raw=True) 397 | print( 398 | f"==Data Response==\nHas Exception: {response_data.has_exception()}\nHex Content: {response_data.tohex()}") 399 | 400 | ################################################################# 401 | # Default Packet Send 402 | else: 403 | # Expecting Slave Address & Function_code or a full blown data payload 404 | # Prepare packet using slave id and function conde 405 | if function_code is None or slave_address is None: 406 | parser.print_help() 407 | logger.error( 408 | "ERROR: Function Code or Slave Address values should be explicitely set when using default packet send.") 409 | sys.exit() 410 | # response_data = modbus_request( 411 | # modbus_client, slave_address, function_code, data, check_response=True) 412 | 413 | pckt = ModBus_Request_Packet( 414 | raw_packet=data, packet_type=type, slave_id=slave_address, function_code=function_code, use_crc=args.use_crc) 415 | print(pckt.finalize_packet()) 416 | response_data = modbus_client.send_and_recv(pckt) 417 | 418 | print( 419 | f"==Data Response==\nHas Exception: {response_data.has_exception()}\nHex Content: {response_data.tohex()}") 420 | 421 | ################################################################# 422 | 423 | except serial.SerialException: 424 | logger.error("Error:", args.path, 'Not Found') 425 | except: 426 | logger.error('ERROR!>>>>>>>>>>>>>>>>>>') 427 | traceback.print_exc() 428 | finally: 429 | try: 430 | modbus_client.close() 431 | except: 432 | pass 433 | --------------------------------------------------------------------------------