OpenSecurity/bin/vmmanager.py
author dyle@opensecurity.d03.arc.local
Wed, 29 Jan 2014 09:23:52 +0000
changeset 53 01839f13cef3
parent 52 1238895dc6b6
child 55 42238cd74afe
permissions -rw-r--r--
SSH cert and ISO creation with shadowrun.exe
     1 '''
     2 Created on Nov 19, 2013
     3 
     4 @author: BarthaM
     5 '''
     6 import os
     7 import os.path
     8 from subprocess import Popen, PIPE, call
     9 import subprocess
    10 import sys
    11 import re
    12 import _winreg
    13 from cygwin import Cygwin
    14 from environment import Environment
    15 import threading
    16 import time
    17 import string
    18 
    19 import shutil
    20 import stat
    21 import tempfile
    22 
    23 
    24 DEBUG = True
    25 class VMManagerException(Exception):
    26     def __init__(self, value):
    27         self.value = value
    28     def __str__(self):
    29         return repr(self.value)
    30 
    31 class USBFilter:
    32     vendorid = ""
    33     productid = ""
    34     revision = ""
    35     
    36     def __init__(self, vendorid, productid, revision):
    37         self.vendorid = vendorid.lower()
    38         self.productid = productid.lower()
    39         self.revision = revision.lower()
    40         return
    41     
    42     def __eq__(self, other):
    43         return self.vendorid == other.vendorid and self.productid == other.productid and self.revision == other.revision
    44     
    45     def __hash__(self):
    46         return hash(self.vendorid) ^ hash(self.productid) ^ hash(self.revision)
    47     
    48     def __repr__(self):
    49         return "VendorId = \'" + str(self.vendorid) + "\' ProductId = \'" + str(self.productid) + "\' Revision = \'" + str(self.revision) + "\'"
    50         
    51 
    52 class VMManager(object):
    53     vmRootName = "SecurityDVM"
    54     systemProperties = None
    55     cygwin_path = 'c:\\cygwin64\\bin\\'
    56     vboxManage = 'VBoxManage'
    57     startNotifications = list()
    58     
    59     _instance = None
    60     #def __new__(cls, *args, **kwargs):
    61     #    if not cls._instance:
    62     #        cls._instance = super(VMManager, cls).__new__(cls, *args, **kwargs)
    63     #    return cls._instance
    64     
    65     _instance = None
    66     #def __new__(cls, *args, **kwargs):
    67     #    if not cls._instance:
    68     #        cls._instance = super(VMManager, cls).__new__(cls, *args, **kwargs)
    69     #    return cls._instance
    70     
    71     def __init__(self):
    72         self.cygwin_path = os.path.join(Cygwin.root(), 'bin') + os.path.sep
    73         self.vboxManage = os.path.join(self.getVBoxManagePath(), 'VBoxManage')
    74         self.systemProperties = self.getSystemProperties()
    75         return
    76     
    77     @staticmethod
    78     def getInstance():
    79         if VMManager._instance == None:
    80             VMManager._instance = VMManager()
    81         return VMManager._instance
    82     
    83     def putStartNotification(self, ip):
    84         self.startNotifications.append(ip)
    85     
    86     def isSDVMStarted(self, ip):
    87         return self.startNotifications.contains(ip)
    88              
    89     def execute(self, cmd, wait_return=True ):
    90         if DEBUG:
    91             print('trying to launch: ' + cmd)
    92         process = Popen(cmd, stdout=PIPE, stderr=PIPE) #shell = True
    93         if DEBUG:
    94             print('launched: ' + cmd)
    95         if not wait_return:
    96             return [0, 'working in background', '']
    97         result = process.wait()
    98         res_stdout = process.stdout.read();
    99         res_stderr = process.stderr.read();
   100         if DEBUG:
   101             if res_stdout != "":
   102                 print res_stdout
   103             if res_stderr != "":
   104                 print res_stderr
   105         if result !=0:
   106             raise VMManagerException(res_stderr)
   107         return result, res_stdout, res_stderr
   108     
   109     def getVBoxManagePath(self):
   110         """get the path to the VirtualBox installation on this system"""
   111         p = None
   112         try:
   113             k = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\Oracle\VirtualBox')
   114             p = _winreg.QueryValueEx(k, 'InstallDir')[0]
   115             _winreg.CloseKey(k)
   116         except:
   117             pass
   118         return p
   119     
   120     # return hosty system properties
   121     def getSystemProperties(self):
   122         cmd = self.vboxManage + ' list systemproperties'
   123         result = self.execute(cmd)
   124         if result[1]=='':
   125             return None
   126         props = dict((k.strip(),v.strip().strip('"')) for k,v in (line.split(':', 1) for line in result[1].strip().splitlines()))
   127         return props
   128     
   129     # return the folder containing the guest VMs     
   130     def getDefaultMachineFolder(self):
   131         return self.systemProperties["Default machine folder"]
   132     
   133     #list the hostonly IFs exposed by the VBox host
   134     def getHostOnlyIFs(self):
   135         cmd = 'VBoxManage list hostonlyifs'
   136         result = self.execute(cmd)[1]
   137         if result=='':
   138             return None
   139         props = dict((k.strip(),v.strip().strip('"')) for k,v in (line.split(':', 1) for line in result.strip().splitlines()))
   140         return props
   141         
   142     def listRSDS(self):
   143         cmd = 'VBoxManage list usbhost'
   144         results = self.execute(cmd)[1]
   145         results = results.split('Host USB Devices:')[1].strip()
   146         
   147         items = list( "UUID:"+result for result in results.split('UUID:') if result != '')
   148         rsds = dict()   
   149         for item in items:
   150             props = dict()
   151             for line in item.splitlines():
   152                 if line != "":         
   153                     k,v = line[:line.index(':')].strip(), line[line.index(':')+1:].strip()
   154                     props[k] = v;
   155             
   156             if 'Product' in props.keys() and props['Product'] == 'Mass Storage':
   157                 usb_filter = USBFilter( re.search(r"\((?P<vid>[0-9A-Fa-f]+)\)", props['VendorId']).groupdict()['vid'], 
   158                                         re.search(r"\((?P<pid>[0-9A-Fa-f]+)\)", props['ProductId']).groupdict()['pid'],
   159                                         re.search(r"\((?P<rev>[0-9A-Fa-f]+)\)", props['Revision']).groupdict()['rev'] )
   160                 rsds[props['UUID']] = usb_filter;
   161                 if DEBUG:
   162                     print usb_filter
   163         return rsds
   164 
   165     # list all existing VMs registered with VBox
   166     def listVM(self):
   167         cmd = 'VBoxManage list vms'
   168         result = self.execute(cmd)[1]
   169         vms = list(k.strip().strip('"') for k,_ in (line.split(' ') for line in result.splitlines()))
   170         return vms
   171     
   172     # list existing SDVMs
   173     def listSDVM(self):
   174         vms = self.listVM()
   175         svdms = []
   176         for vm in vms:
   177             if vm.startswith(self.vmRootName) and vm != self.vmRootName:
   178                 svdms.append(vm)
   179         return svdms
   180     
   181     # generate valid (not already existing SDVM name). necessary for creating a new VM
   182     def generateSDVMName(self):
   183         vms = self.listVM()
   184         for i in range(0,999):
   185             if(not self.vmRootName+str(i) in vms):
   186                 return self.vmRootName+str(i)
   187         return ''
   188     
   189     # return the RSDs attached to all existing SDVMs
   190     def getAttachedRSDs(self):
   191         vms = self.listSDVM()
   192         attached_devices = dict()
   193         for vm in vms:
   194             rsd_filter = self.getUSBFilter(vm)
   195             if rsd_filter != None:
   196                 attached_devices[vm] = rsd_filter
   197         return attached_devices
   198     
   199     # configures hostonly networking and DHCP server. requires admin rights
   200     def configureHostNetworking(self):
   201         #cmd = 'vboxmanage list hostonlyifs'
   202         #self.execute(cmd)
   203         #cmd = 'vboxmanage hostonlyif remove \"VirtualBox Host-Only Ethernet Adapter\"'
   204         #self.execute(cmd)
   205         #cmd = 'vboxmanage hostonlyif create'
   206         #self.execute(cmd)
   207         cmd = 'VBoxManage hostonlyif ipconfig \"VirtualBox Host-Only Ethernet Adapter\" --ip 192.168.56.1 --netmask 255.255.255.0'
   208         self.execute(cmd)
   209         #cmd = 'vboxmanage dhcpserver add'
   210         #self.execute(cmd)
   211         cmd = 'VBoxManage dhcpserver modify --ifname \"VirtualBox Host-Only Ethernet Adapter\" --ip 192.168.56.100 --netmask 255.255.255.0 --lowerip 192.168.56.101 --upperip 192.168.56.200'
   212         self.execute(cmd)
   213     
   214     #create new virtual machine instance based on template vm named SecurityDVM (\SecurityDVM\SecurityDVM.vmdk)
   215     def createVM(self, vm_name):
   216         hostonly_if = self.getHostOnlyIFs()
   217         machineFolder = self.getDefaultMachineFolder()
   218         cmd = 'VBoxManage createvm --name ' + vm_name + ' --ostype Debian --register'
   219         self.execute(cmd)
   220         cmd = 'VBoxManage modifyvm ' + vm_name + ' --memory 512 --vram 10 --cpus 1 --usb on --usbehci on --nic1 hostonly --hostonlyadapter1 \"' + hostonly_if['Name'] + '\" --nic2 nat' 
   221         self.execute(cmd)
   222         cmd = 'VBoxManage storagectl ' + vm_name + ' --name contr1 --add sata --portcount 2'
   223         self.execute(cmd)
   224         cmd = 'VBoxManage storageattach ' + vm_name + ' --storagectl contr1 --port 0 --device 0 --type hdd --medium \"'+ machineFolder + '\SecurityDVM\SecurityDVM.vmdk\"' #--mtype immutable
   225         self.execute(cmd)
   226         return
   227     
   228     #remove VM from the system. should be used on VMs returned by listSDVMs    
   229     def removeVM(self, vm_name):
   230         print('removing ' + vm_name)
   231         cmd = 'VBoxManage unregistervm ' + vm_name + ' --delete'
   232         print self.execute(cmd)
   233         machineFolder = self.getDefaultMachineFolder()
   234         cmd = self.cygwin_path + 'bash.exe --login -c \"rm -rf ' + machineFolder + '\\' + vm_name + '*\"'
   235         print self.execute(cmd)
   236     
   237     # start VM
   238     def startVM(self, vm_name):
   239         print('starting ' +  vm_name)
   240         cmd = 'VBoxManage startvm ' + vm_name + ' --type headless' 
   241         result = self.execute(cmd)
   242         while not string.find(str(result), 'successfully started',):
   243             print "Failed to start SDVM: ", vm_name, " retrying"
   244             time.sleep(1)
   245             result = self.execute(cmd)
   246         return result[0]
   247         
   248     # stop VM    
   249     def stopVM(self, vm_name):
   250         print('stopping ' + vm_name)
   251         cmd = 'VBoxManage controlvm ' + vm_name + ' poweroff'
   252         self.execute(cmd)
   253     
   254     # return the hostOnly IP for a running guest    
   255     def getHostOnlyIP(self, vm_name):
   256         print('gettting hostOnly IP address ' + vm_name)
   257         cmd = 'VBoxManage guestproperty get ' + vm_name + ' /VirtualBox/GuestInfo/Net/0/V4/IP'
   258         result = self.execute(cmd)
   259         if result=='':
   260             return None
   261         result = result[1]
   262         if result.startswith('No value set!'):
   263             return None
   264         return result[result.index(':')+1:].strip()
   265     
   266     # attach removable storage device to VM by provision of filter
   267     def attachRSD(self, vm_name, rsd_filter):
   268         cmd = 'VBoxManage usbfilter add 0 --target ' + vm_name + ' --name OpenSecurityRSD --vendorid ' + rsd_filter.vendorid + ' --productid ' + rsd_filter.productid + ' --revision ' + rsd_filter.revision
   269         print self.execute(cmd)
   270         
   271     
   272     # return the description set for an existing VM
   273     def getVMInfo(self, vm_name):
   274         cmd = 'VBoxManage showvminfo ' + vm_name + ' --machinereadable'
   275         results = self.execute(cmd)[1]
   276         props = dict((k.strip(),v.strip().strip('"')) for k,v in (line.split('=', 1) for line in results.splitlines()))
   277         return props
   278     
   279     # return the configured USB filter for an existing VM 
   280     def getUSBFilter(self, vm_name):
   281         props = self.getVMInfo(vm_name)
   282         keys = set(['USBFilterVendorId1', 'USBFilterProductId1', 'USBFilterRevision1'])
   283         keyset = set(props.keys())
   284         usb_filter = None
   285         if keyset.issuperset(keys):
   286             usb_filter = USBFilter(props['USBFilterVendorId1'], props['USBFilterProductId1'], props['USBFilterRevision1'])
   287         return usb_filter
   288     
   289     #generates ISO containing authorized_keys for use with guest VM
   290     def genCertificateISO(self, vm_name):
   291 
   292         # create a SSH key pair in a machine subfolder
   293         #
   294         # to avoid any DOS window popping up we use
   295         # the cygwin's class which relies on the
   296         # shadowrun.exe.
   297         #
   298         # shadowrun.exe is derived from a run.exe of
   299         # the cygwin utilities but with a fix to
   300         # avoid Console Windows to pop up.
   301         # 
   302         # However, run.exe suffers from bad
   303         # argument handling, when there are spaces
   304         # within and so does shadowrun.exe
   305         #
   306         # In order to avoid any complex mechanics
   307         # we start a bash script, which creates the
   308         # SSH certificate in the local folder.
   309         #
   310         # Even more: to get rid of any potential
   311         # space ' ' hazard in path names, we copy
   312         # the script to the creation side as well.
   313         #
   314         # ... and yes: shadowrun.exe terminates
   315         # with a ACCESS_VIOLATION and creates another
   316         # stack-trace-dump file in the folder.
   317         #
   318         # But so does the original cygwin's run.exe
   319         # too.
   320         #           -.-
   321         #
   322         # (On the good side: the access violation happens
   323         # *after* the wrapped process has been launched)
   324         #
   325         machineFolder = self.getDefaultMachineFolder()
   326         vm_folder = os.path.join(machineFolder, vm_name)
   327         old_dir = os.getcwd()
   328         os.chdir(vm_folder)
   329         print(os.path.join(sys.path[0], 'create-cert-and-iso.sh'))
   330         shutil.copy(os.path.join(sys.path[0], 'create-cert-and-iso.sh'), vm_folder)
   331         p = Cygwin()(['/bin/bash', '-c', './create-cert-and-iso.sh'])
   332         p.communicate()
   333         os.chdir(old_dir)
   334 
   335     
   336     # attaches generated ssh public cert to guest vm
   337     def attachCertificateISO(self, vm_name):
   338         machineFolder = self.getDefaultMachineFolder()
   339         cmd = 'vboxmanage storageattach ' + vm_name + ' --storagectl contr1 --port 1 --device 0 --type dvddrive --mtype readonly --medium \"' + machineFolder + '\\' + vm_name + '\\'+ vm_name + '.iso\"'
   340         result = self.execute(cmd)
   341         return result
   342     
   343     handleDeviceChangeLock = threading.Lock()
   344     
   345     # handles device change events
   346     def handleDeviceChange(self):
   347         if VMManager.handleDeviceChangeLock.acquire(True):
   348             #destroy unused vms
   349             new_ip = None
   350             attached_devices = self.getAttachedRSDs()
   351             connected_devices = self.listRSDS()
   352             for vm_name in attached_devices.keys():
   353                 if attached_devices[vm_name] not in connected_devices.values():
   354                     self.unmapNetworkDrive('h:')
   355                     self.stopVM(vm_name)
   356                     self.removeVM(vm_name)
   357             #create new vm for attached device if any
   358             attached_devices = self.getAttachedRSDs()
   359             for connected_device in connected_devices.values():
   360                 if (attached_devices and False) or (connected_device not in attached_devices.values()):
   361                     new_sdvm = self.generateSDVMName()
   362                     self.createVM(new_sdvm)
   363                     self.attachRSD(new_sdvm, connected_device)
   364 
   365 
   366                     self.startVM(new_sdvm)
   367                     # wait for machine to come up
   368                     while new_ip == None:
   369                         time.sleep(1)
   370                         new_ip = self.getHostOnlyIP(new_sdvm)
   371                     while new_ip not in self.startNotifications:
   372                         time.sleep(1)
   373                     if new_ip != None:
   374                         self.mapNetworkDrive('h:', '\\\\' + new_ip + '\\USB', None, None)
   375                     #TODO: cleanup notifications somwhere else (eg. machine shutdown)
   376                     self.startNotifications.remove(new_ip)
   377             VMManager.handleDeviceChangeLock.release()
   378             return new_ip
   379     
   380     def handleBrowsingRequest(self):
   381         if VMManager.handleDeviceChangeLock.acquire(True):
   382             new_ip = None
   383             new_sdvm = self.generateSDVMName()
   384             self.createVM(new_sdvm)
   385             self.genCertificateISO(new_sdvm)
   386             self.attachCertificateISO(new_sdvm)
   387             self.startVM(new_sdvm)
   388             # wait for machine to come up
   389             while new_ip == None:
   390                 time.sleep(1)
   391                 new_ip = self.getHostOnlyIP(new_sdvm)
   392             while new_ip not in self.startNotifications:
   393                 time.sleep(1)
   394             if new_ip != None:
   395                 self.mapNetworkDrive('g:', '\\\\' + new_ip + '\\Download', None, None)
   396             #TODO: cleanup notifications somwhere else (eg. machine shutdown)
   397             self.startNotifications.remove(new_ip)
   398             VMManager.handleDeviceChangeLock.release()
   399         return new_sdvm
   400     
   401     # executes command over ssh on guest vm
   402     def sshGuestExecute(self, vm_name, prog, user_name='osecuser'):
   403         # get vm ip
   404         address = self.getHostOnlyIP(vm_name)
   405         machineFolder = self.getDefaultMachineFolder()
   406         # run command
   407         cmd = self.cygwin_path+'bash.exe --login -c \"ssh -o StrictHostKeyChecking=no -i \\\"' + machineFolder + '\\' + vm_name + '\\dvm_key\\\"  ' + user_name + '@' + address + ' ' + prog + '\"'
   408         return self.execute(cmd)
   409     
   410     # executes command over ssh on guest vm with X forwarding
   411     def sshGuestX11Execute(self, vm_name, prog, user_name='osecuser'):
   412         #TODO: verify if X server is running on user account 
   413         #TODO: set DISPLAY accordingly
   414         address = self.getHostOnlyIP(vm_name)
   415         machineFolder = self.getDefaultMachineFolder()
   416         # run command
   417         #--login
   418         #cmd = self.cygwin_path+'bash.exe --login -c \"DISPLAY=:0 ssh -v -Y -i \\\"' + machineFolder + '\\' + vm_name + '\\dvm_key\\\"  '  + user_name + '@' + address + ' ' + prog + '\"'
   419         cmd = self.cygwin_path+'mintty.exe -e /bin/env DISPLAY=:0 /usr/bin/ssh -o StrictHostKeyChecking=no -v -Y -i \"' + machineFolder + '\\' + vm_name + '\\dvm_key\"  '  + user_name + '@' + address + ' ' + prog + ''
   420         #cmd = self.cygwin_path+'mintty.exe -e /bin/bash --login -c \"DISPLAY=:0 /usr/bin/ssh -v -Y -i \\\"' + machineFolder + '\\' + vm_name + '\\dvm_key\\\"  '  + user_name + '@' + address + ' ' + prog + '\"'
   421         if DEBUG:
   422             print('trying to launch: ' + cmd)
   423         process = Popen(cmd)
   424         if DEBUG:
   425             print('launched: ' + cmd)
   426         return     
   427     
   428     #Small function to check the availability of network resource.
   429     def isAvailable(self, path):
   430         cmd = 'IF EXIST ' + path + ' echo YES'
   431         result = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()
   432         return string.find(str(result), 'YES',)
   433     
   434     #Small function to check if the mention location is a directory
   435     def isDirectory(self, path):
   436         cmd = 'dir ' + path + ' | FIND ".."'
   437         result = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()
   438         return string.find(str(result), 'DIR',)
   439 
   440     def mapNetworkDrive(self, drive, networkPath, user, password):
   441         self.unmapNetworkDrive('h:')
   442         #Check for drive availability
   443         if self.isAvailable(drive) > -1:
   444             print "Drive letter is already in use: ", drive
   445             return -1
   446         #Check for network resource availability
   447         while self.isAvailable(networkPath) == -1:
   448             time.sleep(1)
   449             print "Path not accessible: ", networkPath, " retrying"
   450             #return -1
   451     
   452         #Prepare 'NET USE' commands
   453         cmd = 'NET USE ' + drive + ' ' + networkPath
   454         if user != None:
   455             cmd = cmd + ' ' + password + ' /User' + user
   456     
   457         print "cmd = ", cmd
   458         #Execute 'NET USE' command with authentication
   459         result = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()
   460         print "Executed: ", cmd
   461         if string.find(str(result), 'successfully',) == -1:
   462             print cmd, " FAILED"
   463             return -1
   464         #Mapped with first try
   465         return 1
   466     
   467     def unmapNetworkDrive(self, drive):
   468         #Check if the drive is in use
   469         if self.isAvailable(drive) == -1:
   470             #Drive is not in use
   471             return -1
   472         #Prepare 'NET USE' command
   473         cmd = 'net use ' + drive + ' /DELETE'
   474         result = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True).communicate()
   475         if string.find(str(result), 'successfully',) == -1:
   476             return -1
   477         return 1
   478 
   479 if __name__ == '__main__':
   480     man = VMManager.getInstance()
   481     #man.removeVM('SecurityDVM0')
   482     #man.netUse('192.168.56.134', 'USB\\')
   483     #ip = '192.168.56.139'
   484     #man.mapNetworkDrive('h:', '\\\\' + ip + '\USB', None, None)
   485     #man.cygwin_path = 'c:\\cygwin64\\bin\\'
   486     #man.handleDeviceChange()
   487     #print man.listSDVM()
   488     #man.configureHostNetworking()
   489     #new_vm = man.generateSDVMName()
   490     #man.createVM(new_vm)
   491     #man.genCertificateISO(new_vm)
   492     #man.attachCertificateISO(new_vm)
   493     
   494     #man.attachCertificateISO(vm_name)
   495     #man.sshGuestExecute(vm_name, "ls")
   496     man.sshGuestX11Execute('SecurityDVM1', '/usr/bin/iceweasel')
   497     time.sleep(60)
   498     #cmd = "c:\\cygwin64\\bin\\bash.exe --login -c \"/bin/ls\""
   499     #man.execute(cmd)
   500