diff --git a/.travis.yml b/.travis.yml index 64764396f..142edd953 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ addons: packages: - gcc-multilib - gcc-4.6-arm-linux-gnueabihf + - lib32stdc++6 cache: - pip - directories: diff --git a/docs/source/adb.rst b/docs/source/adb.rst index efaecbfb5..73d788148 100644 --- a/docs/source/adb.rst +++ b/docs/source/adb.rst @@ -1,6 +1,7 @@ .. testsetup:: * from pwn import * + adb = pwnlib.adb :mod:`pwnlib.adb` --- Android Debug Bridge ===================================================== diff --git a/pwnlib/adb.py b/pwnlib/adb.py index 81ecbbdd2..9aca77d73 100644 --- a/pwnlib/adb.py +++ b/pwnlib/adb.py @@ -1,5 +1,6 @@ """Provides utilities for interacting with Android devices via the Android Debug Bridge. """ +import functools import glob import os import platform @@ -21,7 +22,11 @@ log = getLogger(__name__) def adb(argv, *a, **kw): - """Returns the output of an ADB subcommand.""" + r"""Returns the output of an ADB subcommand. + + >>> adb.adb(['get-serialno']) + 'emulator-5554\n' + """ if isinstance(argv, (str, unicode)): argv = [argv] @@ -29,8 +34,60 @@ def adb(argv, *a, **kw): return tubes.process.process(context.adb + argv, *a, **kw).recvall() +@context.quiet +def devices(serial=None): + """Returns a list of ``Device`` objects corresponding to the connected devices.""" + lines = adb(['devices', '-l']) + result = [] + + for line in lines.splitlines(): + # Skip the first 'List of devices attached' line, and the final empty line. + if 'List of devices' in line or not line.strip(): + continue + device = AdbDevice.from_adb_output(line) + if device.serial == serial: + return device + result.append(device) + + return tuple(result) + +def current_device(any=False): + """Returns an ``AdbDevice`` instance for the currently-selected device + (via ``context.device``). + + Example: + + >>> device = adb.current_device(any=True) + >>> device + AdbDevice(serial='emulator-5554', type='device', port='emulator', product='sdk_phone_armv7', model='sdk phone armv7', device='generic') + >>> device.port + 'emulator' + """ + all_devices = devices() + for device in all_devices: + if any or device == context.device: + return device + +def with_device(f): + @functools.wraps(f) + def wrapper(*a,**kw): + if not context.device: + device = current_device(any=True) + if device: + log.warn_once('Automatically selecting device %s' % device) + context.device = device + if not context.device: + log.error('No devices connected, cannot invoke %s.%s' % (f.__module__, f.__name__)) + return f(*a,**kw) + return wrapper + + +@with_device def root(): - """Restarts adbd as root.""" + """Restarts adbd as root. + + >>> adb.root() + """ log.info("Enabling root on %s" % context.device) with context.quiet: @@ -46,9 +103,20 @@ def root(): else: log.error("Could not run as root:\n%s" % reply) - +def no_emulator(f): + @functools.wraps(f) + def wrapper(*a,**kw): + c = current_device() + if c and c.port == 'emulator': + log.error("Cannot invoke %s.%s on an emulator." % (f.__module__, f.__name__)) + return f(*a,**kw) + return wrapper + +@no_emulator +@with_device def reboot(wait=True): - """Reboots the device.""" + """Reboots the device. + """ log.info('Rebooting device %s' % context.device) with context.quiet: @@ -57,8 +125,11 @@ def reboot(wait=True): if wait: wait_for_device() +@no_emulator +@with_device def reboot_bootloader(): - """Reboots the device to the bootloader.""" + """Reboots the device to the bootloader. + """ log.info('Rebooting %s to bootloader' % context.device) with context.quiet: @@ -66,7 +137,7 @@ def reboot_bootloader(): class AdbDevice(Device): """Encapsulates information about a connected device.""" - def __init__(self, serial, type, port, product='unknown', model='unknown', device='unknown'): + def __init__(self, serial, type, port=None, product='unknown', model='unknown', device='unknown', features=None): self.serial = serial self.type = type self.port = port @@ -85,6 +156,19 @@ def __init__(self, serial, type, port, product='unknown', model='unknown', devic self.bits = context.bits self.endian = context.endian + if self.port == 'emulator': + emulator, port = self.serial.split('-') + port = int(port) + try: + with remote('localhost', port, level='error') as r: + r.recvuntil('OK') + r.recvline() # Rest of the line + r.sendline('avd name') + self.avd = r.recvline().strip() + except: + pass + # r = remote('localhost') + def __str__(self): return self.serial @@ -102,34 +186,40 @@ def from_adb_output(line): ZX1G22LM7G device usb:336789504X product:shamu model:Nexus_6 device:shamu features:cmd,shell_v2 84B5T15A29020449 device usb:336855040X product:angler model:Nexus_6P device:angler 0062741b0e54b353 unauthorized usb:337641472X + emulator-5554 offline + emulator-5554 device product:sdk_phone_armv7 model:sdk_phone_armv7 device:generic """ - # The last few fields need to be split at colons. - split = lambda x: x.split(':')[-1] - fields[3:] = list(map(split, fields[3:])) + fields = line.split() - return AdbDevice(*fields[:6]) + serial = fields[0] + type = fields[1] + kwargs = {} -@context.quiet -def devices(serial=None): - """Returns a list of ``Device`` objects corresponding to the connected devices.""" - lines = adb(['devices', '-l']) - result = [] + if serial.startswith('emulator-'): + kwargs['port'] = 'emulator' - for line in lines.splitlines(): - # Skip the first 'List of devices attached' line, and the final empty line. - if 'List of devices' in line or not line.strip(): - continue - device = AdbDevice.from_adb_output(line) - if device.serial == serial: - return device - result.append(device) + for field in fields[2:]: + k,v = field.split(':') + kwargs[k] = v - return tuple(result) + return AdbDevice(serial, type, **kwargs) @LocalContext def wait_for_device(kick=False): - """Waits for a device to be connected.""" + """Waits for a device to be connected. + + By default, waits for the currently-selected device (via ``context.device``). + To wait for a specific device, set ``context.device``. + To wait for *any* device, clear ``context.device``. + + Return: + An ``AdbDevice`` instance for the device. + + Examples: + + >>> device = adb.wait_for_device() + """ with log.waitfor("Waiting for device to come online") as w: with context.quiet: if kick: @@ -156,6 +246,7 @@ def wait_for_device(kick=False): return context.device +@with_device def disable_verity(): """Disables dm-verity on the device.""" with log.waitfor("Disabling dm-verity on %s" % context.device) as w: @@ -168,10 +259,13 @@ def disable_verity(): return elif 'Now reboot your device' in reply: reboot(wait=True) + elif 'error: closed' in reply: + # Device does not support dm-verity? + return else: log.error("Could not disable verity:\n%s" % reply) - +@with_device def remount(): """Remounts the filesystem as writable.""" with log.waitfor("Remounting filesystem on %s" % context.device) as w: @@ -184,6 +278,7 @@ def remount(): if 'remount succeeded' not in reply: log.error("Could not remount filesystem:\n%s" % reply) +@with_device def unroot(): """Restarts adbd as AID_SHELL.""" log.info("Unrooting %s" % context.device) @@ -191,8 +286,9 @@ def unroot(): reply = adb('unroot') if 'restarting adbd as non root' not in reply: - log.error("Could not run as root:\n%s" % reply) + log.error("Could not unroot:\n%s" % reply) +@with_device def pull(remote_path, local_path=None): """Download a file from the device. @@ -200,6 +296,12 @@ def pull(remote_path, local_path=None): remote_path(str): Path or directory of the file on the device. local_path(str): Path to save the file to. Uses the file's name by default. + + Example: + + >>> _=adb.pull('/proc/version', './proc-version') + >>> print read('./proc-version') # doctest: +ELLIPSIS + Linux version ... """ if local_path is None: local_path = os.path.basename(remote_path) @@ -221,12 +323,20 @@ def pull(remote_path, local_path=None): return result +@with_device def push(local_path, remote_path): """Upload a file to the device. Arguments: local_path(str): Path to the local file to push. remote_path(str): Path or directory to store the file on the device. + + Example: + + >>> write('./filename', 'contents') + >>> _=adb.push('./filename', '/data/local/tmp') + >>> adb.read('/data/local/tmp/filename') + 'contents' """ msg = "Pushing %r to %r" % (local_path, remote_path) @@ -245,6 +355,7 @@ def push(local_path, remote_path): return result @context.quiet +@with_device def read(path, target=None): """Download a file from the device, and extract its contents. @@ -252,6 +363,11 @@ def read(path, target=None): path(str): Path to the file on the device. target(str): Optional, location to store the file. Uses a temporary file by default. + + Examples: + + >>> print read('/proc/version') # doctest: +ELLIPSIS + Linux version ... """ with tempfile.NamedTemporaryFile() as temp: target = target or temp.name @@ -260,17 +376,24 @@ def read(path, target=None): return result @context.quiet +@with_device def write(path, data=''): """Create a file on the device with the provided contents. Arguments: path(str): Path to the file on the device data(str): Contents to store in the file + + Examples: + + >>> adb.write('/dev/null', 'data') + >>> adb.write('/data/local/tmp/') """ with tempfile.NamedTemporaryFile() as temp: misc.write(temp.name, data) push(temp.name, path) +@with_device def process(argv, *a, **kw): """Execute a process on the device. @@ -278,11 +401,21 @@ def process(argv, *a, **kw): Returns: A ``process`` tube. + + Examples: + + >>> adb.root() + >>> print adb.process(['cat','/proc/version']).recvall() # doctest: +ELLIPSIS + Linux version ... """ argv = argv or [] if isinstance(argv, (str, unicode)): argv = [argv] + for i, arg in enumerate(argv): + if ' ' in arg and '"' not in arg: + argv[i] = '"%s"' % arg + display = argv argv = context.adb + ['shell'] + argv @@ -291,22 +424,46 @@ def process(argv, *a, **kw): return tubes.process.process(argv, *a, **kw) +@with_device def interactive(**kw): """Spawns an interactive shell.""" return shell(**kw).interactive() +@with_device def shell(**kw): """Returns an interactive shell.""" return process([], **kw) @context.quiet +@with_device def which(name): - """Retrieves the full path to a binary in ``PATH`` on the device""" - return process(['which', name]).recvall().strip() + """Retrieves the full path to a binary in ``PATH`` on the device + + >>> adb.which('sh') + '/system/bin/sh' + """ + # Unfortunately, there is no native 'which' on many phones. + which_cmd = ''' +IFS=: +BINARY=%s +P=($PATH) +for path in "${P[@]}"; do + if [ -e "$path/$BINARY" ]; then + echo "$path/$BINARY"; + break + fi +done +''' % name + + which_cmd = which_cmd.strip() + return process([which_cmd]).recvall().strip() + +@with_device def whoami(): - return process(['whoami']).recvall().strip() + return process(['sh','-ic','echo $USER']).recvall().strip() +@with_device def forward(port): """Sets up a port to forward to the device.""" tcp_port = 'tcp:%s' % port @@ -314,6 +471,7 @@ def forward(port): atexit.register(lambda: adb(['forward', '--remove', tcp_port])) @context.quiet +@with_device def logcat(stream=False): """Reads the system log file. @@ -333,6 +491,7 @@ def logcat(stream=False): else: return adb(['logcat', '-d']) +@with_device def pidof(name): """Returns a list of PIDs for the named process.""" with context.quiet: @@ -340,13 +499,15 @@ def pidof(name): data = io.recvall().split() return list(map(int, data)) +@with_device def proc_exe(pid): """Returns the full path of the executable for the provided PID.""" with context.quiet: - io = process(['readlink','-e','/proc/%d/exe' % pid]) + io = process(['realpath','/proc/%d/exe' % pid]) data = io.recvall().strip() return data +@with_device def getprop(name=None): """Reads a properties from the system property store. @@ -381,10 +542,12 @@ def getprop(name=None): return props +@with_device def setprop(name, value): """Writes a property to the system property store.""" return process(['setprop', name, value]).recvall().strip() +@with_device def listdir(directory='/'): """Returns a list containing the entries in the provided directory. @@ -392,10 +555,11 @@ def listdir(directory='/'): Because ``adb shell`` is used to retrieve the listing, shell environment variable expansion and globbing are in effect. """ - io = process(['find', directory, '-maxdepth', '1', '-print0']) + # io = process(['find', directory, '-maxdepth', '1', '-print0']) + io = process(['sh','-c',"""'cd %s; for file in ./* ./.*; do [ -e "$file" ] || continue; echo -n "$file"; echo -ne "\\x00"; done'""" % directory]) data = io.recvall() paths = filter(len, data.split('\x00')) - relpaths = [os.path.relpath(path, directory) for path in paths] + relpaths = [os.path.relpath(path, '.') for path in paths] if '.' in relpaths: relpaths.remove('.') return relpaths @@ -411,18 +575,23 @@ def fastboot(args, *a, **kw): log.error("Unknown device") return tubes.process.process(['fastboot', '-s', serial] + list(args), **kw).recvall() +@with_device def fingerprint(): """Returns the device build fingerprint.""" return str(properties.ro.build.fingerprint) +@with_device def product(): """Returns the device product identifier.""" return str(properties.ro.build.product) +@with_device def build(): """Returns the Build ID of the device.""" return str(properties.ro.build.id) +@with_device +@no_emulator def unlock_bootloader(): """Unlocks the bootloader of the device. @@ -631,19 +800,29 @@ def _generate_ndk_project(file_list, abi='arm-v7a', platform_version=21): def compile(source): """Compile a source file or project with the Android NDK.""" - # Ensure that we can find the NDK. - ndk = os.environ.get('NDK', None) - if ndk is None: - log.error('$NDK must be set to the Android NDK directory') - ndk_build = os.path.join(ndk, 'ndk-build') + ndk_build = misc.which('ndk-build') + if not ndk_build: + # Ensure that we can find the NDK. + ndk = os.environ.get('NDK', None) + if ndk is None: + log.error('$NDK must be set to the Android NDK directory') + ndk_build = os.path.join(ndk, 'ndk-build') # Determine whether the source is an NDK project or a single source file. project = find_ndk_project_root(source) if not project: - project = _generate_ndk_project(source, - str(properties.ro.product.cpu.abi), - str(properties.ro.build.version.sdk)) + # Realistically this should inherit from context.arch, but + # this works for now. + abi = 'armeabi-v7a' + sdk = '21' + + # If we have an atatched device, use its settings. + if context.device: + abi = str(properties.ro.product.cpu.abi) + sdk = str(properties.ro.build.version.sdk) + + project = _generate_ndk_project(source, abi, sdk) # Remove any output files lib = os.path.join(project, 'libs') @@ -687,6 +866,7 @@ def __dir__(self): return list(self) @context.quiet + @with_device def __iter__(self): root() diff --git a/travis/install.sh b/travis/install.sh index 120ebb1d0..a84f5c46c 100644 --- a/travis/install.sh +++ b/travis/install.sh @@ -59,12 +59,99 @@ setup_travis() setup_linux() { - sudo apt-get install -y software-properties-common openssh-server libncurses5-dev libncursesw5-dev + sudo apt-get install -y software-properties-common openssh-server libncurses5-dev libncursesw5-dev openjdk-8-jre-headless sudo apt-add-repository --yes ppa:pwntools/binutils sudo apt-get update sudo apt-get install binutils-arm-linux-gnu binutils-mips-linux-gnu binutils-powerpc-linux-gnu } +setup_android_emulator() +{ + # If we are running on Travis CI, and there were no changes to Android + # or ADB code, then we do not need the emulator + if [ -n "$TRAVIS" ]; then + if ! (git log --stat "$TRAVIS_COMMIT_RANGE" | grep -E "android|adb"); then + # In order to avoid running the doctests that require the Android + # emulator, while still leaving the code intact, we remove the + # RST file that Sphinx searches. + rm -f 'docs/source/adb.rst' + + # However, the file needs to be present or else things break. + touch 'docs/source/adb.rst' + + echo "Skipping Android emulator install, Android tests disabled." + return + fi + fi + + + if ! which java; then + echo "OpenJDK-8-JRE is required for Android stuff" + exit 1 + fi + + if (uname | grep -i Darwin &>/dev/null); then + brew install android-sdk android-ndk + else + if [ ! -f android-sdk/android ]; then + # Install the SDK, which gives us the 'android' and 'emulator' commands + wget https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz + tar xf android-sdk_r24.4.1-linux.tgz + rm -f android-sdk_r24.4.1-linux.tgz + + # Travis caching causes this to exist already + rm -rf android-sdk + + mv android-sdk-linux android-sdk + file android-sdk/tools/android + fi + + export PATH="$PWD/android-sdk/tools:$PATH" + export PATH="$PWD/android-sdk/platform-tools:$PATH" + which android + + # Install the NDK, which is required for adb.compile() + NDK_VERSION=android-ndk-r12b + if [ ! -f android-ndk/ndk-build ]; then + wget https://dl.google.com/android/repository/$NDK_VERSION-linux-x86_64.zip + unzip -q android-ndk-*.zip + rm -f android-ndk-*.zip + + # Travis caching causes this to exist already + rm -rf android-ndk + + mv $NDK_VERSION android-ndk + fi + + export NDK=$PWD/android-ndk + export PATH=$NDK:$PATH + fi + + # Grab prerequisites + echo y | android update sdk --no-ui --all --filter platform-tools,extra-android-support + echo y | android update sdk --no-ui --all --filter android-21 + + # Valid ABIs: + # - armeabi-v7a + # - arm64-v8a + # - x86 + # - x86_64 + ABI='armeabi-v7a' + + # Grab the emulator image + echo y | android update sdk --no-ui --all --filter sys-img-$ABI-android-21 + + # Create our emulator Android Virtual Device (AVD) + echo no | android --silent create avd --name android-$ABI --target android-21 --force --snapshot --abi $ABI + + # In the future, it would be nice to be able to use snapshots. + # However, I haven't gotten them to work nicely. + emulator -avd android-$ABI -no-window -no-boot-anim -no-skin -no-audio -no-window -no-snapshot & + adb wait-for-device + adb shell id + adb shell getprop +} + setup_osx() { brew update @@ -74,6 +161,7 @@ setup_osx() if [[ "$USER" == "travis" ]]; then setup_travis + setup_android_emulator elif [[ "$USER" == "shippable" ]]; then sudo apt-get update sudo apt-get install openssh-server gcc-multilib @@ -83,6 +171,8 @@ elif [[ "$(uname)" == "Darwin" ]]; then setup_osx elif [[ "$(uname)" == "Linux" ]]; then setup_linux + setup_android_emulator fi + dpkg -l