├── 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 | 
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 |
--------------------------------------------------------------------------------