#!/usr/bin/env python """ File: modscan.py Desc: Modbus TCP Scanner Version: 0.1 Copyright (c) 2008 Mark Bristow This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import socket import array import optparse from IPy import IP import sys def main(): p = optparse.OptionParser( description=' Finds modbus devices in IP range and determines slave id.\nOutputs in ip:port sid format.', prog='modscan', version='modscan 0.1', usage = "usage: %prog [options] IPRange") p.add_option('--port', '-p', type='int', dest="port", default=502, help='modbus port DEFAULT:502') p.add_option('--timeout', '-t', type='int', dest="timeout", default=500, help='socket timeout (mills) DEFAULT:500') p.add_option('--aggressive', '-a', action ='store_true', help='continues checking past first found SID') p.add_option('--function', '-f', type='int', dest="function", default=17, help='MODBUS Function Code DEFAULT:17') p.add_option('--data', type='string', dest="fdata", help='MODBUS Function Data. Unicode escaped "\x00\x01"') p.add_option('-v', '--verbose', action ='store_true', help='returns verbose output') p.add_option('-d', '--debug', action ='store_true', help='returns extremely verbose output') options, arguments = p.parse_args() #make sure we have at least 1 argument (IP Addresses) if len(arguments) == 1: #build basic packet for this test """ Modbus Packet Structure \x00\x00 \x00\x00 \x00\x00 \x11 \x00 <=================> Trans ID ProtoID(0) Length UnitID FunctCode Data len(0-253byte) """ #this must be stored in a unsigned byte aray so we can make the assignment later... no string[] in python :( rsid = array.array('B') rsid.fromstring("\x00\x00\x00\x00\x00\x02\x01\x01") #set function rsid[7]=options.function #add function data if (options.fdata): aFData = array.array('B') #we must decode the escaped unicode before calling fromstring otherwise the literal \xXX will be interpreted aFData.fromstring(options.fdata.decode('unicode-escape') ) rsid += aFData #update length rsid[5]=len(aFData)+2 #assign IP range iprange=IP(arguments[0]) #print friendly user message print "Starting Scan..." #primary loop over IP addresses for ip in iprange: #print str(ip)+" made it" #loop over possible sid values (1-247) for sid in range (1, 247): #error messaging fError=0 msg = str(ip)+":"+str(options.port)+"\t"+str(sid) #print "msg="+msg #Wrap connect in a try box try: #socket object instantiation s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #set socket timeout, value from cmd is in mills s.settimeout(float(options.timeout) / float(1000)) #connect requires ip addresses in string format so it must be cast s.connect((str(ip), options.port)) except socket.error: #clean up fError=1 msg += "\tFAILED TO CONNECT" s.close() break #end try #send query to device try: #set slave id rsid[6]=sid #send data to device s.send(rsid) except socket.error: #failed send close socket fError=1 msg += "\tFAILED TO SEND" s.close() break #end try try: #recieve data data = s.recv(1024) except socket.timeout: fError=1 msg += "\tFAILED TO RECV" break #end try #examine response if data: #parse response resp = array.array('B') resp.fromstring(data) if (options.debug): print "Recieved: "+str(resp) print (int(resp[7]) == int(options.function)) #if the function matches the one sent we are all good if (int(resp[7]) == int(options.function)): print msg #in aggressive mode we keep going if (not options.aggressive): break #If the function matches the one sent + 0x80 a positive response error code is detected elif int(resp[7]) == (int(options.function)+128): #if debug output message msg += "\tPositive Error Response" if (options.debug): print msg else: #if debug output message if (options.debug): print msg else: fError=1 msg += "\tFAILED TO RECIEVE" s.close() break #end SID for #report based on verbosity if (options.verbose and fError): print msg elif (options.debug): print msg #end IP for #close socket, no longer needed #s.shutdown(socket.SHUT_RDWR) s.close() print "Scan Complete." #bad number of arguments. print help else: p.print_help() if __name__ == '__main__': try : main() except KeyboardInterrupt: print "Scan canceled by user." print "Thank you for using ModScan" except : sys.exit()