├── data └── README.md ├── meta ├── demo.gif ├── fuzzboot.cfg └── data.json ├── .gitignore ├── utils.py ├── serializable.py ├── LICENSE ├── log.py ├── config.py ├── aboot.py ├── README.md ├── fuzzboot.py ├── oemtester.py ├── device.py └── image.py /data/README.md: -------------------------------------------------------------------------------- 1 | populated images will be added here 2 | -------------------------------------------------------------------------------- /meta/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilpan/fuzzboot/HEAD/meta/demo.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | 4 | runtime/ 5 | data/* 6 | !data/README.md 7 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import io 2 | import string 3 | import subprocess as sp 4 | 5 | def shell_exec(cmd): 6 | p = sp.run(cmd, stdout=sp.PIPE, stderr=sp.PIPE) 7 | return p.stdout 8 | 9 | 10 | def shell_get_strings(file, prefix): 11 | cmd = ['strings', file] 12 | sio = io.StringIO(shell_exec(cmd).decode()) 13 | strings = [] 14 | for line in sio: 15 | strings.append(line.rstrip()) 16 | strings.sort() 17 | return strings 18 | 19 | 20 | def get_strings(data, prefix): 21 | s = "" 22 | printable = set(string.printable) 23 | strings = set() 24 | i = 0 25 | for c in data: 26 | if 0 == i % 2**20: 27 | T("%d", i >> 20) 28 | if c in printable: 29 | s += c 30 | else: 31 | if "" != s: 32 | if s.startswith(prefix): 33 | strings.add(s) 34 | s = "" 35 | i += 1 36 | strings = list(strings) 37 | strings.sort() 38 | return strings 39 | -------------------------------------------------------------------------------- /serializable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Roee Hay / Aleph Research / HCL Technologies 3 | """ 4 | 5 | import json 6 | import os.path 7 | 8 | class Serializable(object): 9 | 10 | def __init__(self): 11 | self.__dict__ = {} 12 | 13 | def __getattr__(self, item): 14 | return self.__dict__[item] 15 | 16 | def __getitem__(self, item): 17 | return self.__getattr__(item) 18 | 19 | def __setattr__(self, item, val): 20 | if item == "__dict__": 21 | return 22 | self.__dict__[item] = val 23 | 24 | def __setitem__(self, item, val): 25 | self.__setattr__(item, val) 26 | 27 | def __repr__(self): 28 | return self.dump() 29 | 30 | def dump(self): 31 | return json.dumps(self.__dict__, indent=4, separators=(',', ': ')) 32 | 33 | def set_data(self, data): 34 | self.__dict__.update(data) 35 | return self 36 | 37 | def save(self,path): 38 | if os.path.exists(path): 39 | return False 40 | with open(path, 'w') as f: 41 | f.write(self.dump()) 42 | return True 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Aleph Research, HCL Technologies 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /meta/fuzzboot.cfg: -------------------------------------------------------------------------------- 1 | ; ignore commands with the following pattern 2 | ignore_re: (lock|unlock).* 3 | 4 | ; only fuzz strings beginning with `oem` 5 | oem_only: false 6 | 7 | ; only fuzz strings which contain alpha-numberic characters, hyphens, underscores and spaces. 8 | alphanum_only: true 9 | 10 | ; fuzz substrings 11 | substrings: false 12 | 13 | ; strip whitespaces from tested commands 14 | strip_whitespace: true 15 | 16 | ; split commands in spaces 17 | split_space: true 18 | 19 | ; remove breaks such as `\r` and `\n` from tested commands during fuzzing 20 | remove_breaks: true 21 | 22 | ; max tested command length 23 | max_len: 60 24 | 25 | ; do not compute the number of available strings, degrades the progress indicator but improves the tool's loading time. 26 | use_strings_generator: false 27 | 28 | ; print output to stderr of non-negative commands during testing 29 | show_output: false 30 | 31 | ; USB timeout 32 | timeout: 5000 33 | 34 | ; self-explanatory 35 | log_file: runtime/fuzzboot.log 36 | 37 | ; adb path, used for auto-recovery from reboots 38 | adb_path: adb 39 | 40 | ; used for populating factory images of fugu. 41 | ota_umkbootimg_path: umkbootimg 42 | ota_unpack_ramdisk_path: unpack_ramdisk 43 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from config import Config 4 | 5 | level = logging.INFO 6 | TRACE = 9 7 | 8 | l = logging.getLogger("FUZZBOOT") 9 | 10 | 11 | con = logging.StreamHandler(sys.stderr) 12 | con.setFormatter(logging.Formatter('%(levelname)1s: %(message)s')) 13 | con.setLevel(level) 14 | 15 | logfile = logging.FileHandler(Config.get_config().log_file) 16 | logfile.setFormatter(logging.Formatter('%(asctime)-15s %(levelname)5s: %(message)s')) 17 | logfile.setLevel(logging.DEBUG) 18 | 19 | l.addHandler(con) 20 | l.addHandler(logfile) 21 | l.setLevel(TRACE) 22 | 23 | 24 | logging.addLevelName(TRACE, "TRACE") 25 | 26 | 27 | def adjustLevels(): 28 | for log in logging.Logger.manager.loggerDict: 29 | l.setLevel(logging.CRITICAL) 30 | 31 | for h in logging.root.handlers: 32 | logging.root.removeHandler(h) 33 | 34 | l.setLevel(TRACE) 35 | 36 | 37 | def setVerbose(more = False): 38 | global level 39 | level = more and TRACE or logging.DEBUG 40 | logfile.setLevel(level) 41 | 42 | 43 | def I(msg, *kargs, **kwargs): 44 | l.info(msg, *kargs, **kwargs) 45 | 46 | 47 | def D(msg, *kargs, **kwargs): 48 | l.debug(msg, *kargs, **kwargs) 49 | 50 | 51 | def T(msg, *kargs, **kwargs): 52 | l.log(TRACE, msg, *kargs, **kwargs) 53 | 54 | 55 | def W(msg, *kargs, **kwargs): 56 | l.warn(msg, *kargs, **kwargs) 57 | 58 | 59 | def E(msg, *kargs, **kwargs): 60 | l.error(msg, *kargs, **kwargs) 61 | -------------------------------------------------------------------------------- /meta/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "oem": null, 3 | "device": null, 4 | "ignore_re": null, 5 | "oem_only": false, 6 | "alphanum_only": false, 7 | "use_strings_generator": false, 8 | "substrings": false, 9 | "strip_whitespace": true, 10 | "split_space": true, 11 | "remove_breaks": true, 12 | "show_output": false, 13 | "treat_as_blob": false, 14 | "max_len": 60, 15 | "oem_error_cmd": "foobarbaz123", 16 | "timeout": 5000, 17 | "data_path": "./data", 18 | "string_prefix": "", 19 | "factory_fallback_bootloader": true, 20 | "log_file": "./runtime/fuzzboot.log", 21 | "oems": { 22 | "htc": ["volantis","volantisg","flounder","flounder_lte","marlin","sailfish"], 23 | "motorola": ["shamu", "athene_13mp", "harpia", "cedric", "thea","potter","osprey"], 24 | "huawei": ["angler"], 25 | "lg": ["bullhead", "hammerhead", "occam", "lgh850"], 26 | "google": ["ryu", "tungsten"], 27 | "asus": ["fugu","razor","razorg","nakasi", "nakasig", "grouper", "flo", "deb"], 28 | "oneplus": ["bacon", "oneplus2", "oneplus3", "oneplus3t", "oneplus5"], 29 | "sony": ["f5121"], 30 | "samsung": ["mantaray", "mysid", "mysidspr", "yakju", "takju", "soju", "sojua", "sojuk", "sojus"], 31 | "xiaomi": ["gemini", "capicorn", "nikel", "kenzo", "hennessy", "lithium"] 32 | }, 33 | "bootloader_names": { 34 | "smaug": "ryu", 35 | "volantis": "flounder", 36 | "volantisg": "flounderg", 37 | "grouper": "nakasi", 38 | "15811": "oneplus3t", 39 | "15801": "oneplus3", 40 | "athene_13mp": "athene" 41 | }, 42 | 43 | 44 | "ota_prevalent_aboot_paths": ["bootloader.aboot.img", "firmware-update/emmc_appsboot.mbn", "uboot.img", "firmware-update/lk.bin"], 45 | "ota_umkbootimg_path": "umkbootimg", 46 | "ota_unpack_ramdisk_path": "unpack_ramdisk", 47 | "adb_path": "adb", 48 | "adb_key_path": "~/.android/adbkey" 49 | } 50 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Roee Hay / Aleph Research / HCL Technologies 3 | """ 4 | 5 | import json 6 | import configparser 7 | import io 8 | 9 | from serializable import * 10 | DATA_PATH = "./meta/data.json" 11 | USER_CONFIG_PATH = "./meta/fuzzboot.cfg" 12 | 13 | config = None 14 | 15 | class MetaConfig(type): 16 | 17 | def __getattr__(cls, name): 18 | return cls.get_config().__getattr__(name) 19 | 20 | def __setattr__(cls, name, val): 21 | return cls.get_config().__setattr__(name, val) 22 | 23 | def __repr__(cls): 24 | return cls.get_config().__repr__() 25 | 26 | 27 | class Config(Serializable): 28 | __metaclass__ = MetaConfig 29 | 30 | config = None 31 | 32 | @classmethod 33 | def overlay(cls, data): 34 | dest = {} 35 | for t in data: 36 | if data[t]: 37 | dest[t] = data[t] 38 | cls.get_config().set_data(dest) 39 | 40 | @classmethod 41 | def get_config(cls): 42 | global config 43 | if not config: 44 | config = Config() 45 | config.set_data(json.load(open(DATA_PATH, "rb"))) 46 | 47 | data = "[root]\n"+open(USER_CONFIG_PATH, "rb").read().decode() 48 | fp = io.StringIO(data) 49 | parser = configparser.RawConfigParser() 50 | parser.readfp(fp) 51 | 52 | cfg = {} 53 | for k in parser.options("root"): 54 | try: 55 | cfg[k] = parser.getboolean("root", k) 56 | continue; 57 | except ValueError: 58 | pass 59 | 60 | try: 61 | cfg[k] = parser.getint("root", k) 62 | continue; 63 | except ValueError: 64 | pass 65 | 66 | try: 67 | cfg[k] = parser.getfloat("root", k) 68 | continue; 69 | except ValueError: 70 | pass 71 | 72 | cfg[k] = parser.get("root", k) 73 | 74 | config.set_data(cfg) 75 | 76 | return config 77 | 78 | Config.get_config() 79 | -------------------------------------------------------------------------------- /aboot.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Roee Hay / Aleph Research / HCL Technologies 3 | """ 4 | 5 | import json 6 | import hashlib 7 | import os 8 | from serializable import Serializable 9 | from log import * 10 | from config import Config 11 | import utils 12 | 13 | bootloaders_by_device = {} 14 | bootloaders_by_oem = {} 15 | bootloaders = None 16 | 17 | 18 | def get_bootloaders(path=Config.get_config().data_path): 19 | global bootloaders 20 | if bootloaders: 21 | return bootloaders 22 | 23 | bootloaders = [] 24 | n = 0 25 | for f in os.listdir(path): 26 | if f.endswith(".json"): 27 | bl = ABOOT.create_from_json(os.path.join(path, f)) 28 | bootloaders.append(bl) 29 | if bl.oem not in bootloaders_by_oem: 30 | bootloaders_by_oem[bl.oem] = [] 31 | bootloaders_by_oem[bl.oem].append(bl) 32 | if bl.device not in bootloaders_by_device: 33 | bootloaders_by_device[bl.device] = [] 34 | bootloaders_by_device[bl.device].append(bl) 35 | n+=1 36 | D("loaded %d bootloaders (%d devices, %d OEMs)", n, len(bootloaders_by_device), len(bootloaders_by_oem)) 37 | return bootloaders 38 | 39 | 40 | def by_oem(oem = None): 41 | all() 42 | 43 | if not oem: 44 | return bootloaders_by_oem 45 | 46 | try: 47 | D("bootloader.by_oem[%s]: %s", oem, bootloaders_by_oem[oem]) 48 | return bootloaders_by_oem[oem] 49 | except KeyError: 50 | return [] 51 | 52 | 53 | def by_device(device = None): 54 | all() 55 | 56 | if not device: 57 | return bootloaders_by_device 58 | try: 59 | D("bootloader.by_device[%s]: %s", device, bootloaders_by_device[device]) 60 | return bootloaders_by_device[device] 61 | except KeyError: 62 | return [] 63 | 64 | 65 | def all(): 66 | if not bootloaders: 67 | get_bootloaders() 68 | 69 | return bootloaders 70 | 71 | 72 | class ABOOT(Serializable): 73 | @classmethod 74 | def create_from_json(cls, path): 75 | data = json.load(open(path, "rb")) 76 | return ABOOT().set_data(data) 77 | 78 | @classmethod 79 | def create_from_bootloader_image(cls, fp, oem, device, build, src, name, strprefix=""): 80 | data = fp.read() 81 | sha256 = hashlib.sha256(data).hexdigest() 82 | D("SHA256 = %s", sha256) 83 | # strings = utils.get_strings(data, strprefix) 84 | strings = utils.shell_get_strings(fp.name, strprefix) 85 | return ABOOT().set_data({'src': src, 86 | 'name': name, 87 | 'sha256': sha256, 88 | 'strings': strings, 89 | 'oem': oem, 90 | 'device': device, 91 | 'build': build}) 92 | 93 | def __repr__(self): 94 | return "%s/%s/%s" % (self.oem, self.device, self.build) 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuzzboot 2 | 3 | Simple fuzzer for discovering hidden fastboot gems. 4 | 5 | > Forked from [abootool][abootool] By Roee Hay / Aleph Research, HCL Technologies 6 | 7 | **Modus Operandi**: Based on static knowledge (strings fetched from available bootloader images), dynamically fuzz for hidden fastboot OEM commands. 8 | 9 | Appears in the USENIX WOOT '17 paper: [fastboot oem vuln: Android Bootloader Vulnerabilities in Vendor Customizations (USENIX WOOT '17)][paper] 10 | 11 | ![demo](meta/demo.gif) 12 | 13 | ## Usage 14 | 1. Download your favourite OTAs/Factory images and populate with `fuzzboot.py -a `. 15 | `fuzzboot.py -l` will then show you the populated images. 16 | 2. Hook your device to the nearest USB port and run `fuzzboot.py`. It will try to automatically discover the product or OEM. If it fails, it will fuzz the device with all of the available strings. 17 | One can force a specific OEM using `-e ` parameter. 18 | When it finishes, the tool prints the discovered positive commands (including ones whose response is a fastboot failure), discovered restricted commands, commands which timed-out, and commands which have triggered various errors. 19 | 20 | See [fuzzboot.cfg](meta/fuzzboot.cfg) and `fuzzboot.py -h` for advanced usage. 21 | 22 | Explanation of progress bar: 23 | ``` 24 | [####......] [012923/030245/+01/R02/T01/E02] [CMD: foobar] [LAST: fdsaf] 25 | | | | | | | | | | 26 | | | | | | | | | `-> Last non-neg CMD 27 | | | | | | | | `-----------> Last CMD 28 | | | | | | | `--------------------> # of CMDs that caused USB errors 29 | | | | | | `------------------------> # of CMDs that caused timeouts 30 | | | | | `-----------------------------> # of restricted CMDs 31 | | | | `---------------------------------> # of positive CMDs 32 | | | `--------------------------------------> Total # of CMDs 33 | | `---------------------------------------------> # of tested CMDS 34 | `-----------------------------------------------------------> % completed 35 | ``` 36 | 37 | 38 | 39 | 40 | ## Dependencies 41 | 1. [python-adb](https://github.com/google/python-adb) 42 | 2. [android-sdk-tools](https://developer.android.com/studio/releases/sdk-tools.html) 43 | 3. [Boot.img tools](https://forum.xda-developers.com/showthread.php?t=2319018) (only required for populating `fugu` images) 44 | 45 | 46 | ## Tips 47 | 48 | 1. ADB-authorize your device for automatic-recovery from fastboot reboots. 49 | 2. If you had populated many images, running with `-g` would improve loading times. 50 | 3. If the device hangs, do not reset `fuzzboot`, but rather reboot the device (into `fastboot`). `fuzzboot` will then proceed automatically. 51 | 52 | 53 | ## Tested on 54 | 55 | Host environment: 56 | 57 | - Ubuntu 17.04 `zesty` 58 | - macOS Mojave 10.14.5 59 | 60 | ## Example 61 | 62 | Add device from image file: 63 | 64 | ```terminal 65 | $ ./fuzzboot.py add -p ./runtime/aboot.img --raw 66 | INFO: Welcome to fuzzboot 67 | INFO: ./data/unknown-unknown-unknown.json (2600) 68 | ``` 69 | 70 | [abootool]: https://github.com/alephsecurity/abootool 71 | [paper]: https://www.usenix.org/conference/woot17/workshop-program/presentation/hay 72 | -------------------------------------------------------------------------------- /fuzzboot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Author: Roee Hay / Aleph Research / HCL Technologies 5 | """ 6 | 7 | import device 8 | from oemtester import OEMTester 9 | import argparse 10 | from config import Config 11 | import log 12 | from log import * 13 | import aboot 14 | import image 15 | 16 | 17 | def main(): 18 | 19 | adjustLevels() 20 | parser = argparse.ArgumentParser("fuzzboot", description="Fastboot oem commands fuzzer.") 21 | subparsers = parser.add_subparsers(dest='extend', help='extend commands') 22 | 23 | parser.add_argument('-e','--oem', dest='oem', help='Specify OEM to load ABOOT strings of, otherwise try to autodetect') 24 | parser.add_argument('-d','--device', dest='device', help='Specify device to load ABOOT strings of, otherwise try to autodetect') 25 | parser.add_argument('-b','--build', dest='build', help='Specify build to load ABOOT strings of, otherwise try to autodetect') 26 | 27 | parser.add_argument('-r', '--resume', type=int, default=0, dest='index', help='Resume from specified string index') 28 | parser.add_argument('-i', '--ignore', dest='ignore_re', help='Ignore pattern (regexp)') 29 | parser.add_argument('-g', '--use-strings-generator', action='store_true', default=False, dest='use_strings_generator', help='Use strings generator instead of loading everything a priori (fast but degrades progress)') 30 | parser.add_argument('-o', '--output', action='store_true', dest='show_output', help="Show output of succeeded fastboot commands. Verbose logging overrides this") 31 | parser.add_argument('-l','--aboots-list', action='store_true', help="List available ABOOTs") 32 | 33 | parser.add_argument('-s','--device-serial', dest='serial', help="Specify device fastboot SN") 34 | parser.add_argument('-v', '--verbose', action='store_true', dest='verbose', help='Enable verbose logging') 35 | parser.add_argument('-vv', '--moreverbose', action='store_true', dest='moreverbose', help='Even more logging') 36 | parser.add_argument('-t', '--timeout', type=int, default=5000, dest='timeout', help='USB I/O timeout (ms)') 37 | 38 | parser_add = subparsers.add_parser('add', help='add target image') 39 | parser_add.add_argument('-p','--images-path', help="Add ABOOT strings from OTA/Factory images. Either a file or a directory.") 40 | parser_add.add_argument('-B','--blob', action='store_true', default=False, dest='treat_as_blob', help="Treat specified path as ABOOT blob") 41 | parser_add.add_argument('--raw', action='store_true', help="read images_path as raw binary file") 42 | parser_add.add_argument('-S','--string-prefix', default="", dest='string_prefix', help="When inserting new images, only treat strings with specified prefix") 43 | 44 | args = parser.parse_args() 45 | if args.verbose: 46 | log.setVerbose() 47 | 48 | if args.moreverbose: 49 | log.setVerbose(True) 50 | 51 | if 'raw' in args and args.raw: 52 | args.treat_as_blob = True 53 | args.oem = args.oem or 'unknown' 54 | args.device = args.device or 'unknown' 55 | args.build = args.build or 'unknown' 56 | 57 | I("Welcome to fuzzboot") 58 | 59 | Config.overlay(args.__dict__) 60 | T("Config = %s", Config) 61 | 62 | # if args.treat_as_blob: 63 | # if not args.oem or not args.device or not args.build: 64 | # E("Missing OEM/Device/Build specifiers") 65 | # return 1 66 | 67 | if args.aboots_list: 68 | I("BY OEM:") 69 | I("-------") 70 | dump_data(aboot.by_oem()) 71 | I("") 72 | I("BY DEVICE:") 73 | I("----------") 74 | dump_data(aboot.by_device()) 75 | 76 | return 0 77 | 78 | if args.extend == 'add': 79 | if args.images_path: 80 | image.add(args.images_path) 81 | return 0 82 | 83 | dev = device.Device(args.serial) 84 | 85 | name = dev.device() 86 | adjustLevels() 87 | if name: 88 | I("Device reported name = %s", name) 89 | 90 | OEMTester(dev).test(args.index) 91 | 92 | return 0 93 | 94 | 95 | def dump_data(data): 96 | keys = list(data.keys()) 97 | keys.sort() 98 | nkeys = len(keys) 99 | for i in range(0, nkeys - 1, 2): 100 | I("%17s: %3d %17s: %3d", keys[i], len(data[keys[i]]), keys[i + 1], len(data[keys[i + 1]])) 101 | 102 | if 1 == nkeys % 2: 103 | I("%17s: %3d", keys[nkeys - 1], len(data[keys[nkeys - 1]])) 104 | 105 | 106 | if __name__ == "__main__": 107 | try: 108 | sys.exit(main()) 109 | except KeyboardInterrupt: 110 | print('') 111 | I("User interrupted, quitting...") 112 | 113 | -------------------------------------------------------------------------------- /oemtester.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Roee Hay / Aleph Research / HCL Technologies 3 | """ 4 | 5 | import aboot 6 | from device import * 7 | 8 | 9 | class OEMTester: 10 | def __init__(self, device): 11 | self.device = device 12 | self.bootloaders = self.get_relevant_bootloaders(device) 13 | self.strings = OEMTester.get_strings(self.bootloaders) 14 | self.positives = set() 15 | self.restricted = set() 16 | self.usberror = set() 17 | self.timedout = set() 18 | 19 | def test(self, resume=0): 20 | self.positives.clear() 21 | self.restricted.clear() 22 | self.timedout.clear() 23 | self.usberror.clear() 24 | 25 | if Config.get_config().use_strings_generator: 26 | n = 0 27 | else: 28 | n = len(self.strings) 29 | 30 | self.resume(resume) 31 | 32 | Progress.start() 33 | 34 | for i, cmd, lprcmd in self.test_strings(): 35 | T("fastboot oem %s", cmd) 36 | Progress.show(i+resume, n, len(self.positives), len(self.restricted), len(self.timedout), len(self.usberror), cmd, lprcmd) 37 | 38 | Progress.end() 39 | I("Done.") 40 | self.dump_commands(self.positives, "Positive") 41 | self.dump_commands(self.restricted, "Restricted") 42 | self.dump_commands(self.usberror, "USB Error") 43 | self.dump_commands(self.timedout, "Timed-out") 44 | 45 | 46 | """ 47 | Retrieves the bootloaders for the connected device. 48 | """ 49 | @staticmethod 50 | def get_relevant_bootloaders(device): 51 | if Config.get_config().device: 52 | return aboot.by_device(Config.get_config().device) 53 | 54 | if Config.get_config().oem: 55 | return aboot.by_oem(Config.get_config().oem) 56 | 57 | if "" == device.device(): 58 | I("Cannot detect device identifier, considering all ABOOTs") 59 | return aboot.all() 60 | 61 | bootloaders = aboot.by_device(device.device()) 62 | 63 | if len(bootloaders) == 0: 64 | I("Cannot find bootloader images for %s, trying to resolve its OEM", device.device()) 65 | try: 66 | vendor = Config.get_config().oems[device.device()] 67 | I("Falling back for images of %s", vendor) 68 | return aboot.by_oem(vendor) 69 | 70 | except KeyError: 71 | I("Cannot resolve oem of %s, considering all ABOOTs", device.device()) 72 | return aboot.all() 73 | 74 | return bootloaders 75 | 76 | 77 | """ 78 | Pulls all strings from the generator 79 | """ 80 | @staticmethod 81 | def get_strings(bootloaders): 82 | if Config.get_config().use_strings_generator: 83 | I("Using strings generator...") 84 | return OEMTester.gen_strings(bootloaders) 85 | 86 | I("Loading strings...") 87 | strings = list(OEMTester.gen_strings(bootloaders)) 88 | I("Loaded %d strings from %d ABOOTs", len(strings), len(bootloaders)) 89 | return strings 90 | 91 | """ 92 | Strings generator 93 | Sanitizes and validates strings before their retrieval. 94 | """ 95 | @staticmethod 96 | def gen_strings(bootloaders): 97 | strings = set() 98 | 99 | for bl in bootloaders: 100 | for s in bl.strings: 101 | s = CommandFilter.sanitize(s) 102 | if s in strings: 103 | continue 104 | 105 | if Config.get_config().substrings: 106 | for x in OEMTester.get_substrings(s): 107 | if x in strings: 108 | continue 109 | 110 | if CommandFilter.validate(x): 111 | strings.add(x) 112 | yield x 113 | continue 114 | 115 | if Config.get_config().split_space: 116 | l = re.split(r"\s", s) 117 | if len(l) > 1: 118 | x = l[0] 119 | if x in strings: 120 | continue 121 | if CommandFilter.validate(x): 122 | strings.add(x) 123 | yield x 124 | if CommandFilter.validate(s): 125 | strings.add(s) 126 | yield s 127 | 128 | @staticmethod 129 | def get_substrings(s): 130 | out = set() 131 | for i in xrange(len(s)): 132 | for j in xrange(len(s)-i): 133 | out.add(s[j:j+i+1]) 134 | return out 135 | 136 | 137 | def resume(self, resume): 138 | if resume == 0: 139 | return 140 | I("Resuming from %d", resume) 141 | if Config.get_config().use_strings_generator: 142 | for i, x in enumerate(self.strings): 143 | if i == resume: 144 | break 145 | else: 146 | self.strings = self.strings[resume:] 147 | 148 | def test_strings(self): 149 | prev = "" 150 | r = "" 151 | msg = "" 152 | timeout = False 153 | usb_error = False 154 | lprcmd = "" 155 | 156 | for i, s in enumerate(self.strings): 157 | try: 158 | yield (i, s, lprcmd) 159 | failed = False 160 | 161 | try: 162 | r = self.device.oem(s, not timeout, not usb_error) 163 | timeout, usb_error = (False, False) 164 | 165 | except FastbootRemoteFailure as e: 166 | r = self.device.get_last_fb_output() 167 | msg = e.msg 168 | failed = True 169 | timeout, usb_error = (False, False) 170 | 171 | except FastbootTimeoutException: 172 | timeout, usb_error = (True, False) 173 | 174 | except FastbootCommandNotFound: 175 | timeout, usb_error = (False, False) 176 | continue 177 | 178 | except FastbootUSBException: 179 | timeout, usb_error = (False, True) 180 | 181 | status = '+' 182 | if failed: 183 | status = '-' 184 | o = Device.normalize_fb_error(msg+r) 185 | if "lock" in o or "restricted" in o or "support" in o or "not allowed" in o or "permission denied" in o: 186 | if Config.get_config().show_output: 187 | I("(R) fastboot oem %s", s) 188 | 189 | lprcmd = s 190 | self.restricted.add((s, r)) 191 | continue 192 | 193 | if usb_error: 194 | self.usberror.add((s, r)) 195 | status = 'E' 196 | elif timeout: 197 | self.timedout.add((s,r)) 198 | status = 'T' 199 | else: 200 | self.positives.add((s, r)) 201 | 202 | lprcmd = s 203 | 204 | if Config.get_config().show_output: 205 | I("(%s) fastboot oem %s", status, s) 206 | I("Result =\n"+r) 207 | else: 208 | D("(%s) fastboot oem %s", status, s) 209 | D("Result =\n"+r) 210 | 211 | except FastbootFatalError as e: 212 | E("Failed with index=%d, string=\"%s\", prev=\"%s\". Consider adding them to the filter.", i, s, prev) 213 | break 214 | 215 | finally: 216 | prev = s 217 | 218 | @staticmethod 219 | def dump_commands(cmds, name): 220 | cmds = list(OEMTester.clean_redundant_cmds(cmds)) 221 | cmds.sort() 222 | I("Found %d %s OEM commands", len(cmds), name) 223 | for i,(cmd,resp) in enumerate(cmds, 1): 224 | I("%2d. %s", i, cmd) 225 | D("Result =\n"+resp) 226 | 227 | """ 228 | Removes commands with the same response which include others, e.g.: 229 | 'oem helpfoo' 230 | 'oem help' 231 | We only want to report the latter. 232 | Quick and dirty O(N^2) with additional O(N) mem (small expected cmd set) 233 | """ 234 | @staticmethod 235 | def clean_redundant_cmds(cmds): 236 | out = set() 237 | out.update(cmds) 238 | for c1, r1 in cmds: 239 | for c2, r2 in cmds: 240 | pattern = r"%s\s*.*" % re.escape(c2) 241 | if c1 != c2 and re.match(pattern, c1): 242 | try: 243 | out.remove((c1,r2)) 244 | except KeyError: 245 | pass 246 | break 247 | 248 | return out 249 | 250 | class CommandFilter: 251 | @classmethod 252 | def sanitize(cls, s): 253 | if Config.get_config().strip_whitespace: 254 | s = s.strip() 255 | 256 | if Config.get_config().remove_breaks: 257 | s = s.replace("\n", "").replace("\r", "").replace("\f", "").replace("\v", "") 258 | 259 | return s.replace("oem ", "") 260 | 261 | @classmethod 262 | def validate(cls, s): 263 | if len(s) == 0: 264 | return False 265 | 266 | if Config.get_config().oem_only and not s.startswith("oem "): 267 | return False 268 | 269 | if Config.get_config().ignore_re and re.match(Config.get_config().ignore_re, s): 270 | T("Ignoring %s (matches pattern)", s) 271 | return False 272 | 273 | if Config.get_config().max_len > 0 and len(s) > Config.get_config().max_len: 274 | T("Ignoring %s (%d > %d)", s, len(s), Config.get_config().max_len) 275 | return False 276 | 277 | if Config.get_config().alphanum_only and not re.match("^([0-9a-zA-Z_-]|\s)+$", s): 278 | T("Ignoring %s (not alphanum)" % s) 279 | return False 280 | 281 | return True 282 | 283 | 284 | class Progress: 285 | 286 | @staticmethod 287 | def start(): 288 | pass 289 | 290 | @staticmethod 291 | def show(i, n, npos, nres, ntim, nerr, cmd, last_pos): 292 | cmd = cmd.replace("\t"," ") 293 | last_pos = last_pos.replace("\t", " ") 294 | sys.stdout.write("\r\033[1m") 295 | 296 | if Config.get_config().use_strings_generator: 297 | sys.stdout.write("[%06d/+%02d/R%02d/T%02d/E%02d] [CMD: %13.13s] [LAST: %13.13s]" % (i+1, npos, nres, ntim, nerr, cmd, last_pos)) 298 | else: 299 | sys.stdout.write("[%s] [%06d/%06d/+%02d/R%02d/T%02d/E%02d] [CMD: %8.8s] [LAST: %8.8s]" % (Progress.bar(i+1, n), i+1, n, npos, nres, ntim, nerr, cmd, last_pos)) 300 | sys.stdout.write("\033[0m") 301 | sys.stdout.flush() 302 | 303 | @staticmethod 304 | def end(): 305 | sys.stdout.write("\n") 306 | sys.stdout.flush() 307 | 308 | @staticmethod 309 | def bar(i, n): 310 | t = int((i / float(n))*10) 311 | return "#" * t + "."*(10-t) 312 | 313 | -------------------------------------------------------------------------------- /device.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: Roee Hay / Aleph Research / HCL Technologies 3 | """ 4 | 5 | import os 6 | import re 7 | from serializable import Serializable 8 | from adb import fastboot,common,usb_exceptions,adb_commands, sign_m2crypto 9 | from log import * 10 | from config import Config 11 | from enum import Enum 12 | import time 13 | import usb1 14 | import subprocess 15 | 16 | 17 | class Device: 18 | 19 | def __init__(self, serial=None): 20 | self.connected = False 21 | self.fb = None 22 | self.data = DeviceData() 23 | self.usbdev = None 24 | self.last_output = None 25 | self.set_state(State.DISCONNECTED) 26 | self.serial = serial 27 | self.fb_error = None 28 | self.fb_error_timeout = False 29 | 30 | @staticmethod 31 | def get_fastboot_devices(): 32 | return common.UsbHandle.FindDevices(fastboot.DeviceIsAvailable, timeout_ms=Config.get_config().timeout) 33 | 34 | @staticmethod 35 | def get_adb_devices(): 36 | return common.UsbHandle.FindDevices(adb_commands.DeviceIsAvailable, timeout_ms=Config.get_config().timeout) 37 | 38 | def find_fastboot_device(self): 39 | return self.find_device(Device.get_fastboot_devices()) 40 | 41 | def find_adb_device(self): 42 | return self.find_device(Device.get_adb_devices()) 43 | 44 | def find_device(self, devices): 45 | 46 | i = 0 47 | for d in devices: 48 | i += 1 49 | if not self.serial or d.serial_number == self.serial: 50 | return d 51 | return 52 | 53 | """ 54 | Reboots to bootloaders. First it tries to use python-adb, falling back to the adb binary. 55 | """ 56 | def adb_reboot_bootloader(self): 57 | I("adb: rebooting to bootloader") 58 | try: 59 | self.adb().RebootBootloader() 60 | return 61 | except UnicodeDecodeError: 62 | # https://github.com/google/python-adb/issues/52 63 | D("python-adb bug, falling-back to adb bin") 64 | except usb_exceptions.WriteFailedError: 65 | # Happens when adb server is running 66 | D("adb server is running, cannot use python-adb, falling-back to adb bin") 67 | 68 | # fall-backs to adb binary 69 | if self.serial and re.match(r"\w+", self.serial()): 70 | adb_cmd = Config.get_config().adb_path + " -s %s reboot bootloader 2>/dev/null" % self.serial 71 | else: 72 | adb_cmd = Config.get_config().adb_path + " reboot bootloader 2>/dev/null" 73 | 74 | os.system(adb_cmd) 75 | time.sleep(5) 76 | self.set_state(State.DISCONNECTED) 77 | 78 | """ 79 | Wait for the device to be connected in either fastboot or adb. 80 | """ 81 | def wait_for_device(self): 82 | while State.DISCONNECTED == self.state: 83 | usbdev = self.find_fastboot_device() 84 | if None != usbdev: 85 | try: 86 | usbdev.Open() 87 | I("fastboot connected to %s", usbdev.serial_number) 88 | except usb1.USBError as e: 89 | usbdev.Close() 90 | time.sleep(5) 91 | continue 92 | 93 | self.usbdev = usbdev 94 | self.set_state(State.CONNECTED_FB) 95 | continue 96 | 97 | usbdev = self.find_adb_device() 98 | if None != usbdev: 99 | try: 100 | usbdev.Open() 101 | I("adb: connected") 102 | self.set_state(State.CONNECTED_ADB_DEVICE) 103 | 104 | except usb1.USBErrorBusy as e: 105 | self.set_state(self.adb_get_state()) 106 | if State.CONNECTED_ADB_DEVICE == self.state: 107 | I("adb: connected") 108 | 109 | except usb1.USBError: 110 | usbdev.Close() 111 | time.sleep(5) 112 | continue 113 | 114 | self.usbdev = usbdev 115 | continue 116 | 117 | I("Waiting for device...") 118 | time.sleep(5) 119 | 120 | """ 121 | Waits for the device to be in fastboot mode. If it's in adb, it will reboot it to bootloader 122 | """ 123 | def wait_for_fastboot(self): 124 | 125 | while State.CONNECTED_FB != self.state: 126 | 127 | if State.CONNECTED_ADB_DEVICE == self.state: 128 | self.adb_reboot_bootloader() 129 | self.wait_for_device() 130 | 131 | if State.DISCONNECTED == self.state: 132 | self.wait_for_device() 133 | 134 | def adb(self): 135 | signer = sign_m2crypto.M2CryptoSigner(os.path.expanduser(Config.get_config().adb_key_path)) 136 | return adb_commands.AdbCommands.Connect(self.usbdev, rsa_keys=[signer]) 137 | 138 | def fastboot(self): 139 | return fastboot.FastbootCommands().ConnectDevice(handle=self.usbdev) 140 | 141 | def serial_number(self): 142 | self.wait_for_device() 143 | return self.usbdev.serial_number 144 | 145 | def get_last_fb_output(self): 146 | return self.last_output.get() 147 | 148 | 149 | """ 150 | Conduct a single fastboot command 151 | """ 152 | def do_fb_command(self, func, allow_timeout=False, *args, **kargs): 153 | self.wait_for_fastboot() 154 | self.last_output = CmdLogger() 155 | 156 | try: 157 | getattr(self.fastboot(), func)(info_cb=self.last_output, *args, **kargs) 158 | return self.last_output.get() 159 | 160 | except fastboot.FastbootRemoteFailure as e: 161 | r = self.get_last_fb_output() 162 | msg = e.args[1] 163 | raise FastbootRemoteFailure(msg) 164 | 165 | except fastboot.FastbootStateMismatch as e: 166 | W("fastboot state mistmatch") 167 | self.disconnect() 168 | raise FastbootUSBException("") 169 | 170 | except usb_exceptions.LibusbWrappingError as e: 171 | if "LIBUSB_ERROR_TIMEOUT" in str(e.usb_error): 172 | if allow_timeout: 173 | D("Allowed timeout during FB command: %s, args = %s, kargs = %s", func, str(*args), str(**kargs)) 174 | raise FastbootTimeoutException() 175 | 176 | D("timeout during FB command: %s, args = %s, kargs = %s", func, str(*args), str(**kargs)) 177 | 178 | self.disconnect() 179 | raise FastbootUSBException(e.usb_error) 180 | 181 | """ 182 | Conduct a fastboot command until success (handles USB disconnections etc). 183 | It first resolves (if needed) the command not found error, by issuing a bogus command. 184 | """ 185 | def wait_for_fb_command(self, func, allow_timeout = False, allow_usb_error = False, *args, **kargs): 186 | while True: 187 | try: 188 | self.resolve_fb_error() 189 | return self.do_fb_command(func, allow_timeout, *args, **kargs) 190 | except FastbootUSBException as e: 191 | if allow_usb_error: 192 | raise e 193 | W("USB Error (%s) detected during command: %s, args = %s, kargs = %s", e.msg, func, str(*args), 194 | str(**kargs)) 195 | 196 | def set_state(self, state): 197 | self.state = state 198 | 199 | """ 200 | Disconnect the device and clean-up everything. 201 | """ 202 | def disconnect(self): 203 | self.clear_fb_error() 204 | self.set_state(State.DISCONNECTED) 205 | if None != self.usbdev: 206 | self.usbdev.Close() 207 | self.usbdev = None 208 | 209 | """ 210 | Issue a fastboot oem command. 211 | """ 212 | def oem(self, cmd, allow_timeout=False, allow_usb_error=False): 213 | try: 214 | r = self.wait_for_fb_command("Oem", allow_timeout, allow_usb_error, cmd) 215 | if self.is_fb_error(r, cmd): 216 | raise FastbootCommandNotFound() 217 | return r 218 | 219 | except FastbootTimeoutException: 220 | if self.fb_error_timeout: 221 | raise FastbootCommandNotFound() 222 | raise FastbootTimeoutException 223 | 224 | except FastbootRemoteFailure as e: 225 | r = self.get_last_fb_output() 226 | error = e.msg.decode() 227 | if self.is_fb_error(error+r, cmd): 228 | raise FastbootCommandNotFound() 229 | raise FastbootRemoteFailure(error) 230 | 231 | """ 232 | Get a remote variable through fastboot 233 | """ 234 | def getvar(self, k): 235 | try: 236 | return self.data[k] 237 | except KeyError: 238 | try: 239 | self.data[k] = self.wait_for_fb_command("Getvar", False, False, k) 240 | except fastboot.FastbootRemoteFailure: 241 | return "" 242 | 243 | return self.data[k] 244 | 245 | def product(self): 246 | return self.getvar("product") 247 | 248 | def unlocked(self): 249 | return self.getvar("unlocked") 250 | 251 | def oemprojectname(self): 252 | return self.getvar("oem_project_name") 253 | 254 | """ 255 | Try to resolve the bootloader name according to hints from fastboot info 256 | """ 257 | def bootloader_name(self): 258 | p = self.product() 259 | # OnePlus devices 260 | if p.startswith("msm") and self.oemprojectname(): 261 | return self.oemprojectname() 262 | 263 | return p 264 | 265 | """ 266 | Resolve the real device name 267 | """ 268 | def device(self): 269 | try: 270 | return Config.get_config().bootloader_names[self.bootloader_name()] 271 | except KeyError: 272 | return self.bootloader_name() 273 | 274 | 275 | """ 276 | Query the ADB state 277 | """ 278 | def adb_get_state(self): 279 | try: 280 | output = subprocess.check_output([Config.get_config().adb_path, "get-state"], stderr=subprocess.STDOUT) 281 | except subprocess.CalledProcessError: 282 | return State.DISCONNECTED 283 | 284 | if "device" in output: 285 | return State.CONNECTED_ADB_DEVICE 286 | 287 | return State.DISCONNECTED 288 | 289 | def clear_fb_error(self): 290 | self.fb_error_timeout = False 291 | self.fb_error = None 292 | 293 | """ 294 | Resolve the fastboot command not found error by issuing a bogus command. 295 | Some devices do not return when issuing a non-existing command, we handle those too. 296 | """ 297 | def resolve_fb_error(self): 298 | if None != self.fb_error: 299 | return 300 | 301 | try: 302 | self.fb_error = self.do_fb_command("Oem", True, Config.get_config().oem_error_cmd) 303 | except FastbootRemoteFailure as e: 304 | self.fb_error = e.msg.decode() + self.get_last_fb_output() 305 | except FastbootTimeoutException as e: 306 | D("Error is indicated by USB timeout") 307 | self.fb_error_timeout = True 308 | return 309 | self.fb_error = self.normalize_fb_error(self.fb_error) 310 | D("Error str: " + self.fb_error) 311 | 312 | """ 313 | Classifies whether a given response for a command indicates it's a non-existing one. 314 | """ 315 | def is_fb_error(self, msg, cmd): 316 | if self.fb_error_timeout: 317 | return False 318 | 319 | cmd = self.normalize_fb_error(cmd) 320 | msg = self.normalize_fb_error(msg) 321 | 322 | if msg == self.fb_error: 323 | return True 324 | 325 | if self.normalize_fb_error(self.fb_error.replace(Config.get_config().oem_error_cmd, cmd)) == msg: 326 | return True 327 | 328 | if self.normalize_fb_error(self.fb_error.replace(Config.get_config().oem_error_cmd, re.split("\s", cmd)[0])) == msg: 329 | return True 330 | 331 | return False 332 | 333 | @staticmethod 334 | def normalize_fb_error(error): 335 | try: 336 | return error.replace("\n", "").lower() 337 | except UnicodeDecodeError: 338 | return error 339 | 340 | 341 | class FastbootException(Exception): pass 342 | class FastbootNotConnectedException(FastbootException): pass 343 | class FastbootTimeoutException(FastbootException): pass 344 | class FastbootCommandNotFound(FastbootException): pass 345 | class FastbootFatalError(FastbootException): pass 346 | class FastbootRemoteFailure(FastbootException): 347 | def __init__(self, msg): 348 | self.msg = msg 349 | 350 | class FastbootUSBException(FastbootException): 351 | def __init__(self, msg): 352 | self.msg = msg 353 | 354 | 355 | class DeviceData(Serializable): 356 | pass 357 | 358 | class CmdLogger: 359 | 360 | def __init__(self): 361 | self.output = [] 362 | 363 | def __call__(self, fbmsg): 364 | self.output.append(fbmsg.message.decode()) 365 | 366 | def get(self): 367 | return "\n".join(self.output) 368 | 369 | class State(Enum): 370 | DISCONNECTED = 0, 371 | CONNECTED_FB = 1, 372 | CONNECTED_ADB_DEVICE = 2 373 | -------------------------------------------------------------------------------- /image.py: -------------------------------------------------------------------------------- 1 | """ 2 | Roee Hay / Aleph Research / HCL Technologies 3 | """ 4 | 5 | import os 6 | import zipfile 7 | import io 8 | import tempfile 9 | import shutil 10 | import subprocess 11 | import aboot 12 | from log import * 13 | import re 14 | 15 | 16 | def add(path): 17 | 18 | if os.path.isfile(path): 19 | T(path) 20 | if Config.get_config().treat_as_blob: 21 | add_blob_file(path) 22 | else: 23 | add_image(path) 24 | else: 25 | if Config.get_config().treat_as_blob: 26 | E("Specified path must be a blob") 27 | return 28 | 29 | for root,dirs,files in os.walk(path): 30 | for f in files: 31 | T(f) 32 | add_image(os.path.join(root, f)) 33 | 34 | 35 | def add_image(path): 36 | return add_ota_file(path) or add_factory_file(path) or add_moto_file(path) or add_sony_file(path) 37 | 38 | 39 | def add_blob_file(path): 40 | return add_any_image(BlobArchive, path, Config.get_config().oem, Config.get_config().device, Config.get_config().build) 41 | 42 | 43 | def add_ota_file(path): 44 | return add_any_image(OTA, path) 45 | 46 | 47 | def add_factory_file(path): 48 | return add_any_image(Factory, path) 49 | 50 | 51 | def add_moto_file(path): 52 | return add_any_image(MotoArchive, path) 53 | 54 | 55 | def add_sony_file(path): 56 | return add_any_image(SonyArchive, path) 57 | 58 | 59 | def add_any_image(cls, *kargs): 60 | try: 61 | img = cls(*kargs) 62 | if not img.get_aboot_image(): 63 | return 64 | return add_aboot(img) 65 | 66 | except (zipfile.BadZipfile, IOError, ImageArchiveParseException) as e: 67 | E('error: %s', str(e)) 68 | pass 69 | 70 | 71 | def add_aboot(archive): 72 | name, fp = archive.get_aboot_image() 73 | bl = aboot.ABOOT.create_from_bootloader_image(fp=fp, oem=archive.get_oem(), 74 | device=archive.get_device(), build=archive.get_build(), 75 | src=os.path.basename(archive.get_path()), 76 | name=name, 77 | strprefix=Config.get_config().string_prefix) 78 | 79 | skipped = "(SKIPPED)" 80 | fname = '%s/%s-%s-%s.json' % (Config.get_config().data_path, bl.oem, bl.device, bl.build) 81 | if bl.save(fname): 82 | skipped = "" 83 | 84 | I("%s (%d) %s" % (fname, len(bl.strings), skipped)) 85 | return bl 86 | 87 | 88 | class ImageArchiveParseException(Exception): 89 | pass 90 | 91 | 92 | class FactoryParseException(ImageArchiveParseException): 93 | pass 94 | 95 | 96 | class OTAParseException(ImageArchiveParseException): 97 | pass 98 | 99 | 100 | class MotoParseException(ImageArchiveParseException): 101 | pass 102 | 103 | class SonyParseException(ImageArchiveParseException): 104 | pass 105 | 106 | class BlobArchiveParseException(ImageArchiveParseException): 107 | pass 108 | 109 | 110 | class ImageArchive(object): 111 | 112 | 113 | def __init__(self, path): 114 | self.path = path 115 | self.oems = None 116 | self.parse() 117 | 118 | def get_path(self): 119 | return self.path 120 | 121 | def parse(self): 122 | raise NotImplementedError() 123 | 124 | def get_device(self): 125 | raise NotImplementedError() 126 | 127 | def get_aboot_image(self): 128 | raise NotImplementedError() 129 | 130 | def get_build(self): 131 | raise NotImplementedError() 132 | 133 | def get_timestamp(self): 134 | raise NotImplementedError() 135 | 136 | def get_oem(self): 137 | raise NotImplementedError() 138 | 139 | def __getitem__(self, k): 140 | return self.__dict__[k] 141 | 142 | 143 | class OTA(ImageArchive): 144 | 145 | def parse(self): 146 | self.zip = zipfile.ZipFile(self.path) 147 | self.metadata = None 148 | self.fingerprint = None 149 | 150 | def get_metadata(self): 151 | if self.metadata: 152 | return self.__dict__ 153 | 154 | try: 155 | self.metadata = self.zip.read("META-INF/com/android/metadata") 156 | D("metadata = %s", self.metadata) 157 | except KeyError: 158 | raise OTAParseException() 159 | 160 | for line in self.metadata.split("\n"): 161 | 162 | s = line.split('=') 163 | if len(s) < 2: 164 | continue 165 | k,v = s 166 | self.__dict__[k] = v 167 | 168 | return self.__dict__ 169 | 170 | def get_device(self): 171 | self.get_metadata() 172 | return self.get_buildfingerprint().split(':')[0].split('/')[2] 173 | 174 | def get_buildfingerprint(self): 175 | # e.g. google/volantis/flounder:7.1.1/N4F27B/3853226:user/release-keys 176 | 177 | if None != self.fingerprint: 178 | return self.fingerprint 179 | 180 | try: 181 | self.fingerprint = self.get_metadata()["post-build"] 182 | return self.fingerprint 183 | except KeyError: 184 | pass 185 | 186 | try: 187 | otaid = self.get_metadata()["ota-id"] 188 | vendor = None 189 | device = None 190 | if "ONELOxygen" in otaid: 191 | vendor = 'oneplus' 192 | device = 'oneplus1' 193 | elif "OnePlus" in otaid: 194 | vendor = 'oneplus' 195 | if "OnePlus3T" in otaid: 196 | device = 'oneplus3t' 197 | elif "OnePlus3" in otaid: 198 | device = 'oneplus3' 199 | elif "OnePlus2" in otaid: 200 | device = 'oneplus2' 201 | elif "OnePlusX" in otaid: 202 | device = 'oneplusx' 203 | 204 | if vendor and device: 205 | self.fingerprint = '%s/%s/%s:?/%s/?:?' % (vendor, device, device, otaid) 206 | return self.fingerprint 207 | except KeyError: 208 | pass 209 | 210 | raise OTAParseException() 211 | 212 | def get_timestamp(self): 213 | self.get_metadata() 214 | try: 215 | ts = self.get_metadata()["post-timestamp"] 216 | return ts 217 | except KeyError: 218 | return None 219 | 220 | def get_vendor(self): 221 | return self.get_buildfingerprint().split(':')[0].split('/')[0].lower() 222 | 223 | def get_oem(self): 224 | d = self.get_device() 225 | try: 226 | return OEMS.dev2oem(d) 227 | except KeyError: 228 | return self.get_vendor() 229 | 230 | def get_aospver(self): 231 | return self.get_buildfingerprint().split(':')[0].split('/')[0] 232 | 233 | def get_build(self): 234 | return self.get_buildfingerprint().split(':')[1].split('/')[1].lower() 235 | 236 | def get_keys(self): 237 | return self.get_buildfingerprint().split(':')[2] 238 | 239 | 240 | 241 | def get_aboot_image(self): 242 | d = self.get_device() 243 | 244 | for path in Config.get_config().ota_prevalent_aboot_paths: 245 | try: 246 | return (os.path.basename(path), self.zip.open(path)) 247 | except KeyError: 248 | pass 249 | 250 | if 'flounder' in d: 251 | data = self.zip.read('bootloader.img')[256:] 252 | fp = io.BytesIO(data) 253 | return ('hboot.img', zipfile.ZipFile(fp).open('hboot.img')) 254 | 255 | if 'fugu' == d: 256 | tmpdir = tempfile.mkdtemp() 257 | tmpfile = tempfile.NamedTemporaryFile() 258 | self.zip.extract('droidboot.img', tmpdir) 259 | curdir = shutil.abspath(".") 260 | os.chdir(tmpdir) 261 | try: 262 | subprocess.check_output([Config.get_config().ota_umkbootimg_path, "droidboot.img"], stderr=subprocess.STDOUT) 263 | subprocess.check_output([Config.get_config().ota_unpack_ramdisk_path, "initramfs.cpio.gz"],stderr=subprocess.STDOUT) 264 | except OSError as e: 265 | E("Cannot execute umkbootimg/unpack_ramdisk while handling %s. " % self.path) 266 | E("ota_umkbootimg_path = %s" % Config.get_config().ota_umkbootimg_path) 267 | E("ota_unpack_ramdisk_path = %s" % Config.get_config().ota_unpack_ramdisk_path) 268 | raise OTAParseException() 269 | 270 | data = file("./ramdisk/system/bin/droidboot", "rb").read() 271 | tmpfile.write(data) 272 | tmpfile.seek(0) 273 | os.chdir(curdir) 274 | shutil.rmtree(tmpdir) 275 | return ('droidboot',tmpfile) 276 | 277 | 278 | class Factory(ImageArchive): 279 | 280 | def parse(self): 281 | self.zip = zipfile.ZipFile(self.path) 282 | self._device = None 283 | self._build = None 284 | self._image = None 285 | 286 | root = len(self.zip.namelist()) > 1 and self.zip.namelist()[0] or None 287 | if not root: 288 | raise FactoryParseException() 289 | 290 | try: 291 | self._device, self._build = root[:-1].split("-") 292 | except ValueError: 293 | raise FactoryParseException() 294 | 295 | for n in self.zip.namelist(): 296 | if "/image-" in n: 297 | self._image = n 298 | return 299 | 300 | raise FactoryParseException() 301 | 302 | def get_image_path(self): 303 | return self._image 304 | 305 | def get_device(self): 306 | return self._device 307 | 308 | def get_oem(self): 309 | d = self.get_device() 310 | try: 311 | return OEMS.dev2oem(d) 312 | except KeyError: 313 | return "unknown" 314 | 315 | def get_build(self): 316 | return self._build.lower(); 317 | 318 | def get_aboot_image(self): 319 | D("Factory detected device = %s " % self.get_device()) 320 | if self.get_device() == "marlin" or self.get_device() == "sailfish": 321 | if not self.get_image_path(): 322 | raise FactoryParseException() 323 | 324 | data = self.zip.read(self.get_image_path()) 325 | fp = io.BytesIO(data) 326 | return ('aboot.img',zipfile.ZipFile(fp).open("aboot.img")) 327 | 328 | # ryu is still unsupported 329 | 330 | if "ryu" == self.get_device(): 331 | raise FactoryParseException() 332 | 333 | # unsupported, use OTA 334 | if "volantis" in self.get_device() or "fugu" in self.get_device(): 335 | raise FactoryParseException() 336 | 337 | # for the rest we fallback to bootloader-*.img which is supposed to contain aboot. 338 | # Not very robust as data may be compressed, encoded, whatever. 339 | 340 | if Config.get_config().factory_fallback_bootloader: 341 | for n in self.zip.namelist(): 342 | if "bootloader-" in n: 343 | D("Found bootloader: %s" % n) 344 | name = os.path.basename(n) 345 | data = self.zip.read(n) 346 | return (n, io.BytesIO(data)) 347 | 348 | raise FactoryParseException() 349 | 350 | 351 | """ 352 | For Moto we over-approximate and return all of the bootloader strings (not just ABOOT) 353 | """ 354 | class MotoArchive(ImageArchive): 355 | 356 | def get_oem(self): 357 | return "motorola" 358 | 359 | def parse(self): 360 | self.zip = zipfile.ZipFile(self.path) 361 | self.device = None 362 | self.build = None 363 | 364 | try: 365 | data = self.zip.open("flashfile.xml").read() 366 | self.device = re.search(r"phone_model model=\"(.*?)\"", data).group(1) 367 | self.build = re.search(r"software_version version=\"(.*?)\"", data).group(1).split()[2] 368 | D("model = %s", self.device) 369 | D("version = %s", self.build) 370 | except (IOError, KeyError): 371 | raise MotoParseException() 372 | 373 | def get_device(self): 374 | return self.device 375 | 376 | def get_aboot_image(self): 377 | try: 378 | return ("bootloader.img",self.zip.open("bootloader.img")) 379 | except KeyError: 380 | raise MotoParseException() 381 | 382 | def get_build(self): 383 | return self.build 384 | 385 | 386 | class BlobArchive(ImageArchive): 387 | 388 | def __init__(self, path, oem, device, build): 389 | self.oem = oem 390 | self.device = device 391 | self.build = build 392 | super(BlobArchive, self).__init__(path) 393 | 394 | def parse(self): 395 | pass 396 | 397 | def get_device(self): 398 | return self.device 399 | 400 | def get_aboot_image(self): 401 | try: 402 | return (os.path.basename(self.path), open(self.path, 'rb')) 403 | except IOError: 404 | raise BlobArchiveParseException() 405 | 406 | def get_build(self): 407 | return self.build 408 | 409 | def get_oem(self): 410 | return self.oem 411 | 412 | class SonyArchive(ImageArchive): 413 | 414 | def get_oem(self): 415 | return "sony" 416 | 417 | def parse(self): 418 | self.zip = zipfile.ZipFile(self.path) 419 | self.device = None 420 | self.build = None 421 | 422 | try: 423 | data = self.zip.read("META-INF/MANIFEST.MF") 424 | for line in data.split("\r\n"): 425 | s = re.split(r"[:]\s+", line) 426 | if len(s) != 2: 427 | continue 428 | 429 | k,v = s 430 | if k == "device": 431 | self.device = v.lower() 432 | 433 | if k == "version": 434 | self.build = v 435 | 436 | except (IOError, KeyError): 437 | raise SonyParseException() 438 | 439 | def get_device(self): 440 | return self.device 441 | 442 | def get_aboot_image(self): 443 | names = [] 444 | for n in self.zip.namelist(): 445 | if "emmc_appsboot" in n: 446 | names.append(os.path.basename(n)) 447 | data = self.zip.read(n) 448 | 449 | if len(names) == 0: 450 | raise SonyParseException() 451 | 452 | return ("/".join(names), io.BytesIO(data)) 453 | 454 | def get_build(self): 455 | return self.build 456 | 457 | 458 | class OEMS: 459 | 460 | _oems = None 461 | 462 | @classmethod 463 | def load(cls): 464 | cls._oems = {} 465 | for o in Config.get_config().oems: 466 | for d in Config.get_config().oems[o]: 467 | cls._oems[d] = o 468 | 469 | @classmethod 470 | def dev2oem(cls, dev): 471 | if None != cls._oems: 472 | return cls._oems[dev] 473 | 474 | cls.load() 475 | return cls._oems[dev] 476 | --------------------------------------------------------------------------------