class LinuxDevice(DesktopDevice):
    def __init__(self, config, connector):
        super().__init__(config, connector)
        self._connector_helper = ConnectorHelper(self)

    @staticmethod
    def local_ips():
        raise XVEx("TODO: Local IPs for Linux")

    @staticmethod
    def open_app(binary_path, root=False):
        unused(root)
        if binary_path is None:
            L.debug('Application has no binary path; not opening')
        # TODO: open the application here

    @staticmethod
    def close_app(binary_path, root=False):
        unused(root)
        if binary_path is None:
            L.debug('Application has no binary path; not closing')
        # TODO: close the application here

    def os_name(self):
        return 'linux'

    def os_version(self):
        return " ".join(platform.linux_distribution())

    def report_info(self):
        info = super().report_info()
        commands = [
            ['uname', '-a'],
            ['lsb_release', '-a'],
            ['lscpu'],
        ]
        for command in commands:
            try:
                info += self._connector_helper.check_command(command)[0]
            except XVProcessException as ex:
                L.warning(
                    "Couldn't get system info using command {}:\n{}".format(
                        command, ex))

        return info

    def kill_process(self, pid):
        L.debug("Killing process {}".format(pid))
        return self._connector_helper.execute_scriptlet(
            'remote_os_kill.py', [pid, int(signal.SIGKILL)], root=True)

    def pgrep(self, process_name):
        L.debug("pgrep-ing for {}".format(process_name))
        return self._connector_helper.execute_scriptlet('pgrep.py',
                                                        [process_name],
                                                        root=True)

    def command_line_for_pid(self, pid):
        return self._connector_helper.execute_scriptlet(
            'command_line_for_pid.py', [pid], root=True)
class MacOSRoute(Route):

    PROG_ROW = re.compile(
        r"^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*([^\s]*)$")

    def __init__(self, device, config):
        super().__init__(device, config)
        self._connector_helper = ConnectorHelper(self._device)

# Internet:
# Destination        Gateway            Flags        Refs      Use   Netif Expire
# default            192.168.56.1       UGSc          100       90     en0
# default            192.168.104.1      UGScI           1        0     en4
# default            192.168.216.1      UGScI           0        0   vlan0

    def get_v4_routes(self):
        routes = []
        lines = self._connector_helper.check_command(['netstat', '-rn'])[0].splitlines()
        for line in lines:
            if "Destination" in line:
                continue
            match = MacOSRoute.PROG_ROW.match(line)
            if not match:
                continue
            entry = RouteEntry(
                dest=match.group(1),
                gway=match.group(2),
                flags=match.group(3),
                refs=match.group(4),
                use=match.group(5),
                iface=match.group(6),
                expire=match.group(7)
            )
            routes.append(entry)
        return routes
Esempio n. 3
0
class MacOSDevice(DesktopDevice):

    def __init__(self, config, connector):
        super().__init__(config, connector)
        # TODO: I think this should be part of DesktopDevice. Need to clarify what all these thigns
        # mean. I think we should move to DesktopDevice meaning anything with the tools. Maybe even
        # this becomes ToolsDevice.
        self._connector_helper = ConnectorHelper(self)

    # TODO: This needs to execute remotely in general. Let's make a scriptlet. Let's ensure that
    # nothing on the device classes themselves restricts the devices to being the localhost
    @staticmethod
    def local_ips():
        ips = []
        for iface in netifaces.interfaces():
            if netifaces.AF_INET in netifaces.ifaddresses(iface):
                ips.append(netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr'])
        return [ipaddress.ip_address(ip) for ip in ips]

    def open_app(self, bundle_path, root=False):
        # Quote the bundle path as some have spaces in
        self._connector_helper.execute_scriptlet(
            'macos_open_app.py', ["'{}'".format(bundle_path)], root=root)

    def close_app(self, bundle_path, root=False):
        # Quit by sending quit signal to the window so the app shuts down how a user would shut it
        # down. In theory it's equivalent to a pkill but slightly more realistic this way
        self._connector_helper.execute_command(
            ['osascript', '-e', "'quit app \"{}\"'".format(bundle_path)], root=root)

    def os_name(self):
        return 'macos'

    def os_version(self):
        return self._connector_helper.execute_scriptlet('remote_mac_ver.py', [])[0]

    def report_info(self):
        info = super().report_info()
        try:
            info += self._connector_helper.check_command(
                ['system_profiler', 'SPSoftwareDataType'])[0]
        except XVProcessException as ex:
            L.warning("Couldn't get OS info from system_profiler:\n{}".format(ex))

        return info

    def kill_process(self, pid):
        L.debug("Killing process {}".format(pid))
        return self._connector_helper.execute_scriptlet(
            'remote_os_kill.py', [pid, int(signal.SIGKILL)], root=True)

    def pgrep(self, process_name):
        '''Similar to the posix pgrep program, however it will return any process ids where
        process_name is a a substring of the whole process command line.'''
        L.debug("pgrep-ing for {}".format(process_name))
        return self._connector_helper.execute_scriptlet('pgrep.py', [process_name], root=True)

    def command_line_for_pid(self, pid):
        return self._connector_helper.execute_scriptlet('command_line_for_pid.py', [pid], root=True)
Esempio n. 4
0
class LinuxRoute(Route):

    PROG_ROW = re.compile(
        r"^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$"
    )

    def __init__(self, device, config):
        super().__init__(device, config)
        self._connector_helper = ConnectorHelper(self._device)

# Kernel IP routing table
# Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
# 0.0.0.0         172.16.49.2     0.0.0.0         UG        0 0          0 ens33
# 169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 ens33
# 172.16.49.0     0.0.0.0         255.255.255.0   U         0 0          0 ens33

    def get_v4_routes(self):
        routes = []
        lines = self._connector_helper.check_command(['netstat',
                                                      '-rn'])[0].splitlines()
        for line in lines:
            if "Destination" in line:
                continue
            match = LinuxRoute.PROG_ROW.match(line)
            print(line)
            if not match:
                continue
            # Windows routes use IPs for the netmask rather than CIDR blocks.
            dest = "{}/{}".format(
                match.group(1),
                netaddr.IPAddress(match.group(3)).netmask_bits())
            entry = RouteEntry(
                dest=dest,
                gway=match.group(2),
                flags=match.group(4),
                iface=match.group(8),
            )
            routes.append(entry)
        return routes
class WindowsRoute(Route):

    PROG_ROW = re.compile(
        r"^\s*([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)$")

    def __init__(self, device, config):
        super().__init__(device, config)
        self._connector_helper = ConnectorHelper(self._device)

# Network Destination        Netmask          Gateway       Interface  Metric
#           0.0.0.0          0.0.0.0      172.16.49.2    172.16.49.174   2000
#           0.0.0.0          0.0.0.0    192.168.216.1  192.168.216.139  11000
#           0.0.0.0          0.0.0.0      172.16.49.2    172.16.49.173      2
#         127.0.0.0        255.0.0.0         On-link         127.0.0.1    331
#         127.0.0.1  255.255.255.255         On-link         127.0.0.1    331
#   127.255.255.255  255.255.255.255         On-link         127.0.0.1    331
#       169.254.0.0      255.255.0.0         On-link    169.254.25.248    262

    def get_v4_routes(self):
        routes = []
        lines = self._connector_helper.check_command(['netstat',
                                                      '-rn'])[0].splitlines()
        for line in lines:
            if "Destination" in line:
                continue
            match = WindowsRoute.PROG_ROW.match(line)
            if not match:
                continue
            # Windows routes use IPs for the netmask rather than CIDR blocks.
            dest = "{}/{}".format(
                match.group(1),
                netaddr.IPAddress(match.group(2)).netmask_bits())
            entry = RouteEntry(dest=dest,
                               gway=match.group(3),
                               flags="",
                               iface=match.group(4))
            routes.append(entry)
        return routes
class WindowsDNSTool(DNSLookupTool):
    def __init__(self, device, config):
        super().__init__(device, config)
        self._connector_helper = ConnectorHelper(self._device)

    def known_servers(self):
        output = self._connector_helper.check_command([
            'wmic.exe', 'nicconfig', 'where', '"IPEnabled = True"', 'get',
            'DNSServerSearchOrder', '/format:rawxml'
        ])[0]

        L.verbose("Got raw wmic output: {}".format(output))
        dns_servers = []
        for nic in fromstring(output).findall("./RESULTS/CIM/INSTANCE"):
            for prop in nic:
                if prop.tag != 'PROPERTY.ARRAY':
                    continue
                for val in prop.findall("./VALUE.ARRAY/VALUE"):
                    ip = ipaddress.ip_address(val.text)
                    dns_servers.append(ip)

        self._check_current_dns_server_is_known(dns_servers)
        return dns_servers
class IPToolCurl(LocalComponent):

    ACCEPTABLE_CURL_ERRORS = [
        "Connection timed out", "Couldn't connect to server",
        "Could not resolve host", "Resolving timed out after",
        "Operation timed out"
    ]

    def __init__(self, device, config):
        super().__init__(device, config)
        # Separating out DynDNS allows us to easily use hosts to get the IPs if we wish.
        self._dyndns = ICanHazIP(self._curl_url)
        self._connector_helper = ConnectorHelper(device)
        self._max_time = None

    def _execute(self, cmd):
        return self._connector_helper.check_command(cmd)

    def _curl_url(self, url):
        L.debug("Curl-ing url {} to find ips".format(url))
        # TODO: Can't use pycurl on cygwin. Need to figure out how to compile cleanly.
        # try:
        #     L.debug("curl-ing {}".format(url))
        #     curl_instance = curl.Curl(url)
        #     curl_instance.set_option(pycurl.TIMEOUT, 5)
        #     response = curl_instance.get()
        #     print response
        # except pycurl.error as ex:
        #     errno, message = ex.args
        #     if errno == pycurl.E_COULDNT_CONNECT:
        #         L.warning(
        #             "pycurl couldn't connect to {}. Assuming no public IP available.".format(url))
        #         return None
        #     # Be cautious. Haven't assessed what other errors are acceptable so let's
        #     # just raise up
        #     # the error.
        #     raise

        try:

            cmd = ['curl', url, '--connect-timeout', '5']
            if self._max_time:
                cmd += ['-m', self._max_time]
            return self._execute(cmd)
        except XVProcessException as ex:
            for error in IPToolCurl.ACCEPTABLE_CURL_ERRORS:
                if error in ex.stderr:
                    L.verbose(
                        "curl couldn't connect to {}. Assuming no public IP available."
                        .format(url))
                    return "", ""
            # Be cautious. Haven't assessed what other errors are acceptable so let's just raise up
            # the error.
            raise

    def public_ipv4_addresses(self, timeout=None):
        # TODO: Yuck! Redo this class. Hierarchy is bad. Just inherit things like DynDNS from
        # IPToolCurl
        self._max_time = timeout
        return self._dyndns.public_ipv4_addresses()

    def public_ipv6_addresses(self, timeout=None):
        self._max_time = timeout
        return self._dyndns.public_ipv6_addresses()

    def all_public_ip_addresses(self, timeout=None):
        self._max_time = timeout
        return self._dyndns.all_public_ip_addresses()
class WindowsDevice(DesktopDevice):  # pylint: disable=no-self-use

    PROG_OS_VERSION = re.compile(r'OS Version:\s*([^\s]+).*')

    def __init__(self, config, connector):
        super().__init__(config, connector)
        self._connector_helper = ConnectorHelper(self)

    def _pgrep_cygwin(self, process_name):
        # -W shows windows processes under cygwin
        lines = self._connector_helper.check_command(['ps',
                                                      '-efW'])[0].splitlines()
        lines = [line for line in lines if process_name in line]
        return [int(line.split()[1]) for line in lines]

    def is_cygwin(self):
        ret = self._connector_helper.execute_command(['uname'])[0]
        # Don't even need to check the output. This will fail if DOS.
        return ret == 0

    def os_name(self):
        return 'windows'

    def os_version(self):
        try:
            # Use systeminfo on Windows as platform.win32_ver doesn't work for cygwin. That way the
            # code is agnostic to the shell.
            output = self._connector_helper.check_command(['systeminfo'])[0]
            for line in output.splitlines():
                match = WindowsDevice.PROG_OS_VERSION.match(line)
                if match:
                    return match.group(1)
            raise XVEx(
                "Couldn't determine Windows Version from systeminfo output:\n{}"
                .format(output))
        except XVProcessException as ex:
            raise XVEx(
                "Couldn't determine Windows Version as systeminfo failed:\n{}".
                format(ex))

    # TODO: I think all desktop devices share this, so derive instead.
    @staticmethod
    def local_ips():
        ips = []
        for iface in netifaces.interfaces():
            if netifaces.AF_INET in netifaces.ifaddresses(iface):
                ips.append(
                    netifaces.ifaddresses(iface)[netifaces.AF_INET][0]['addr'])
        return [ipaddress.ip_address(ip) for ip in ips]

    # TODO: Not sure this will either work at all or work on cygwin
    def open_app(self, app_path, root=False):  # pylint: disable=unused-argument
        cmd = ['run', "\"{}\"".format(windows_safe_path(app_path))]
        L.debug("Executing cmd '{}'".format(cmd))
        self._connector_helper.check_command(cmd)

    def close_app(self, app_path, root=False):  # pylint: disable=unused-argument
        pname = windows_path_split(app_path)[1]
        pids = [str(pid) for pid in self.pgrep(pname)]
        if len(pids) > 1:
            L.warning(
                "Closing all pids {} associated to application {}".format(
                    ', '.join(pids), pname))
        for pid in pids:
            self.kill_process(pid)

    def kill_process(self, pid):
        L.debug("Killing process {}".format(pid))
        # taskkill is the most generic way to handle windows
        self._connector_helper.check_command(
            ['taskkill', '/PID', str(pid), '/F'], root=True)

    def pgrep(self, process_name):
        '''Similar to the posix pgrep program, however it will return any process ids where
        process_name is a a substring of the whole process command line.'''
        L.debug("pgrep-ing for {}".format(process_name))
        if self.is_cygwin():
            return self._pgrep_cygwin(process_name)
        return self._connector_helper.execute_scriptlet('pgrep.py',
                                                        [process_name],
                                                        root=True)

    # This is pretty sketchy. Do better!
    @staticmethod
    def _fix_quotes(cmd_line):
        args = []
        next_arg = ""
        start_quote = False
        escaping = False
        for ichar, char in enumerate(cmd_line):
            if char not in ["\\", "\"", " "]:
                next_arg += char
                continue
            if char == "\"":
                if escaping:
                    next_arg += char
                    escaping = False
                    continue
                elif start_quote:
                    args.append(next_arg)
                    start_quote = False
                    next_arg = ""
                    continue
                else:
                    start_quote = True
                    continue
            elif char == "\\":
                if cmd_line[ichar + 1] == "\"":
                    escaping = True
                    next_arg += char
                else:
                    next_arg += char
                    continue
            elif char == " ":
                if start_quote:
                    next_arg += char
                    continue
                else:
                    if next_arg:
                        args.append(next_arg)
                        next_arg = ""
                    continue
        if next_arg != "":
            args.append(next_arg)
        return args

    def command_line_for_pid(self, pid):
        if self.is_cygwin():
            # The psutil module isn't supported on cygwin
            cmd = [
                "wmic", "process", "where", "ProcessID='{}'".format(pid),
                "get", "CommandLine"
            ]
            args = self._connector_helper.check_command(cmd)[0]
            L.verbose("Raw wmic command line was: {}".format(args))
            return WindowsDevice._fix_quotes(args)

        return self._connector_helper.execute_scriptlet(
            'command_line_for_pid.py', [pid])

    def report_info(self):
        info = super().report_info()
        try:
            info += self._connector_helper.check_command(['systeminfo'])[0]
        except XVProcessException as ex:
            L.warning("Couldn't get OS info from systeminfo:\n{}".format(ex))

        return info
Esempio n. 9
0
class Dig:  # pylint: disable=too-few-public-methods

    # Server line looks like:
    # ;; SERVER: 8.8.8.8#53(8.8.8.8)
    PROG_DIG_SERVER = re.compile(
        r";; SERVER:\s*({}).*".format(RE_IPV4_ADDRESS))

    # Answers look like:
    # google.com.     39  IN  A   216.58.200.14
    PROG_DIG_ANSWER = re.compile(
        r"[^\s]+\s+\d+\s+[A-Z]+\s+[A-Z]+\s+({})".format(RE_IPV4_ADDRESS))

    def __init__(self, device):
        self._connector_helper = ConnectorHelper(device)

    def _execute(self, cmd):
        return self._connector_helper.check_command(cmd)

    @staticmethod
    def _parse_answers(lines):
        ips = []
        for line in lines:
            if line.strip() == "":
                break
            matches = Dig.PROG_DIG_ANSWER.match(line)
            if not matches:
                continue
            ips.append(ipaddress.ip_address(matches.group(1)))

        return ips

    @staticmethod
    def _parse_output(output):
        server = None
        lines = output.splitlines()
        for line in lines:
            matches = Dig.PROG_DIG_SERVER.match(line)
            if not matches:
                continue
            server = matches.group(1)
            break

        if server is None:
            raise XVEx("Couldn't parse dig output: {}".format(output))

        ips = []
        for iline, line in enumerate(lines):
            if 'ANSWER SECTION' not in line:
                continue
            ips = Dig._parse_answers(lines[iline + 1:])
            break

        if len(ips) == 0:
            raise XVEx(
                "dig failed to return any IPs. Output was: {}".format(output))

        return ipaddress.ip_address(server), ips

    # TODO: The timeout here isn't reliable. The process can lock. Let's wrap this with a Popen
    # and just hard kill the process if it time's out. Note that this will be fiddly for remote
    # execution!
    def lookup(self, hostname, timeout, server=None):
        # dig doesn't like floats for timeout
        timeout = int(math.ceil(timeout))
        if server:
            cmd = [
                'dig', "+time={}".format(timeout), hostname,
                "@{}".format(server)
            ]
        else:
            cmd = ['dig', "+time={}".format(timeout), hostname]

        # Prevent the output from being empty
        stdout = None
        while not stdout:
            stdout = self._execute(cmd)[0]
            if not stdout:
                L.verbose("dig output was empty; doing another lookup.")

        L.verbose("dig output: {}".format(stdout))
        return Dig._parse_output(stdout)
Esempio n. 10
0
class LinuxFirewall(Firewall):
    def __init__(self, device, config):
        super().__init__(device, config)
        self._connector_helper = ConnectorHelper(self._device)
        # Having a randomized test chain name is a cheap way of ensuring that two instances of this
        # class will never interfere with one another. Unlikely we'll ever need two but you never
        # know
        self._testing_chain_name = "xv_leak_testing_" + "".join(
            random.choice(string.ascii_uppercase) for _ in range(8))
        self._create_testing_chain()

    def __del__(self):
        self._delete_testing_chain()

    def _block_ip_args_rules(self, ip):
        return [
            [self._testing_chain_name, "-s", ip, "-j", "DROP"],
            [self._testing_chain_name, "-d", ip, "-j", "DROP"],
        ]

    def _chain_exists(self, chain):
        ret, _, _ = self._connector_helper.execute_command(
            ["iptables", "-w", "--list", chain], root=True)
        return ret == 0

    def _create_chain(self, chain):
        L.debug("Creating iptables chain {}".format(chain))
        if self._chain_exists(chain):
            L.debug("iptables chain {} exists".format(chain))
            return
        self._connector_helper.check_command(["iptables", "-w", "-N", chain],
                                             root=True)

    def _delete_chain(self, chain):
        L.debug("Deleting iptables chain {}".format(chain))
        if not self._chain_exists(chain):
            L.debug("iptables chain {} doesn't exist".format(chain))
        self._connector_helper.check_command(["iptables", "-w", "-X", chain],
                                             root=True)

    def _rule_exists(self, rule_args):
        ret, _, _ = self._connector_helper.execute_command(
            ["iptables", "-w", "-C"] + rule_args, root=True)
        return ret == 0

    def _create_rule(self, rule_args):
        L.debug("Creating iptables rule {}".format(rule_args))
        if self._rule_exists(rule_args):
            L.debug("iptables rule {} already exists".format(rule_args))

        self._connector_helper.check_command(["iptables", "-w", "-A"] +
                                             rule_args,
                                             root=True)

    def _delete_rule(self, rule_args):
        L.debug("Deleting iptables rule {}".format(rule_args))
        if not self._rule_exists(rule_args):
            L.debug("iptables rule {} doesn't exist".format(rule_args))
            return

        self._connector_helper.check_command(["iptables", "-w", "-D"] +
                                             rule_args,
                                             root=True)

    def _jump_rule_args(self, chain):
        return [chain, "-j", self._testing_chain_name]

    def _delete_testing_chain(self):
        # Flush all rules in our chain
        self._connector_helper.check_command(
            ["iptables", "-w", "-F", self._testing_chain_name], root=True)

        # Remove all references (jumps) to our chain
        L.debug("Cleaning up iptables testing chain")
        for source_chain in ["INPUT", "OUTPUT"]:
            self._delete_rule(self._jump_rule_args(source_chain))

        self._delete_chain(self._testing_chain_name)

    def _create_testing_chain(self):
        self._create_chain(self._testing_chain_name)

        for source_chain in ["INPUT", "OUTPUT"]:
            self._create_rule(self._jump_rule_args(source_chain))

    def block_ip(self, ip):
        rules = self._block_ip_args_rules(ip)
        for rule in rules:
            self._create_rule(rule)

    def unblock_ip(self, ip):
        rules = self._block_ip_args_rules(ip)
        for rule in rules:
            self._delete_rule(rule)
class PosixGoPacketCapturer:
    def __init__(self, device, interface):
        self._device = device
        self._connector_helper = ConnectorHelper(self._device)
        self._interface = interface
        self._capture_file = 'capture.{}.json'.format(self._interface)
        self._pid_file = None

    def _binary_location(self):
        here_relative = os.path.relpath(
            os.path.dirname(os.path.realpath(__file__)), tools_root())
        return os.path.join(self._device.tools_root(), here_relative, '..',
                            'bin', "xv_packet_capture")

    def _get_packets(self):
        # TODO: This highlights a weakness in the framework. I can't pull to my local device
        # because I don't know where to pull to! Just pulling to a temp file for now
        src = os.path.join(self._device.temp_directory(), self._capture_file)
        file_, dst = tempfile.mkstemp(prefix="xv_leak_test_",
                                      suffix="_{}".format(self._capture_file))
        os.close(file_)
        os.remove(dst)

        timeup = TimeUp(5)
        while not timeup:
            try:
                self._device.connector().pull(src, dst)
                break
            except XVEx:
                L.warning(
                    "Waiting for capture file to be written: {}".format(src))
                time.sleep(1)

        if not os.path.exists(dst):
            raise XVEx("Couldn't get capture file from capture device")

        packets = object_from_json_file(dst, 'attribute')
        return packets['data']

    @staticmethod
    def _random_pid_file():
        return "xv_packet_capture_{}.pid".format(''.join(
            random.choice(string.ascii_uppercase) for _ in range(10)))

    def start(self):
        L.debug("Starting packet capture on interface {}".format(
            self._interface))

        if self._pid_file:
            raise XVEx("Packet capture already started!")

        # TODO: Use PID file
        self._pid_file = os.path.join(self._device.temp_directory(),
                                      PosixGoPacketCapturer._random_pid_file())

        cmd = [
            '/usr/local/bin/daemon', '-o',
            os.path.join(self._device.temp_directory(), 'daemon.out'), '--',
            self._binary_location(), '-i', self._interface, '-o',
            self._device.temp_directory(), '-f',
            'capture.{}.json'.format(self._interface), '--preserve', '--debug'
        ]
        self._connector_helper.check_command(cmd, root=True)

    def stop(self):
        L.debug("Stopping packet capture on interface {} and getting packets".
                format(self._interface))

        if not self._pid_file:
            raise XVEx("Packet capture not started!")

        # TODO: Switch to pid file kill
        cmd = ['killall', '-SIGINT', 'xv_packet_capture']
        self._connector_helper.check_command(cmd, root=True)

        self._pid_file = None
        return self._get_packets()