diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d6575a3..9fcd100b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2405][2405] Add "none" ssh authentication method - [#2427][2427] Document behaviour of remote()'s sni argument as string. - [#2382][2382] added optional port, gdb_args and gdbserver_args parameters to gdb.debug() +- [#2434][2434] upstreaming [vagd](https://github.com/gfelber/vagd) functionality #2434 [2360]: https://github.com/Gallopsled/pwntools/pull/2360 [2356]: https://github.com/Gallopsled/pwntools/pull/2356 @@ -105,6 +106,7 @@ The table below shows which release corresponds to each branch, and what date th [2405]: https://github.com/Gallopsled/pwntools/pull/2405 [2427]: https://github.com/Gallopsled/pwntools/pull/2405 [2382]: https://github.com/Gallopsled/pwntools/pull/2382 +[2434]: https://github.com/Gallopsled/pwntools/pull/2434 ## 4.13.0 (`beta`) diff --git a/docs/source/globals.rst b/docs/source/globals.rst index bca88e855..fd4d37dcc 100644 --- a/docs/source/globals.rst +++ b/docs/source/globals.rst @@ -54,6 +54,9 @@ This is a quick list of most of the objects and routines imported, in rough orde - ``ROP`` - :mod:`pwnlib.rop` - Automatically generate ROP chains using a DSL to describe what you want to do, rather than raw addresses +- ``Virtualization`` + - :mod:`pwnlib.virtualization` + - Automatically virtualize our exploit in different environments - ``gdb.debug`` and ``gdb.attach`` - :mod:`pwnlib.gdb` - Launch a binary under GDB and pop up a new terminal to interact with it. Automates setting breakpoints and makes iteration on exploits MUCH faster. diff --git a/docs/source/index.rst b/docs/source/index.rst index 051ece0af..43a180f8e 100755 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -77,6 +77,8 @@ Each of the ``pwntools`` modules is documented here. update useragents util/* + virtualization + virtualization/* windbg .. toctree:: diff --git a/docs/source/virtualization.rst b/docs/source/virtualization.rst new file mode 100644 index 000000000..cfbedf66a --- /dev/null +++ b/docs/source/virtualization.rst @@ -0,0 +1,28 @@ +.. testsetup:: * + + from pwn import * + +:mod:`pwnlib.virtualization` --- Virtualizing your exploits +============================================================= + +.. automodule:: pwnlib.tubes + + +Types of Virtualization +---------------------------- + +.. toctree:: + :maxdepth: 3 + :glob: + + virtualization/* + + +:mod:`pwnlib.virtualization.pwnvirt` --- Base class +----------------------------------------------------- + + +.. automodule:: pwnlib.virtualization.pwnvirt + + .. autoclass:: pwnlib.virtualization.pwnvirt.pwnvirt() + :members: diff --git a/docs/source/virtualization/sshvirt.rst b/docs/source/virtualization/sshvirt.rst new file mode 100644 index 000000000..0b0024c71 --- /dev/null +++ b/docs/source/virtualization/sshvirt.rst @@ -0,0 +1,11 @@ +.. testsetup:: * + + from pwn import * + context.arch = 'amd64' + context.terminal = [os.path.join(os.path.dirname(pwnlib.__file__), 'gdb_faketerminal.py')] + +:mod:`pwnlib.virtualization.sshvirt` --- Working with Sshvirt +=============================================================== + +.. automodule:: pwnlib.virtualization.sshvirt + :members: diff --git a/pwn/toplevel.py b/pwn/toplevel.py index 92b01ad1d..d5658f944 100644 --- a/pwn/toplevel.py +++ b/pwn/toplevel.py @@ -70,6 +70,7 @@ from pwnlib.util.sh_string import sh_string, sh_prepare, sh_command_with from pwnlib.util.splash import * from pwnlib.util.web import * +from pwnlib.virtualization.sshvirt import sshvirt # Promote these modules, so that "from pwn import *" will let you access them diff --git a/pwnlib/tubes/ssh.py b/pwnlib/tubes/ssh.py index 5463df518..29942ece1 100644 --- a/pwnlib/tubes/ssh.py +++ b/pwnlib/tubes/ssh.py @@ -597,6 +597,9 @@ def __init__(self, user=None, host=None, port=22, password=None, key=None, NOTE: The proxy_command and proxy_sock arguments is only available if a fairly new version of paramiko is used. + Note: alternativly use :meth:`.virtulization.sshvirt`. + + Example proxying: .. doctest:: diff --git a/pwnlib/virtualization/__init__.py b/pwnlib/virtualization/__init__.py new file mode 100644 index 000000000..91767ab4a --- /dev/null +++ b/pwnlib/virtualization/__init__.py @@ -0,0 +1,3 @@ +from __future__ import absolute_import + +__all__ = ['sshvirt'] \ No newline at end of file diff --git a/pwnlib/virtualization/pwnvirt.py b/pwnlib/virtualization/pwnvirt.py new file mode 100644 index 000000000..ec54f00a9 --- /dev/null +++ b/pwnlib/virtualization/pwnvirt.py @@ -0,0 +1,365 @@ +import os + +import pwnlib.args +import pwnlib.filesystem +import pwnlib.gdb +import pwnlib.tubes +import pwnlib.util.misc as misc + +log = pwnlib.log.getLogger(__name__) + +# abstract class +class pwnvirt(object): + """ + start binary inside virtualized environment and return pwnlib.tubes.process.process using Pwnvirt.start() + + Arguments: + + binary(str): + binary for virtualization debugging + files(list): + other files or directories that need to be uploaded to VM + packages(list): + packages to install on vm + symbols(bool): + additionally install libc6 debug symbols + tmp(bool): + if a temporary directory should be created for files + gdb_port(int): + specify static gdbserver port + fast(bool): + mounts libs locally for faster symbol extraction (experimental) + """ + LOCAL_DIR = './.pwntools/' + HOME_DIR = os.path.expanduser('~/share/pwntools/') + SYSROOT = LOCAL_DIR + 'sysroot/' + LOCKFILE = LOCAL_DIR + 'vagd.lock' + SYSROOT_LIB = SYSROOT + 'lib/' + SYSROOT_LIB_DEBUG = SYSROOT + 'lib/debug' + KEYFILE = HOME_DIR + 'keyfile' + PUBKEYFILE = KEYFILE + '.pub' + DEFAULT_PORT = 2222 + STATIC_GDBSRV_PORT = 42069 + + #: if the pwnvirt was newly created (``bool``) + is_new = False + + _path = None + _gdb_port = None + _binary = None + _ssh = None + _fast = False + + def __init__(self, + binary, + libs=False, + files=None, + packages=None, + symbols=True, + tmp=False, + gdb_port=0, + fast=False): + + self._path = binary + self._gdb_port = gdb_port + self._binary = './' + os.path.basename(binary) + + pwnlib.context.context.ssh_session = self._ssh + + if tmp: + self._ssh.set_working_directory() + + if self._sync(self._path): + self.system('chmod +x ' + self._binary) + + if self.is_new and libs: + if not (os.path.exists(pwnvirt.LIBS_DIRECTORY)): + os.makedirs(pwnvirt.LIBS_DIRECTORY) + + self.libs(pwnvirt.LIBS_DIRECTORY) + + if self.is_new and packages is not None: + if symbols: + packages.append(pwnvirt.LIBC6_DEBUG) + try: + elf = pwnlib.elf.ELF(binary) + if elf.arch == 'i386': + packages.append(pwnvirt.LIBC6_I386) + except: + log.warn("failed to get architecture from binary") + self._install_packages(packages) + + self._fast = fast + + if self._fast: + self._mount_lib() + + # Copy files to remote + if isinstance(files, str): + self._sync(files) + elif hasattr(files, '__iter__'): + for file in files: + self._sync(file) + + def _vm_setup(self): + """ + setup virtualized machine + """ + pass + + def _ssh_setup(self): + """ + setup ssh connection + """ + pass + + def _sync(self, file): + """ + upload file on remote if it doesn't exist + Arguments: + + file(str): + file to upload + + Returns: + if the file was uploaded + """ + sshpath = pwnlib.filesystem.SSHPath(file) + if not sshpath.exists(): + self.put(file) + return True + return False + + _SSHFS_TEMPLATE = \ + 'sshfs -p {port} -o StrictHostKeyChecking=no,ro,IdentityFile={keyfile} {user}@{host}:{remote_dir} {local_dir}' + + def _mount(self, remote_dir, local_dir): + """ + mount remote dir on locally using sshfs + + Arguments: + + remote_dir(str): + directory on remote to mount + local_dir(str): + local mount point + """ + if not misc.which('sshfs'): + log.error('sshfs isn\'t installed') + cmd = pwnvirt._SSHFS_TEMPLATE.format(port=self._ssh.port, + keyfile=self._ssh.keyfile, + user=self._ssh.user, + host=self._ssh.host, + remote_dir=remote_dir, + local_dir=local_dir) + log.info(cmd) + os.system(cmd) + + def _lock(self, typ): + """ + create lock file vor current virtualization type + + Arguments: + + typ(str): + the type of virtualization + """ + if not os.path.exists(pwnvirt.LOCAL_DIR): + os.makedirs(pwnvirt.LOCAL_DIR) + + with open(pwnvirt.LOCKFILE, 'w') as lfile: + lfile.write(typ) + + def _mount_lib(self, remote_lib='/usr/lib'): + """ + mount the lib directory of remote + + Arguments: + + remote_lib(str): + the lib directory to mount locally + """ + if not (os.path.exists(pwnvirt.SYSROOT) and os.path.exists(pwnvirt.SYSROOT_LIB)): + os.makedirs(pwnvirt.SYSROOT_LIB) + if not os.path.ismount(pwnvirt.SYSROOT_LIB): + log.info('mounting libs in sysroot') + self._mount(remote_lib, pwnvirt.SYSROOT_LIB) + + def system(self, cmd): + """ + executes command on vm, interface to :class: pwnlib.tubes.ssh.ssh.system + + Arguments: + + cmd(str): + command to execute on virtualized environment + + Returns: + + :class:`pwnlib.tubes.ssh.ssh_channel.SSHChannel` + """ + return self._ssh.system(cmd) + + DEFAULT_PACKAGES = ['gdbserver', 'python3', 'sudo'] + LIBC6_DEBUG = 'libc6-dbg' + LIBC6_I386 = 'libc6-i386' + + def _install_packages(self, packages): + """ + install packages on remote machine + + Arguments: + + packages(list): + packages to install on remote machine + """ + self.system("sudo apt update").recvall() + packages_str = " ".join(packages) + self.system("sudo NEEDRESTART_MODE=a apt install -y {}".format(packages_str)).recvall() + + def put(self, file, remote=None): + """ + upload file or dir on vm + + Arguments: + + file(str): + file to upload + remote(str): + remote location of file, working directory if not specified + """ + if os.path.isdir(file): + self._ssh.upload_dir(file, remote=remote) + else: + self._ssh.upload(file, remote=remote) + + def pull(self, file, local=None): + """ + download file or dir on vm + + Arguments: + + file(str): + remote location of file, working directory if not specified + local(str): + local location of file, current directory if not specified + """ + sshpath = pwnlib.filesystem.SSHPath(os.path.basename(file)) + if sshpath.is_dir(): + self._ssh.download_dir(file, local=local) + else: + self._ssh.download_file(file, local=local) + + LIBS_DIRECTORY = "libs" + + def close(self): + """ + closing vm + """ + self._ssh.close() + + def libs(self, directory=None): + """ + Downloads the libraries referred to by a file. + This is done by running ldd on the remote server, parsing the output and downloading the relevant files. + + Arguments: + + directory(str): + Output directory + """ + for lib in self._ssh._libs_remote(self._binary).keys(): + self.pull(lib, directory + '/' + os.path.basename(lib)) + + def debug(self, argv=None, ssh=None, gdb_args=None, gdbscript='', sysroot=None, **kwargs): + """ + run binary in vm with gdb (pwnlib feature set) + + Arguments: + + argv(list): + comandline arguments for binary + ssh(None): + ignored self._ssh is used instead + gdb_args(list): + gdb args to forward to gdb + gdbscript(str): + GDB script for GDB + sysroot(str): + sysroot dir + \**kwargs: + passthrough arguments to pwnlib.gdb.debug + + Returns: + :class:`pwnlib.tubes.process.process` + """ + + if argv is None: + argv = list() + + if gdb_args is None: + gdb_args = list() + + if self._fast: + if sysroot is not None: + log.warn('fast enabled but sysroot set, sysroot is ignored') + sysroot = pwnvirt.SYSROOT_LIB + + if sysroot is not None: + gdbscript = "set debug-file-directory {}\n".format(pwnvirt.SYSROOT_LIB_DEBUG) + gdbscript + + gdb_args += ["-ex", "file -readnow {}".format(self._path)] + + return pwnlib.gdb.debug([self._binary] + argv, ssh=self._ssh, gdb_args=gdb_args, port=self._gdb_port, + gdbscript=gdbscript, sysroot=sysroot, **kwargs) + + def process(self, argv=None, **kwargs): + """ + run binary in vm as process + + Arguments: + + argv(list): + commandline arguments for binary + \**kwargs: + passthrough arguments to pwnlib.ssh.ssh.process + + Returns: + :class:`pwnlib.tubes.process.process` + """ + if argv is None: + argv = list() + return self._ssh.process([self._binary] + argv, **kwargs) + + def start(self, + argv=None, + gdbscript='', + api=None, + sysroot=None, + gdb_args=None, + **kwargs): + """ + start binary on remote and return pwnlib.tubes.process.process + + Arguments: + argv(list): + commandline arguments for binary + gdbscript(str): + GDB script for GDB + api(bool): + if GDB API should be enabled + sysroot(str): + sysroot dir + gdb_args(list): + extra gdb args + \**kwargs: + passthrough arguments + + Returns: + :class:`pwnlib.tubes.process.process` + """ + if pwnlib.args.args.GDB: + return self.debug(argv=argv, gdbscript=gdbscript, gdb_args=gdb_args, sysroot=sysroot, + api=api, **kwargs) + else: + return self.process(argv=argv, **kwargs) diff --git a/pwnlib/virtualization/sshvirt.py b/pwnlib/virtualization/sshvirt.py new file mode 100644 index 000000000..925abb117 --- /dev/null +++ b/pwnlib/virtualization/sshvirt.py @@ -0,0 +1,156 @@ +import time +import pwnlib.tubes.ssh +from pwnlib.virtualization.pwnvirt import pwnvirt + +log = pwnlib.log.getLogger(__name__) + + +class sshvirt(pwnvirt): + r""" + ssh virtualization interface for pwntools + + Arguments: + binary(str): + binary to execute + user(str): + ssh user + host(str): + ssh hostname + port(int): + ssh port + keyfile(str): + ssh keyfile + password(str): + ssh password + ignore_config(bool): + If :const:`True`, disable usage of ~/.ssh/config and ~/.ssh/authorized_keys + \**kwargs: + Passthrough arguments to :class: Pwnvirt + + :meth:`.ssh.process`. + Running as process (or using start()): + + >>> with open('test', 'w') as f: + ... _ = f.write('#!/bin/echo') + >>> vm = sshvirt('./test', user='travis', host='example.pwnme', password='demopass') + >>> vm.system('ls ./test').recvall(timeout=1) + b'./test\n' + + >>> io = vm.process() + >>> io.recvall(timeout=1) + b'./test\n' + + >>> io.close() + + Running with gdb (or using start() and args.GDB): + + >>> io = vm.debug(gdbscript='continue') + >>> io.recvline(timeout=5) + b'./test\n' + >>> io.close() + + Running with gdb and api: + + .. doctest:: + :skipif: is_python2 + + >>> io = vm.debug(api=True) + >>> bp = io.gdb.Breakpoint('write', temporary=True) + >>> io.gdb.continue_and_wait() + >>> count = io.gdb.parse_and_eval('$rdx') + >>> long = io.gdb.lookup_type('long') + >>> int(count.cast(long)) + 7 + >>> io.gdb.continue_nowait() + >>> io.recvline(timeout=1) + b'./test\n' + >>> io.close() + + Closing vm (optional): + >>> vm.close() + """ + + DEFAULT_HOST = 'localhost' + DEFAULT_PORT = 22 + DEFAULT_USER = 'root' + + _user = None + _host = None + _port = 0 + _keyfile = None + _password = None + _ssh = None + + def __init__(self, + binary, + user=DEFAULT_USER, + host=DEFAULT_HOST, + port=DEFAULT_PORT, + keyfile=None, + password=None, + ignore_config=False, + **kwargs): + self._user = user + self._host = host + self._port = port + self._keyfile = keyfile + self._password = password + self._ignore_config = ignore_config + + self._ssh_setup() + + super(sshvirt, self).__init__(binary=binary, **kwargs) + + def bind(self, port): + """ + bind port from ssh connection locally + :param port: + :return: + """ + + remote = self._ssh.connect_remote('127.0.0.1', port) + listener = pwnlib.tubes.listen.listen(0) + port = listener.lport + + # Disable showing GDB traffic when debugging verbosity is increased + remote.level = 'error' + listener.level = 'error' + + # Hook them up + remote.connect_both(listener) + + return port + + def _vm_setup(self): + """ + pass + """ + pass + + _TRIES = 3 # three times the charm + + def _ssh_setup(self): + """ + setup ssh connection + """ + progress = log.progress("connecting to ssh") + for i in range(sshvirt._TRIES): + try: + self._ssh = pwnlib.tubes.ssh.ssh( + user=self._user, + host=self._host, + port=self._port, + password=self._password, + keyfile=self._keyfile, + ignore_config=self._ignore_config + ) + progress.success("Done") + break + except: + if i + 1 >= sshvirt._TRIES: + progress.failure('Failed') + log.error("Failed to connect to ssh") + else: + progress.status('Trying again') + # shorter pause for first two tries + time.sleep(1 if i == 0 else 10)