tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

list_class_verification_failures.py (9569B)


      1 #!/usr/bin/env vpython3
      2 # Copyright 2018 The Chromium Authors
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """A helper script to list class verification errors.
      7 
      8 This is a wrapper around the device's oatdump executable, parsing desired output
      9 and accommodating API-level-specific details, such as file paths.
     10 """
     11 
     12 
     13 
     14 import argparse
     15 import dataclasses  # pylint: disable=wrong-import-order
     16 import logging
     17 import os
     18 import re
     19 
     20 import devil_chromium
     21 from devil.android import device_errors
     22 from devil.android import device_temp_file
     23 from devil.android import device_utils
     24 from devil.android.ndk import abis
     25 from devil.android.sdk import version_codes
     26 from devil.android.tools import script_common
     27 from devil.utils import logging_common
     28 from py_utils import tempfile_ext
     29 
     30 STATUSES = [
     31    'NotReady',
     32    'RetryVerificationAtRuntime',
     33    'Verified',
     34    'Initialized',
     35    'SuperclassValidated',
     36 ]
     37 
     38 
     39 def DetermineDeviceToUse(devices):
     40  """Like DeviceUtils.HealthyDevices(), but only allow a single device.
     41 
     42  Args:
     43    devices: A (possibly empty) list of serial numbers, such as from the
     44        --device flag.
     45  Returns:
     46    A single device_utils.DeviceUtils instance.
     47  Raises:
     48    device_errors.NoDevicesError: Raised when no non-denylisted devices exist.
     49    device_errors.MultipleDevicesError: Raise when multiple devices exist, but
     50        |devices| does not distinguish which to use.
     51  """
     52  if not devices:
     53    # If the user did not specify which device, we let HealthyDevices raise
     54    # MultipleDevicesError.
     55    devices = None
     56  usable_devices = device_utils.DeviceUtils.HealthyDevices(device_arg=devices)
     57  # If the user specified more than one device, we still only want to support a
     58  # single device, so we explicitly raise MultipleDevicesError.
     59  if len(usable_devices) > 1:
     60    raise device_errors.MultipleDevicesError(usable_devices)
     61  return usable_devices[0]
     62 
     63 
     64 class DeviceOSError(Exception):
     65  """Raised when a file is missing from the device, or something similar."""
     66 
     67 
     68 class UnsupportedDeviceError(Exception):
     69  """Raised when the device is not supported by this script."""
     70 
     71 
     72 def _GetFormattedArch(device):
     73  abi = device.product_cpu_abi
     74  # Some architectures don't map 1:1 with the folder names.
     75  return {abis.ARM_64: 'arm64', abis.ARM: 'arm'}.get(abi, abi)
     76 
     77 
     78 def FindOdexFiles(device, package_name):
     79  """Gets the full paths to the dex files on the device."""
     80  sdk_level = device.build_version_sdk
     81  paths_to_apk = device.GetApplicationPaths(package_name)
     82  if not paths_to_apk:
     83    raise DeviceOSError(
     84        'Could not find data directory for {}. Is it installed?'.format(
     85            package_name))
     86 
     87  ret = []
     88  for path_to_apk in paths_to_apk:
     89    if version_codes.LOLLIPOP <= sdk_level <= version_codes.LOLLIPOP_MR1:
     90      # Of the form "com.example.foo-\d", where \d is a digit (usually 1 or 2).
     91      package_with_suffix = os.path.basename(os.path.dirname(path_to_apk))
     92      arch = _GetFormattedArch(device)
     93      dalvik_prefix = '/data/dalvik-cache/{arch}'.format(arch=arch)
     94      odex_file = '{prefix}/data@app@{package}@base.apk@classes.dex'.format(
     95          prefix=dalvik_prefix, package=package_with_suffix)
     96    elif sdk_level >= version_codes.MARSHMALLOW:
     97      arch = _GetFormattedArch(device)
     98      odex_file = '{data_dir}/oat/{arch}/base.odex'.format(
     99          data_dir=os.path.dirname(path_to_apk), arch=arch)
    100    else:
    101      raise UnsupportedDeviceError(
    102          'Unsupported API level: {}'.format(sdk_level))
    103 
    104    odex_file_exists = device.FileExists(odex_file)
    105    if odex_file_exists:
    106      ret.append(odex_file)
    107    elif sdk_level >= version_codes.PIE:
    108      raise DeviceOSError(
    109          'Unable to find odex file: you must run dex2oat on debuggable apps '
    110          'on >= P after installation.')
    111    else:
    112      raise DeviceOSError('Unable to find odex file ' + odex_file)
    113  return ret
    114 
    115 
    116 def _AdbOatDump(device, odex_file, out_file):
    117  """Runs oatdump on the device."""
    118  # Get the path to the odex file.
    119  with device_temp_file.DeviceTempFile(device.adb) as device_file:
    120    device.RunShellCommand(
    121        ['oatdump', '--oat-file=' + odex_file, '--output=' + device_file.name],
    122        timeout=420,
    123        shell=True,
    124        check_return=True)
    125    device.PullFile(device_file.name, out_file, timeout=220)
    126 
    127 
    128 @dataclasses.dataclass(order=True, frozen=True)
    129 class JavaClass:
    130  """This represents a Java Class and its ART Class Verification status."""
    131  name: str
    132  verification_status: str
    133 
    134 
    135 def _ParseMappingFile(proguard_map_file):
    136  """Creates a map of obfuscated names to deobfuscated names."""
    137  mappings = {}
    138  with open(proguard_map_file, 'r') as f:
    139    pattern = re.compile(r'^(\S+) -> (\S+):')
    140    for line in f:
    141      m = pattern.match(line)
    142      if m is not None:
    143        deobfuscated_name = m.group(1)
    144        obfuscated_name = m.group(2)
    145        mappings[obfuscated_name] = deobfuscated_name
    146  return mappings
    147 
    148 
    149 def _DeobfuscateJavaClassName(dex_code_name, proguard_mappings):
    150  return proguard_mappings.get(dex_code_name, dex_code_name)
    151 
    152 
    153 def FormatJavaClassName(dex_code_name, proguard_mappings):
    154  obfuscated_name = dex_code_name.replace('/', '.')
    155  if proguard_mappings is not None:
    156    return _DeobfuscateJavaClassName(obfuscated_name, proguard_mappings)
    157  return obfuscated_name
    158 
    159 
    160 def ParseOatdump(oatdump_output, proguard_mappings):
    161  """Lists all Java classes in the dex along with verification status."""
    162  java_classes = []
    163  pattern = re.compile(r'\d+: L([^;]+).*\(type_idx=[^(]+\((\w+)\).*')
    164  for line in oatdump_output:
    165    m = pattern.match(line)
    166    if m is not None:
    167      name = FormatJavaClassName(m.group(1), proguard_mappings)
    168      # Some platform levels prefix this with "Status" while other levels do
    169      # not. Strip this for consistency.
    170      verification_status = m.group(2).replace('Status', '')
    171      java_classes.append(JavaClass(name, verification_status))
    172  return java_classes
    173 
    174 
    175 def _PrintVerificationResults(target_status, java_classes, show_summary):
    176  """Prints results for user output."""
    177  # Sort to keep output consistent between runs.
    178  java_classes.sort(key=lambda c: c.name)
    179  d = {}
    180  for status in STATUSES:
    181    d[status] = 0
    182 
    183  for java_class in java_classes:
    184    if java_class.verification_status == target_status:
    185      print(java_class.name)
    186    if java_class.verification_status not in d:
    187      raise RuntimeError('Unexpected status: {0}'.format(
    188          java_class.verification_status))
    189    d[java_class.verification_status] += 1
    190 
    191  if show_summary:
    192    for status in d:
    193      count = d[status]
    194      print('Total {status} classes: {num}'.format(
    195          status=status, num=count))
    196    print('Total number of classes: {num}'.format(
    197        num=len(java_classes)))
    198 
    199 
    200 def RealMain(mapping, device_arg, package, status, hide_summary, workdir):
    201  if mapping is None:
    202    logging.warning('Skipping deobfuscation because no map file was provided.')
    203    proguard_mappings = None
    204  else:
    205    proguard_mappings = _ParseMappingFile(mapping)
    206  device = DetermineDeviceToUse(device_arg)
    207  host_tempfile = os.path.join(workdir, 'out.dump')
    208  device.EnableRoot()
    209  odex_files = FindOdexFiles(device, package)
    210  java_classes = set()
    211  for odex_file in odex_files:
    212    _AdbOatDump(device, odex_file, host_tempfile)
    213    with open(host_tempfile, 'r') as f:
    214      java_classes.update(ParseOatdump(f, proguard_mappings))
    215  _PrintVerificationResults(status, sorted(java_classes), not hide_summary)
    216 
    217 
    218 def main():
    219  parser = argparse.ArgumentParser(description="""
    220 List Java classes in an APK which fail ART class verification.
    221 """)
    222  parser.add_argument(
    223      '--package',
    224      '-P',
    225      type=str,
    226      default=None,
    227      required=True,
    228      help='Specify the full application package name')
    229  parser.add_argument(
    230      '--mapping',
    231      '-m',
    232      type=os.path.realpath,
    233      default=None,
    234      help='Mapping file for the desired APK to deobfuscate class names')
    235  parser.add_argument(
    236      '--hide-summary',
    237      default=False,
    238      action='store_true',
    239      help='Do not output the total number of classes in each Status.')
    240  parser.add_argument(
    241      '--status',
    242      type=str,
    243      default='RetryVerificationAtRuntime',
    244      choices=STATUSES,
    245      help='Which category of classes to list at the end of the script')
    246  parser.add_argument(
    247      '--workdir',
    248      '-w',
    249      type=os.path.realpath,
    250      default=None,
    251      help=('Work directory for oatdump output (default = temporary '
    252            'directory). If specified, this will not be cleaned up at the end '
    253            'of the script (useful if you want to inspect oatdump output '
    254            'manually)'))
    255 
    256  script_common.AddEnvironmentArguments(parser)
    257  script_common.AddDeviceArguments(parser)
    258  logging_common.AddLoggingArguments(parser)
    259 
    260  args = parser.parse_args()
    261  devil_chromium.Initialize(adb_path=args.adb_path)
    262  logging_common.InitializeLogging(args)
    263 
    264  if args.workdir:
    265    if not os.path.isdir(args.workdir):
    266      raise RuntimeError('Specified working directory does not exist')
    267    RealMain(args.mapping, args.devices, args.package, args.status,
    268             args.hide_summary, args.workdir)
    269    # Assume the user wants the workdir to persist (useful for debugging).
    270    logging.warning('Not cleaning up explicitly-specified workdir: %s',
    271                    args.workdir)
    272  else:
    273    with tempfile_ext.NamedTemporaryDirectory() as workdir:
    274      RealMain(args.mapping, args.devices, args.package, args.status,
    275               args.hide_summary, workdir)
    276 
    277 
    278 if __name__ == '__main__':
    279  main()