Repository: x56/airpyrt-tools Branch: main Commit: 64f526b3e0a9 Files: 19 Total size: 77.6 KB Directory structure: gitextract_nzg9t11t/ ├── .gitignore ├── LICENSE ├── README.md ├── acp/ │ ├── __init__.py │ ├── __main__.py │ ├── basebinary.py │ ├── cflbinary.py │ ├── cli.py │ ├── clibs/ │ │ ├── AppleSRP.py │ │ └── __init__.py │ ├── client.py │ ├── encryption.py │ ├── exception.py │ ├── keystream.py │ ├── message.py │ ├── misc.py │ ├── property.py │ └── session.py └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store *.pyc *.egg-info build/ dist/ ================================================ FILE: LICENSE ================================================ Copyright (c) 2016 Vince Cali Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # AirPyrt Tools ### License See LICENSE ### Requirements - python 2.7 - pycrypto ### Installation `python setup.py install [--user]` ### Usage `python [-B] -m acp` usage: __main__.py [-h] [-t address] [-p password] [--listprop] [--helpprop property] [--getprop property] [--setprop property value] [--dumpprop] [--acpprop] [--dump-syslog] [--reboot] [--factory-reset] [--flash-primary firmware_path] [--do-feat-command] [--decrypt inpath outpath] [--extract inpath outpath] [--srp-test] optional arguments: -h, --help show this help message and exit AirPort client parameters: -t address, --target address IP address or hostname of the target router -p password, --password password router admin password AirPort client commands: --listprop list supported properties --helpprop property print the description of the specified property --getprop property get the value of the specified property --setprop property value set the value of the specified property --dumpprop dump values of all supported properties --acpprop get acp acpprop list --dump-syslog dump the router system log --reboot reboot device --factory-reset RESET EVERYTHING and reboot; you have been warned! --flash-primary firmware_path flash primary partition firmware --do-feat-command send 0x1b (feat) command Basebinary commands: --decrypt inpath outpath decrypt the basebinary --extract inpath outpath extract the gzimg contents Test arguments: --srp-test SRP (requires OS X) ### Notes **IMPORTANT** This still uses the old ACP protocol implementation, which puts the admin password of the device over the wire in a trivially recoverable format. This was fixed by in the new protocol which uses SRP authentication and better encryption of requests to/from the device. Until this is implemented this tool is entirely unsafe to use, especially for remote administration (which you should have disabled anyway...). This project grew organically out of my understanding of various pieces of the ACP protocol. I've restructured the code a few times as it has improved, but there are still many gaps in the implementation, and a lot of code smell. Between sitting on this indefinitely making incremental improvements (and probably never releasing a "finished" product) and releasing it in a rougher state for others to explore, the latter made far more sense. Return value of 0xfffffff6 when using --getprop means the property is not avaliable/readable ## TODO (very incomplete list in no particular order) - add IP address type for properties, make sure it supports IPv4 and IPv6 - specify RO/WO/RW attribute for properties - exception handling: - invalid struct fields aren't handled well in many cases - finish adding custom exception classes and make sure we're using them - logging (mostly done, still looks horrible) with verbosity controls - review and update docstrings - SRP support (fix pysrp because ctypes hax, while fun, are horrible and non-portable) - ACP protocol version 2 (full session encryption) - handle encrypted property elements - basebinary repacking/reencryption - basebinary rootfs mounting - threaded server - handle protocol v1 (for old firmwares/devices) - bonjour announcement/discovery - options to specify no encryption, old method, and new (SRP) method - ACPMonitorSession support - ACPRPC support ================================================ FILE: acp/__init__.py ================================================ ================================================ FILE: acp/__main__.py ================================================ import cli cli.main() ================================================ FILE: acp/basebinary.py ================================================ import logging import os.path import struct import zlib from Crypto.Cipher import AES #XXX: ugh... from .misc import cast_u32 # hardcoded 128 bit values # 107: 52 49 C3 51 02 8B F1 FD 2B D1 84 9E 28 B2 3F 24 # 108: BB 7D EB 09 70 D8 EE 2E 00 FA 46 CB 1C 3C 09 8E # 115: 10 75 E8 06 F4 77 0C D4 76 3B D2 85 A6 4E 91 74 # 120: 68 8C DD 3B 1B 6B DD A2 07 B6 CE C2 73 52 92 D2 _basebinary_keys = { #3 : "", #102 : "", #104 : "", #105 : "", #106 : "", 107 : "5249c351028bf1fd2bd1849e28b23f24", 108 : "bb7deb0970d8ee2e00fa46cb1c3c098e", #109 : "", #113 : "", #114 : "", 115 : "1075e806f4770cd4763bd285a64e9174", #116 : "", #117 : "", #119 : "", 120 : "688cdd3b1b6bdda207b6cec2735292d2", } def _derive_key(model): if model not in _basebinary_keys: return None key = _basebinary_keys[model].decode("hex") derived_key = "" for i in range(len(key)): derived_key += chr(ord(key[i]) ^ (i + 0x19)) logging.debug("derived key {0}".format(derived_key.encode("hex"))) return derived_key class BasebinaryError(Exception): pass class Basebinary(object): _header_magic = "APPLE-FIRMWARE\x00" #XXX: do we need to fix shitty Python struct member signdness things here too? _header_format = struct.Struct(">15sB2I4BI") header_size = _header_format.size @classmethod def parse(cls, data): if len(data) < (cls.header_size + 4): raise BasebinaryError("not enough data to parse") header_data = data[:cls.header_size] inner_data = data[cls.header_size:-4] #XXX: and here?? stored_checksum, = struct.unpack(">I", data[-4:]) (byte_0x0F, model, version, byte_0x18, byte_0x19, byte_0x1A, flags, unk_0x1C) = cls.parse_header(header_data) if flags & 2: inner_data = cls.decrypt(inner_data, model, byte_0x0F) #XXX: why is Python so shitty about this comparison <.< checksum = cast_u32(zlib.adler32(header_data+inner_data)) logging.debug("stored checksum {0:#x}".format(stored_checksum)) logging.debug("calculated checksum {0:#x}".format(checksum)) logging.debug("data length {0:#x}".format(len(header_data+inner_data))) if stored_checksum != checksum: raise BasebinaryError("bad checksum") return inner_data @classmethod def compose(cls, data): #TODO pass @classmethod def parse_header(cls, data): magic, byte_0x0F, model, version, byte_0x18, byte_0x19, byte_0x1A, flags, unk_0x1C = cls._header_format.unpack(data) if magic != cls._header_magic: raise BasebinaryError("bad header magic") return (byte_0x0F, model, version, byte_0x18, byte_0x19, byte_0x1A, flags, unk_0x1C) @classmethod def compose_header(cls, byte_0x0F, model, version, byte_0x18, byte_0x19, byte_0x1A, flags, unk_0x1C): #TODO pass @classmethod def decrypt(cls, data, model, byte_0x0F): iv = cls._header_magic+chr(byte_0x0F) key = _derive_key(model) if key is None: raise BasebinaryError("key missing for model {0}".format(model)) decrypted_data = "" remaining_length = len(data) chunk_length = 0x8000 while remaining_length: if remaining_length > chunk_length: decrypted_data += cls.decrypt_chunk(data[-remaining_length:-(remaining_length-chunk_length)], key, iv) remaining_length -= chunk_length else: decrypted_data += cls.decrypt_chunk(data[-remaining_length:], key, iv) remaining_length = 0 return decrypted_data @classmethod def decrypt_chunk(cls, encrypted_data, key, iv): cipher = AES.new(key, AES.MODE_CBC, iv) decrypted_data = "" bytes_left = len(encrypted_data) while bytes_left: #logging.debug("bytes left: {0:#x}".format(bytes_left)) if bytes_left > 0x10: decrypted_data += cipher.decrypt(encrypted_data[-bytes_left:-(bytes_left-0x10)]) bytes_left -= 0x10 elif bytes_left == 0x10: decrypted_data += cipher.decrypt(encrypted_data[-bytes_left:]) bytes_left = 0 else: # bytes_left < 0x10 #LOL: odd-sized chunk at the end is left unencrypted decrypted_data += encrypted_data[-bytes_left:] bytes_left = 0 return decrypted_data @classmethod def encrypt(cls, data): #TODO: not really necessary; router doesn't require an encrypted firmware if flag is unset pass @classmethod def extract(cls, data): #TODO: proper gzip header validation? gzip_offset = data.index("\x1f\x8b\x08") gzdata = data[gzip_offset:] return zlib.decompress(gzdata, 16+zlib.MAX_WBITS) ================================================ FILE: acp/cflbinary.py ================================================ import logging import struct from collections import OrderedDict from math import log from types import * _header_magic = "CFB0" _footer_magic = "END!" _header_size = len(_header_magic) _footer_size = len(_footer_magic) def _lslice(data, length): """ Slice object into two counting from the left Returns: (left_slice, right_slice) """ return data[:length], data[length:] class CFLBinaryPListComposeError(Exception): """Exception raised for errors composing a cflbinary format property list""" pass class CFLBinaryPListParseError(Exception): """Exception raised for errors parsing a cflbinary format property list""" pass class CFLBinaryPListComposer(object): """Write cflbinary format property list""" @classmethod def _pack_object(cls, obj): """ Pack a supported Python built-in object Note: this function is super hacky and needs to be fixed Returns: data Raises: CFLBinaryPListComposeError """ data = "" object_type = type(obj) if object_type == NoneType: data += "\x00" elif object_type == BooleanType: if not obj: data += "\x08" else: data += "\x09" elif object_type == IntType: object_marker = 0x10 buf = "" #XXX: need to actually catch unsupported packed sizes for fmt in [">B", ">H", ">I", ">Q"]: try: buf = struct.pack(fmt, obj) except struct.error: logging.debug("XXX: skipping {0}".format(fmt)) pass else: break object_marker += int(log(len(buf), 2)) data += chr(object_marker) data += buf elif object_type == FloatType: object_marker = 0x20 buf = "" #XXX: need to actually catch unsupported packed sizes for fmt in ["!f", "!d"]: try: buf = struct.pack(fmt, obj) except struct.error: logging.debug("XXX: skipping {0}".format(fmt)) pass else: break object_marker += int(log(len(buf), 2)) data += chr(object_marker) data += buf #XXX: DateType? elif object_type == StringType: object_marker = 0x40 data_len = len(obj) if data_len < 0xF: object_marker += data_len data += chr(object_marker) else: object_marker += 0xF data += chr(object_marker) data += cls._pack_object(data_len) data += obj elif object_type == UnicodeType: data += "\x70" data += obj.encode("utf-8") data += "\x00" elif object_type == ListType: data += "\xA0" for element in obj: data += cls._pack_object(element) data += "\x00" elif object_type in [DictType, OrderedDict]: data += "\xD0" for k, v in obj.iteritems(): data += cls._pack_object(k) data += cls._pack_object(v) data += "\x00" else: raise CFLBinaryPListComposeError("unsupported Python built-in type: {0}".format(type(obj))) return data @classmethod def compose(cls, object): """ Compose Python object into equivalent plist Returns: plist_data Raises: CFLBinaryPListComposeError """ data = _header_magic # assume one root object data += cls._pack_object(object) data += _footer_magic return data class CFLBinaryPListParser(object): """Read cflbinary format property list""" @classmethod def _unpack_int(cls, size_exponent, data): """ Unpack an int object as a Python int from the provided data Returns: (int, remaining_data) Raises: CFLBinaryPListParseError """ int_size = 2**size_exponent int_bytes, data = _lslice(data, int_size) #XXX: are these supposed to be signed or unsigned? if int_size == 1: int_fmt = ">B" elif int_size == 2: int_fmt = ">H" elif int_size == 4: int_fmt = ">I" elif int_size == 8: int_fmt = ">Q" else: raise CFLBinaryPListParseError("unsupported int packed object size of {0} bytes") try: (int_val, ) = struct.unpack(int_fmt, int_bytes) except struct.error: raise CFLBinaryPListParseError("failed to unpack int value") return int_val, data @classmethod def _unpack_real(cls, size_exponent, data): """ Unpack a real object as a Python float from the provided data Returns: (float, remaining_data) Raises: CFLBinaryPListParseError """ real_size = 2**size_exponent real_bytes, data = _lslice(data, real_size) if real_size == 4: real_fmt = ">f" elif real_size == 8: real_fmt = ">d" else: raise CFLBinaryPListParseError("unsupported real packed object size of {0} bytes") try: (float_val, ) = struct.unpack(real_fmt, real_bytes) except struct.error: raise CFLBinaryPListParseError("failed to unpack float value") return float_val, data @classmethod def _unpack_count(cls, object_info, data): """ Unpack count from object info nibble and/or packed int value Returns: (count, remaining_data) Raises: CFLBinaryPListParseError """ if object_info == 0x0F: # count is the following packed int object marker, data = cls._unpack_object_marker(data) count_object_type = marker & 0xF0 count_object_info = marker & 0x0F if count_object_type != 0x10: raise CFLBinaryPListParseError("expected count to be a packed int object") count, data = cls._unpack_int(count_object_info, data) else: count = object_info return count, data @classmethod def _unpack_object_marker(cls, data): """ Unpack an object marker from the provided data Returns: (marker, remaining_data) Raises: CFLBinaryPListParseError """ marker_byte, data = _lslice(data, 1) try: (marker, ) = struct.unpack(">B", marker_byte) except struct.error: raise CFLBinaryPListParseError("failed to unpack object marker") return marker, data @classmethod def _unpack_object(cls, data): """ Unpack an object from the provided data Returns: (obj, remaining_data) Raises: CFLBinaryPListParseError """ obj = None marker, data = cls._unpack_object_marker(data) object_type = marker & 0xF0 object_info = marker & 0x0F if object_type == 0x00: if object_info == 0x00: # null, null object return None, data elif object_info == 0x08: # bool, false return False, data elif object_info == 0x09: # bool, true return True, data else: raise CFLBinaryPListParseError("unsupported object info value for object type 0x00: {0:#x}".format(object_info)) elif object_type == 0x10: # int, big-endian return cls._unpack_int(object_info, data) elif object_type == 0x20: # real, big-endian return cls._unpack_real(object_info, data) elif object_type == 0x30: # date #XXX: not sure if this is actually used raise CFLBinaryPListParseError("date support not implemented") elif object_type == 0x40: # data size, data = cls._unpack_count(object_info, data) #XXX: we return data as str type, is this ok? return _lslice(data, size) elif object_type == 0x50: # string, ASCII raise CFLBinaryPListParseError("ASCII string support not implemented") elif object_type == 0x60: # string, Unicode raise CFLBinaryPListParseError("Unicode string support not implemented") elif object_type == 0x70: # string, UTF8, NULL terminated raw = "" while True: byte, data = _lslice(data, 1) if byte == "\x00": break raw += byte #XXX: what exceptions could we get here? obj = raw.decode("utf-8") return obj, data elif object_type == 0x80: # uid raise CFLBinaryPListParseError("uid support not implemented") elif object_type == 0xA0: # array obj = [] while True: element, data = cls._unpack_object(data) if element == None: break obj.append(element) return obj, data elif object_type == 0xB0: # ordset raise CFLBinaryPListParseError("ordset support not implemented") elif object_type == 0xC0: # set raise CFLBinaryPListParseError("set support not implemented") elif object_type == 0xD0: # dict keys = [] values = [] while True: key, data = cls._unpack_object(data) if key == None: break keys.append(key) value, data = cls._unpack_object(data) values.append(value) obj = OrderedDict() for i in range(len(keys)): obj[keys[i]] = values[i] return obj, data else: raise CFLBinaryPListParseError("unsupported object type: {0:#x}".format(object_type)) @classmethod def parse(cls, data): """ Parse plist data into equivalent Python built-in object type Returns: obj Raises: CFLBinaryPListParseError """ # bail now if there isn't enough data for header, footer, and at least one object if len(data) < (_header_size + _footer_size + 1): raise CFLBinaryPListParseError("not enough data to parse") header_data, data = _lslice(data, _header_size) if header_data != _header_magic: raise CFLBinaryPListParseError("bad header magic") # read object stream (assume one root object) obj, remaining_data = cls._unpack_object(data) if len(remaining_data) > _footer_size: raise CFLBinaryPListParseError("extra data found after unpacking root object") if remaining_data != _footer_magic: raise CFLBinaryPListParseError("bad footer magic") return obj ================================================ FILE: acp/cli.py ================================================ import argparse import logging import os.path import sys import time from collections import OrderedDict from .basebinary import * from .client import ACPClient from .exception import * from .property import ACPProperty class _ArgParser(argparse.ArgumentParser): def error(self, message): sys.stderr.write("error: {0}\n".format(message)) #self.print_help() sys.exit(2) def _cmd_not_implemented(*unused): raise ACPCommandLineError("command handler not implemented") def _cmd_listprop(unused): print "\nSupported properties:\n" prop_names = ACPProperty.get_supported_property_names() for name in prop_names: print "{0}: {1}".format(name, ACPProperty.get_property_info_string(name, "description")) print def _cmd_helpprop(args): prop_name = args.pop() description = ACPProperty.get_property_info_string(prop_name, "description") prop_type = ACPProperty.get_property_info_string(prop_name, "type") validation = ACPProperty.get_property_info_string(prop_name, "validation") s = "{0} ({1}".format(description, prop_type) if validation: s += ", {0})".format(validation) else: s += ")" print s def _cmd_getprop(client, args): prop_name = args.pop() prop = client.get_properties([prop_name]) if len(prop): print ACPProperty(prop_name, prop[0].value) def _cmd_setprop(client, args): prop_name, prop_value = args prop_type = ACPProperty.get_property_info_string(prop_name, "type") prop = ACPProperty() if prop_type == "dec": try: prop = ACPProperty(prop_name, int(prop_value)) except ValueError: logging.error("value for \"{0}\" has the wrong type, should be {0}".format(prop_name, prop_type)) elif prop_type == "hex": try: #XXX: this is not the right way to do exceptions prop = ACPProperty(prop_name, int(prop_value, 16)) except ValueError: logging.error("value for \"{0}\" has the wrong type, should be {0}".format(prop_name, prop_type)) elif prop_type == "mac": #XXX: not catching our exception prop = ACPProperty(prop_name, prop_value) elif prop_type == "bin": prop = ACPProperty(prop_name, prop_value.decode("hex")) elif prop_type == "str": prop = ACPProperty(prop_name, prop_value) elif prop_type in ["cfb", "log"]: logging.error("unsupported prop type: {0}".format(prop_type)) client.set_properties({prop_name : prop}) def _cmd_dumpprop(client, unused): prop_names = ACPProperty.get_supported_property_names() properties = client.get_properties(prop_names) for prop in properties: padded_description = ACPProperty.get_property_info_string(prop.name, "description").ljust(32, " ") print "{0}: {1}".format(padded_description, prop) def _cmd_acpprop(client, unused): props_reply = client.get_properties(["prop"]) props_raw = props_reply[0].value props = "" for i in range(len(props_raw) / 4): props += "{0}\n".format(props_raw[i*4:i*4+4]) print props def _cmd_dump_syslog(client, unused): print "{0}".format(client.get_properties(["logm"])[0]) def _cmd_reboot(client, unused): print "Rebooting device" client.set_properties({"acRB" : ACPProperty("acRB", 0)}) def _cmd_factory_reset(client, unused): print "Performing factory reset" client.set_properties(OrderedDict([("acRF",ACPProperty("acRF", 0)), ("acRB",ACPProperty("acRB", 0))])) def _cmd_flash_primary(client, args): fw_path = args.pop() if os.path.exists(fw_path): with open(fw_path, "rb") as fw_file: fw_data = fw_file.read() print "Flashing primary firmware partition" client.flash_primary(fw_data) else: logging.error("Basebinary not readable at path: {0}".format(fw_path)) def _cmd_do_feat_command(client, unused): print client.get_features() def _cmd_decrypt(args): (inpath, outpath) = args with open(inpath, "rb") as infile: indata = infile.read() #XXX: lazy, fixme try: outdata = Basebinary.parse(indata) except BasebinaryError: raise else: with open(outpath, "wb") as outfile: outfile.write(outdata) def _cmd_extract(args): (inpath, outpath) = args with open(inpath, "rb") as infile: indata = infile.read() #XXX: lazy, fixme try: outdata = Basebinary.extract(indata) except BasebinaryError: raise else: with open(outpath, "wb") as outfile: outfile.write(outdata) def _cmd_srp_test(client, unused): print "SRP testing" client.authenticate_AppleSRP() client.close() def main(): #TODO: add CLI arg for verbosity logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) parser = _ArgParser() parameters_group = parser.add_argument_group("AirPort client parameters") parameters_group.add_argument("-t", "--target", metavar="address", help="IP address or hostname of the target router") parameters_group.add_argument("-p", "--password", metavar="password", help="router admin password") airport_client_group = parser.add_argument_group("AirPort client commands") airport_client_group.add_argument("--listprop", action="store_const", const=True, help="list supported properties") airport_client_group.add_argument("--helpprop", metavar="property", nargs=1, help="print the description of the specified property") airport_client_group.add_argument("--getprop", metavar="property", nargs=1, help="get the value of the specified property") airport_client_group.add_argument("--setprop", metavar=("property", "value"), nargs=2, help="set the value of the specified property") airport_client_group.add_argument("--dumpprop", action="store_const", const=True, help="dump values of all supported properties") airport_client_group.add_argument("--acpprop", action="store_const", const=True, help="get acp acpprop list") airport_client_group.add_argument("--dump-syslog", action="store_const", const=True, help="dump the router system log") airport_client_group.add_argument("--reboot", action="store_const", const=True, help="reboot device") airport_client_group.add_argument("--factory-reset", action="store_const", const=True, help="RESET EVERYTHING and reboot; you have been warned!") airport_client_group.add_argument("--flash-primary", metavar="firmware_path", nargs=1, help="flash primary partition firmware") airport_client_group.add_argument("--do-feat-command", action="store_const", const=True, help="send 0x1b (feat) command") basebinary_group = parser.add_argument_group("Basebinary commands") basebinary_group.add_argument("--decrypt", metavar=("inpath", "outpath"), nargs=2, help="decrypt the basebinary") basebinary_group.add_argument("--extract", metavar=("inpath", "outpath"), nargs=2, help="extract the gzimg contents") test_group = parser.add_argument_group("Test arguments") test_group.add_argument("--srp-test", action="store_const", const=True, help="SRP (requires OS X)") args_dict = vars(parser.parse_args()) #TODO: give each element a dict containing parameter requirements/argparse infos, then generate parser based on this commands = { "listprop": "local", "helpprop": "local", "getprop": "remote_admin", "setprop": "remote_admin", "dumpprop": "remote_admin", "acpprop": "remote_admin", "dump_syslog": "remote_admin", "reboot": "remote_admin", "factory_reset": "remote_admin", "flash_primary": "remote_admin", "do_feat_command": "remote_noauth", "decrypt": "local", "extract": "local", "srp_test": "remote_admin", } target = args_dict["target"] password = args_dict["password"] command_args = {k: v for k, v in args_dict.items() if k in commands and v is not None} if len(command_args) == 0: logging.error("must specify a command") elif len(command_args) == 1: #TODO: clean this up a bit cmd, arg = command_args.popitem() assert commands[cmd] in ["local", "remote_noauth", "remote_admin"], "unknown command type \"{0}\"".format(commands[cmd]) cmd_handler_name = "_cmd_{0}".format(cmd) cmd_handler = globals().get(cmd_handler_name, _cmd_not_implemented) if commands[cmd] == "local": cmd_handler(arg) if commands[cmd] == "remote_noauth": if target is not None: c = ACPClient(target) c.connect() cmd_handler(c, arg) c.close() else: logging.error("must specify a target") if commands[cmd] == "remote_admin": if target is not None and password is not None: c = ACPClient(target, password) c.connect() cmd_handler(c, arg) c.close() else: logging.error("must specify a target and administrator password") else: logging.error("multiple commands not supported, choose only one") ================================================ FILE: acp/clibs/AppleSRP.py ================================================ from ctypes import * #XXX: hax to display NULL pointer thingies def _fmt_void_ptr(value): if value is None: return 0 return value def _fmt_cstr(value): if cast(value, c_void_p).value is None: return "" return value.contents.get_data_buffer().encode("hex") def _fmt_ccz_class(value): if cast(value, c_void_p).value is None: return "" return value.contents def _fmt_ccz(value): if cast(value, c_void_p).value is None: return "" return value.contents class cstr(Structure): _fields_ = [("data", c_void_p), ("length", c_long), ("cap", c_long), ("ref", c_int), ("allocator", c_void_p)] def __str__(self): s = "cstr: {0!r}\n".format(self) s += "data: {0}\n".format(self.get_data_buffer().encode("hex")) s += "len: {0}\n".format(self.length) s += "cap: {0}\n".format(self.cap) s += "ref: {0}\n".format(self.ref) s += "alloc: {0:#x}".format(self.allocator) return s def get_data_buffer(self): return string_at(self.data, self.length) # define SHA_LBLOCK 16 ''' typedef struct SHAstate_st { SHA_LONG h0, h1, h2, h3, h4; SHA_LONG Nl, Nh; SHA_LONG data[SHA_LBLOCK]; unsigned int num; } SHA_CTX; ''' class SHA1_CTX(Structure): _fields_ = [("h0", c_uint), ("h1", c_uint), ("h2", c_uint), ("h3", c_uint), ("h4", c_uint), ("Nl", c_uint), ("Nh", c_uint), ("data", c_uint * 16), # uninitialized data??? ("num", c_uint)] def __str__(self): s = "SHA1_CTX: {0!r}\n".format(self) s += "h0: {0:#x}\n".format(self.h0) s += "h1: {0:#x}\n".format(self.h1) s += "h2: {0:#x}\n".format(self.h2) s += "h3: {0:#x}\n".format(self.h3) s += "h4: {0:#x}\n".format(self.h4) s += "Nl: {0}\n".format(self.Nl) s += "Nh: {0}\n".format(self.Nh) s += "data: {0}\n".format("".join(["{0:08x}".format(self.data[i]) for i in range(16)])) # uninitialized data??? s += "num: {0}".format(self.num) return s #define RFC2945_KEY_LEN 40 /* length of session key (bytes) */ #define RFC2945_RESP_LEN 20 /* length of proof hashes (bytes) */ ''' struct client_meth_st { SHA1_CTX hash; SHA1_CTX ckhash; unsigned char k[RFC2945_KEY_LEN]; }; ''' class client_meth_st(Structure): _fields_ = [("hash", SHA1_CTX), ("ckhash", SHA1_CTX), ("k", c_ubyte * 40)] def __str__(self): s = "client_meth_st: {0!r}\n".format(self) s += "hash: {0}\n".format(self.hash) s += "ckhash: {0}\n".format(self.ckhash) s += "k: {0}".format(self.k) return s ''' struct server_meth_st { SHA1_CTX hash; SHA1_CTX ckhash; SHA1_CTX oldhash; SHA1_CTX oldckhash; unsigned char k[RFC2945_KEY_LEN]; unsigned char r[RFC2945_RESP_LEN]; }; ''' class server_meth_st(Structure): _fields_ = [("hash", SHA1_CTX), ("ckhash", SHA1_CTX), ("oldhash", SHA1_CTX), ("oldckhash", SHA1_CTX), ("k", c_ubyte * 40), ("r", c_ubyte * 20)] def __str__(self): s = "server_meth_st: {0!r}\n".format(self) s += "hash: {0}\n".format(self.hash) s += "ckhash: {0}\n".format(self.ckhash) s += "oldhash: {0}\n".format(self.oldhash) s += "oldckhash: {0}\n".format(self.oldckhash) s += "k: {0}\n".format(self.k) s += "r: {0}".format(self.r) return s ''' struct ccz_class { void *ctx; void *(*ccz_alloc)(void *, size_t); void *(*ccz_realloc)(void *, size_t, void *, size_t); void (*ccz_free)(void *, size_t, void *); }; ''' class ccz_class(Structure): _fields_ = [("ctx", c_void_p), ("ccz_alloc", c_void_p), ("ccz_realloc", c_void_p), ("ccz_free", c_void_p)] def __str__(self): s = "ccz_class: {0!r}\n".format(self) s += "ctx: {0:#x}\n".format(_fmt_void_ptr(self.ctx)) s += "ccz_alloc: {0:#x}\n".format(self.ccz_alloc) s += "ccz_realloc: {0:#x}\n".format(self.ccz_realloc) s += "ccz_free: {0:#x}".format(self.ccz_free) return s ''' struct ccz { size_t n; struct ccz_class *isa; int sac; cc_unit *u; }; typedef struct ccz ccz; ''' class ccz(Structure): _fields_ = [("n", c_size_t), ("isa", POINTER(ccz_class)), ("sac", c_int), ("u", c_void_p)] def __str__(self): s = "ccz: {0!r}\n".format(self) s += "n: {0}\n".format(self.n) s += "isa: {0}\n".format(_fmt_ccz_class(self.isa)) s += "sac: {0}\n".format(self.sac) s += "u: {0:#x}".format(_fmt_void_ptr(self.u)) return s ''' struct srp_st { int magic; /* To distinguish client from server (and for sanity) */ int flags; cstr * username; BigInteger modulus; BigInteger generator; cstr * salt; BigInteger verifier; BigInteger password; BigInteger pubkey; BigInteger secret; BigInteger u; BigInteger key; cstr * ex_data; SRP_METHOD * meth; void * meth_data; BigIntegerCtx bctx; /* to cache temporaries if available */ BigIntegerModAccel accel; /* to accelerate modexp if available */ SRP_CLIENT_PARAM_VERIFY_CB param_cb; /* to verify params */ SRP_SERVER_LOOKUP * slu; /* to look up users */ }; ''' class srp_st(Structure): _fields_ = [("magic", c_int), ("flags", c_int), ("username", POINTER(cstr)), ("modulus", POINTER(ccz)), ("generator", POINTER(ccz)), ("salt", POINTER(cstr)), ("verifier", POINTER(ccz)), ("password", POINTER(ccz)), ("pubkey", POINTER(ccz)), ("secret", POINTER(ccz)), ("u", POINTER(ccz)), ("key", POINTER(ccz)), ("ex_data", POINTER(cstr)), ("meth", c_void_p), #XXXXXXXXXXXXXXXX ("meth_data", POINTER(client_meth_st)), #("meth_data", POINTER(server_meth_st)), #XXXXXXXXXXXXXXXX ("bctx", c_void_p), ("accel", c_void_p), ("param_cb", c_void_p), ("slu", c_void_p)] def __str__(self): s = "*** START ***\n" s += "srp_st: {0!r}\n".format(self) s += "magic: {0}\n".format(self.magic) s += "flags: {0}\n".format(self.flags) s += "username: {0}\n".format(_fmt_cstr(self.username)) s += "modulus: {0}\n".format(_fmt_ccz(self.modulus)) s += "generator: {0}\n".format(_fmt_ccz(self.generator)) s += "salt: {0}\n".format(_fmt_cstr(self.salt)) s += "verifier: {0}\n".format(_fmt_ccz(self.verifier)) s += "password: {0}\n".format(_fmt_ccz(self.password)) s += "pubkey: {0}\n".format(_fmt_ccz(self.pubkey)) s += "secret: {0}\n".format(_fmt_ccz(self.secret)) s += "u: {0}\n".format(_fmt_ccz(self.u)) s += "key: {0}\n".format(_fmt_ccz(self.key)) s += "ex_data: {0}\n".format(_fmt_cstr(self.ex_data)) s += "meth: {0:#x}\n".format(_fmt_void_ptr(self.meth)) s += "meth_data: {0}\n".format(self.meth_data.contents) #XXXXXXXXXXXXXXXX s += "bctx: {0:#x}\n".format(_fmt_void_ptr(self.bctx)) s += "accel: {0:#x}\n".format(_fmt_void_ptr(self.accel)) s += "param_cb: {0:#x}\n".format(_fmt_void_ptr(self.param_cb)) s += "slu: {0:#x}\n".format(_fmt_void_ptr(self.slu)) s += "**** END ****" return s __AppleSRP = cdll.LoadLibrary("/System/Library/PrivateFrameworks/AppleSRP.framework/Versions/A/AppleSRP") #print "AppleSRP:", __AppleSRP # SRP_METHOD *SRP6a_client_method(void) SRP6a_client_method = __AppleSRP.SRP6a_client_method SRP6a_client_method.restype = c_void_p # SRP_METHOD *SRP6a_server_method(void) SRP6a_server_method = __AppleSRP.SRP6a_server_method SRP6a_server_method.restype = c_void_p # SRP *SRP_new(SRP_METHOD *meth) SRP_new = __AppleSRP.SRP_new #SRP_new.restype = c_void_p SRP_new.restype = POINTER(srp_st) SRP_new.argtypes = [ c_void_p ] # SRP_RESULT SRP_set_username(SRP *srp, const char *username) SRP_set_username = __AppleSRP.SRP_set_username SRP_set_username.argtypes = [ c_void_p, c_char_p ] # SRP_RESULT SRP_set_params(SRP *srp, const unsigned char *modulus, int modlen, # const unsigned char *generator, int genlen, # const unsigned char *salt, int saltlen) SRP_set_params = __AppleSRP.SRP_set_params SRP_set_params.argtypes = [ c_void_p, c_char_p, c_int, c_char_p, c_int, c_char_p, c_int ] # SRP_RESULT SRP_gen_pub(SRP *srp, cstr **result) SRP_gen_pub = __AppleSRP.SRP_gen_pub SRP_gen_pub.argtypes = [ c_void_p, POINTER(POINTER(cstr)) ] # SRP_RESULT SRP_set_auth_password(SRP *srp, const unsigned char *password, int passlen) SRP_set_auth_password = __AppleSRP.SRP_set_auth_password SRP_set_auth_password.argtypes = [ c_void_p, c_char_p, c_int ] # SRP_RESULT SRP_compute_key(SRP *srp, cstr **result, const unsigned char *pubkey, int pubkeylen) SRP_compute_key = __AppleSRP.SRP_compute_key SRP_compute_key.argtypes = [ c_void_p, POINTER(POINTER(cstr)), c_char_p, c_int ] # SRP_RESULT SRP_respond(SRP *srp, cstr **proof) SRP_respond = __AppleSRP.SRP_respond SRP_respond.argtypes = [ c_void_p, POINTER(POINTER(cstr)) ] # SRP_RESULT SRP_verify(SRP *srp, const unsigned char *proof, int prooflen) SRP_verify = __AppleSRP.SRP_verify SRP_verify.argtypes = [ c_void_p, c_char_p, c_int ] # SRP_RESULT SRP_free(SRP *srp) SRP_free = __AppleSRP.SRP_free SRP_free.argtypes = [ c_void_p ] # cstr *cstr_new(void) cstr_new = __AppleSRP.cstr_new cstr_new.restype = POINTER(cstr) # void cstr_free(cstr *str) cstr_free = __AppleSRP.cstr_free cstr_free.restype = None cstr_free.argtypes = [ POINTER(cstr) ] ================================================ FILE: acp/clibs/__init__.py ================================================ ================================================ FILE: acp/client.py ================================================ import logging import os import struct import time from .cflbinary import CFLBinaryPListComposer, CFLBinaryPListParser from .message import ACPMessage from .property import ACPProperty from .session import ACPClientSession class ACPClient(object): def __init__(self, target, password=""): self.target = target self.password = password self.session = ACPClientSession(target, password) def connect(self): self.session.connect() def close(self): self.session.close() def send(self, data): self.session.send(data) def recv(self, size): return self.session.recv(size) def recv_message_header(self): return self.recv(ACPMessage.header_size) def recv_property_element_header(self): return self.recv(ACPProperty.element_header_size) def get_properties(self, prop_names=[]): # request property by sending name and "null" value payload = "" for name in prop_names: payload += ACPProperty.compose_raw_element(0, ACPProperty(name)) request = ACPMessage.compose_getprop_command(4, self.password, payload) self.send(request) raw_reply = self.recv_message_header() reply_header = ACPMessage.parse_raw(raw_reply) if reply_header.error_code != 0: print "get_properties error code: {0:#x}".format(reply_header.error_code) #XXX: blah, what to do... return [] props = [] while True: prop_header = self.recv_property_element_header() name, flags, size = ACPProperty.parse_raw_element_header(prop_header) logging.debug("name ".format(name)) logging.debug("flags ".format(flags)) logging.debug("size ".format(size)) prop_data = self.recv(size) logging.debug("prop_data {0!r}".format(prop_data)) if flags & 1: (error_code, ) = struct.unpack(">I", prop_data) print "error requesting value for property \"{0}\": {1:#x}".format(name, error_code) continue prop = ACPProperty(name, prop_data) logging.debug("prop {0!r}".format(prop)) #XXX: this is still a bit ugly if prop.name is None and prop.value is None: logging.debug("found empty prop end marker") break #XXX: should we should return dict(name=name, prop=ACPProperty(name, value)) instead? props.append(prop) return props def set_properties(self, props_dict={}): payload = "" for name, prop in props_dict.iteritems(): logging.debug("prop: {0!r}".format(prop)) payload += ACPProperty.compose_raw_element(0, prop) request = ACPMessage.compose_setprop_command(0, self.password, payload) self.send(request) raw_reply = self.recv_message_header() reply_header = ACPMessage.parse_raw(raw_reply) if reply_header.error_code != 0: print "set_properties error code: {0:#x}".format(reply_header.error_code) #XXX: blah, what to do... return prop_header = self.recv_property_element_header() name, flags, size = ACPProperty.parse_raw_element_header(prop_header) logging.debug("name {0!r}".format(name)) logging.debug("flags {0!r}".format(flags)) logging.debug("size {0!r}".format(size)) prop_data = self.recv(size) logging.debug("prop_data {0!r}".format(prop_data)) if flags & 1: (error_code, ) = struct.unpack(">I", prop_data) print "error setting value for property \"{0}\": {1:#x}".format(name, error_code) return prop = ACPProperty(name, prop_data) logging.debug("prop {0!r}".format(prop)) #XXX: this is still a bit ugly if prop.name is None and prop.value is None: logging.debug("found empty prop end marker") def get_features(self): self.send(ACPMessage.compose_feat_command(0)) reply_header = ACPMessage.parse_raw(self.recv_message_header()) reply = self.recv(reply_header.body_size) return CFLBinaryPListParser.parse(reply) def flash_primary(self, payload): self.send(ACPMessage.compose_flash_primary_command(0, self.password, payload)) reply_header = ACPMessage.parse_raw(self.recv_message_header()) return self.recv(reply_header.body_size) def authenticate_AppleSRP(self): #XXX: STILL TESTING SHIT import ctypes from .clibs import AppleSRP from collections import OrderedDict username = u"admin" dic = OrderedDict([(u"state", 1), (u"username", username)]) payload = CFLBinaryPListComposer.compose(dic) raw_message = ACPMessage.compose_auth_command(4, payload) self.send(raw_message) raw_reply_header = self.recv_message_header() reply_header = ACPMessage.parse_raw(raw_reply_header) if reply_header.error_code != 0: logging.error("authenticate error code: {0:#x}".format(reply_header.error_code)) #XXX: blah, what to do... return logging.debug("recv_size: {0}".format(reply_header.body_size)) raw_message = self.recv(reply_header.body_size) logging.debug("raw_message: {0!r}".format(raw_message)) params1 = CFLBinaryPListParser.parse(raw_message) logging.debug(params1) n = params1[u"modulus"] g = params1[u"generator"] salt = params1[u"salt"] server_pkey = params1[u"publicKey"] nhex = n.encode("hex") ghex = g.encode("hex") logging.debug("nhex: {0}".format(nhex)) logging.debug("ghex: {0}".format(ghex)) logging.debug("salt: {0}".format(salt.encode("hex"))) logging.debug("server_pkey: {0}".format(server_pkey.encode("hex"))) # create SRP context asrp = AppleSRP.SRP_new(AppleSRP.SRP6a_client_method()) #logging.debug(asrp.contents) # set username logging.debug("SRP_set_username: {0}".format(AppleSRP.SRP_set_username(asrp, username))) #logging.debug(asrp.contents) # set parameters from server logging.debug("SRP_set_params: {0}".format(AppleSRP.SRP_set_params(asrp, n, len(n), g, len(g), salt, len(salt)))) #logging.debug(asrp.contents) # generate public key client_gen_pubkey_ptr = AppleSRP.cstr_new() logging.debug("SRP_gen_pub: {0}".format(AppleSRP.SRP_gen_pub(asrp, ctypes.byref(client_gen_pubkey_ptr)))) client_gen_pubkey = client_gen_pubkey_ptr.contents logging.debug(client_gen_pubkey) #logging.debug(asrp.contents) # set password logging.debug("SRP_set_auth_password: {0}".format(AppleSRP.SRP_set_auth_password(asrp, self.password, len(self.password)))) #logging.debug(asrp.contents) # compute key client_computed_key_ptr = AppleSRP.cstr_new() logging.debug("SRP_compute_key: {0}".format(AppleSRP.SRP_compute_key(asrp, ctypes.byref(client_computed_key_ptr), server_pkey, len(server_pkey)))) client_computed_key = client_computed_key_ptr.contents logging.debug(client_computed_key) client_computed_key_buf = client_computed_key.get_data_buffer() #logging.debug(asrp.contents) # generate challenge response client_proof_ptr = AppleSRP.cstr_new() logging.debug("SRP_respond: {0}".format(AppleSRP.SRP_respond(asrp, ctypes.byref(client_proof_ptr)))) client_proof = client_proof_ptr.contents logging.debug(client_proof) #logging.debug(asrp.contents) client_iv = os.urandom(0x10) client_pkey = client_gen_pubkey.get_data_buffer() client_proof = client_proof.get_data_buffer() dic = OrderedDict([(u"iv", client_iv), (u"publicKey", client_pkey), (u"state", 3), (u"response", client_proof)]) payload = CFLBinaryPListComposer.compose(dic) raw_message = ACPMessage.compose_auth_command(4, payload) self.send(raw_message) raw_reply_header = self.recv_message_header() reply_header = ACPMessage.parse_raw(raw_reply_header) if reply_header.error_code != 0: logging.debug("authenticate error code: {0:#x}".format(reply_header.error_code)) #XXX: blah, what to do... return logging.debug("recv_size: {0}".format(reply_header.body_size)) raw_message = self.recv(reply_header.body_size) logging.debug("raw_message: {0!r}".format(raw_message)) params2 = CFLBinaryPListParser.parse(raw_message) logging.debug(params2) server_proof = params2[u"response"] server_iv = params2[u"iv"] # verify server response logging.debug("SRP_verify: {0}".format(AppleSRP.SRP_verify(asrp, server_proof, len(server_proof)))) #logging.debug(asrp.contents) # cleanup logging.debug("Freeing cstr(s)") AppleSRP.cstr_free(client_gen_pubkey_ptr) AppleSRP.cstr_free(client_computed_key_ptr) AppleSRP.cstr_free(client_proof_ptr) logging.debug("SRP_free: {0}".format(AppleSRP.SRP_free(asrp))) ###self.session.enable_encryption(client_computed_key_buf, client_iv, server_iv) ================================================ FILE: acp/encryption.py ================================================ from Crypto.Cipher import AES from Crypto.Protocol import KDF from Crypto.Util import Counter PBKDF_salt0 = "F072FA3F66B410A135FAE8E6D1D43D5F".decode("hex") PBKDF_salt1 = "BD0682C9FE79325BC73655F4174B996C".decode("hex") class _ACPEncryptionContext(object): def __init__(self, key, iv): self.key = key self.iv = iv self.ctr = Counter.new(128, initial_value=int(iv.encode("hex"), 16)) self.cipher = AES.new(key, AES.MODE_CTR, counter=self.ctr) class ACPEncryption(object): def __init__(self, key, client_iv, server_iv): self._client_context = self._init_client_context(key, client_iv) self._server_context = self._init_server_context(key, server_iv) def _init_client_context(cls, key, iv): derived_key = KDF.PBKDF2(key, PBKDF_salt0, 16, 5) return _ACPEncryptionContext(derived_key, iv) def _init_server_context(cls, key, iv): derived_key = KDF.PBKDF2(key, PBKDF_salt1, 16, 7) return _ACPEncryptionContext(derived_key, iv) def client_decrypt(self, data): return self._client_context.cipher.decrypt(data) def client_encrypt(self, data): return self._client_context.cipher.encrypt(data) def server_decrypt(self, data): return self._server_context.cipher.decrypt(data) def server_encrypt(self, data): return self._server_context.cipher.encrypt(data) ================================================ FILE: acp/exception.py ================================================ #TODO: put other exceptions in here... class ACPError(Exception): """Base class for exceptions in this module.""" pass class ACPClientError(ACPError): """Exception raised for errors in the ACP client""" pass class ACPCommandLineError(ACPError): """Exception raised for command line invocation errors""" pass class ACPMessageError(ACPError): """Exception raised for errors processing ACP packets""" pass class ACPPropertyError(ACPError): """Exception raised for errors processing ACP properties""" pass ================================================ FILE: acp/keystream.py ================================================ """Static key/seed for keystream generation""" ACP_STATIC_KEY = "5b6faf5d9d5b0e1351f2da1de7e8d673".decode("hex") def generate_acp_keystream(length): """Get key used to encrypt the header key (and some message data?) Args: length (int): length of keystream to generate Returns: String of requested length Note: Keystream repeats every 256 bytes """ key = "" key_idx = 0 while (key_idx < length): key += chr((key_idx + 0x55 & 0xFF) ^ ord(ACP_STATIC_KEY[key_idx % len(ACP_STATIC_KEY)])) key_idx += 1 return key ================================================ FILE: acp/message.py ================================================ import logging import struct import zlib from .exception import ACPMessageError from .keystream import * def _generate_acp_header_key(password): """ Encrypt password for ACP message header key field Note: Truncates the password at 0x20 bytes, not sure if this is the right thing to use in all cases Args: password (str): system password of the router (syAP) Returns: String containing encrypted password of proper length for the header field """ pw_len = 0x20 pw_key = generate_acp_keystream(pw_len) # pad with NULLs pw_buf = password[:pw_len].ljust(pw_len, "\x00") enc_pw_buf = "" for i in range(pw_len): enc_pw_buf += chr(ord(pw_key[i]) ^ ord(pw_buf[i])) return enc_pw_buf class ACPMessage(object): """ACP message composition and parsing""" #XXX: struct is stupid about unpacking unsigned ints > 0x7fffffff, so treat everything as signed and # "cast" where necessary. Should we switch to using ctypes? _header_format = struct.Struct("!4s8i12x32s48x") _header_magic = "acpp" header_size = _header_format.size def __init__(self, version, flags, unused, command, error_code, key, body=None, body_size=None): self.version = version self.flags = flags self.unused = unused self.command = command self.error_code = error_code # body is not specified, this is a stream header if body == None: # the body size is already specified, don't override it self.body_size = body_size if body_size != None else -1 self.body_checksum = 1 # equivalent to zlib.adler32("") else: # the body size is already specified, don't override it self.body_size = body_size if body_size != None else len(body) self.body_checksum = zlib.adler32(body) self.key = key self.body = body def __str__(self): s = "ACPMessage: {0!r}\n".format(self) s += "body_checksum: {0:#x}\n".format(self.body_checksum) s += "body_size: {0:#x}\n".format(self.body_size) s += "flags: {0:#x}\n".format(self.flags) s += "unused: {0:#x}\n".format(self.unused) s += "command: {0:#x}\n".format(self.command) s += "error_code: {0:#x}\n".format(self.error_code) s += "key: {0!r}".format(self.key) return s @classmethod def parse_raw(cls, data): # bail early if there is not enough data if len(data) < cls.header_size: raise ACPMessageError("need to pass at least {0} bytes".format(cls.header_size)) header_data = data[:cls.header_size] # make sure there's data beyond the header before we try to access it body_data = data[cls.header_size:] if len(data) > cls.header_size else None (magic, version, header_checksum, body_checksum, body_size, flags, unused, command, error_code, key) = cls._header_format.unpack(header_data) logging.debug("ACP message header fields, parsed not validated") logging.debug("magic {0!r}".format(magic)) logging.debug("header_checksum {0:#x}".format(header_checksum)) logging.debug("body_checksum {0:#x}".format(body_checksum)) logging.debug("body_size {0:#x}".format(body_size)) logging.debug("flags {0:#x}".format(flags)) logging.debug("unused {0:#x}".format(unused)) logging.debug("command {0:#x}".format(command)) logging.debug("error_code {0:#x}".format(error_code)) logging.debug("key {0!r}".format(key)) if magic != cls._header_magic: raise ACPMessageError("bad header magic") if version not in [0x00000001, 0x00030001]: raise ACPMessageError("invalid version") #TODO: can we zero the header_checksum field without recreating the struct (how?) tmphdr = cls._header_format.pack(magic, version, 0, body_checksum, body_size, flags, unused, command, error_code, key) if header_checksum != zlib.adler32(tmphdr): raise ACPMessageError("header checksum does not match") if body_data and body_size == -1: raise ACPMessageError("cannot handle stream header with data attached") if body_data and body_size != len(body_data): raise ACPMessageError("message body size does not match available data") if body_data and body_checksum != zlib.adler32(body_data): raise ACPMessageError("body checksum does not match") #TODO: check flags #TODO: check status if command not in [1, 3, 4, 5, 6, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b]: raise ACPMessageError("unknown command") #TODO: check error code return cls(version, flags, unused, command, error_code, key, body_data, body_size) @classmethod def compose_echo_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 1, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_flash_primary_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 3, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_flash_secondary_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 5, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_flash_bootloader_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 6, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_getprop_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 0x14, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_setprop_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 0x15, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_perform_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 0x16, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_monitor_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 0x18, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_rpc_command(cls, flags, password, payload): return cls(0x00030001, flags, 0, 0x19, 0, _generate_acp_header_key(password), payload)._compose_raw_packet() @classmethod def compose_auth_command(cls, flags, payload): return cls(0x00030001, flags, 0, 0x1a, 0, _generate_acp_header_key(""), payload)._compose_raw_packet() @classmethod def compose_feat_command(cls, flags): return cls(0x00030001, flags, 0, 0x1b, 0, _generate_acp_header_key(""))._compose_raw_packet() @classmethod def compose_message_ex(cls, version, flags, unused, command, error_code, password, payload, payload_size): return cls(version, flags, unused, command, error_code, _generate_acp_header_key(password), payload, payload_size)._compose_raw_packet() def _compose_raw_packet(self): """Compose a request from the client to ACP daemon Returns: String containing message to send """ reply = self._compose_header() if self.body: reply += self.body return reply def _compose_header(self): """Compose the message header Returns: String containing header data """ tmphdr = self._header_format.pack(self._header_magic, self.version, 0, self.body_checksum, self.body_size, self.flags, self.unused, self.command, self.error_code, self.key) header = self._header_format.pack(self._header_magic, self.version, zlib.adler32(tmphdr), self.body_checksum, self.body_size, self.flags, self.unused, self.command, self.error_code, self.key) return header ================================================ FILE: acp/misc.py ================================================ #XXX: this file exists until I think of a better way to do this def cast_u32(value): #XXX: lazy, how do we do this correctly? if value < -0x80000000 or value > 0x7FFFFFFF: raise Exception("value outside u32 range") return value & 0xFFFFFFFF ================================================ FILE: acp/property.py ================================================ import logging import pprint import struct from .cflbinary import CFLBinaryPListParser from .exception import ACPPropertyError _acp_properties = [ # Uncomment and fill out relevant fields to add support for a property # Properties must be in the following format: # (name, type, description, validation), where # name (required) is a 4 character string, # type (required) is a valid property type (str, dec, hex, log, mac, cfb, bin) # description (required) is a short, one-line description of the property # validation (optional) is eval()d to verify the input value for setting a property ("buil","str","Build string?",""), ("DynS","cfb","DNS",""), #("cfpf","","",""), #("cloC","","",""), #("cloD","","",""), #("conf","","",""), ("fire","cfb","Firewall???",""), #("prob","","",""), ("srcv","str","Source Version",""), ("syNm","str","Device name",""), #("syDN","","",""), #("syPI","","",""), ("syPW","str","Router administration password",""), ("syPR","str","syPR string???",""), ("syGP","str","Router guest password???",""), #("syCt","","",""), #("syLo","","",""), ("syDs","str","System description",""), ("syVs","str","System version",""), ("syVr","str","System version???",""), ("syIn","str","System information???",""), ("syFl","hex","????",""), ("syAM","str","Model Identifier",""), ("syAP","dec","Product ID",""), ("sySN","str","Apple Serial Number",""), #("ssSN","","",""), #("sySK","","",""), ("ssSK","str","Apple SKU",""), #("syRe","","",""), ("syLR","cfb","syLR Blob",""), ("syAR","cfb","syAR Blob",""), ("syUT","dec","System Uptime",""), #("minV","","",""), ("minS","str","apple-minver",""), ("chip","str","SoC Description",""), #("card","","",""), #("memF","","",""), #("pool","","",""), #("tmpC","","",""), #("RPMs","","",""), ("sySI","cfb","System Info Blob?",""), #("fDCY","","",""), ("TMEn","hex","TMEn???",""), ("CLTM","cfb","CLTM???",""), #("sPLL","","",""), #("syTL","","",""), #("syST","","",""), ("sySt","cfb","System Status",""), #("syIg","","",""), ("syBL","str","Bootloader vesrsion string",""), ("time","dec","System time",""), ("timz","cfb","Timezone Config Blob",""), ("usrd","cfb","usrd???",""), #("uuid","","",""), #("drTY","","",""), #("sttE","","",""), #("sttF","","",""), #("stat","","",""), #("sRnd","","",""), #("Accl","","",""), ("dSpn","cfb","Disk spin status?",""), ("syMS","str","MLB Serial Number",""), #("IGMP","","",""), ("diag","bin","diag???",""), #("paFR","","",""), ("raNm","str","Radio Name",""), #("raCl","","",""), #("raSk","","",""), #("raWM","","",""), #("raEA","","",""), #("raWE","","",""), #("raCr","","",""), #("raKT","","",""), #("raNN","","",""), #("raGK","","",""), #("raHW","","",""), #("raCM","","",""), #("raRo","","",""), #("raCA","","",""), #("raCh","","",""), #("rCh2","","",""), #("raWC","","",""), #("raDe","","",""), #("raMu","","",""), #("raLC","","",""), #("raLF","","",""), #("ra1C","","",""), #("raVs","","",""), ("raMA","mac","Radio MAC Address",""), #("raM2","","",""), #("raMO","","",""), #("raLO","","",""), #("raDS","","",""), #("raNA","","",""), #("raWB","","",""), #("raIS","","",""), #("raMd","","",""), #("raPo","","",""), #("raPx","","",""), #("raTr","","",""), #("raDt","","",""), #("raFC","","",""), #("raEC","","",""), #("raMX","","",""), #("raIE","","",""), #("raII","","",""), #("raB0","","",""), #("raB1","","",""), #("raB2","","",""), #("raSt","","",""), #("APSR","","",""), #("raTX","","",""), #("raRX","","",""), #("raAC","","",""), ("raSL","cfb","Radios list?",""), #("raMI","","",""), #("raST","","",""), #("raDy","","",""), #("raEV","","",""), #("rTSN","","",""), ("raSR","cfb","Radio scan results?",""), #("eaRA","","",""), ("WiFi","cfb","Wifi configuration?",""), ("rCAL","cfb","Radio calibration data?",""), #("moPN","","",""), #("moAP","","",""), #("moUN","","",""), #("moPW","","",""), #("moIS","","",""), #("moLS","","",""), #("moLI","","",""), #("moID","","",""), #("moDT","","",""), #("moPD","","",""), #("moAD","","",""), #("moCC","","",""), #("moCR","","",""), #("moCI","","",""), #("^moM","","",""), #("moVs","","",""), #("moMP","","",""), #("moMF","","",""), #("moFV","","",""), #("pdFl","","",""), #("pdUN","","",""), #("pdPW","","",""), #("pdAR","","",""), #("pdID","","",""), #("pdMC","","",""), #("peSN","","",""), #("peUN","","",""), #("pePW","","",""), #("peSC","","",""), #("peAC","","",""), #("peID","","",""), #("peAO","","",""), ("waCV","bin","WAN Config Mode?",""), ("waIn","bin","WAN Interface Mode?",""), #("waD1","","",""), #("waD2","","",""), #("waD3","","",""), #("waC1","","",""), #("waC2","","",""), #("waC3","","",""), ("waIP","bin","WAN IP",""), #("waSM","","",""), ("waRA","bin","WAN Upstream Gateway IP",""), #("waDC","","",""), #("waDS","","",""), ("waMA","mac","WAN MAC Address",""), #("waMO","","",""), #("waDN","","",""), #("waCD","","",""), #("waIS","","",""), #("waNM","","",""), #("waSD","","",""), #("waFF","","",""), #("waRO","","",""), #("waW1","","",""), #("waW2","","",""), #("waW3","","",""), #("waLL","","",""), #("waUB","","",""), ("waDI","cfb","WAN DHCP Info?",""), #("laCV","","",""), #("laIP","","",""), #("laSM","","",""), #("laRA","","",""), #("laDC","","",""), #("laDS","","",""), #("laNA","","",""), ("laMA","mac","LAN MAC Address",""), #("laIS","","",""), #("laSD","","",""), #("laIA","","",""), #("gn6?","","",""), #("gn6A","","",""), #("gn6P","","",""), #("dhFl","","",""), #("dhBg","","",""), #("dhEn","","",""), #("dhSN","","",""), #("dhRo","","",""), #("dhLe","","",""), #("dhMg","","",""), #("dh95","","",""), ("DRes","cfb","DHCP Reservations",""), #("dhWA","","",""), #("dhDS","","",""), #("dhDB","","",""), #("dhDE","","",""), #("dhDL","","",""), ("dhSL","cfb","DHCP Server leases?",""), #("gnFl","","",""), #("gnBg","","",""), #("gnEn","","",""), #("gnSN","","",""), #("gnRo","","",""), #("gnLe","","",""), #("gnMg","","",""), #("gn95","","",""), #("gnDi","","",""), #("naFl","","",""), #("naBg","","",""), #("naEn","","",""), #("naSN","","",""), #("naRo","","",""), #("naAF","","",""), #("nDMZ","","",""), #("pmPI","","",""), #("pmPS","","",""), #("pmPR","","",""), #("pmTa","","",""), #("acEn","","",""), #("acTa","","",""), ("tACL","cfb","Timed Access Control",""), #("wdFl","","",""), #("wdLs","","",""), #("dWDS","","",""), #("cWDS","","",""), #("dwFl","","",""), #("raFl","","",""), #("raI1","","",""), #("raTm","","",""), #("raAu","","",""), #("raAc","","",""), #("raSe","","",""), #("raRe","","",""), #("raF2","","",""), #("raI2","","",""), #("raT2","","",""), #("raU2","","",""), #("raC2","","",""), #("raS2","","",""), #("raR2","","",""), #("raCi","","",""), ("ntSV","str","NTP Server Hostname",""), #("ntpC","","",""), #("smtp","","",""), #("slog","","",""), #("slgC","","",""), #("slCl","","",""), ("slvl","dec","System log severity level?",""), #("slfl","","",""), ("logm","log","System log data",""), #("snAF","","",""), #("snLW","","",""), #("snLL","","",""), #("snRW","","",""), #("snWW","","",""), #("snRL","","",""), #("snWL","","",""), #("snCS","","",""), #("srtA","","",""), #("srtF","","",""), #("upsF","","",""), #("usbF","","",""), ("USBi","cfb","USB Info",""), #("USBL","","",""), #("USBR","","",""), #("USBO","","",""), #("USBs","","",""), #("USBo","","",""), #("USBh","","",""), #("USBb","","",""), #("USBn","","",""), ("prni","cfb","Printer Info?",""), #("prnM","","",""), #("prnI","","",""), #("prnR","","",""), #("RUdv","","",""), #("RUfl","","",""), ("MaSt","cfb","USB Mass Storage Info",""), #("SMBw","","",""), #("SMBs","","",""), #("fssp","","",""), #("diSD","","",""), #("diCS","","",""), #("deSt","","",""), #("daSt","","",""), #("dmSt","","",""), #("adNm","","",""), #("adBD","","",""), #("adAD","","",""), #("adHU","","",""), #("IDNm","","",""), ("seFl","bin","????",""), #???? #("nvVs","","",""), #("dbRC","","",""), ("dbug","hex","Debug flags","0 <= value <= 0xFFFFFFFF"), #("dlvl","","",""), #("dcmd","","",""), #("dsps","","",""), #("logC","","",""), #("cver","","",""), ("ctim","hex","ctim???",""), #("svMd","","",""), #("serM","","",""), #("serT","","",""), #("emNo","","",""), #("effF","","",""), #("LLnk","","",""), #("WLnk","","",""), #("PHYS","","",""), #("PHYN","","",""), #("Rnfo","","",""), #("evtL","","",""), #("isAC","","",""), #("Adet","","",""), ("Prof","cfb","Restore Profile Blob",""), #("maAl","","",""), #("maPr","","",""), #("leAc","","",""), #("APID","","",""), #("AAU ","","",""), ("lcVs","str","lcVs Version String?",""), #("lcVr","","",""), #("lcmV","","",""), #("lcMV","","",""), #("iMTU","","",""), ("wsci","cfb","wsci Blob",""), #("FlSu","","",""), ("OTPR","hex","machdep.otpval",""), ("acRB","dec","Reboot device flag","value == 0"), ("acRI","dec","Reload services??","value == 0"), #("acPC","","",""), #("acDD","","",""), #("acPD","","",""), #("acPG","","",""), #("acDS","","",""), #("acFN","","",""), #("acRP","","",""), ("acRN","dec","Resets something... (?)","value == 0"), ("acRF","dec","Reset to factory defaults","value == 0"), #("MdmH","","",""), #("dirf","","",""), #("Afrc","","",""), #("lebl","","",""), #("lebs","","",""), ("LEDc","dec","LED color/pattern","0 <= value <= 3"), #("acEf","","",""), #("invr","","",""), #("FLSH","","",""), #("acPL","","",""), #("rReg","","",""), #("dReg","","",""), ("GPIs","bin","GPIOs values","len(value) == 8"), #("play","","",""), #("paus","","",""), #("ffwd","","",""), #("rwnd","","",""), #("itun","","",""), #("plls","","",""), #("User","","",""), #("Pass","","",""), #("itIP","","",""), #("itpt","","",""), #("daap","","",""), #("song","","",""), #("arti","","",""), #("albm","","",""), #("volm","","",""), #("rvol","","",""), #("Tcnt","","",""), #("Bcnt","","",""), #("shfl","","",""), #("rept","","",""), #("auPr","","",""), #("auJD","","",""), #("auNN","","",""), #("auNP","","",""), #("aFrq","","",""), #("aChn","","",""), #("aLvl","","",""), #("aPat","","",""), #("aSta","","",""), #("aStp","","",""), #("auCC","","",""), #("acmp","","",""), #("aenc","","",""), #("anBf","","",""), #("aWan","","",""), #("auRR","","",""), #("auMt","","",""), #("aDCP","","",""), #("DCPc","","",""), #("DACP","","",""), #("DCPi","","",""), #("auSl","","",""), #("auFl","","",""), ("fe01","hex","????",""), ("feat","str","Supported features?",""), ("prop","str","Valid acp properties",""), ("hw01","hex","????",""), #("fltr","","",""), #("wdel","","",""), #("plEB","","",""), #("rWSC","","",""), #("uDFS","","",""), #("dWPA","","",""), #("dpFF","","",""), #("duLF","","",""), #("ieHT","","",""), #("dwlX","","",""), #("dd11","","",""), #("dRdr","","",""), #("dotD","","",""), #("dotH","","",""), #("dPwr","","",""), #("wlBR","","",""), #("iTIM","","",""), #("idAG","","",""), #("mvFL","","",""), #("mvFM","","",""), #("dPPP","","",""), #("!mta","","",""), #("minR","","",""), #("SpTr","","",""), #("dRBT","","",""), #("dRIR","","",""), ("pECC","cfb","PCIe ECC Blob?",""), #("fxEB","","",""), #("fxID","","",""), #("fuup","","",""), #("fust","","",""), #("fuca","","",""), ("fugp","str","Firmware upgrade progress",""), ("cks0","hex","Bootloader Flash Checksum",""), ("cks1","hex","Primary Flash Checksum",""), ("cks2","hex","Secondary Flash Checksum",""), #("ddBg","","",""), #("ddEn","","",""), #("ddIn","","",""), #("ddSm","","",""), #("ddEC","","",""), #("ddFE","","",""), #("ddSR","","",""), #("6cfg","","",""), #("6aut","","",""), #("6Qpd","","",""), #("6Wad","","",""), #("6Wfx","","",""), #("6Wgw","","",""), #("6Wte","","",""), #("6Lfw","","",""), #("6Lad","","",""), #("6Lfx","","",""), #("6sfw","","",""), #("6pmp","","",""), #("6trd","","",""), #("6sec","","",""), #("6fwl","","",""), #("6NS1","","",""), #("6NS2","","",""), #("6NS3","","",""), #("6ahr","","",""), #("6dhs","","",""), #("6dso","","",""), #("6PDa","","",""), #("6PDl","","",""), #("6vlt","","",""), #("6plt","","",""), #("6CWa","","",""), #("6CWp","","",""), #("6CWg","","",""), #("6CLa","","",""), #("6NSa","","",""), #("6NSb","","",""), #("6NSc","","",""), #("6CPa","","",""), #("6CPl","","",""), #("6!at","","",""), ("rteI","cfb","rteI Blob",""), #("PCLI","","",""), #("dxEM","","",""), #("dxID","","",""), #("dxAI","","",""), #("dxIP","","",""), #("dxOA","","",""), #("dxIA","","",""), #("dxC1","","",""), #("dxP1","","",""), #("dxC2","","",""), #("dxP2","","",""), #("bjFl","","",""), #("bjSd","","",""), #("bjSM","","",""), #("wbEn","","",""), #("wbHN","","",""), #("wbHU","","",""), #("wbHP","","",""), #("wbRD","","",""), #("wbRU","","",""), #("wbRP","","",""), #("wbAC","","",""), #("dMac","","",""), #("iCld","","",""), #("iCLH","","",""), #("iCLB","","",""), #("SUEn","","",""), #("SUAI","","",""), #("SUFq","","",""), #("SUSv","","",""), #("suPR","","",""), #("msEn","","",""), #("trCo","","",""), #("EZCF","","",""), #("ezcf","","",""), #("gVID","","",""), #("wcfg","","",""), #("awce","","",""), #("wcgu","","",""), #("wcgs","","",""), #("awcc","","",""), ] def _generate_acp_property_dict(): props = {} for (name, type, description, validation) in _acp_properties: # basic validation of tuples assert len(name) == 4, "bad name in _acp_properties list: {0}".format(name) assert type in ["str", "dec", "hex", "log", "mac", "cfb", "bin"], "bad type in _acp_properties list for name: {0}".format(name) assert description, "missing description in _acp_properties list for name: {0}".format(name) props[name] = dict(type=type, description=description, validation=validation) return props class ACPPropertyInitValueError(ACPPropertyError): pass class ACPProperty(object): _acpprop = _generate_acp_property_dict() _element_header_format = struct.Struct("!4s2I") element_header_size = _element_header_format.size def __init__(self, name=None, value=None): # handle "null" property packed name and value first if name == "\x00\x00\x00\x00" and value == "\x00\x00\x00\x00": name = None value = None if name and name not in self.get_supported_property_names(): raise ACPPropertyError("invalid property name passed to initializer: {0}".format(name)) if value is not None: # accept value as packed binary string or Python type prop_type = self.get_property_info_string(name, "type") _init_handler_name = "_init_{0}".format(prop_type) assert hasattr(self, _init_handler_name), "missing init handler for \"{0}\" property type".format(prop_type) _init_handler = getattr(self, _init_handler_name) logging.debug("old value: {0!r} type: {1}".format(value, type(value))) try: value = _init_handler(value) except ACPPropertyInitValueError as e: raise ACPPropertyError("{0!s} provided for \"{1}\" property type: {2!r}".format(e, prop_type, value)) logging.debug("new value: {0!r} type: {1}".format(value, type(value))) #XXX: this is still really hacky, should probably do something with anonymous functions or introspection validation_expr = self.get_property_info_string(name, "validation") if validation_expr and not eval(validation_expr): raise ACPPropertyError("invalid value passed to initializer for property \"{0}\": {1}".format(name, repr(value))) self.name = name self.value = value def _init_dec(self, value): if type(value) == int: return value elif type(value) == str: try: return struct.unpack("!I", value)[0] except: raise ACPPropertyInitValueError("invalid packed binary string") else: raise ACPPropertyInitValueError("invalid built-in type") def _init_hex(self, value): if type(value) == int: return value elif type(value) == str: try: return struct.unpack("!I", value)[0] except: raise ACPPropertyInitValueError("invalid packed binary string") else: raise ACPPropertyInitValueError("invalid built-in type") def _init_mac(self, value): if type(value) == str: # first, try as packed binary value if len(value) == 6: return value # second, attempt to unpack colon delimited value mac_bytes = value.split(":") if len(mac_bytes) == 6: try: return "".join(mac_bytes).decode("hex") except TypeError: raise ACPPropertyInitValueError("non-hex digit in value") # fallthrough raise ACPPropertyInitValueError("invalid value") else: raise ACPPropertyInitValueError("invalid built-in type") def _init_bin(self, value): if type(value) == str: return value else: raise ACPPropertyInitValueError("invalid built-in type") def _init_cfb(self, value): if type(value) == str: return value else: raise ACPPropertyInitValueError("invalid built-in type") def _init_log(self, value): if type(value) == str: return value else: raise ACPPropertyInitValueError("invalid built-in type") def _init_str(self, value): if type(value) == str: return value else: raise ACPPropertyInitValueError("invalid built-in type") def __repr__(self): #XXX: return tuple or dict? return repr((self.name, self.value)) #TODO: make this function return something other than the formatted value of the property...I keep getting confused by its current shittiness def __str__(self): #XXX: is this the correct thing to do? if self.name is None or self.value is None: return "" prop_type = self.get_property_info_string(self.name, "type") _format_handler_name = "_format_{0}".format(prop_type) assert hasattr(self, _format_handler_name), "missing format handler for \"{0}\" property type".format(prop_type) return getattr(self, _format_handler_name)(self.value) def _format_dec(self, value): return str(value) def _format_hex(self, value): return hex(value) def _format_mac(self, value): mac_bytes = [] for i in range(6): mac_bytes.append(value[i].encode("hex")) return "{0}:{1}:{2}:{3}:{4}:{5}".format(*mac_bytes) def _format_bin(self, value): return value.encode("hex") def _format_cfb(self, value): return pprint.pformat(CFLBinaryPListParser.parse(value)) def _format_log(self, value): s = "" for line in value.strip("\x00").split("\x00"): s += "{0}\n".format(line) return s def _format_str(self, value): return value @classmethod def get_supported_property_names(cls): props = [] for name in cls._acpprop: props.append(name) return props @classmethod def get_property_info_string(cls, prop_name, key): #XXX: should we do this differently? if prop_name is None: return None if prop_name not in cls._acpprop: logging.error("property \"{0}\" not supported".format(prop_name)) return None prop_info = cls._acpprop[prop_name] if key not in prop_info: logging.error("invalid property info key \"{0}\"".format(key)) return None return prop_info[key] @classmethod def parse_raw_element(cls, data): name, flags, size = cls.parse_raw_element_header(data[:cls.element_header_size]) #TODO: handle flags!??? return cls(name, data[cls.element_header_size:]) @classmethod def parse_raw_element_header(cls, data): try: return cls._element_header_format.unpack(data) except struct.error: raise ACPPropertyError("failed to parse property element header") @classmethod def compose_raw_element(cls, flags, property): #TODO: handle flags!??? #XXX: handles "null" name or value first, but this is currently garbage name = property.name if property.name is not None else "\x00\x00\x00\x00" value = property.value if property.value is not None else "\x00\x00\x00\x00" if type(value) == int: st = struct.Struct(">I") #XXX: this could throw an exception, we need to range check int/hex values to ensure they pack into 32 bits still return cls.compose_raw_element_header(name, flags, st.size) + st.pack(value) elif type(value) == str: return cls.compose_raw_element_header(name, flags, len(value)) + value else: raise ACPPropertyError("unhandled property type for raw element composition") @classmethod def compose_raw_element_header(cls, name, flags, size): try: return cls._element_header_format.pack(name, flags, size) except struct.error: raise ACPPropertyError("failed to compose property header") ================================================ FILE: acp/session.py ================================================ import logging import socket from .encryption import ACPEncryption ACP_SERVER_PORT = 5009 class _ACPSession(object): def __init__(self, target, password): #XXX: how should we make this abstract enough to cover client and server? self.target = target self.password = password self.sock = None #self.encryption_context = None self.encrypt_method = None self.decrypt_method = None #XXX: AppleSRP hax #self.SRP = None #self.state = 0 def connect(self, port=ACP_SERVER_PORT): self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) logging.info("connecting to host {0}:{1}".format(self.target, self.port)) self.sock.connect((self.target, self.port)) def close(self): if self.sock: self.sock.close() self.sock = None def send(self, data): if self.encrypt_method: data = self.encrypt_method(data) if self.sock: self.sock.sendall(data) #TODO: else? what if sock is not None but not valid in some other way? def _recv_size(self, size): recvd_chunks = [] recvd_size = 0 while True: if recvd_size == size: break #XXX: this is broken for server receiving stream headers data = self.sock.recv(size - recvd_size) if not data: break recvd_chunks.append(data) recvd_size += len(data) return "".join(recvd_chunks) def _recv_size_timeout(self, size, timeout): #XXX: blargh self.sock.setblocking(0) recvd_chunks = [] recvd_size = 0 begin=time.time() while True: if recvd_size == size: break if recvd_chunks and time.time()-begin > timeout: break if time.time()-begin > timeout*2: break try: #XXX: this is broken for server receiving stream headers data = self.sock.recv(size - recvd_size) if data: recvd_chunks.append(data) recvd_size += len(data) begin = time.time() else: time.sleep(0.1) except socket.error: pass #XXX: should non-blocking just be the default? self.sock.setblocking(1) return "".join(recvd_chunks) def recv(self, size, timeout=0): if not self.sock: #XXX: do nothing? throw an exception? return "" data = "" if timeout: data = self._recv_size_timeout(size, timeout) else: data = self._recv_size(size) if self.decrypt_method: data = self.decrypt_method(data) return data class ACPClientSession(_ACPSession): def enable_encryption(self, key, client_iv, server_iv): self.encryption_context = ACPEncryption(key, client_iv, server_iv) self.encrypt_method = encryption_context.client_encrypt self.decrypt_method = encryption_context.server_decrypt class ACPServerSession(_ACPSession): def enable_encryption(self, key, client_iv, server_iv): self.encryption_context = ACPEncryption(key, client_iv, server_iv) self.encrypt_method = self.encryption_context.server_encrypt self.decrypt_method = self.encryption_context.client_decrypt ================================================ FILE: setup.py ================================================ from setuptools import setup setup( name="acp", version="1.0", description="AirPyrt Tools", author="Vince Cali", author_email="0x56.0x69.0x6e.0x63.0x65@gmail.com", packages=["acp"], entry_points = { "console_scripts": ["acp=acp.cli:main"], }, install_requires=[ "pycrypto", ] )