silent_running.py
#!/usr/bin/python

""" Script for attempting to put a machine into stealth mode so that it isn't giving away
    personally identifiable information to a network.

    For privacy reasons obviously, definitely shouldn't be used for stealing wireless or anything
    else unruly or nefarious.

    Author: sham (sham@southerndarkness.net)

    Usage:
      First define a policy, generally one of the built in ones will work for you. Then run it with
      silentrunning.py {policy_name}

      Policies are defined as dicts, the following keys are available for use:
      firewall: A dict describing a firewall policy
      bad_services: List of service names controlled by OS service handler e.g. init.d that must be stopped 
      un_services:  List of service names that don't need to be there, cos they could give away things, but doesn't
                    really matter if they don't stop
      bad_process:  List of processes we need dead, things like NetworkManager that could give away SSIDs etc
      commands:     List of commands you want run as part of setting a policy
      out_iface:    The name of the interface packets will go out
"""


import logging
import os
import sys
import socket
import struct
import optparse
from fcntl import ioctl

import shamlib

BIN_REQUIRES = ['pidof']
AUTO_SUDO = True

fw_policies = {}
policies = {}

## Firewall Policies

fw_policies['dns_outbound'] = {
                        'name' : 'dns_outbound',
                        'outbound_tcp' : ['53'], 
                        'outbound_udp' : ['53'],
                        'inbound_tcp' : [],
                        'inbound_udp' : []
                        }

fw_policies['tor_only'] =  {
                        'name' : 'tor_only',
                        'outbound_tcp' : [9050], 
                        'outbound_udp' : [],
                        'inbound_tcp' : [],
                        'inbound_udp' : []
                        }

fw_policies['ssh_only'] =  {
                        'name' : 'ssh_only',
                        'outbound_tcp' : [22], 
                        'outbound_udp' : [53],
                        'inbound_tcp' : [],
                        'inbound_udp' : []
                        }


fw_policies['open_outbound'] = {
                        'name' : 'open_outbound',
                        'outbound_tcp' : ['any'], 
                        'outbound_udp' : ['any'],
                        'inbound_tcp' : [],
                        'inbound_udp' : []
                        }

fw_policies['open'] = { 
                        'name' : 'open',
                        'outbound_tcp' : ['any'], 
                        'outbound_udp' : ['any'],
                        'inbound_tcp' : ['any'],
                        'inbound_udp' : ['any']
                       }

## Full Policies
"""
Policy configuration options:
name: string, display name of the policy
bad_services: list of strings, services that are critical to stop via system stop method (/init.d)
un_services: as per bad_services, but not considered critical if they don't stop
good_services: list of strings, services to start via system service manager
bad_processes: list of strings, processes to kill by name (kill -9)
commands: list of strings, commands to run once things are configured
firewall: dict of firewall policy as per fw_policies dict
out_iface: string, the outbound interface (if non-existant or default we will guess)


"""

policies['dns_tunnel'] = {
    'name' : 'DNS Tunnel', 
    'bad_services' : ['cupsys', 'bluetooth', 'avahi-daemon', 'vmware', 'lirc',
                      'vmware-server'],
    'un_services' : ['xinetd', 'inetd', 'mysql', 'mysql-server', 'lirc'],
    'good_services' : ['tor', 'nstx'],
    'bad_processes' : ['NetworkManager'], 
    'commands' : [],
    'firewall' : fw_policies['dns_outbound'],
    'out_iface' : 'ath0'
    }

policies['ssh_tunnel'] = {
                        'name': 'SSH Tunnel', 
                        'bad_services': ['cupsys', 'bluetooth', 'avahi-daemon', 'vmware', 'lirc', 'vmware-server'],
                        'un_services': ['xinetd', 'inetd', 'mysql', 'mysql-server', 'lirc'],
                        'good_services': [],
                        'bad_processes': ['NetworkManager'], 
                        'commands': [],
                        'firewall': fw_policies['ssh_only'],
                        'options': []
                        }



# globals
originalmacs = {}

def main():
  print ''
  usagestring = '%prog [-v] policy\n'
  usagestring += 'where policy is one of [' + ' '.join(policies) + ']'
  version = '0.1'
  parser = optparse.OptionParser(usage = usagestring, version = version)

  parser.add_option('-v', '--verbose', action="count", dest='verbosity', default=0, 
                    help='turn on verbosity')

  (options, args) = parser.parse_args()

  # setup logging
  stdout_logformat='%(message)s'
  if options.verbosity > 1:
    loglevel = logging.DEBUG
  #elif options.verbosity > 0:
  #  loglevel = logging.INFO
  else:
    loglevel = logging.INFO
  logging.basicConfig(level=loglevel, format=stdout_logformat)

  # check we have a policy arg
  if len(args) < 1:
    logging.error("You didn't provide a policy")
    parser.print_usage() 
    sys.exit(1)
  else:
    policy_str = sys.argv[1]
    if not policy_str in policies:
      logging.error("Error: invalid location")
      parser.print_usage() 
      sys.exit()
    else:
      policy = policies[policy_str]

  logging.info('#############################################################################')
  logging.info('Silent Running')
  logging.info('sham@southerndarkness.net')
  logging.info('#############################################################################')

  logging.info('using policy ' + policy_str + ': ' + policy['name'])
  logging.debug(policy)

  # root privs check
  if os.getuid():
    if AUTO_SUDO and shamlib.BinDep('sudo'):
      # this is probably bad practice, but it saves typing :)
      iam = os.path.abspath(__file__)
      logging.warn('!!!!!!!!!!!! Lacking privs, attempting to respawn with sudo using: ' + iam) 
      args = ['sudo',iam]
      args.extend(sys.argv[1:])
      os.execvp('sudo', args)
    else:
      logging.error("You normally need root priviliges to run this, you will probably get errors!")

  # check perms
  if shamlib.CheckSecurePerms(__file__):
    logging.warn('This script will run as root, but is world writeable, you should fix this')

  # get the MAC addresses for each interface
  original_macs = {}
  for interface in shamlib.GetInterfaces():
    original_macs['interface'] = shamlib.GetInterfaceMAC(interface)

  # services that can sometimes actively give away information about your machine
  # stop services
  if policy.has_key('bad_services'):  
    logging.info('Shutting down services')
    for service in policy['bad_services']:
      if not shamlib.StopService(service, True):
        logging.error('Could not stop bad service "%s" terminating' % service)
        sys.exit(1)

  # un services
  if policy.has_key('un_services'):
    logging.info('Shutting down unecessary services')
    for service in policy['bad_services']:
      shamlib.StopService(service)


  # start services
  if policy.has_key('good_services'):
    logging.info('Starting up services')
    for service in policy['good_services']:
      shamlib.StartService(service)

  # TODO stop ipv6 in case we leak inadvertently

  # Take out any automated network managers they tend to do undesirable things like give away our home SSID
  if policy.has_key('bad_processes'):
    logging.info('Killing Bad Processs')
    for process in policy['bad_processes']:
      logging.info('    killing %s' % process)
      if not shamlib.ProcessKillByName(process, confirm=True, signal=9):
        logging.error('Could not stop bad process "%s" terminating' % process)
        sys.exit(1)

  ## Randomize macs
  logging.info('Randomizing MAC addresses')
  # get original mac addresses
  interfaces = shamlib.GetInterfaces()
  for interface in interfaces:
    originalmacs['interface'] = shamlib.GetInterfaceMAC(interface)

  # do the changes
  up_interfaces = shamlib.GetUpInterfaces()
  interfaces.remove('lo')
  for interface in interfaces:
    # an interface usually has to be down to accept a MAC change
    if interface in up_interfaces:
      logging.info('Downing interface %s' % interface)
      os.system('ifconfig %s down' % interface)
      shamlib.RandomizeMAC(interface)
      logging.info('Raising interface %s' % interface)
      os.system('ifconfig %s up' % interface)
    else:
      shamlib.RandomizeMAC(interface)

  # TODO check that none of our interfaces have their original MAC (in case something failed)
  shamlib.CheckMAC(interfaces, original_macs)


  # Kill all interfaces except for required ones
  # TODO 


  # Run up an iptables to block everything outbound except what is specifically required
  logging.info('Setting up iptables policies')
  logging.debug('Using policy ' + str(policy))
  # Run up an iptables 
  if policy.has_key('firewall') and policy['firewall']:
    if 'out_iface' not in policy:
      policy['out_iface'] = shamlib.GetDefaultRouteInterface()

    if not policy['out_iface']:
      logging.warn('Cannot set firewall policy, no out interface specified')
    else:
      logging.info('Setting up iptables policies')
      logging.info('Using policy ' + policy['firewall']['name'])
      iptables = shamlib.IPTablesGen()
      iptables_script = iptables.Generate(policy['firewall'], policy['out_iface'])
      logging.debug(iptables_script)
      for line in iptables_script:
        os.system(line)

      logging.info('Firewall policy configured')

  else:
    logging.warn('No firewall policy configured for this policy, leaving as is')


if __name__ == "__main__":
    main()