Advertisement
EyeRA

modscan.py

Feb 27th, 2017
251
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.21 KB | None | 0 0
  1. #!/usr/bin/env python
  2.  
  3.  
  4. """
  5.  
  6. File: modscan.py
  7. Desc: Modbus TCP Scanner
  8. Version: 0.1
  9.  
  10. Copyright (c) 2008 Mark Bristow
  11.  
  12. This program is free software: you can redistribute it and/or modify
  13. it under the terms of the GNU General Public License as published by
  14. the Free Software Foundation version either version 3 of the License,
  15. or (at your option) any later version.
  16.  
  17.  
  18. This program is distributed in the hope that it will be useful,
  19. but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  21. GNU General Public License for more details.
  22.  
  23. You should have received a copy of the GNU General Public License
  24. along with this program.  If not, see <http://www.gnu.org/licenses/>.
  25.  
  26. ModScan is a new tool designed to map a SCADA MODBUS TCP based network. The tool is written in python for portability and can be used on virtually any system with few required libraries.
  27.  
  28. """
  29.  
  30. import socket
  31. import array
  32. import optparse
  33. from IPy import IP
  34. import sys
  35.  
  36. def main():
  37.  
  38.   p = optparse.OptionParser(  description=' Finds modbus devices in IP range and determines slave id.\nOutputs in ip:port <tab> sid format.',
  39.                 prog='modscan',
  40.                 version='modscan 0.1',
  41.                 usage = "usage: %prog [options] IPRange")
  42.   p.add_option('--port', '-p', type='int', dest="port", default=502, help='modbus port DEFAULT:502')
  43.   p.add_option('--timeout', '-t', type='int', dest="timeout", default=500, help='socket timeout (mills) DEFAULT:500')
  44.   p.add_option('--aggressive', '-a', action ='store_true', help='continues checking past first found SID')
  45.   p.add_option('--function', '-f', type='int', dest="function", default=17, help='MODBUS Function Code DEFAULT:17')
  46.   p.add_option('--data', type='string', dest="fdata", help='MODBUS Function Data.  Unicode escaped "\x00\x01"')
  47.   p.add_option('-v', '--verbose', action ='store_true', help='returns verbose output')
  48.   p.add_option('-d', '--debug', action ='store_true', help='returns extremely verbose output')
  49.  
  50.   options, arguments = p.parse_args()
  51.  
  52.   #make sure we have at least 1 argument (IP Addresses)
  53.   if len(arguments) == 1:
  54.  
  55.     #build basic packet for this test
  56.  
  57.     """
  58.    Modbus Packet Structure
  59.    \x00\x00  \x00\x00  \x00\x00  \x11    \x00    <=================>
  60.    Trans ID  ProtoID(0)  Length    UnitID    FunctCode  Data len(0-253byte)
  61.    """
  62.  
  63.     #this must be stored in a unsigned byte aray so we can make the assignment later... no string[] in python :(
  64.     rsid = array.array('B')
  65.     rsid.fromstring("\x00\x00\x00\x00\x00\x02\x01\x01")
  66.  
  67.     #set function
  68.     rsid[7]=options.function
  69.  
  70.     #add function data
  71.     if (options.fdata):
  72.       aFData = array.array('B')
  73.  
  74.       #we must decode the escaped unicode before calling fromstring otherwise the literal \xXX will be interpreted
  75.       aFData.fromstring(options.fdata.decode('unicode-escape') )
  76.       rsid += aFData
  77.      
  78.       #update length
  79.       rsid[5]=len(aFData)+2
  80.  
  81.     #assign IP range
  82.     iprange=IP(arguments[0])
  83.    
  84.     #print friendly user message
  85.     print "Starting Scan..."
  86.  
  87.     #primary loop over IP addresses
  88.     for ip in iprange:
  89.    
  90.       #print str(ip)+" made it"
  91.       #loop over possible sid values (1-247)
  92.       for sid in range (1, 247):  
  93.      
  94.         #error messaging
  95.         fError=0
  96.         msg = str(ip)+":"+str(options.port)+"\t"+str(sid)
  97.        
  98.         #print "msg="+msg
  99.  
  100.         #Wrap connect in a try box
  101.         try:
  102.           #socket object instantiation
  103.           s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  104.  
  105.           #set socket timeout, value from cmd is in mills
  106.           s.settimeout(float(options.timeout) / float(1000))      
  107.  
  108.           #connect requires ip addresses in string format so it must be cast
  109.           s.connect((str(ip), options.port))
  110.  
  111.         except socket.error:
  112.           #clean up
  113.           fError=1
  114.           msg += "\tFAILED TO CONNECT"
  115.           s.close()
  116.           break
  117.         #end try
  118.        
  119.  
  120.         #send query to device
  121.         try:
  122.           #set slave id
  123.           rsid[6]=sid    
  124.  
  125.           #send data to device
  126.           s.send(rsid)
  127.          
  128.         except socket.error:
  129.           #failed send close socket
  130.           fError=1
  131.           msg += "\tFAILED TO SEND"
  132.           s.close()
  133.           break
  134.         #end try
  135.        
  136.         try:
  137.  
  138.           #recieve data
  139.           data = s.recv(1024)
  140.          
  141.         except socket.timeout:
  142.           fError=1
  143.           msg += "\tFAILED TO RECV"
  144.           break
  145.         #end try
  146.  
  147.         #examine response
  148.         if data:
  149.           #parse response
  150.           resp = array.array('B')
  151.           resp.fromstring(data)
  152.  
  153.           if (options.debug):
  154.             print "Recieved: "+str(resp)
  155.             print (int(resp[7]) == int(options.function))
  156.  
  157.           #if the function matches the one sent we are all good
  158.           if (int(resp[7]) == int(options.function)):
  159.             print msg
  160.            
  161.             #in aggressive mode we keep going
  162.             if (not options.aggressive):
  163.               break
  164.              
  165.           #If the function matches the one sent + 0x80 a positive response error code is detected
  166.           elif int(resp[7]) == (int(options.function)+128):
  167.             #if debug output message
  168.             msg += "\tPositive Error Response"
  169.             if (options.debug):
  170.               print msg              
  171.           else:
  172.             #if debug output message
  173.             if (options.debug):
  174.               print msg          
  175.         else:
  176.           fError=1
  177.           msg += "\tFAILED TO RECIEVE"
  178.           s.close()
  179.           break
  180.        
  181.       #end SID for
  182.      
  183.  
  184.       #report based on verbosity
  185.       if (options.verbose and fError):
  186.         print msg
  187.       elif (options.debug):
  188.         print msg
  189.     #end IP for
  190.        
  191.     #close socket, no longer needed
  192.     #s.shutdown(socket.SHUT_RDWR)
  193.     s.close()
  194.    
  195.     print "Scan Complete."
  196.  
  197.   #bad number of arguments.  print help
  198.   else:
  199.     p.print_help()
  200.  
  201.  
  202. if __name__ == '__main__':
  203.   try : main()
  204.   except KeyboardInterrupt:
  205.     print "Scan canceled by user."
  206.     print "Thank you for using ModScan"
  207.   except :
  208.     sys.exit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement