├── .gitattributes ├── .gitignore ├── CREDITS.txt ├── LICENSE.txt ├── README.md ├── pymobiledevice ├── __init__.py ├── afc.py ├── apis.py ├── apps.py ├── ca.py ├── diagnostics_relay.py ├── file_relay.py ├── house_arrest.py ├── installation_proxy.py ├── lockdown.py ├── mobile_config.py ├── mobilebackup.py ├── mobilebackup2.py ├── notification_proxy.py ├── pcapd.py ├── plist_service.py ├── sbservices.py ├── screenshotr.py ├── syslog.py ├── usbmux │ ├── __init__.py │ ├── tcprelay.py │ └── usbmux.py ├── util │ ├── __init__.py │ ├── asciitables.py │ ├── bdev.py │ ├── bpatch.py │ ├── bplist.py │ ├── ccl_bplist.py │ ├── cert.py │ ├── cpio.py │ └── lzss.py └── version.py ├── requirements.txt ├── setup.py └── test ├── __init__.py ├── afc_test.py ├── crashreportcopymobile_test.py ├── diagnostics_relay_test.py ├── house_arrest_test.py ├── installation_proxy_test.py ├── listdevice_test.py ├── screenshotr_test.py └── syslog_relay_test.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.py filter=tabspace 3 | *.py filter=indent 4 | *.py filter=ident 5 | utils/*.py filter=tabspace 6 | utils/*.py filter=indent 7 | utils/*py filter=ident 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | pymobiledevice2.egg-info/ 5 | version.txt 6 | debug/ 7 | *.DS_Store 8 | 9 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Jan0 2 | chronic dev team 3 | idroid/openiboot team 4 | iphone dev team 5 | Jonathan Zdziarski 6 | msftguy 7 | planetbeing 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymobiledevice 2 | 3 | [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) 4 | 5 | pymobiledevice is a cross-platform implementation of the mobiledevice library 6 | that talks the protocols to support iPhone®, iPod Touch®, iPad® and Apple TV® devices. 7 | 8 | 9 | ## Requirements 10 | 11 | * Python 2.7 and 3.x 12 | * M2Crypto 13 | * construct >= 2.9.29 14 | * pyasn1 15 | * future 16 | * six 17 | * biplist 18 | * usbmuxd must be installed and running 19 | 20 | ## Lockdownd.py [com.apple.lockownd] 21 | 22 | This script can be used in order to pair with the device & starts other services. 23 | 24 | *Other services can only be accessed after successful pairing. 25 | Succesful pairing requires the device to be unlocked and the user to click 26 | "Trust this device" on their phone screen.* 27 | 28 | 29 | ## afc.py [com.apple.afc] 30 | 31 | This service is responsible for things such as copying music and photos. AFC Clients like iTunes 32 | are allowed accessing to a “jailed” or limited area of the device filesystem. Actually, AFC clients can 33 | only access certain files, namely those located in the Media folder. 34 | 35 | 36 | ## house_arrest.py [com.apple.mobile.house_arrest] 37 | 38 | This service allows accessing to AppStore applications folders and their content. 39 | In other words, by using an AFC client, a user/attacker can download the application resources and data. 40 | It also includes the “default preferences” file where credentials are sometimes stored. 41 | 42 | 43 | ## installation_proxy.py [com.apple.mobile.installation_proxy] 44 | 45 | The installation proxy manages applications on a device. 46 | It allows execution of the following commands: 47 | - List installed applications 48 | - List archived applications 49 | - ... 50 | 51 | 52 | ## mobilebackup.py & mobilebackup2.py [ com.apple.mobilebackup & com.apple.mobilebackup2 ] 53 | 54 | Those services are used by iTunes to backup the device. 55 | 56 | 57 | ## diagnostics_relay.py [com.apple.mobile.diagnostics_relay] 58 | 59 | The diagnostic relay allows requesting iOS diagnostic information. 60 | The service handles the following actions: 61 | - [ Sleep ]Puts the device into deep sleep mode and disconnects from host. 62 | - [ Restart ] Restart the device and optionally show a user notification. 63 | - [ Shutdown ] Shutdown of the device and optionally show a user notification. 64 | - [ NAND, IORegistry, GasGauge, MobileGestalt ] Querry diagnostic informations. 65 | - ... 66 | 67 | 68 | ## filerelay.py [com.apple.mobile.file_relay] 69 | 70 | Depending of the iOS version, the file relay service may support the following commands: 71 | Accounts, AddressBook, AppleSupport, AppleTV, Baseband, Bluetooth, CrashReporter, CLTM 72 | Caches, CoreLocation, DataAccess, DataMigrator, demod, Device-o-Matic, EmbeddedSocial, FindMyiPhone 73 | GameKitLogs, itunesstored, IORegUSBDevice, HFSMeta, Keyboard, Lockdown, MapsLogs, MobileAsset, 74 | MobileBackup, MobileCal, MobileDelete, MobileInstallation, MobileMusicPlayer, MobileNotes, NANDDebugInfo 75 | Network, Photos, SafeHarbor, SystemConfiguration, tmp, Ubiquity, UserDatabases, VARFS, VPN, Voicemail 76 | WiFi, WirelessAutomation. 77 | 78 | All the files returned by the iPhone are stored in clear text in a gziped CPIO archive. 79 | 80 | 81 | ## pcapd.py [com.apple.pcapd] 82 | 83 | Starting iOS 5, apple added a remote virtual interface (RVI) facility that allows mirroring networks trafic from an iOS device. 84 | On Mac OSX the virtual interface can be enabled with the rvictl command. This script allows to use this service on other systems. 85 | 86 | 87 | # How to contribute 88 | Contributors are essential to pymobiledevice (as they are to most open source projects). 89 | Drop us a line if you want to contribute. 90 | We also accept pull request. 91 | 92 | 93 | # Reporting issues 94 | ### Questions 95 | It is OK so submit issues to ask questions (more than OK, encouraged). There is a label "question" that you can use for that. 96 | 97 | ### Bugs 98 | If you have installed pymobiledevice through a package manager (from your Linux or BSD system, from PyPI, etc.), please get and install the current development code, and check that the bug still exists before submitting an issue. 99 | 100 | Please label your issues "bug". 101 | 102 | If you're not sure whether a behavior is a bug or not, submit an issue and ask, don't be shy! 103 | 104 | Enhancements / feature requests 105 | If you want a feature in pymobiledevice, but cannot implement it yourself or want some hints on how to do that, open an issue with label "enhancement". 106 | 107 | Explain if possible the API you would like to have (e.g., give examples of function calls, etc.). 108 | -------------------------------------------------------------------------------- /pymobiledevice/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iOSForensics/pymobiledevice/c13e035e11f71c1048e25d123c04c83e684be82f/pymobiledevice/__init__.py -------------------------------------------------------------------------------- /pymobiledevice/afc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | from __future__ import print_function 25 | 26 | import os 27 | import struct 28 | import plistlib 29 | import posixpath 30 | import logging 31 | 32 | from construct.core import Struct 33 | from construct.lib.containers import Container 34 | from construct import Const, Int64ul 35 | 36 | from pymobiledevice.lockdown import LockdownClient 37 | 38 | from cmd import Cmd 39 | from past.builtins import xrange 40 | from six import PY3 41 | from pprint import pprint 42 | 43 | from pymobiledevice.util import hexdump, parsePlist 44 | 45 | MODEMASK = 0o0000777 46 | 47 | AFC_OP_STATUS = 0x00000001 48 | AFC_OP_DATA = 0x00000002 #Data */ 49 | AFC_OP_READ_DIR = 0x00000003 #ReadDir */ 50 | AFC_OP_READ_FILE = 0x00000004 #ReadFile */ 51 | AFC_OP_WRITE_FILE = 0x00000005 #WriteFile */ 52 | AFC_OP_WRITE_PART = 0x00000006 #WritePart */ 53 | AFC_OP_TRUNCATE = 0x00000007 #TruncateFile */ 54 | AFC_OP_REMOVE_PATH = 0x00000008 #RemovePath */ 55 | AFC_OP_MAKE_DIR = 0x00000009 #MakeDir */ 56 | AFC_OP_GET_FILE_INFO = 0x0000000a #GetFileInfo */ 57 | AFC_OP_GET_DEVINFO = 0x0000000b #GetDeviceInfo */ 58 | AFC_OP_WRITE_FILE_ATOM = 0x0000000c #WriteFileAtomic (tmp file+rename) */ 59 | AFC_OP_FILE_OPEN = 0x0000000d #FileRefOpen */ 60 | AFC_OP_FILE_OPEN_RES = 0x0000000e #FileRefOpenResult */ 61 | AFC_OP_READ = 0x0000000f #FileRefRead */ 62 | AFC_OP_WRITE = 0x00000010 #FileRefWrite */ 63 | AFC_OP_FILE_SEEK = 0x00000011 #FileRefSeek */ 64 | AFC_OP_FILE_TELL = 0x00000012 #FileRefTell */ 65 | AFC_OP_FILE_TELL_RES = 0x00000013 #FileRefTellResult */ 66 | AFC_OP_FILE_CLOSE = 0x00000014 #FileRefClose */ 67 | AFC_OP_FILE_SET_SIZE = 0x00000015 #FileRefSetFileSize (ftruncate) */ 68 | AFC_OP_GET_CON_INFO = 0x00000016 #GetConnectionInfo */ 69 | AFC_OP_SET_CON_OPTIONS = 0x00000017 #SetConnectionOptions */ 70 | AFC_OP_RENAME_PATH = 0x00000018 #RenamePath */ 71 | AFC_OP_SET_FS_BS = 0x00000019 #SetFSBlockSize (0x800000) */ 72 | AFC_OP_SET_SOCKET_BS = 0x0000001A #SetSocketBlockSize (0x800000) */ 73 | AFC_OP_FILE_LOCK = 0x0000001B #FileRefLock */ 74 | AFC_OP_MAKE_LINK = 0x0000001C #MakeLink */ 75 | AFC_OP_SET_FILE_TIME = 0x0000001E #set st_mtime */ 76 | 77 | AFC_E_SUCCESS = 0 78 | AFC_E_UNKNOWN_ERROR = 1 79 | AFC_E_OP_HEADER_INVALID = 2 80 | AFC_E_NO_RESOURCES = 3 81 | AFC_E_READ_ERROR = 4 82 | AFC_E_WRITE_ERROR = 5 83 | AFC_E_UNKNOWN_PACKET_TYPE = 6 84 | AFC_E_INVALID_ARG = 7 85 | AFC_E_OBJECT_NOT_FOUND = 8 86 | AFC_E_OBJECT_IS_DIR = 9 87 | AFC_E_PERM_DENIED =10 88 | AFC_E_SERVICE_NOT_CONNECTED =11 89 | AFC_E_OP_TIMEOUT =12 90 | AFC_E_TOO_MUCH_DATA =13 91 | AFC_E_END_OF_DATA =14 92 | AFC_E_OP_NOT_SUPPORTED =15 93 | AFC_E_OBJECT_EXISTS =16 94 | AFC_E_OBJECT_BUSY =17 95 | AFC_E_NO_SPACE_LEFT =18 96 | AFC_E_OP_WOULD_BLOCK =19 97 | AFC_E_IO_ERROR =20 98 | AFC_E_OP_INTERRUPTED =21 99 | AFC_E_OP_IN_PROGRESS =22 100 | AFC_E_INTERNAL_ERROR =23 101 | 102 | AFC_E_MUX_ERROR =30 103 | AFC_E_NO_MEM =31 104 | AFC_E_NOT_ENOUGH_DATA =32 105 | AFC_E_DIR_NOT_EMPTY =33 106 | 107 | AFC_FOPEN_RDONLY = 0x00000001 #/**< r O_RDONLY */ 108 | AFC_FOPEN_RW = 0x00000002 #/**< r+ O_RDWR | O_CREAT */ 109 | AFC_FOPEN_WRONLY = 0x00000003 #/**< w O_WRONLY | O_CREAT | O_TRUNC */ 110 | AFC_FOPEN_WR = 0x00000004 #/**< w+ O_RDWR | O_CREAT | O_TRUNC */ 111 | AFC_FOPEN_APPEND = 0x00000005 #/**< a O_WRONLY | O_APPEND | O_CREAT */ 112 | AFC_FOPEN_RDAPPEND = 0x00000006 #/**< a+ O_RDWR | O_APPEND | O_CREAT */ 113 | 114 | AFC_HARDLINK = 1 115 | AFC_SYMLINK = 2 116 | 117 | AFC_LOCK_SH = 1 | 4 #/**< shared lock */ 118 | AFC_LOCK_EX = 2 | 4 #/**< exclusive lock */ 119 | AFC_LOCK_UN = 8 | 4 #/**< unlock */ 120 | 121 | if PY3: 122 | AFCMAGIC = b"CFA6LPAA" 123 | else: 124 | AFCMAGIC = "CFA6LPAA" 125 | 126 | AFCPacket = Struct( 127 | "magic" / Const(AFCMAGIC), 128 | "entire_length" /Int64ul, 129 | "this_length" / Int64ul, 130 | "packet_num" / Int64ul, 131 | "operation" / Int64ul, 132 | ) 133 | 134 | class AFCClient(object): 135 | def __init__(self, lockdown=None, serviceName="com.apple.afc", service=None, udid=None, logger=None): 136 | self.logger = logger or logging.getLogger(__name__) 137 | self.serviceName = serviceName 138 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 139 | self.service = service if service else self.lockdown.startService(self.serviceName) 140 | self.packet_num = 0 141 | 142 | 143 | def stop_session(self): 144 | self.logger.info("Disconecting...") 145 | self.service.close() 146 | 147 | 148 | def dispatch_packet(self, operation, data, this_length=0): 149 | afcpack = Container(magic=AFCMAGIC, 150 | entire_length=40 + len(data), 151 | this_length=40 + len(data), 152 | packet_num=self.packet_num, 153 | operation=operation) 154 | if this_length: 155 | afcpack.this_length = this_length 156 | header = AFCPacket.build(afcpack) 157 | self.packet_num += 1 158 | if PY3 and isinstance(data, str): 159 | data = data.encode('utf-8') 160 | self.service.send(header + data) 161 | 162 | 163 | def receive_data(self): 164 | res = self.service.recv_exact(40) 165 | status = AFC_E_SUCCESS 166 | data = "" 167 | if res: 168 | res = AFCPacket.parse(res) 169 | assert res["entire_length"] >= 40 170 | length = res["entire_length"] - 40 171 | data = self.service.recv_exact(length) 172 | if res.operation == AFC_OP_STATUS: 173 | if length != 8: 174 | self.logger.error("Status length != 8") 175 | status = struct.unpack(" 0: 303 | if sz > MAXIMUM_READ_SIZE: 304 | toRead = MAXIMUM_READ_SIZE 305 | else: 306 | toRead = sz 307 | try: 308 | self.dispatch_packet(AFC_OP_READ, struct.pack(" 12 | Date created: 2016-06-19 13 | """ 14 | 15 | from os import path 16 | # from pprint import pprint 17 | 18 | 19 | def get_lockdown_and_service(udid): 20 | from pymobiledevice.lockdown import LockdownClient 21 | lockdown = LockdownClient(udid) 22 | service = lockdown.startService("com.apple.mobile.installation_proxy") 23 | return lockdown, service 24 | 25 | 26 | def run_command(service, uuid, cmd): 27 | service.sendPlist(cmd) 28 | z = service.recvPlist() 29 | while 'PercentComplete' in z: 30 | if not z: 31 | break 32 | if z.get("Status") == "Complete": 33 | return z.get("Status") 34 | z = service.recvPlist() 35 | service.close() 36 | return z 37 | 38 | 39 | def install_ipa(uuid, ipa_path): 40 | """ 41 | docstring for install_ipa 42 | """ 43 | from pymobiledevice.afc import AFCClient 44 | lockdown, service = get_lockdown_and_service(uuid) 45 | afc = AFCClient(lockdown=lockdown) 46 | afc.set_file_contents( 47 | path.basename(ipa_path), open(ipa_path, "rb").read()) 48 | cmd = {"Command": "Install", "PackagePath": path.basename(ipa_path)} 49 | return run_command(service, uuid, cmd) 50 | 51 | 52 | def uninstall_ipa(uuid, bundle_id): 53 | lockdown, service = get_lockdown_and_service(uuid) 54 | cmd = {"Command": "Uninstall", "ApplicationIdentifier": bundle_id} 55 | return run_command(service, uuid, cmd) 56 | 57 | 58 | def list_ipas(uuid): 59 | lockdown, service = get_lockdown_and_service(uuid) 60 | cmd = {"Command": "Lookup"} 61 | result = run_command(service, uuid, cmd) 62 | apps_details = result.get("LookupResult") 63 | apps = [] 64 | for app in apps_details: 65 | if apps_details[app]['ApplicationType'] == 'User': 66 | apps.append(app) 67 | return apps 68 | -------------------------------------------------------------------------------- /pymobiledevice/apps.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | from __future__ import print_function 26 | import os 27 | import warnings 28 | from pprint import * 29 | from pymobiledevice.lockdown import LockdownClient 30 | from pymobiledevice.afc import AFCClient, AFCShell 31 | from optparse import OptionParser 32 | warnings.warn( 33 | """The libraries upon which this program depends will soon be deprecated in 34 | favor of the house_arrest.py and installation_proxy.py libraries. 35 | See those files for example program written using the new libraries.""" 36 | ) 37 | 38 | 39 | def house_arrest(lockdown, applicationId): 40 | try: 41 | mis = lockdown.startService("com.apple.mobile.house_arrest") 42 | except: 43 | lockdown = LockdownClient() 44 | mis = lockdown.startService("com.apple.mobile.house_arrest") 45 | 46 | if mis == None: 47 | return 48 | mis.sendPlist({"Command": "VendDocuments", "Identifier": applicationId}) 49 | res = mis.recvPlist() 50 | if res.get("Error"): 51 | pprint(res) 52 | print("Unable to Lookup the selected application: You probably trying to access to a system app...") 53 | return None 54 | return AFCClient(lockdown, service=mis) 55 | 56 | def house_arrest_shell(lockdown, applicationId): 57 | afc = house_arrest(lockdown, applicationId) 58 | if afc: AFCShell(client=afc).cmdloop() 59 | 60 | """ 61 | "Install" 62 | "Upgrade" 63 | "Uninstall" 64 | "Lookup" 65 | "Browse" 66 | "Archive" 67 | "Restore" 68 | "RemoveArchive" 69 | "LookupArchives" 70 | "CheckCapabilitiesMatch" 71 | 72 | installd 73 | if stat("/var/mobile/tdmtanf") => "TDMTANF Bypass" => SignerIdentity bypass 74 | """ 75 | 76 | def mobile_install(lockdown,ipaPath): 77 | #Start afc service & upload ipa 78 | afc = AFCClient(lockdown) 79 | afc.set_file_contents("/" + os.path.basename(ipaPath), open(ipaPath,'rb').read()) 80 | mci = lockdown.startService("com.apple.mobile.installation_proxy") 81 | #print mci.sendPlist({"Command":"Archive","ApplicationIdentifier": "com.joystickgenerals.STActionPig"}) 82 | mci.sendPlist({"Command":"Install", 83 | #"ApplicationIdentifier": "com.gotohack.JBChecker", 84 | "PackagePath": os.path.basename(ipaPath)}) 85 | while True: 86 | z = mci.recvPlist() 87 | if not z: 88 | break 89 | completion = z.get('PercentComplete') 90 | if completion: 91 | print('Installing, %s: %s %% Complete' % (ipaPath, z['PercentComplete'])) 92 | if z.get('Status') == 'Complete': 93 | print("Installation %s\n" % z['Status']) 94 | break 95 | 96 | def list_apps(lockdown): 97 | mci = lockdown.startService("com.apple.mobile.installation_proxy") 98 | mci.sendPlist({"Command":"Lookup"}) 99 | res = mci.recvPlist() 100 | for app in res["LookupResult"].values(): 101 | if app.get("ApplicationType") != "System": 102 | print(app["CFBundleIdentifier"], "=>", app.get("Container")) 103 | else: 104 | print(app["CFBundleIdentifier"], "=> N/A") 105 | 106 | 107 | def get_apps_BundleID(lockdown,appType="User"): 108 | appList = [] 109 | mci = lockdown.startService("com.apple.mobile.installation_proxy") 110 | mci.sendPlist({"Command":"Lookup"}) 111 | res = mci.recvPlist() 112 | for app in res["LookupResult"].values(): 113 | if app.get("ApplicationType") == appType: 114 | appList.append(app["CFBundleIdentifier"]) 115 | #else: #FIXME 116 | # appList.append(app["CFBundleIdentifier"]) 117 | mci.close() 118 | #pprint(appList) 119 | return appList 120 | 121 | def main(): 122 | parser = OptionParser(usage="%prog") 123 | parser.add_option("-l", "--list", dest="list", action="store_true", default=False, 124 | help="List installed applications (non system apps)") 125 | parser.add_option("-a", "--app", dest="app", action="store", default=None, 126 | metavar="APPID", help="Access application files with AFC") 127 | parser.add_option("-i", "--install", dest="installapp", action="store", default=None, 128 | metavar="FILE", help="Install an application package") 129 | 130 | (options, args) = parser.parse_args() 131 | if options.list: 132 | lockdown = LockdownClient() 133 | list_apps(lockdown) 134 | elif options.app: 135 | lockdown = LockdownClient() 136 | house_arrest_shell(lockdown, options.app) 137 | elif options.installapp: 138 | lockdown = LockdownClient() 139 | mobile_install(lockdown, options.installapp) 140 | else: 141 | parser.print_help() 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /pymobiledevice/ca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | How to create a CA certificate with Python. 5 | 6 | WARNING: This sample only demonstrates how to use the objects and methods, 7 | not how to create a safe and correct certificate. 8 | 9 | Copyright (c) 2004 Open Source Applications Foundation. 10 | Authors: Heikki Toivonen 11 | Mathieu RENARD 12 | """ 13 | 14 | from M2Crypto import RSA, X509, EVP, m2, BIO 15 | from M2Crypto.RSA import load_pub_key_bio 16 | from pyasn1.type import univ 17 | from pyasn1.codec.der import encoder as der_encoder 18 | from pyasn1.codec.der import decoder as der_decoder 19 | import struct 20 | import base64 21 | from pprint import * 22 | 23 | 24 | def convertPKCS1toPKCS8pubKey(bitsdata): 25 | pubkey_pkcs1_b64 = b''.join(bitsdata.split(b'\n')[1:-2]) 26 | pubkey_pkcs1, restOfInput = der_decoder.decode(base64.b64decode(pubkey_pkcs1_b64)) 27 | bitstring = univ.Sequence() 28 | bitstring.setComponentByPosition(0, univ.Integer(pubkey_pkcs1[0])) 29 | bitstring.setComponentByPosition(1, univ.Integer(pubkey_pkcs1[1])) 30 | bitstring = der_encoder.encode(bitstring) 31 | try: 32 | bitstring = ''.join([('00000000'+bin(ord(x))[2:])[-8:] for x in list(bitstring)]) 33 | except: 34 | bitstring = ''.join([('00000000'+bin(x)[2:])[-8:] for x in list(bitstring)]) 35 | bitstring = univ.BitString("'%s'B" % bitstring) 36 | pubkeyid = univ.Sequence() 37 | pubkeyid.setComponentByPosition(0, univ.ObjectIdentifier('1.2.840.113549.1.1.1')) # == OID for rsaEncryption 38 | pubkeyid.setComponentByPosition(1, univ.Null('')) 39 | pubkey_seq = univ.Sequence() 40 | pubkey_seq.setComponentByPosition(0, pubkeyid) 41 | pubkey_seq.setComponentByPosition(1, bitstring) 42 | base64.MAXBINSIZE = (64//4)*3 43 | res = b"-----BEGIN PUBLIC KEY-----\n" 44 | res += base64.encodestring(der_encoder.encode(pubkey_seq)) 45 | res += b"-----END PUBLIC KEY-----\n" 46 | return res 47 | 48 | def generateRSAKey(): 49 | return RSA.gen_key(2048, m2.RSA_F4) 50 | 51 | def makePKey(key): 52 | pkey = EVP.PKey() 53 | pkey.assign_rsa(key) 54 | return pkey 55 | 56 | def makeRequest(pkey, cn): 57 | req = X509.Request() 58 | req.set_version(2) 59 | req.set_pubkey(pkey) 60 | name = X509.X509_Name() 61 | name.CN = cn 62 | req.set_subject_name(name) 63 | ext1 = X509.new_extension('subjectAltName', 'DNS:foobar.example.com') 64 | ext2 = X509.new_extension('nsComment', 'Hello there') 65 | extstack = X509.X509_Extension_Stack() 66 | extstack.push(ext1) 67 | extstack.push(ext2) 68 | 69 | assert(extstack[1].get_name() == 'nsComment') 70 | 71 | req.add_extensions(extstack) 72 | # req.sign(pkey, 'sha1') 73 | return req 74 | 75 | def makeCert(req, caPkey): 76 | pkey = req.get_pubkey() 77 | # woop = makePKey(generateRSAKey()) 78 | # if not req.verify(woop.pkey): 79 | if not req.verify(pkey): 80 | # XXX What error object should I use? 81 | raise ValueError('Error verifying request') 82 | sub = req.get_subject() 83 | # If this were a real certificate request, you would display 84 | # all the relevant data from the request and ask a human operator 85 | # if you were sure. Now we just create the certificate blindly based 86 | # on the request. 87 | cert = X509.X509() 88 | # We know we are making CA cert now... 89 | # Serial defaults to 0. 90 | cert.set_serial_number(1) 91 | cert.set_version(2) 92 | cert.set_subject(sub) 93 | issuer = X509.X509_Name() 94 | issuer.CN = 'The Issuer Monkey' 95 | issuer.O = 'The Organization Otherwise Known as My CA, Inc.' 96 | cert.set_issuer(issuer) 97 | cert.set_pubkey(pkey) 98 | notBefore = m2.x509_get_not_before(cert.x509) 99 | notAfter = m2.x509_get_not_after(cert.x509) 100 | m2.x509_gmtime_adj(notBefore, 0) 101 | days = 30 102 | m2.x509_gmtime_adj(notAfter, 60 * 60 * 24 * days) 103 | cert.add_ext( 104 | X509.new_extension('subjectAltName', 'DNS:foobar.example.com')) 105 | ext = X509.new_extension('nsComment', 'M2Crypto generated certificate') 106 | ext.set_critical(0) # Defaults to non-critical, but we can also set it 107 | cert.add_ext(ext) 108 | cert.sign(caPkey, 'sha1') 109 | 110 | assert(cert.get_ext('subjectAltName').get_name() == 'subjectAltName') 111 | assert(cert.get_ext_at(0).get_name() == 'subjectAltName') 112 | assert(cert.get_ext_at(0).get_value() == 'DNS:foobar.example.com') 113 | 114 | return cert 115 | 116 | def ca(): 117 | key = generateRSAKey() 118 | pkey = makePKey(key) 119 | req = makeRequest(pkey) 120 | cert = makeCert(req, pkey) 121 | return (cert, pkey) 122 | 123 | def ca_do_everything(DevicePublicKey): 124 | rsa = generateRSAKey() 125 | privateKey = makePKey(rsa) 126 | req = makeRequest(privateKey, "The Issuer Monkey") 127 | cert = makeCert(req, privateKey) 128 | rsa2 = load_pub_key_bio(BIO.MemoryBuffer( 129 | convertPKCS1toPKCS8pubKey(DevicePublicKey))) 130 | pkey2 = EVP.PKey() 131 | pkey2.assign_rsa(rsa2) 132 | req = makeRequest(pkey2, "Device") 133 | cert2 = makeCert(req, privateKey) 134 | return cert.as_pem(), privateKey.as_pem(None), cert2.as_pem() 135 | 136 | if __name__ == '__main__': 137 | rsa = generateRSAKey() 138 | pkey = makePKey(rsa) 139 | print(pkey.as_pem(None)) 140 | req = makeRequest(pkey, "The Issuer Monkey") 141 | cert = makeCert(req, pkey) 142 | print(cert.as_text()) 143 | cert.save_pem('my_ca_cert.pem') 144 | rsa.save_key('my_key.pem', None) 145 | -------------------------------------------------------------------------------- /pymobiledevice/diagnostics_relay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | 26 | from pymobiledevice.lockdown import LockdownClient 27 | from pprint import pprint 28 | import plistlib 29 | from optparse import OptionParser 30 | 31 | Requests = """Goodbye 32 | All 33 | GasGauge 34 | WiFi 35 | Shutdown 36 | Restart 37 | MobileGestalt 38 | Sleep 39 | NAND 40 | IORegistry 41 | Obliterate 42 | """ 43 | 44 | """ 45 | BasebandKeyHashInformation 46 | BasebandFirmwareManifestData 47 | """ 48 | 49 | MobileGestaltKeys = """DieId 50 | SerialNumber 51 | UniqueChipID 52 | WifiAddress 53 | CPUArchitecture 54 | BluetoothAddress 55 | EthernetMacAddress 56 | FirmwareVersion 57 | MLBSerialNumber 58 | ModelNumber 59 | RegionInfo 60 | RegionCode 61 | DeviceClass 62 | ProductType 63 | DeviceName 64 | UserAssignedDeviceName 65 | HWModelStr 66 | SigningFuse 67 | SoftwareBehavior 68 | SupportedKeyboards 69 | BuildVersion 70 | ProductVersion 71 | ReleaseType 72 | InternalBuild 73 | CarrierInstallCapability 74 | IsUIBuild 75 | InternationalMobileEquipmentIdentity 76 | MobileEquipmentIdentifier 77 | DeviceColor 78 | HasBaseband 79 | SupportedDeviceFamilies 80 | SoftwareBundleVersion 81 | SDIOManufacturerTuple 82 | SDIOProductInfo 83 | UniqueDeviceID 84 | InverseDeviceID 85 | ChipID 86 | PartitionType 87 | ProximitySensorCalibration 88 | CompassCalibration 89 | WirelessBoardSnum 90 | BasebandBoardSnum 91 | HardwarePlatform 92 | RequiredBatteryLevelForSoftwareUpdate 93 | IsThereEnoughBatteryLevelForSoftwareUpdate 94 | BasebandRegionSKU 95 | encrypted-data-partition 96 | SysCfg 97 | DiagData 98 | SIMTrayStatus 99 | CarrierBundleInfoArray 100 | AllDeviceCapabilities 101 | wi-fi 102 | SBAllowSensitiveUI 103 | green-tea 104 | not-green-tea 105 | AllowYouTube 106 | AllowYouTubePlugin 107 | SBCanForceDebuggingInfo 108 | AppleInternalInstallCapability 109 | HasAllFeaturesCapability 110 | ScreenDimensions 111 | IsSimulator 112 | BasebandSerialNumber 113 | BasebandChipId 114 | BasebandCertId 115 | BasebandSkeyId 116 | BasebandFirmwareVersion 117 | cellular-data 118 | contains-cellular-radio 119 | RegionalBehaviorGoogleMail 120 | RegionalBehaviorVolumeLimit 121 | RegionalBehaviorShutterClick 122 | RegionalBehaviorNTSC 123 | RegionalBehaviorNoWiFi 124 | RegionalBehaviorChinaBrick 125 | RegionalBehaviorNoVOIP 126 | RegionalBehaviorAll 127 | ApNonce""" 128 | 129 | class DIAGClient(object): 130 | def __init__(self, lockdown=None, serviceName="com.apple.mobile.diagnostics_relay"): 131 | if lockdown: 132 | self.lockdown = lockdown 133 | else: 134 | self.lockdown = LockdownClient() 135 | 136 | self.service = self.lockdown.startService(serviceName) 137 | self.packet_num = 0 138 | 139 | 140 | def stop_session(self): 141 | print("Disconecting...") 142 | self.service.close() 143 | 144 | 145 | def query_mobilegestalt(self, MobileGestalt=MobileGestaltKeys.split("\n")): 146 | self.service.sendPlist({"Request": "MobileGestalt", 147 | "MobileGestaltKeys": MobileGestalt}) 148 | 149 | res = self.service.recvPlist() 150 | d = res.get("Diagnostics") 151 | if d: 152 | return d.get("MobileGestalt") 153 | return None 154 | 155 | 156 | def action(self, action="Shutdown", flags=None): 157 | self.service.sendPlist({"Request": action }) 158 | res = self.service.recvPlist() 159 | return res.get("Diagnostics") 160 | 161 | 162 | def restart(self): 163 | return self.action("Restart") 164 | 165 | 166 | def shutdown(self): 167 | return self.action("Shutdown") 168 | 169 | 170 | def diagnostics(self, diagType="All"): 171 | self.service.sendPlist({"Request": diagType}) 172 | res = self.service.recvPlist() 173 | if res: 174 | return res.get("Diagnostics") 175 | return None 176 | 177 | 178 | def ioregistry_entry(self, name=None, ioclass=None): 179 | d = {} 180 | if name: 181 | d["EntryName"] = name 182 | 183 | if ioclass: 184 | d["EntryClass"] = ioclass 185 | 186 | d["Request"] = "IORegistry" 187 | 188 | self.service.sendPlist(d) 189 | res = self.service.recvPlist() 190 | pprint(res) 191 | if res: 192 | return res.get("Diagnostics") 193 | return None 194 | 195 | 196 | def ioregistry_plane(self, plane=""): 197 | d = {} 198 | if plane: 199 | d["CurrentPlane"] = plane 200 | 201 | else: 202 | d["CurrentPlane"] = "" 203 | d["Request"] = "IORegistry" 204 | 205 | self.service.sendPlist(d) 206 | res = self.service.recvPlist() 207 | dd = res.get("Diagnostics") 208 | if dd: 209 | return dd.get("IORegistry") 210 | return None 211 | 212 | 213 | def main(): 214 | parser = OptionParser(usage="%prog") 215 | parser.add_option("-c", "--cmd", dest="cmd", default=False, 216 | help="Launch diagnostic command", type="string") 217 | parser.add_option("-m", "--mobilegestalt", dest="mobilegestalt_key", default=False, 218 | help="Request mobilegestalt key", type="string") 219 | parser.add_option("-i", "--ioclass", dest="ioclass", default=False, 220 | help="Request ioclass", type="string") 221 | parser.add_option("-n", "--ioname", dest="ioname", default=False, 222 | help="Request ionqme", type="string") 223 | 224 | (options, args) = parser.parse_args() 225 | 226 | diag = DIAGClient() 227 | if not options.cmd: 228 | res = diag.diagnostics() 229 | 230 | elif options.cmd == "IORegistry": 231 | res = diag.ioregistry_plane() 232 | 233 | elif options.cmd == "MobileGestalt": 234 | 235 | if not options.mobilegestalt_key or options.mobilegestalt_key not in MobileGestaltKeys.split("\n"): 236 | res = diag.query_mobilegestalt() 237 | 238 | else: 239 | res = diag.query_mobilegestalt([options.mobilegestalt_key]) 240 | 241 | else: 242 | res = diag.action(options.cmd) 243 | 244 | if res: 245 | for k in res.keys(): 246 | print(" %s \t: %s" % (k,res[k])) 247 | 248 | 249 | if __name__ == "__main__": 250 | main() 251 | -------------------------------------------------------------------------------- /pymobiledevice/file_relay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | import os 25 | import zlib 26 | import gzip 27 | import logging 28 | 29 | from pymobiledevice.lockdown import LockdownClient 30 | from pymobiledevice.util.cpio import CpioArchive 31 | from pymobiledevice.util import MultipleOption 32 | 33 | from pprint import pprint 34 | from tempfile import mkstemp 35 | from optparse import OptionParser 36 | from io import BytesIO 37 | 38 | SRCFILES = """Baseband 39 | CrashReporter 40 | MobileAsset 41 | VARFS 42 | HFSMeta 43 | Lockdown 44 | MobileBackup 45 | MobileDelete 46 | MobileInstallation 47 | MobileNotes 48 | Network 49 | UserDatabases 50 | WiFi 51 | WirelessAutomation 52 | NANDDebugInfo 53 | SystemConfiguration 54 | Ubiquity 55 | tmp 56 | WirelessAutomation""" 57 | 58 | class DeviceVersionNotSupported(Exception): 59 | pass 60 | 61 | class FileRelay(object): 62 | def __init__(self, lockdown=None, serviceName="com.apple.mobile.file_relay", 63 | udid=None, logger=None): 64 | self.logger = logger or logging.getLogger(__name__) 65 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 66 | ProductVersion = self.lockdown.getValue("", "ProductVersion") 67 | 68 | if ProductVersion[0] >= "8": 69 | raise DeviceVersionNotSupported 70 | 71 | self.service = self.lockdown.startService(serviceName) 72 | self.packet_num = 0 73 | 74 | def stop_session(self): 75 | self.logger.info("Disconecting...") 76 | self.service.close() 77 | 78 | def request_sources(self, sources=["UserDatabases"]): 79 | self.service.sendPlist({"Sources": sources}) 80 | while 1: 81 | res = self.service.recvPlist() 82 | if res: 83 | s = res.get("Status") 84 | if s == "Acknowledged": 85 | z = "" 86 | while True: 87 | x = self.service.recv() 88 | if not x: 89 | break 90 | z += x 91 | return z 92 | else: 93 | print(res.get("Error")) 94 | break 95 | return None 96 | 97 | def main(): 98 | parser = OptionParser(option_class=MultipleOption,usage="%prog") 99 | parser.add_option("-s", "--sources", 100 | action="extend", 101 | dest="sources", 102 | metavar='SOURCES', 103 | choices=SRCFILES.split("\n"), 104 | help="comma separated list of file relay source to dump") 105 | parser.add_option("-e", "--extract",dest="extractpath" , default=False, 106 | help="Extract archive to specified location", type="string") 107 | parser.add_option("-o", "--output", dest="outputfile", default=False, 108 | help="Output location", type="string") 109 | 110 | (options, args) = parser.parse_args() 111 | 112 | sources = [] 113 | if options.sources: 114 | sources = options.sources 115 | else: 116 | sources = ["UserDatabases"] 117 | print("Downloading: %s" % "".join([str(item)+" " for item in sources])) 118 | fc = None 119 | try: 120 | fc = FileRelay() 121 | except: 122 | print("Device with product vertion >= 8.0 does not allow access to fileRelay service") 123 | exit() 124 | 125 | data = fc.request_sources(sources) 126 | 127 | if data: 128 | if options.outputfile: 129 | path = options.outputfile 130 | else: 131 | _,path = mkstemp(prefix="fileRelay_dump_",suffix=".gz",dir=".") 132 | 133 | open(path,'wb').write(data) 134 | self.logger.info("Data saved to: %s ", path) 135 | 136 | if options.extractpath: 137 | with open(path, 'r') as f: 138 | gz = gzip.GzipFile(mode='rb', fileobj=f) 139 | cpio = CpioArchive(fileobj=BytesIO(gz.read())) 140 | cpio.extract_files(files=None,outpath=options.extractpath) 141 | 142 | if __name__ == "__main__": 143 | main() 144 | -------------------------------------------------------------------------------- /pymobiledevice/house_arrest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import os 26 | import logging 27 | 28 | from pymobiledevice.lockdown import LockdownClient 29 | from pymobiledevice.afc import AFCClient, AFCShell 30 | 31 | from pprint import pprint 32 | from optparse import OptionParser 33 | 34 | 35 | class HouseArrestClient(AFCClient): 36 | 37 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 38 | self.serviceName = serviceName 39 | self.service = service if service else self.lockdown.startService(self.serviceName) 40 | 41 | def __init__(self, udid=None,logger=None): 42 | self.logger = logger or logging.getLogger(__name__) 43 | lockdownClient = LockdownClient(udid) 44 | serviceName = "com.apple.mobile.house_arrest" 45 | super(HouseArrestClient, self).__init__(lockdownClient, serviceName) 46 | 47 | def stop_session(self): 48 | self.logger.info("Disconecting...") 49 | self.service.close() 50 | 51 | def send_command(self, applicationId, cmd="VendContainer"): 52 | self.service.sendPlist({"Command": cmd, "Identifier": applicationId}) 53 | res = self.service.recvPlist() 54 | if res.get("Error"): 55 | self.logger.error("%s : %s", applicationId, res.get("Error")) 56 | return False 57 | else: 58 | return True 59 | 60 | def shell(self, applicationId, cmd="VendContainer"): 61 | res = self.send_command(applicationId, cmd) 62 | if res: 63 | AFCShell(client=self).cmdloop() 64 | 65 | def main(): 66 | logging.basicConfig(level=logging.WARN) 67 | parser = OptionParser(usage="%prog -a applicationId") 68 | parser.add_option("-a", "--application", dest="applicationId", default=False, 69 | help="Application ID ", type="string") 70 | parser.add_option("-c", "--command", dest="cmd", default=False, 71 | help="House_Arrest commands: ", type="string") 72 | 73 | (options, args) = parser.parse_args() 74 | h = HouseArrestClient() 75 | if options.cmd: 76 | h.shell(options.applicationId, cmd=options.cmd) 77 | else: 78 | h.shell(options.applicationId) 79 | 80 | 81 | 82 | if __name__ == "__main__": 83 | main() 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /pymobiledevice/installation_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import os 26 | import logging 27 | 28 | from optparse import OptionParser 29 | from pymobiledevice.afc import AFCClient 30 | from pymobiledevice.lockdown import LockdownClient, list_devices 31 | 32 | client_options = { 33 | "SkipUninstall" : False, 34 | "ApplicationSINF" : False, 35 | "iTunesMetadata" : False, 36 | "ReturnAttributes" : False 37 | } 38 | 39 | 40 | 41 | class installation_proxy(object): 42 | 43 | def __init__(self, lockdown=None, udid=None, logger=None): 44 | self.logger = logger or logging.getLogger(__name__) 45 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 46 | if not self.lockdown: 47 | raise Exception("Unable to start lockdown") 48 | self.start() 49 | 50 | def start(self): 51 | self.service = self.lockdown.startService("com.apple.mobile.installation_proxy") 52 | if not self.service: 53 | raise Exception("installation_proxy init error : Could not start com.apple.mobile.installation_proxy") 54 | 55 | 56 | def watch_completion(self,handler=None,*args): 57 | while True: 58 | z = self.service.recvPlist() 59 | if not z: 60 | break 61 | completion = z.get("PercentComplete") 62 | if completion: 63 | if handler: 64 | self.logger.debug("calling handler") 65 | handler(completion,*args) 66 | self.logger.info("%s %% Complete",z.get("PercentComplete")) 67 | if z.get("Status") == "Complete": 68 | return z.get("Status") 69 | return "Error" 70 | 71 | def send_cmd_for_bid(self, bid, cmd="Archive", options=None, handler=None, *args): 72 | cmd = { "Command": cmd, 73 | 74 | "ApplicationIdentifier": bid } 75 | if options: 76 | cmd.update({"ClientOptions": options}) 77 | self.logger.info("%s : %s\n", cmd, self.watch_completion(handler, *args)) 78 | 79 | 80 | def uninstall(self, bid, options=None, handler=None, *args): 81 | self.send_cmd_for_bid(bid, "Uninstall", options, handler, args) 82 | 83 | def install_or_upgrade(self, ipaPath, cmd="Install", options={}, handler=None, *args): 84 | afc = AFCClient(self.lockdown) 85 | afc.set_file_contents("/" + os.path.basename(ipaPath), open(ipaPath,"rb").read()) 86 | cmd = { "Command": cmd, 87 | "ClientOptions": options, 88 | "PackagePath": os.path.basename(ipaPath)} 89 | 90 | self.service.sendPlist(cmd) 91 | self.watch_completion(handler, args) 92 | 93 | def install(self, ipaPath, options={}, handler=None, *args): 94 | return self.install_or_upgrade(ipaPath, "Install", options, handler, args) 95 | 96 | def upgrade(self, ipaPath, options={}, handler=None, *args): 97 | return self.install_or_upgrade(ipaPath, "Upgrade", options, handler, args) 98 | 99 | def check_capabilities_match(self, capabilities, options={}): 100 | cmd = { "Command": "CheckCapabilitiesMatch", 101 | "ClientOptions": options } 102 | 103 | if capabilities: 104 | cmd["Capabilities"] = capabilities 105 | 106 | self.service.sendPlist(cmd) 107 | result = self.service.recvPlist().get("LookupResult") 108 | return result 109 | 110 | 111 | def browse(self, options={}, attributes=None, handler=None, *args): 112 | if attributes: 113 | options["ReturnAttributes"] = attributes 114 | 115 | cmd = { "Command": "Browse", 116 | "ClientOptions": options } 117 | 118 | self.service.sendPlist(cmd) 119 | 120 | result = [] 121 | while True: 122 | z = self.service.recvPlist() 123 | if not z: 124 | break 125 | 126 | data = z.get("CurrentList") 127 | if data: 128 | result += data 129 | 130 | if z.get("Status") == "Complete": 131 | break 132 | 133 | return result 134 | 135 | def apps_info(self, options={}): 136 | cmd = {"Command": "Lookup", 137 | "ClientOptions": options } 138 | 139 | self.service.sendPlist(cmd) 140 | return self.service.recvPlist().get('LookupResult') 141 | 142 | def archive(self, bid, options={}, handler=None, *args): 143 | self.send_cmd_for_bid(bid, "Archive", options, handler, args) 144 | 145 | def restore_archive(self, bid, options={}, handler=None, *args): 146 | self.send_cmd_for_bid(bid, "Restore", options, handler, args) 147 | 148 | def remove_archive(self, bid, options={}, handler=None, *args): 149 | self.send_cmd_for_bid(bid, "RemoveArchive", options, handler, args) 150 | 151 | def archives_info(self, options={}): 152 | cmd = {"Command": "LookupArchive", 153 | "ClientOptions": options} 154 | return self.service.sendRequest(cmd).get("LookupResult") 155 | 156 | def search_path_for_bid(self, bid): 157 | path = None 158 | for a in self.get_apps(appTypes=["User","System"]): 159 | if a.get("CFBundleIdentifier") == bid: 160 | path = a.get("Path")+"/"+a.get("CFBundleExecutable") 161 | return path 162 | 163 | def get_apps(self,appTypes=["User"]): 164 | return [app for app in self.apps_info().values() 165 | if app.get("ApplicationType") in appTypes] 166 | 167 | def print_apps(self, appType=["User"]): 168 | for app in self.get_apps(appType): 169 | print(("%s : %s => %s" % (app.get("CFBundleDisplayName"), 170 | app.get("CFBundleIdentifier"), 171 | app.get("Path") if app.get("Path") 172 | else app.get("Container"))).encode('utf-8')) 173 | 174 | def get_apps_bid(self,appTypes=["User"]): 175 | return [app["CFBundleIdentifier"] 176 | for app in self.get_apps() 177 | if app.get("ApplicationType") in appTypes] 178 | 179 | def close(self): 180 | self.service.close() 181 | 182 | 183 | if __name__ == "__main__": 184 | parser = OptionParser(usage="%prog -u cmd ") 185 | parser.add_option("-u", "--udid", 186 | default=False, 187 | action="store", 188 | dest="device_udid", 189 | metavar="DEVICE_UDID", 190 | help="Device udid") 191 | parser.add_option("-l", "--listapps", 192 | default=False, 193 | action="store_true", 194 | help="List installed applications") 195 | parser.add_option("-i", "--install", 196 | default=False, 197 | action="store", 198 | dest="install_ipapath", 199 | metavar="FILE", 200 | help="Install application on device") 201 | parser.add_option("-r", "--remove", 202 | default=False, 203 | action="store", 204 | dest="remove_bundleid", 205 | metavar="BUNDLE_ID", 206 | help="Remove (uninstall) application from device") 207 | parser.add_option("-g", "--upgrade", 208 | default=False, 209 | action="store", 210 | dest="upgrade_bundleid", 211 | metavar="BUNDLE_ID", 212 | help="Upgrade application on device") 213 | parser.add_option("-a", "--archive", 214 | default=False, 215 | action="store", 216 | dest="archive_bundleid", 217 | metavar="BUNDLE_ID", 218 | help="Archive application on device") 219 | 220 | (options, args) = parser.parse_args() 221 | logging.basicConfig(level=logging.INFO) 222 | logging.getLogger(__name__) 223 | #FIXME We should handle all installation proxy commands 224 | try: 225 | instpxy = installation_proxy(udid=options.device_udid) 226 | except: 227 | logger.error("Unable to connect to device") 228 | exit(-1) 229 | 230 | if options.listapps: 231 | instpxy.print_apps(["User","System"]) 232 | elif options.install_ipapath: 233 | instpxy = installation_proxy(udid=options.device_udid) 234 | instpxy.install(options.install_ipapath) 235 | elif options.remove_bundleid: 236 | instpxy = installation_proxy(udid=options.device_udid) 237 | instpxy.remove(options.remove_bundleid) 238 | elif options.upgrade_bundleid: 239 | instpxy = installation_proxy(udid=options.device_udid) 240 | instpxy.upgrade(options.upgrade_bundleid) 241 | elif options.archive_bundleid: 242 | instpxy = installation_proxy(udid=options.device_udid) 243 | instpxy.archive(options.archive_bundleid) 244 | else: 245 | parser.error("Incorrect number of arguments") 246 | -------------------------------------------------------------------------------- /pymobiledevice/lockdown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2023 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import os 26 | import plistlib 27 | import sys 28 | import uuid 29 | import platform 30 | import time 31 | import logging 32 | import re 33 | 34 | from pymobiledevice.plist_service import PlistService 35 | from pymobiledevice.ca import ca_do_everything 36 | from pymobiledevice.util import readHomeFile, writeHomeFile, getHomePath 37 | from pymobiledevice.usbmux import usbmux 38 | 39 | from six import PY3 40 | 41 | if PY3: 42 | plistlib.readPlistFromString = plistlib.loads 43 | plistlib.writePlistToString = plistlib.dumps 44 | plistlib.readPlist = plistlib.load 45 | 46 | 47 | class NotTrustedError(Exception): 48 | pass 49 | 50 | 51 | class PairingError(Exception): 52 | pass 53 | 54 | 55 | class NotPairedError(Exception): 56 | pass 57 | 58 | 59 | class CannotStopSessionError(Exception): 60 | pass 61 | 62 | 63 | class StartServiceError(Exception): 64 | def __init__(self, message): 65 | print("[ERROR] %s" % message) 66 | 67 | 68 | class FatalPairingError(Exception): 69 | pass 70 | 71 | 72 | # we store pairing records and ssl keys in ~/.pymobiledevice 73 | HOMEFOLDER = ".pymobiledevice" 74 | MAXTRIES = 20 75 | 76 | 77 | def list_devices(): 78 | mux = usbmux.USBMux() 79 | mux.process(1) 80 | return [d.serial for d in mux.devices] 81 | 82 | 83 | class LockdownClient(object): 84 | 85 | def __init__(self, udid=None, logger=None): 86 | self.logger = logger or logging.getLogger(__name__) 87 | self.paired = False 88 | self.SessionID = None 89 | self.c = PlistService(62078, udid) 90 | self.hostID = self.generate_hostID() 91 | self.SystemBUID = self.generate_hostID() 92 | self.paired = False 93 | self.label = "pyMobileDevice" 94 | 95 | assert self.queryType() == "com.apple.mobile.lockdown" 96 | 97 | self.allValues = self.getValue() 98 | self.udid = self.allValues.get("UniqueDeviceID") 99 | self.UniqueChipID = self.allValues.get("UniqueChipID") 100 | self.DevicePublicKey = self.allValues.get("DevicePublicKey") 101 | self.ios_version = self.allValues.get("ProductVersion") 102 | self.identifier = self.udid 103 | if not self.identifier: 104 | if self.UniqueChipID: 105 | self.identifier = "%x" % self.UniqueChipID 106 | else: 107 | raise Exception("Could not get UDID or ECID, failing") 108 | 109 | if not self.validate_pairing(): 110 | self.pair() 111 | self.c = PlistService(62078, udid) 112 | if not self.validate_pairing(): 113 | raise FatalPairingError 114 | self.paired = True 115 | return 116 | 117 | def compare_ios_version(self, ios_version): 118 | """ 119 | currrent_version > ios_version return 1 120 | currrent_version = ios_version return 0 121 | currrent_version < ios_version return -1 122 | :param ios_version: 123 | :return: int 124 | """ 125 | version_reg = r'^\d*\.\d*\.?\d*$' 126 | if not re.match(version_reg, ios_version): 127 | raise Exception('ios_version invalid:%s' % ios_version) 128 | a = self.ios_version.split('.') 129 | b = ios_version.split('.') 130 | length = min(len(a), len(b)) 131 | for i in range(length): 132 | if int(a[i]) < int(b[i]): 133 | return -1 134 | if int(a[i]) > int(b[i]): 135 | return 1 136 | if len(a) > len(b): 137 | return 1 138 | elif len(a) < len(b): 139 | return -1 140 | else: 141 | return 0 142 | 143 | def queryType(self): 144 | self.c.sendPlist({"Request": "QueryType"}) 145 | res = self.c.recvPlist() 146 | return res.get("Type") 147 | 148 | def generate_hostID(self): 149 | hostname = platform.node() 150 | hostid = uuid.uuid3(uuid.NAMESPACE_DNS, hostname) 151 | return str(hostid).upper() 152 | 153 | def enter_recovery(self): 154 | self.c.sendPlist({"Request": "EnterRecovery"}) 155 | res = self.c.recvPlist() 156 | logger.debug(res) 157 | 158 | def stop_session(self): 159 | if self.SessionID and self.c: 160 | self.c.sendPlist({"Label": self.label, "Request": "StopSession", "SessionID": self.SessionID}) 161 | self.SessionID = None 162 | res = self.c.recvPlist() 163 | if not res or res.get("Result") != "Success": 164 | raise CannotStopSessionError 165 | return res 166 | 167 | 168 | def validate_pairing(self): 169 | pair_record = None 170 | certPem = None 171 | privateKeyPem = None 172 | 173 | if sys.platform == "win32": 174 | folder = os.environ["ALLUSERSPROFILE"] + "/Apple/Lockdown/" 175 | elif sys.platform == "darwin": 176 | folder = "/var/db/lockdown/" 177 | elif len(sys.platform) >= 5: 178 | if sys.platform[0:5] == "linux": 179 | folder = "/var/lib/lockdown/" 180 | try: 181 | pair_record = plistlib.readPlist(folder + "%s.plist" % self.identifier) 182 | print("Using iTunes pair record: %s.plist" % self.identifier) 183 | except: 184 | print("No iTunes pairing record found for device %s" % self.identifier) 185 | if self.compare_ios_version("13.0") >= 0: 186 | self.logger.warn("Getting pair record from usbmuxd") 187 | client = usbmux.UsbmuxdClient() 188 | pair_record = client.get_pair_record(self.udid) 189 | else: 190 | self.logger.warn("Looking for pymobiledevice pairing record") 191 | record = readHomeFile(HOMEFOLDER, "%s.plist" % self.identifier) 192 | if record: 193 | pair_record = plistlib.readPlistFromString(record) 194 | self.logger.warn("Found pymobiledevice pairing record for device %s" % self.udid) 195 | else: 196 | self.logger.error("No pymobiledevice pairing record found for device %s" % self.identifier) 197 | return False 198 | self.record = pair_record 199 | 200 | if PY3: 201 | certPem = pair_record["HostCertificate"] 202 | privateKeyPem = pair_record["HostPrivateKey"] 203 | else: 204 | certPem = pair_record["HostCertificate"].data 205 | privateKeyPem = pair_record["HostPrivateKey"].data 206 | 207 | if self.compare_ios_version("11.0") < 0: 208 | ValidatePair = {"Label": self.label, "Request": "ValidatePair", "PairRecord": pair_record} 209 | self.c.sendPlist(ValidatePair) 210 | r = self.c.recvPlist() 211 | if not r or "Error" in r: 212 | pair_record = None 213 | self.logger.error("ValidatePair fail", ValidatePair) 214 | return False 215 | 216 | self.hostID = pair_record.get("HostID", self.hostID) 217 | self.SystemBUID = pair_record.get("SystemBUID", self.SystemBUID) 218 | d = {"Label": self.label, "Request": "StartSession", "HostID": self.hostID, 'SystemBUID': self.SystemBUID} 219 | self.c.sendPlist(d) 220 | startsession = self.c.recvPlist() 221 | self.SessionID = startsession.get("SessionID") 222 | if startsession.get("EnableSessionSSL"): 223 | self.sslfile = self.identifier + "_ssl.txt" 224 | lf = "\n" 225 | if PY3: 226 | lf = b"\n" 227 | self.sslfile = writeHomeFile(HOMEFOLDER, self.sslfile, certPem + lf + privateKeyPem) 228 | self.c.ssl_start(self.sslfile, self.sslfile) 229 | 230 | self.paired = True 231 | return True 232 | 233 | def get_itunes_record_path(self): 234 | folder = None 235 | if sys.platform == "win32": 236 | folder = os.environ["ALLUSERSPROFILE"] + "/Apple/Lockdown/" 237 | elif sys.platform == "darwin": 238 | folder = "/var/db/lockdown/" 239 | elif len(sys.platform) >= 5: 240 | if sys.platform[0:5] == "linux": 241 | folder = "/var/lib/lockdown/" 242 | try: 243 | pair_record = plistlib.readPlist(folder + "%s.plist" % self.identifier) 244 | print("Using iTunes pair record: %s.plist" % self.identifier) 245 | except: 246 | print("No iTunes pairing record found for device %s" % self.identifier) 247 | if self.compare_ios_version("13.0") >= 0: 248 | print("Getting pair record from usbmuxd") 249 | client = usbmux.UsbmuxdClient() 250 | pair_record = client.get_pair_record(self.udid) 251 | else: 252 | print("Looking for pymobiledevice pairing record") 253 | record = readHomeFile(HOMEFOLDER, "%s.plist" % self.identifier) 254 | if record: 255 | pair_record = plistlib.readPlistFromString(record) 256 | print("Found pymobiledevice pairing record for device %s" % self.udid) 257 | else: 258 | print("No pymobiledevice pairing record found for device %s" % self.identifier) 259 | return False 260 | self.record = pair_record 261 | 262 | if PY3: 263 | certPem = pair_record["HostCertificate"] 264 | privateKeyPem = pair_record["HostPrivateKey"] 265 | else: 266 | certPem = pair_record["HostCertificate"].data 267 | privateKeyPem = pair_record["HostPrivateKey"].data 268 | 269 | if self.compare_ios_version("11.0") < 0: 270 | ValidatePair = {"Label": self.label, "Request": "ValidatePair", "PairRecord": pair_record} 271 | self.c.sendPlist(ValidatePair) 272 | r = self.c.recvPlist() 273 | if not r or "Error" in r: 274 | pair_record = None 275 | self.logger.error("ValidatePair fail: %s", ValidatePair) 276 | return False 277 | 278 | self.hostID = pair_record.get("HostID", self.hostID) 279 | self.SystemBUID = pair_record.get("SystemBUID", self.SystemBUID) 280 | d = {"Label": self.label, "Request": "StartSession", "HostID": self.hostID, 'SystemBUID': self.SystemBUID} 281 | self.c.sendPlist(d) 282 | startsession = self.c.recvPlist() 283 | self.SessionID = startsession.get("SessionID") 284 | if startsession.get("EnableSessionSSL"): 285 | self.sslfile = self.identifier + "_ssl.txt" 286 | lf = "\n" 287 | if PY3: 288 | lf = b"\n" 289 | self.sslfile = writeHomeFile(HOMEFOLDER, self.sslfile, certPem + lf + privateKeyPem) 290 | self.c.ssl_start(self.sslfile, self.sslfile) 291 | 292 | self.paired = True 293 | return True 294 | 295 | def pair(self): 296 | self.DevicePublicKey = self.getValue("", "DevicePublicKey") 297 | if self.DevicePublicKey == '': 298 | self.logger.error("Unable to retreive DevicePublicKey") 299 | return False 300 | 301 | self.logger.info("Creating host key & certificate") 302 | certPem, privateKeyPem, DeviceCertificate = ca_do_everything(self.DevicePublicKey) 303 | 304 | pair_record = {"DevicePublicKey": plistlib.Data(self.DevicePublicKey), 305 | "DeviceCertificate": plistlib.Data(DeviceCertificate), 306 | "HostCertificate": plistlib.Data(certPem), 307 | "HostID": self.hostID, 308 | "RootCertificate": plistlib.Data(certPem), 309 | "SystemBUID": "30142955-444094379208051516"} 310 | 311 | pair = {"Label": self.label, "Request": "Pair", "PairRecord": pair_record} 312 | self.c.sendPlist(pair) 313 | pair = self.c.recvPlist() 314 | 315 | if pair and pair.get("Result") == "Success" or "EscrowBag" in pair: 316 | pair_record["HostPrivateKey"] = plistlib.Data(privateKeyPem) 317 | pair_record["EscrowBag"] = pair.get("EscrowBag") 318 | writeHomeFile(HOMEFOLDER, "%s.plist" % self.identifier, plistlib.writePlistToString(pair_record)) 319 | self.paired = True 320 | return True 321 | 322 | elif pair and pair.get("Error") == "PasswordProtected": 323 | self.c.close() 324 | raise NotTrustedError 325 | else: 326 | self.logger.error(pair.get("Error")) 327 | self.c.close() 328 | raise PairingError 329 | 330 | def getValue(self, domain=None, key=None): 331 | 332 | if (isinstance(key, str) and hasattr(self, 'record') and hasattr(self.record, key)): 333 | return self.record[key] 334 | 335 | req = {"Request": "GetValue", "Label": self.label} 336 | 337 | if domain: 338 | req["Domain"] = domain 339 | if key: 340 | req["Key"] = key 341 | 342 | self.c.sendPlist(req) 343 | res = self.c.recvPlist() 344 | if res: 345 | r = res.get("Value") 346 | if hasattr(r, "data"): 347 | return r.data 348 | return r 349 | 350 | def setValue(self, value, domain=None, key=None): 351 | req = {"Request": "SetValue", "Label": self.label} 352 | 353 | if domain: 354 | req["Domain"] = domain 355 | if key: 356 | req["Key"] = key 357 | 358 | req["Value"] = value 359 | self.c.sendPlist(req) 360 | res = self.c.recvPlist() 361 | self.logger.debug(res) 362 | return res 363 | 364 | def startService(self, name): 365 | if not self.paired: 366 | self.logger.info("NotPaired") 367 | raise NotPairedError 368 | 369 | self.c.sendPlist({"Label": self.label, "Request": "StartService", "Service": name}) 370 | startService = self.c.recvPlist() 371 | ssl_enabled = startService.get("EnableServiceSSL", False) 372 | if not startService or startService.get("Error"): 373 | raise StartServiceError(startService.get("Error")) 374 | plist_service = PlistService(startService.get("Port"), self.udid) 375 | if ssl_enabled: 376 | plist_service.ssl_start(self.sslfile, self.sslfile) 377 | return plist_service 378 | 379 | def startServiceWithEscrowBag(self, name, escrowBag=None): 380 | if not self.paired: 381 | self.logger.info("NotPaired") 382 | raise NotPairedError 383 | 384 | if (not escrowBag): 385 | escrowBag = self.record['EscrowBag'] 386 | 387 | self.c.sendPlist({"Label": self.label, "Request": "StartService", "Service": name, 'EscrowBag': escrowBag}) 388 | StartService = self.c.recvPlist() 389 | if not StartService or StartService.get("Error"): 390 | if StartService.get("Error", "") == 'PasswordProtected': 391 | raise StartServiceError( 392 | 'your device is protected with password, please enter password in device and try again') 393 | raise StartServiceError(StartService.get("Error")) 394 | ssl_enabled = StartService.get("EnableServiceSSL", False) 395 | plist_service = PlistService(StartService.get("Port"), self.udid) 396 | if ssl_enabled: 397 | plist_service.ssl_start(self.sslfile, self.sslfile) 398 | return plist_service 399 | 400 | def main(): 401 | logging.basicConfig(level=logging.INFO) 402 | logger = logging.getLogger(__name__) 403 | l = LockdownClient() 404 | if l: 405 | n = writeHomeFile(HOMEFOLDER, "%s_infos.plist" % l.udid, plistlib.writePlistToString(l.allValues)) 406 | logger.info("Wrote infos to %s",n) 407 | else: 408 | logger.error("Unable to connect to device") 409 | 410 | if __name__ == "__main__": 411 | main() 412 | -------------------------------------------------------------------------------- /pymobiledevice/mobile_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import logging 26 | import plistlib 27 | 28 | from pymobiledevice.util import read_file 29 | from pymobiledevice.lockdown import LockdownClient 30 | 31 | from optparse import OptionParser 32 | from pprint import pprint 33 | 34 | class MobileConfigService(object): 35 | def __init__(self, lockdown, udid=None, logger=None): 36 | self.logger = logger or logging.getLogger(__name__) 37 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 38 | self.service = lockdown.startService("com.apple.mobile.MCInstall") 39 | 40 | def GetProfileList(self): 41 | self.service.sendPlist({"RequestType":"GetProfileList"}) 42 | res = self.service.recvPlist() 43 | if res.get("Status") != "Acknowledged": 44 | self.logger.error("GetProfileList error") 45 | self.logger.error(res) 46 | return 47 | return res 48 | 49 | def InstallProfile(self, s): 50 | #s = plistlib.writePlistToString(payload) 51 | self.service.sendPlist({"RequestType":"InstallProfile", "Payload": plistlib.Data(s)}) 52 | return self.service.recvPlist() 53 | 54 | def RemoveProfile(self, ident): 55 | profiles = self.GetProfileList() 56 | if not profiles: 57 | return 58 | if not ident in profiles["ProfileMetadata"]: 59 | self.logger.info("Trying to remove not installed profile %s", ident) 60 | return 61 | meta = profiles["ProfileMetadata"][ident] 62 | pprint(meta) 63 | data = plistlib.writePlistToString({"PayloadType": "Configuration", 64 | "PayloadIdentifier": ident, 65 | "PayloadUUID": meta["PayloadUUID"], 66 | "PayloadVersion": meta["PayloadVersion"] 67 | }) 68 | self.service.sendPlist({"RequestType":"RemoveProfile", "ProfileIdentifier": plistlib.Data(data)}) 69 | return self.service.recvPlist() 70 | 71 | def main(): 72 | parser = OptionParser(usage="%prog") 73 | parser.add_option("-l", "--list", dest="list", action="store_true", 74 | default=False, help="List installed profiles") 75 | parser.add_option("-i", "--install", dest="install", action="store", 76 | metavar="FILE", help="Install profile") 77 | parser.add_option("-r", "--remove", dest="remove", action="store", 78 | metavar="IDENTIFIER", help="Remove profile") 79 | (options, args) = parser.parse_args() 80 | 81 | if not options.list and not options.install and not options.remove: 82 | parser.print_help() 83 | return 84 | lockdown = LockdownClient() 85 | mc = MobileConfigService(lockdown) 86 | 87 | if options.list: 88 | pprint(mc.GetProfileList()) 89 | elif options.install: 90 | mc.InstallProfile(read_file(options.install)) 91 | elif options.remove: 92 | pprint(mc.RemoveProfile(options.remove)) 93 | 94 | if __name__ == "__main__": 95 | main() 96 | 97 | -------------------------------------------------------------------------------- /pymobiledevice/mobilebackup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | 26 | import struct 27 | import plistlib 28 | import os 29 | import datetime 30 | import logging 31 | import codecs 32 | 33 | from six import PY3 34 | 35 | from pymobiledevice.lockdown import LockdownClient 36 | from pymobiledevice.afc import AFCClient 37 | from pymobiledevice.util import makedirs 38 | from pymobiledevice.installation_proxy import installation_proxy 39 | 40 | # 41 | # Fix plistlib.py line 364 42 | # def asBase64(self, maxlinelength=76): 43 | # if self.data != None: 44 | # return _encodeBase64(self.data, maxlinelength) 45 | # return "" 46 | # 47 | # 48 | 49 | 50 | MOBILEBACKUP_E_SUCCESS = 0 51 | MOBILEBACKUP_E_INVALID_ARG = -1 52 | MOBILEBACKUP_E_PLIST_ERROR = -2 53 | MOBILEBACKUP_E_MUX_ERROR = -3 54 | MOBILEBACKUP_E_BAD_VERSION = -4 55 | MOBILEBACKUP_E_REPLY_NOT_OK = -5 56 | MOBILEBACKUP_E_UNKNOWN_ERROR = -256 57 | 58 | DEVICE_LINK_FILE_STATUS_NONE = 0 59 | DEVICE_LINK_FILE_STATUS_HUNK = 1 60 | DEVICE_LINK_FILE_STATUS_LAST_HUNK = 2 61 | 62 | class DeviceVersionNotSupported(Exception): 63 | def __str__(self): 64 | return "Device version not supported, please use mobilebackup2" 65 | 66 | 67 | class MobileBackup(object): 68 | def __init__(self, lockdown=None, udid=None, logger=None): 69 | self.logger = logger or logging.getLogger(__name__) 70 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 71 | ProductVersion = self.lockdown.getValue("", "ProductVersion") 72 | if ProductVersion[0] >= "5": 73 | raise DeviceVersionNotSupported 74 | self.start() 75 | 76 | def start(self): 77 | self.service = self.lockdown.startService("com.apple.mobilebackup") 78 | self.udid = self.lockdown.udid 79 | DLMessageVersionExchange = self.service.recvPlist() 80 | version_major = DLMessageVersionExchange[1] 81 | self.service.sendPlist(["DLMessageVersionExchange", "DLVersionsOk", version_major]) 82 | DLMessageDeviceReady = self.service.recvPlist() 83 | if DLMessageDeviceReady and DLMessageDeviceReady[0] == "DLMessageDeviceReady": 84 | self.logger.info("Got DLMessageDeviceReady") 85 | 86 | def check_filename(self, name): 87 | print(type(name), name) 88 | if PY3 and not isinstance(name, str): 89 | name = codecs.decode(name) 90 | if "../" in name: 91 | raise Exception("HAX, sneaky dots in path %s" % name) 92 | if not name.startswith(self.backupPath): 93 | if name.startswith(self.udid): 94 | name = os.path.join(self.backupPath, name) 95 | return name 96 | name = os.path.join(self.backupPath, self.udid, name) 97 | return name 98 | return name 99 | 100 | 101 | def read_file(self, filename): 102 | filename = self.check_filename(filename) 103 | if os.path.isfile(filename): 104 | with open(filename, 'rb') as f: 105 | data = f.read() 106 | f.close() 107 | return data 108 | return None 109 | 110 | 111 | def write_file(self, filename, data): 112 | filename = self.check_filename(filename) 113 | with open(filename, 'wb') as f: 114 | f.write(data) 115 | f.close() 116 | 117 | def create_info_plist(self): 118 | root_node = self.lockdown.allValues 119 | info = {"BuildVersion": root_node.get("BuildVersion") or "", 120 | "DeviceName": root_node.get("DeviceName") or "", 121 | "Display Name": root_node.get("DeviceName") or "", 122 | "GUID": "---", 123 | "ProductType": root_node.get("ProductType") or "", 124 | "ProductVersion": root_node.get("ProductVersion") or "", 125 | "Serial Number": root_node.get("SerialNumber") or "", 126 | "Unique Identifier": self.udid.upper(), 127 | "Target Identifier": self.udid, 128 | "Target Type": "Device", 129 | "iTunes Version": "10.0.1" 130 | } 131 | info["ICCID"] = root_node.get("IntegratedCircuitCardIdentity") or "" 132 | info["IMEI"] = root_node.get("InternationalMobileEquipmentIdentity") or "" 133 | info["Last Backup Date"] = datetime.datetime.now() 134 | 135 | afc = AFCClient(self.lockdown) 136 | iTunesFilesDict = {} 137 | iTunesFiles = afc.read_directory("/iTunes_Control/iTunes/") 138 | 139 | for i in iTunesFiles: 140 | data = afc.get_file_contents("/iTunes_Control/iTunes/" + i) 141 | if data: 142 | iTunesFilesDict[i] = plistlib.Data(data) 143 | info["iTunesFiles"] = iTunesFilesDict 144 | 145 | iBooksData2 = afc.get_file_contents("/Books/iBooksData2.plist") 146 | if iBooksData2: 147 | info["iBooks Data 2"] = plistlib.Data(iBooksData2) 148 | 149 | info["iTunes Settings"] = self.lockdown.getValue("com.apple.iTunes") 150 | self.logger.info("Creating: %s", os.path.join(self.udid,"Info.plist")) 151 | self.write_file(os.path.join(self.udid,"Info.plist"), plistlib.writePlistToString(info)) 152 | 153 | def ping(self, message): 154 | self.service.sendPlist(["DLMessagePing", message]) 155 | res = self.service.recvPlist() 156 | self.logger.debug("ping response:", res) 157 | 158 | def device_link_service_send_process_message(self, msg): 159 | return self.service.sendPlist(["DLMessageProcessMessage", msg]) 160 | 161 | def device_link_service_receive_process_message(self): 162 | req = self.service.recvPlist() 163 | if req: 164 | assert req[0] == "DLMessageProcessMessage" 165 | return req[1] 166 | 167 | def send_file_received(self): 168 | return self.device_link_service_send_process_message({"BackupMessageTypeKey": "kBackupMessageBackupFileReceived"}) 169 | 170 | def request_backup(self): 171 | req = {"BackupComputerBasePathKey": "/", 172 | "BackupMessageTypeKey": "BackupMessageBackupRequest", 173 | "BackupProtocolVersion": "1.6" 174 | } 175 | self.create_info_plist() 176 | self.device_link_service_send_process_message(req) 177 | res = self.device_link_service_receive_process_message() 178 | if not res: 179 | return 180 | if res["BackupMessageTypeKey"] != "BackupMessageBackupReplyOK": 181 | self.logger.error(res) 182 | return 183 | self.device_link_service_send_process_message(res) 184 | 185 | filedata = "" 186 | f = None 187 | outpath = None 188 | while True: 189 | res = self.service.recvPlist() 190 | if not res or res[0] != "DLSendFile": 191 | if res[0] == "DLMessageProcessMessage": 192 | if res[1].get("BackupMessageTypeKey") == "BackupMessageBackupFinished": 193 | self.logger.info("Backup finished OK !") 194 | #TODO BackupFilesToDeleteKey 195 | plistlib.writePlist(res[1]["BackupManifestKey"], self.check_filename("Manifest.plist")) 196 | break 197 | data = res[1].data 198 | info = res[2] 199 | if not f: 200 | outpath = self.check_filename(info.get("DLFileDest")) 201 | self.logger.debug("%s %s", info["DLFileAttributesKey"]["Filename"], info.get("DLFileDest")) 202 | f = open(outpath + ".mddata", "wb") 203 | f.write(data) 204 | if info.get("DLFileStatusKey") == DEVICE_LINK_FILE_STATUS_LAST_HUNK: 205 | self.send_file_received() 206 | f.close() 207 | if not info.get("BackupManifestKey", False): 208 | plistlib.writePlist(info.get("BackupFileInfo"), outpath + ".mdinfo") 209 | f = None 210 | 211 | def main(): 212 | logging.basicConfig(level=logging.INFO) 213 | mb = MobileBackup() 214 | mb.request_backup() 215 | 216 | 217 | if __name__ == "__main__": 218 | main() 219 | -------------------------------------------------------------------------------- /pymobiledevice/notification_proxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import time 26 | import plistlib 27 | import time 28 | import logging 29 | 30 | from pymobiledevice.lockdown import LockdownClient 31 | 32 | from six.moves import _thread as thread 33 | from pprint import pprint 34 | 35 | # NP Client to device Notifications (post_notification) 36 | NP_SYNC_WILL_START = "com.apple.itunes-mobdev.syncWillStart" 37 | NP_SYNC_DID_START = "com.apple.itunes-mobdev.syncDidStart" 38 | NP_SYNC_DID_FINISH = "com.apple.itunes-mobdev.syncDidFinish" 39 | NP_SYNC_LOCK_REQUEST = "com.apple.itunes-mobdev.syncLockRequest" 40 | 41 | # Device to NP Client Notifications (get_notification) 42 | NP_SYNC_CANCEL_REQUEST = "com.apple.itunes-client.syncCancelRequest" 43 | NP_SYNC_SUSPEND_REQUEST = "com.apple.itunes-client.syncSuspendRequest" 44 | NP_SYNC_RESUME_REQUEST = "com.apple.itunes-client.syncResumeRequest" 45 | NP_PHONE_NUMBER_CHANGED = "com.apple.mobile.lockdown.phone_number_changed" 46 | NP_DEVICE_NAME_CHANGED = "com.apple.mobile.lockdown.device_name_changed" 47 | NP_TIMEZONE_CHANGED = "com.apple.mobile.lockdown.timezone_changed" 48 | NP_TRUSTED_HOST_ATTACHED = "com.apple.mobile.lockdown.trusted_host_attached" 49 | NP_HOST_DETACHED = "com.apple.mobile.lockdown.host_detached" 50 | NP_HOST_ATTACHED = "com.apple.mobile.lockdown.host_attached" 51 | NP_REGISTRATION_FAILED = "com.apple.mobile.lockdown.registration_failed" 52 | NP_ACTIVATION_STATE = "com.apple.mobile.lockdown.activation_state" 53 | NP_BRICK_STATE = "com.apple.mobile.lockdown.brick_state" 54 | NP_DISK_USAGE_CHANGED = "com.apple.mobile.lockdown.disk_usage_changed" 55 | NP_DS_DOMAIN_CHANGED = "com.apple.mobile.data_sync.domain_changed" 56 | NP_BACKUP_DOMAIN_CHANGED = "com.apple.mobile.backup.domain_changed" 57 | NP_APP_INSTALLED = "com.apple.mobile.application_installed" 58 | NP_APP_UNINSTALLED = "com.apple.mobile.application_uninstalled" 59 | NP_DEV_IMAGE_MOUNTED = "com.apple.mobile.developer_image_mounted" 60 | NP_ATTEMPTACTIVATION = "com.apple.springboard.attemptactivation" 61 | NP_ITDBPREP_DID_END = "com.apple.itdbprep.notification.didEnd" 62 | NP_LANGUAGE_CHANGED = "com.apple.language.changed" 63 | NP_ADDRESS_BOOK_PREF_CHANGED = "com.apple.AddressBook.PreferenceChanged" 64 | 65 | 66 | class NPClient(object): 67 | def __init__(self, lockdown=None, serviceName="com.apple.mobile.notification_proxy", udid=None, logger=None): 68 | self.logger = logger or logging.getLogger(__name__) 69 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 70 | self.service = self.lockdown.startService(serviceName) 71 | 72 | def stop_session(self): 73 | self.logger.info("Disconecting...") 74 | self.service.close() 75 | 76 | 77 | def post_notification(self, notification): 78 | #Sends a notification to the device's notification_proxy. 79 | 80 | self.service.sendPlist({"Command": "PostNotification", 81 | "Name": notification}) 82 | 83 | self.service.sendPlist({"Command": "Shutdown"}) 84 | res = self.service.recvPlist() 85 | #pprint(res) 86 | if res: 87 | if res.get("Command") == "ProxyDeath": 88 | return res.get("Command") 89 | else: 90 | self.logger.error("Got unknown NotificationProxy command %s", res.get("Command")) 91 | self.logger.debug(res) 92 | return 93 | 94 | 95 | def observe_notification(self, notification): 96 | #Tells the device to send a notification on the specified event 97 | self.logger.info("Observing %s", notification) 98 | self.service.sendPlist({"Command": "ObserveNotification", 99 | "Name": notification}) 100 | 101 | 102 | def get_notification(self, notification): 103 | #Checks if a notification has been sent by the device 104 | 105 | res = self.service.recvPlist() 106 | if res: 107 | if res.get("Command") == "RelayNotification": 108 | if res.get("Name"): 109 | return res.get("Name") 110 | 111 | elif res.get("Command") == "ProxyDeath": 112 | self.logger.error("NotificationProxy died!") 113 | else: 114 | self.logger.warn("Got unknown NotificationProxy command %s", res.get("Command")) 115 | self.logger.debug(res) 116 | return 117 | 118 | 119 | def notifier(self, name, args=None): 120 | 121 | if args == None: 122 | return None 123 | 124 | self.observe_notification(args.get("notification")) 125 | 126 | while args.get("running") == True: 127 | np_name = self.get_notification(args.get("notification")) 128 | if np_name: 129 | userdata = args.get("userdata") 130 | try: 131 | thread.start_new_thread( args.get("callback") , (np_name, userdata, ) ) 132 | except: 133 | self.logger.error("Error: unable to start thread") 134 | 135 | def subscribe(self, notification, cb, data=None): 136 | 137 | np_data = { 138 | "running": True, 139 | "notification": notification, 140 | "callback": cb, 141 | "userdata": data, 142 | } 143 | 144 | thread.start_new_thread( self.notifier, ("NotificationProxyNotifier_"+notification, np_data, ) ) 145 | while(1): 146 | time.sleep(1) 147 | 148 | 149 | 150 | def cb_test(name,data=None): 151 | print("Got Notification >> %s" % name) 152 | print("Data:") 153 | pprint(data) 154 | 155 | def main(): 156 | np = NPClient() 157 | np.subscribe(NP_DEVICE_NAME_CHANGED, cb_test, data=None) 158 | 159 | 160 | if __name__ == "__main__": 161 | main() 162 | -------------------------------------------------------------------------------- /pymobiledevice/pcapd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | from __future__ import print_function 25 | from six import PY3 26 | import struct 27 | import time 28 | import sys 29 | import logging 30 | from hexdump import * 31 | 32 | from pymobiledevice.lockdown import LockdownClient 33 | 34 | from tempfile import mkstemp 35 | from optparse import OptionParser 36 | 37 | """ 38 | struct pcap_hdr_s { 39 | guint32 magic_number; /* magic number */ 40 | guint16 version_major; /* major version number */ 41 | guint16 version_minor; /* minor version number */ 42 | gint32 thiszone; /* GMT to local correction */ 43 | guint32 sigfigs; /* accuracy of timestamps */ 44 | guint32 snaplen; /* max length of captured packets, in octets */ 45 | guint32 network; /* data link type */ 46 | } pcap_hdr_t; 47 | typedef struct pcaprec_hdr_s { 48 | guint32 ts_sec; /* timestamp seconds */ 49 | guint32 ts_usec; /* timestamp microseconds */ 50 | guint32 incl_len; /* number of octets of packet saved in file */ 51 | guint32 orig_len; /* actual length of packet */ 52 | } pcaprec_hdr_t; 53 | """ 54 | LINKTYPE_ETHERNET = 1 55 | LINKTYPE_RAW = 101 56 | 57 | class PcapOut(object): 58 | 59 | def __init__(self, pipename=r'test.pcap'): 60 | self.pipe = open(pipename,'wb') 61 | self.pipe.write(struct.pack("LBL", d[:9]) 126 | flags1, flags2, offset_to_ip_data, zero = struct.unpack(">LLLL", d[9:0x19]) 127 | 128 | assert hdrsize >= 0x19 129 | if PY3: 130 | interfacetype= d[0x19:hdrsize].strip(b"\x00") 131 | else: 132 | interfacetype= d[0x19:hdrsize].strip("\x00") 133 | interfacetype = "b'"+"\\x".join("{:02x}".format(ord(c)) for c in interfacetype)+"'" 134 | t = time.time() 135 | packet = d[hdrsize:] 136 | assert packet_size == len(packet) 137 | 138 | if offset_to_ip_data == 0: 139 | #add fake ethernet header for pdp packets 140 | if PY3: 141 | packet = b"\xBE\xEF" * 6 + b"\x08\x00" + packet 142 | else: 143 | packet = "\xBE\xEF" * 6 + "\x08\x00" + packet 144 | 145 | if options.output: 146 | if not output.writePacket(packet): 147 | break 148 | else: 149 | print(packet_size, t) 150 | hexdump(packet) 151 | 152 | 153 | if __name__ == "__main__": 154 | main() 155 | -------------------------------------------------------------------------------- /pymobiledevice/plist_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2023 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import plistlib 26 | import ssl 27 | import struct 28 | import logging 29 | import codecs 30 | from re import sub 31 | from six import PY3 32 | 33 | from pymobiledevice.usbmux import usbmux 34 | 35 | if PY3: 36 | plistlib.readPlistFromString = plistlib.loads 37 | plistlib.writePlistToString = plistlib.dumps 38 | 39 | 40 | class PlistService(object): 41 | 42 | def __init__(self, port, udid=None, logger=None): 43 | self.logger = logger or logging.getLogger(__name__) 44 | self.port = port 45 | self.connect(udid) 46 | 47 | def connect(self, udid=None): 48 | mux = usbmux.USBMux() 49 | mux.process(1.0) 50 | dev = None 51 | 52 | while not dev and mux.devices : 53 | mux.process(1.0) 54 | if udid: 55 | for d in mux.devices: 56 | if d.serial == udid: 57 | dev = d 58 | else: 59 | dev = mux.devices[0] 60 | self.logger.info("Connecting to device: " + dev.serial) 61 | try: 62 | self.s = mux.connect(dev, self.port) 63 | except: 64 | raise Exception("Connexion to device port %d failed" % self.port) 65 | return dev.serial 66 | 67 | def close(self): 68 | self.s.close() 69 | 70 | def recv(self, length=4096): 71 | return self.s.recv(length) 72 | 73 | def send(self, data): 74 | try: 75 | self.s.send(data) 76 | except: 77 | self.logger.error("Sending data to device failled") 78 | return -1 79 | return 0 80 | 81 | def sendRequest(self, data): 82 | res = None 83 | if self.sendPlist(data) >= 0: 84 | res = self.recvPlist() 85 | return res 86 | 87 | 88 | def recv_exact(self, l): 89 | data = "" 90 | if PY3: 91 | data = b"" 92 | while l > 0: 93 | d = self.recv(l) 94 | if not d or len(d) == 0: 95 | break 96 | data += d 97 | l -= len(d) 98 | return data 99 | 100 | def recv_raw(self): 101 | l = self.recv_exact(4) 102 | if not l or len(l) != 4: 103 | return 104 | l = struct.unpack(">L", l)[0] 105 | return self.recv_exact(l) 106 | 107 | def send_raw(self, data): 108 | if PY3 and isinstance(data, str): 109 | data = codecs.encode(data) 110 | hdr = struct.pack(">L", len(data)) 111 | msg = b"".join([hdr,data]) 112 | return self.send(msg) 113 | 114 | def recvPlist(self): 115 | payload = self.recv_raw() 116 | if not payload: 117 | return 118 | bplist_header = "bplist00" 119 | xml_header = "\/ \-_0-9\"\'\\=\.\?\!\+]+','', payload.decode('utf-8')).encode('utf-8') 132 | return plistlib.readPlistFromString(payload) 133 | else: 134 | raise Exception("recvPlist invalid data : %s" % payload[:100].encode("hex")) 135 | 136 | def sendPlist(self, d): 137 | payload = plistlib.writePlistToString(d) 138 | l = struct.pack(">L", len(payload)) 139 | return self.send(l + payload) 140 | 141 | def ssl_start(self, keyfile, certfile): 142 | self.s = ssl.wrap_socket(self.s, keyfile, certfile) 143 | -------------------------------------------------------------------------------- /pymobiledevice/sbservices.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from pymobiledevice.lockdown import LockdownClient 4 | 5 | from pprint import * 6 | 7 | SB_PORTRAIT = 1 8 | SB_PORTRAIT_UPSIDE_DOWN = 2 9 | SB_LANDSCAPE = 3 10 | SB_LANDSCAPE_HOME_TO_LEFT = 4 11 | 12 | 13 | class SBServiceClient(object): 14 | 15 | service = None 16 | def __init__(self, lockdown = None, udid=None, logger=None): 17 | self.logger = logger or logging.getLogger(__name__) 18 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 19 | if not self.lockdown: 20 | raise Exception("Unable to start lockdown") 21 | self.start() 22 | 23 | def start(self): 24 | self.service = self.lockdown.startService("com.apple.springboardservices") 25 | if not self.service: 26 | raise Exception("SBService init error : Could not start com.apple.springboardservices") 27 | 28 | 29 | def get_icon_state(self, format_version="2"): 30 | cmd = { "command": "getIconState" } 31 | if format_version: 32 | cmd["formatVersion"] = format_version 33 | 34 | self.service.sendPlist(cmd) 35 | res = self.service.recvPlist() 36 | return res 37 | 38 | 39 | def set_icon_state(self, newstate={}): 40 | cmd = { "command": "setIconState", 41 | "iconState": newstate } 42 | 43 | self.service.sendPlist(cmd) 44 | 45 | 46 | def get_icon_pngdata(self, bid): 47 | cmd = { "command": "getIconPNGData", 48 | "bundleId": bid } 49 | 50 | self.service.sendPlist(cmd) 51 | res = self.service.recvPlist() 52 | pngdata = res.get("pngData") 53 | if res: 54 | return pngdata 55 | return None 56 | 57 | def get_interface_orientation(self): 58 | cmd = { "command": "getInterfaceOrientation" } 59 | self.service.sendPlist(cmd) 60 | res = self.service.recvPlist() 61 | return res.get('interfaceOrientation') 62 | 63 | 64 | def get_wallpaper_pngdata(self): 65 | cmd = { "command": "getHomeScreenWallpaperPNGData" } 66 | self.service.sendPlist(cmd) 67 | res = self.service.recvPlist() 68 | if res: 69 | return res.get("pngData") 70 | return None 71 | 72 | 73 | if __name__ == "__main__": 74 | s = SBServiceClient() 75 | print(s.get_icon_pngdata("com.apple.weather")) 76 | -------------------------------------------------------------------------------- /pymobiledevice/screenshotr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import os 26 | import plistlib 27 | import logging 28 | 29 | from pymobiledevice.lockdown import LockdownClient 30 | 31 | from six import PY3 32 | from pprint import pprint 33 | from time import gmtime, strftime 34 | from optparse import OptionParser 35 | 36 | class screenshotr(object): 37 | def __init__(self, lockdown=None, serviceName='com.apple.mobile.screenshotr', udid=None, logger=None): 38 | self.logger = logger or logging.getLogger(__name__) 39 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 40 | self.service = self.lockdown.startService(serviceName) 41 | DLMessageVersionExchange = self.service.recvPlist() 42 | version_major = DLMessageVersionExchange[1] 43 | self.service.sendPlist(["DLMessageVersionExchange", "DLVersionsOk", version_major ]) 44 | DLMessageDeviceReady = self.service.recvPlist() 45 | 46 | def stop_session(self): 47 | self.service.close() 48 | 49 | def take_screenshot(self): 50 | self.service.sendPlist(['DLMessageProcessMessage', {'MessageType': 'ScreenShotRequest'}]) 51 | res = self.service.recvPlist() 52 | 53 | assert len(res) == 2 54 | assert res[0] == "DLMessageProcessMessage" 55 | 56 | if res[1].get('MessageType') == 'ScreenShotReply': 57 | if PY3: 58 | screen_data = res[1]['ScreenShotData'] 59 | else: 60 | screen_data = res[1]['ScreenShotData'].data 61 | return screen_data 62 | return None 63 | 64 | def main(): 65 | parser = OptionParser(usage='%prog') 66 | parser.add_option("-u", "--udid", 67 | default=False, action="store", dest="device_udid", metavar="DEVICE_UDID", 68 | help="Device udid") 69 | parser.add_option('-p', '--path', dest='outDir', default=False, 70 | help='Output Directory (default: . )', type='string') 71 | (options, args) = parser.parse_args() 72 | 73 | outPath = '.' 74 | if options.outDir: 75 | outPath = options.outDir 76 | 77 | logging.basicConfig(level=logging.INFO) 78 | lckdn = LockdownClient(options.device_udid) 79 | screenshotr = screenshotr(lockdown=lckdn) 80 | data = screenshotr.take_screenshot() 81 | if data: 82 | filename = strftime('screenshot-%Y-%m-%d-%H-%M-%S.tif',gmtime()) 83 | outPath = os.path.join(outPath, filename) 84 | print('Saving Screenshot at %s' % outPath) 85 | o = open(outPath,'wb') 86 | o.write(data) 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /pymobiledevice/syslog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | import re 26 | import logging 27 | 28 | from pymobiledevice.lockdown import LockdownClient 29 | from six import PY3 30 | from sys import exit 31 | from datetime import datetime 32 | from util import getHomePath 33 | from util import hexdump 34 | from optparse import OptionParser 35 | import time 36 | 37 | TIME_FORMAT = '%H:%M:%S' 38 | 39 | 40 | class Syslog(object): 41 | ''' 42 | View system logs 43 | ''' 44 | def __init__(self, lockdown=None, udid=None, logger=None): 45 | self.logger = logger or logging.getLogger(__name__) 46 | self.lockdown = lockdown if lockdown else LockdownClient(udid=udid) 47 | self.c = self.lockdown.startService("com.apple.syslog_relay") 48 | if self.c: 49 | self.c.send("watch") 50 | else: 51 | exit(1) 52 | 53 | def watch(self, watchtime=None, logFile=None, procName=None): 54 | '''View log 55 | :param watchtime: time (seconds) 56 | :type watchtime: int 57 |         :param logFile: full path to the log file 58 |         :type logFile: str 59 |         :param procName: process name 60 |         :type proName: str 61 | ''' 62 | begin = time.strftime(TIME_FORMAT) 63 | while True: 64 | d = self.c.recv(4096) 65 | if PY3: 66 | d = d.decode('utf-8') 67 | if procName: 68 | procFilter = re.compile(procName,re.IGNORECASE) 69 | if len(d.split(" ")) > 4 and not procFilter.search(d): 70 | continue 71 | s = d.strip("\n\x00\x00") 72 | #self.logger.info(s) 73 | print(s) 74 | if logFile: 75 | with open(logFile, 'a') as f: 76 | f.write(d.replace("\x00", "")) 77 | if watchtime: 78 | now = self.time_match(s[7:15]) 79 | if now: 80 | time_spend = self.time_caculate(str(begin), now) 81 | if time_spend > watchtime : 82 | break 83 | 84 | 85 | def time_match(self, str_time): 86 | ''' 87 | Determine if the time format matches 88 | ''' 89 | pattern = re.compile(r'\d{2}:\d{2}:\d{2}') 90 | match = pattern.match(str_time) 91 | if match: 92 | return str_time 93 | else: 94 | return False 95 | 96 | def time_caculate(self, a, b): 97 | ''' 98 | Calculate the time difference between two strings 99 | ''' 100 | time_a = int(a[6:8])+60*int(a[3:5])+3600*int(a[0:2]) 101 | time_b = int(b[6:8])+60*int(b[3:5])+3600*int(b[0:2]) 102 | time_a = int(a[6:8])+60*int(a[3:5])+3600*int(a[0:2]) 103 | time_b = int(b[6:8])+60*int(b[3:5])+3600*int(b[0:2]) 104 | return time_b - time_a 105 | 106 | 107 | if __name__ == "__main__": 108 | parser = OptionParser(usage="%prog") 109 | parser.add_option("-u", "--udid", 110 | default=False, action="store", dest="device_udid", metavar="DEVICE_UDID", 111 | help="Device udid") 112 | parser.add_option("-p", "--process", dest="procName", default=False, 113 | help="Show process log only", type="string") 114 | parser.add_option("-o", "--logfile", dest="logFile", default=False, 115 | help="Write Logs into specified file", type="string") 116 | parser.add_option("-w", "--watch-time", 117 | default=False, action="store", dest="watchtime", metavar="WATCH_TIME", 118 | help="watchtime") 119 | (options, args) = parser.parse_args() 120 | 121 | try: 122 | try: 123 | logging.basicConfig(level=logging.INFO) 124 | lckdn = LockdownClient(options.device_udid) 125 | syslog = Syslog(lockdown=lckdn) 126 | syslog.watch(watchtime=int(options.watchtime), procName=options.procName,logFile=options.logFile) 127 | except KeyboardInterrupt: 128 | print("KeyboardInterrupt caught") 129 | raise 130 | else: 131 | pass 132 | 133 | 134 | except (KeyboardInterrupt, SystemExit): 135 | exit() 136 | -------------------------------------------------------------------------------- /pymobiledevice/usbmux/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iOSForensics/pymobiledevice/c13e035e11f71c1048e25d123c04c83e684be82f/pymobiledevice/usbmux/__init__.py -------------------------------------------------------------------------------- /pymobiledevice/usbmux/tcprelay.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # tcprelay.py - TCP connection relay for usbmuxd 5 | # 6 | # Copyright (C) 2009 Hector Martin "marcan" 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 or version 3. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20 | 21 | import sys 22 | import select 23 | from optparse import OptionParser 24 | from six import PY3 25 | from six.moves import socketserver 26 | 27 | from pymobiledevice.usbmux import usbmux 28 | 29 | 30 | class SocketRelay(object): 31 | def __init__(self, a, b, maxbuf=65535): 32 | self.a = a 33 | self.b = b 34 | self.atob = "" 35 | self.btoa = "" 36 | if PY3: 37 | self.atob = b"" 38 | self.btoa = b"" 39 | self.maxbuf = maxbuf 40 | def handle(self): 41 | while True: 42 | rlist = [] 43 | wlist = [] 44 | xlist = [self.a, self.b] 45 | if self.atob: 46 | wlist.append(self.b) 47 | if self.btoa: 48 | wlist.append(self.a) 49 | if len(self.atob) < self.maxbuf: 50 | rlist.append(self.a) 51 | if len(self.btoa) < self.maxbuf: 52 | rlist.append(self.b) 53 | rlo, wlo, xlo = select.select(rlist, wlist, xlist) 54 | if xlo: 55 | return 56 | if self.a in wlo: 57 | n = self.a.send(self.btoa) 58 | self.btoa = self.btoa[n:] 59 | if self.b in wlo: 60 | n = self.b.send(self.atob) 61 | self.atob = self.atob[n:] 62 | if self.a in rlo: 63 | s = self.a.recv(self.maxbuf - len(self.atob)) 64 | if not s: 65 | return 66 | self.atob += s 67 | if self.b in rlo: 68 | s = self.b.recv(self.maxbuf - len(self.btoa)) 69 | if not s: 70 | return 71 | self.btoa += s 72 | #print("Relay iter: %8d atob, %8d btoa, lists: %r %r %r"%(len(self.atob), len(self.btoa), rlo, wlo, xlo)) 73 | 74 | 75 | class TCPRelay(socketserver.BaseRequestHandler): 76 | def handle(self): 77 | if 'options' in globals(): 78 | sockpath = options.sockpath 79 | else: 80 | sockpath = None 81 | mux = usbmux.USBMux(sockpath) 82 | print("Waiting for devices...") 83 | mux.process(1.0) 84 | 85 | dev = None 86 | if 'options' in globals(): 87 | udid = options.udid 88 | else: 89 | udid = self.server.udid 90 | if udid: 91 | for device in mux.devices: 92 | if device.serial == udid: 93 | dev = device 94 | break 95 | if not dev: 96 | for _ in range(20): 97 | mux.process(0.5) 98 | for device in mux.devices: 99 | if device.serial == udid: 100 | dev = device 101 | break 102 | if dev : 103 | break 104 | else: 105 | dev = mux.devices[0] 106 | 107 | if not mux.devices: 108 | print("No device found") 109 | self.request.close() 110 | return 111 | print("Connecting to device %s"%str(dev)) 112 | dsock = mux.connect(dev, self.server.rport) 113 | lsock = self.request 114 | print("Connection established, relaying data") 115 | try: 116 | fwd = SocketRelay(dsock, lsock, self.server.bufsize * 1024) 117 | fwd.handle() 118 | finally: 119 | dsock.close() 120 | lsock.close() 121 | print("Connection closed") 122 | 123 | 124 | class TCPServer(socketserver.TCPServer): 125 | allow_reuse_address = True 126 | 127 | 128 | class ThreadedTCPServer(socketserver.ThreadingMixIn, TCPServer): 129 | pass 130 | 131 | 132 | def forward_ports(pair_ports, udid=None, threaded=True, bufsize=128): 133 | '''iOS真机设备的端口转发 134 | 135 | :param pair_ports: 端口对的数组,每对端口中前一个代表远程端口,后一个代表本地端口,例如:["8100:8100", "8200:8200"] 136 | :type pair_ports: list 137 | ''' 138 | servers=[] 139 | if threaded: 140 | serverclass = ThreadedTCPServer 141 | else: 142 | serverclass = TCPServer 143 | 144 | for pair_port in pair_ports: 145 | rport, lport = pair_port.split(":") 146 | rport = int(rport) 147 | lport = int(lport) 148 | print("Forwarding local port %d to remote port %d"%(lport, rport)) 149 | server = serverclass(("localhost", lport), TCPRelay) 150 | server.rport = rport 151 | server.bufsize = bufsize 152 | server.udid = udid 153 | servers.append(server) 154 | 155 | alive = True 156 | while alive: 157 | try: 158 | rl, wl, xl = select.select(servers, [], []) 159 | for server in rl: 160 | server.handle_request() 161 | except: 162 | alive = False 163 | 164 | 165 | if __name__ == '__main__': 166 | parser = OptionParser(usage="usage: %prog [OPTIONS] RemotePort[:LocalPort] [RemotePort[:LocalPort]]...") 167 | parser.add_option("-t", "--threaded", dest='threaded', action='store_true', default=False, help="use threading to handle multiple connections at once") 168 | parser.add_option("-b", "--bufsize", dest='bufsize', action='store', metavar='KILOBYTES', type='int', default=128, help="specify buffer size for socket forwarding") 169 | parser.add_option("-s", "--socket", dest='sockpath', action='store', metavar='PATH', type='str', default=None, help="specify the path of the usbmuxd socket") 170 | parser.add_option("-u", "--udid", dest='udid', action='store', metavar='UDID', type='str', default=None, help="specify the udid of iOS device") 171 | 172 | options, args = parser.parse_args() 173 | 174 | serverclass = TCPServer 175 | if options.threaded: 176 | serverclass = ThreadedTCPServer 177 | 178 | if len(args) == 0: 179 | parser.print_help() 180 | sys.exit(1) 181 | 182 | ports = [] 183 | 184 | for arg in args: 185 | try: 186 | if ':' in arg: 187 | rport, lport = arg.split(":") 188 | rport = int(rport) 189 | lport = int(lport) 190 | ports.append((rport, lport)) 191 | else: 192 | ports.append((int(arg), int(arg))) 193 | except: 194 | parser.print_help() 195 | sys.exit(1) 196 | 197 | servers=[] 198 | HOST = "localhost" 199 | 200 | for rport, lport in ports: 201 | print("Forwarding local port %d to remote port %d"%(lport, rport)) 202 | server = serverclass((HOST, lport), TCPRelay) 203 | server.rport = rport 204 | server.bufsize = options.bufsize 205 | servers.append(server) 206 | 207 | alive = True 208 | 209 | while alive: 210 | try: 211 | rl, wl, xl = select.select(servers, [], []) 212 | for server in rl: 213 | server.handle_request() 214 | except: 215 | alive = False 216 | -------------------------------------------------------------------------------- /pymobiledevice/usbmux/usbmux.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # usbmux.py - usbmux client library for Python 5 | # 6 | # Copyright (C) 2009 Hector Martin "marcan" 7 | # 8 | # This program is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 2 or version 3. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program; if not, write to the Free Software 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 20 | 21 | import socket, struct, select, sys 22 | from six import PY3 23 | 24 | 25 | haveplist = False 26 | if PY3: 27 | try: 28 | import plistlib 29 | haveplist = True 30 | except: 31 | pass 32 | else: 33 | try: 34 | import biplist as plistlib 35 | haveplist = True 36 | except: 37 | pass 38 | 39 | class MuxError(Exception): 40 | pass 41 | 42 | class MuxVersionError(MuxError): 43 | pass 44 | 45 | class SafeStreamSocket: 46 | def __init__(self, address, family): 47 | self.sock = socket.socket(family, socket.SOCK_STREAM) 48 | self.sock.connect(address) 49 | def send(self, msg): 50 | totalsent = 0 51 | while totalsent < len(msg): 52 | sent = self.sock.send(msg[totalsent:]) 53 | if sent == 0: 54 | raise MuxError("socket connection broken") 55 | totalsent = totalsent + sent 56 | def recv(self, size): 57 | msg = '' 58 | if PY3: 59 | msg = b'' 60 | while len(msg) < size: 61 | chunk = self.sock.recv(size-len(msg)) 62 | empty_chunk = '' 63 | if PY3: 64 | empty_chunk = b'' 65 | if chunk == empty_chunk: 66 | raise MuxError("socket connection broken") 67 | msg = msg + chunk 68 | return msg 69 | 70 | class MuxDevice(object): 71 | def __init__(self, devid, usbprod, serial, location): 72 | self.devid = devid 73 | self.usbprod = usbprod 74 | self.serial = serial 75 | self.location = location 76 | def __str__(self): 77 | return ""%(self.devid, self.usbprod, self.serial, self.location) 78 | 79 | class BinaryProtocol(object): 80 | TYPE_RESULT = 1 81 | TYPE_CONNECT = 2 82 | TYPE_LISTEN = 3 83 | TYPE_DEVICE_ADD = 4 84 | TYPE_DEVICE_REMOVE = 5 85 | VERSION = 0 86 | def __init__(self, socket): 87 | self.socket = socket 88 | self.connected = False 89 | 90 | def _pack(self, req, payload): 91 | if req == self.TYPE_CONNECT: 92 | connect_data = "\x00\x00" 93 | if PY3: 94 | connect_data = b"\x00\x00" 95 | return struct.pack("IH", payload['DeviceID'], payload['PortNumber']) + connect_data 96 | elif req == self.TYPE_LISTEN: 97 | if PY3: 98 | return b"" 99 | else: 100 | return "" 101 | else: 102 | raise ValueError("Invalid outgoing request type %d"%req) 103 | 104 | def _unpack(self, resp, payload): 105 | if resp == self.TYPE_RESULT: 106 | return {'Number':struct.unpack("I", payload)[0]} 107 | elif resp == self.TYPE_DEVICE_ADD: 108 | devid, usbpid, serial, pad, location = struct.unpack("IH256sHI", payload) 109 | serial = serial.split(b"\0")[0] 110 | return {'DeviceID': devid, 'Properties': {'LocationID': location, 'SerialNumber': serial, 'ProductID': usbpid}} 111 | elif resp == self.TYPE_DEVICE_REMOVE: 112 | devid = struct.unpack("I", payload)[0] 113 | return {'DeviceID': devid} 114 | else: 115 | raise MuxError("Invalid incoming response type %d"%resp) 116 | 117 | def sendpacket(self, req, tag, payload={}): 118 | payload = self._pack(req, payload) 119 | if self.connected: 120 | raise MuxError("Mux is connected, cannot issue control packets") 121 | length = 16 + len(payload) 122 | data = struct.pack("IIII", length, self.VERSION, req, tag) + payload 123 | self.socket.send(data) 124 | def getpacket(self): 125 | if self.connected: 126 | raise MuxError("Mux is connected, cannot issue control packets") 127 | dlen = self.socket.recv(4) 128 | dlen = struct.unpack("I", dlen)[0] 129 | body = self.socket.recv(dlen - 4) 130 | version, resp, tag = struct.unpack("III",body[:0xc]) 131 | if version != self.VERSION: 132 | raise MuxVersionError("Version mismatch: expected %d, got %d"%(self.VERSION,version)) 133 | payload = self._unpack(resp, body[0xc:]) 134 | return (resp, tag, payload) 135 | 136 | class PlistProtocol(BinaryProtocol): 137 | TYPE_RESULT = "Result" 138 | TYPE_CONNECT = "Connect" 139 | TYPE_LISTEN = "Listen" 140 | TYPE_DEVICE_ADD = "Attached" 141 | TYPE_DEVICE_REMOVE = "Detached" #??? 142 | TYPE_PLIST = 8 143 | VERSION = 1 144 | def __init__(self, socket): 145 | if not haveplist: 146 | raise Exception("You need the plistlib module") 147 | BinaryProtocol.__init__(self, socket) 148 | 149 | def _pack(self, req, payload): 150 | return payload 151 | 152 | def _unpack(self, resp, payload): 153 | return payload 154 | 155 | def sendpacket(self, req, tag, payload={}): 156 | payload['ClientVersionString'] = 'qt4i-usbmuxd' 157 | if isinstance(req, int): 158 | req = [self.TYPE_CONNECT, self.TYPE_LISTEN][req-2] 159 | payload['MessageType'] = req 160 | payload['ProgName'] = 'tcprelay' 161 | if PY3: 162 | wrapped_payload = plistlib.dumps(payload) 163 | else: 164 | wrapped_payload = plistlib.writePlistToString(payload) 165 | BinaryProtocol.sendpacket(self, self.TYPE_PLIST, tag, wrapped_payload) 166 | def getpacket(self): 167 | resp, tag, payload = BinaryProtocol.getpacket(self) 168 | if resp != self.TYPE_PLIST: 169 | raise MuxError("Received non-plist type %d"%resp) 170 | if PY3: 171 | payload = plistlib.loads(payload) 172 | else: 173 | payload = plistlib.readPlistFromString(payload) 174 | return payload.get('MessageType', ''), tag, payload 175 | 176 | class MuxConnection(object): 177 | def __init__(self, socketpath, protoclass): 178 | self.socketpath = socketpath 179 | if sys.platform in ['win32', 'cygwin']: 180 | family = socket.AF_INET 181 | address = ('127.0.0.1', 27015) 182 | else: 183 | family = socket.AF_UNIX 184 | address = self.socketpath 185 | self.socket = SafeStreamSocket(address, family) 186 | self.proto = protoclass(self.socket) 187 | self.pkttag = 1 188 | self.devices = [] 189 | 190 | def _getreply(self): 191 | while True: 192 | resp, tag, data = self.proto.getpacket() 193 | if resp == self.proto.TYPE_RESULT: 194 | return tag, data 195 | else: 196 | raise MuxError("Invalid packet type received: %d"%resp) 197 | def _processpacket(self): 198 | resp, tag, data = self.proto.getpacket() 199 | if resp == self.proto.TYPE_DEVICE_ADD: 200 | self.devices.append(MuxDevice(data['DeviceID'], data['Properties']['ProductID'], data['Properties']['SerialNumber'], data['Properties']['LocationID'])) 201 | elif resp == self.proto.TYPE_DEVICE_REMOVE: 202 | for dev in self.devices: 203 | if dev.devid == data['DeviceID']: 204 | self.devices.remove(dev) 205 | elif resp == self.proto.TYPE_RESULT: 206 | raise MuxError("Unexpected result: %d"%resp) 207 | else: 208 | raise MuxError("Invalid packet type received: %d"%resp) 209 | def _exchange(self, req, payload={}): 210 | mytag = self.pkttag 211 | self.pkttag += 1 212 | self.proto.sendpacket(req, mytag, payload) 213 | recvtag, data = self._getreply() 214 | if recvtag != mytag: 215 | raise MuxError("Reply tag mismatch: expected %d, got %d"%(mytag, recvtag)) 216 | return data['Number'] 217 | 218 | def listen(self): 219 | ret = self._exchange(self.proto.TYPE_LISTEN) 220 | if ret != 0: 221 | raise MuxError("Listen failed: error %d"%ret) 222 | def process(self, timeout=None): 223 | if self.proto.connected: 224 | raise MuxError("Socket is connected, cannot process listener events") 225 | rlo, wlo, xlo = select.select([self.socket.sock], [], [self.socket.sock], timeout) 226 | if xlo: 227 | self.socket.sock.close() 228 | raise MuxError("Exception in listener socket") 229 | if rlo: 230 | self._processpacket() 231 | def connect(self, device, port): 232 | ret = self._exchange(self.proto.TYPE_CONNECT, {'DeviceID':device.devid, 'PortNumber':((port<<8) & 0xFF00) | (port>>8)}) 233 | if ret != 0: 234 | raise MuxError("Connect failed: error %d"%ret) 235 | self.proto.connected = True 236 | return self.socket.sock 237 | def close(self): 238 | self.socket.sock.close() 239 | 240 | class USBMux(object): 241 | def __init__(self, socketpath=None): 242 | if socketpath is None: 243 | if sys.platform == 'darwin': 244 | socketpath = "/var/run/usbmuxd" 245 | else: 246 | socketpath = "/var/run/usbmuxd" 247 | self.socketpath = socketpath 248 | self.listener = MuxConnection(socketpath, BinaryProtocol) 249 | try: 250 | self.listener.listen() 251 | self.version = 0 252 | self.protoclass = BinaryProtocol 253 | except MuxVersionError: 254 | self.listener = MuxConnection(socketpath, PlistProtocol) 255 | self.listener.listen() 256 | self.protoclass = PlistProtocol 257 | self.version = 1 258 | self.devices = self.listener.devices 259 | def process(self, timeout=10): 260 | self.listener.process(timeout) 261 | def connect(self, device, port): 262 | connector = MuxConnection(self.socketpath, self.protoclass) 263 | return connector.connect(device, port) 264 | 265 | 266 | class UsbmuxdClient(MuxConnection): 267 | 268 | def __init__(self): 269 | super(UsbmuxdClient, self).__init__("/var/run/usbmuxd", PlistProtocol) 270 | 271 | def get_pair_record(self, udid): 272 | tag = self.pkttag 273 | self.pkttag += 1 274 | payload = {"PairRecordID": udid} 275 | self.proto.sendpacket("ReadPairRecord", tag, payload) 276 | _, recvtag, data = self.proto.getpacket() 277 | if recvtag != tag: 278 | raise MuxError("Reply tag mismatch: expected %d, got %d" % (tag, recvtag)) 279 | if PY3: 280 | pair_record = data['PairRecordData'] 281 | pair_record = plistlib.loads(pair_record) 282 | else: 283 | pair_record = data['PairRecordData'] 284 | pair_record = plistlib.readPlist(pair_record) 285 | return pair_record 286 | 287 | 288 | if __name__ == "__main__": 289 | mux = USBMux() 290 | print("Waiting for devices...") 291 | if not mux.devices: 292 | mux.process(0.1) 293 | while True: 294 | for dev in mux.devices: 295 | print("Device:", dev) 296 | mux.process() 297 | -------------------------------------------------------------------------------- /pymobiledevice/util/__init__.py: -------------------------------------------------------------------------------- 1 | from past.builtins import xrange 2 | import glob 3 | import plistlib 4 | import os 5 | import gzip 6 | from optparse import * 7 | from six import PY3 8 | 9 | try: 10 | import cPickle 11 | except ImportError: 12 | import pickle as cPickle 13 | plistlib.readPlistFromString = plistlib.loads 14 | plistlib.readPlist = plistlib.load 15 | 16 | def read_file(filename): 17 | f = open(filename, "rb") 18 | data = f.read() 19 | f.close() 20 | return data 21 | 22 | def write_file(filename,data): 23 | f = open(filename, "wb") 24 | f.write(data) 25 | f.close() 26 | 27 | def makedirs(dirs): 28 | try: 29 | os.makedirs(dirs) 30 | except: 31 | pass 32 | 33 | def getHomePath(foldername, filename): 34 | home = os.path.expanduser('~') 35 | folderpath = os.path.join(home, foldername) 36 | if not os.path.exists(folderpath): 37 | makedirs(folderpath) 38 | return os.path.join(folderpath, filename) 39 | 40 | def readHomeFile(foldername, filename): 41 | path = getHomePath(foldername, filename) 42 | if not os.path.exists(path): 43 | return None 44 | return read_file(path) 45 | 46 | #return path to HOME+foldername+filename 47 | def writeHomeFile(foldername, filename, data): 48 | filepath = getHomePath(foldername, filename) 49 | write_file(filepath, data) 50 | return filepath 51 | 52 | def readPlist(filename): 53 | return plistlib.readPlist(filename) 54 | 55 | def parsePlist(s): 56 | return plistlib.readPlistFromString(s) 57 | 58 | #http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size 59 | def sizeof_fmt(num): 60 | for x in ['bytes','KB','MB','GB','TB']: 61 | if num < 1024.0: 62 | return "%d%s" % (num, x) 63 | num /= 1024.0 64 | 65 | #http://www.5dollarwhitebox.org/drupal/node/84 66 | def convert_bytes(bytes): 67 | bytes = float(bytes) 68 | if bytes >= 1099511627776: 69 | terabytes = bytes / 1099511627776 70 | size = '%.2fT' % terabytes 71 | elif bytes >= 1073741824: 72 | gigabytes = bytes / 1073741824 73 | size = '%.2fG' % gigabytes 74 | elif bytes >= 1048576: 75 | megabytes = bytes / 1048576 76 | size = '%.2fM' % megabytes 77 | elif bytes >= 1024: 78 | kilobytes = bytes / 1024 79 | size = '%.2fK' % kilobytes 80 | else: 81 | size = '%.2fb' % bytes 82 | return size 83 | 84 | def xor_strings(a,b): 85 | r="" 86 | for i in xrange(len(a)): 87 | r+= chr(ord(a[i])^ord(b[i])) 88 | return r 89 | 90 | 91 | if PY3: 92 | hex = lambda data: " ".join("%02X" % i for i in data) 93 | ascii = lambda data: "".join(chr(c) if 31 < c < 127 else "." for c in data) 94 | else: 95 | hex = lambda data: " ".join("%02X" % ord(i) for i in data) 96 | ascii = lambda data: "".join(c if 31 < ord(c) < 127 else "." for c in data) 97 | 98 | def hexdump(d): 99 | for i in xrange(0,len(d),16): 100 | data = d[i:i+16] 101 | print("%08X | %s | %s" % (i, hex(data).ljust(47), ascii(data))) 102 | 103 | def search_plist(directory, matchDict): 104 | for p in map(os.path.normpath, glob.glob(directory + "/*.plist")): 105 | try: 106 | d = plistlib.readPlist(p) 107 | ok = True 108 | for k,v in matchDict.items(): 109 | if d.get(k) != v: 110 | ok = False 111 | break 112 | if ok: 113 | print("Using plist file %s" % p) 114 | return d 115 | except: 116 | continue 117 | 118 | def save_pickle(filename,data): 119 | f = gzip.open(filename,"wb") 120 | cPickle.dump(data, f, cPickle.HIGHEST_PROTOCOL) 121 | f.close() 122 | 123 | def load_pickle(filename): 124 | f = gzip.open(filename,"rb") 125 | data = cPickle.load(f) 126 | f.close() 127 | return data 128 | 129 | 130 | class MultipleOption(Option): 131 | ACTIONS = Option.ACTIONS + ("extend",) 132 | STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) 133 | TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) 134 | ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) 135 | 136 | def take_action(self, action, dest, opt, value, values, parser): 137 | if action == "extend": 138 | values.ensure_value(dest, []).append(value) 139 | else: 140 | Option.take_action(self, action, dest, opt, value, values, parser) 141 | -------------------------------------------------------------------------------- /pymobiledevice/util/asciitables.py: -------------------------------------------------------------------------------- 1 | 2 | def print_table(title, headers, rows): 3 | widths = [] 4 | 5 | for i in range(len(headers)): 6 | z = list(map(len, [row[i] for row in rows])) 7 | z.append(len(headers[i])) 8 | widths.append(max(z)) 9 | 10 | width = sum(widths) + len(headers) + 1 11 | print("-"* width) 12 | print("|" + title.center(width-2) + "|") 13 | print("-"* width) 14 | hline = "|" 15 | for i in range(len(headers)): 16 | hline += headers[i].ljust(widths[i]) + "|" 17 | print(hline) 18 | 19 | print("-"* width) 20 | for row in rows: 21 | line = "|" 22 | for i in range(len(row)): 23 | line += row[i].ljust(widths[i]) + "|" 24 | print(line) 25 | 26 | if len(rows) == 0: 27 | print("|" + "No entries".center(width-2) + "|") 28 | print("-"* width) 29 | print("") 30 | -------------------------------------------------------------------------------- /pymobiledevice/util/bdev.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from util import sizeof_fmt, hexdump 4 | from progressbar import ProgressBar 5 | from crypto.aes import AESdecryptCBC, AESencryptCBC 6 | 7 | class FileBlockDevice(object): 8 | def __init__(self, filename, offset=0, write=False): 9 | flag = os.O_RDONLY if not write else os.O_RDWR 10 | if sys.platform == 'win32': 11 | flag = flag | os.O_BINARY 12 | self.filename = filename 13 | self.fd = os.open(filename, flag) 14 | self.offset = offset 15 | self.writeFlag = write 16 | self.size = os.path.getsize(filename) 17 | self.setBlockSize(8192) 18 | 19 | def setBlockSize(self, bs): 20 | self.blockSize = bs 21 | self.nBlocks = self.size / bs 22 | 23 | def readBlock(self, blockNum): 24 | os.lseek(self.fd, self.offset + self.blockSize * blockNum, os.SEEK_SET) 25 | return os.read(self.fd, self.blockSize) 26 | 27 | def write(self, offset, data): 28 | if self.writeFlag: #fail silently for testing 29 | os.lseek(self.fd, self.offset + offset, os.SEEK_SET) 30 | return os.write(self.fd, data) 31 | 32 | def writeBlock(self, lba, block): 33 | return self.write(lba*self.blockSize, block) 34 | 35 | class FTLBlockDevice(object): 36 | def __init__(self, nand, first_lba, last_lba, defaultKey=None): 37 | self.nand = nand 38 | self.pageSize = nand.pageSize 39 | self.blockSize = 0 #not used 40 | self.key = defaultKey 41 | self.lbaoffset = first_lba 42 | self.last_lba = last_lba 43 | self.setBlockSize(self.pageSize) 44 | 45 | def setBlockSize(self, bs): 46 | self.blockSize = bs 47 | self.lbasPerPage = self.pageSize / bs 48 | self.lbaToLpnFactor = bs / (self.pageSize+0.0) 49 | self.pagesPerLBA = bs / self.pageSize 50 | if bs > self.pageSize: 51 | pass#raise Exception("FTLBlockDevice lba-size > pageSize not handled") 52 | 53 | def readBlock(self, blockNum): 54 | #if (self.lbaoffset + blockNum / self.lbasPerPage) > self.last_lba: 55 | # print "readBlock past last lba", blockNum 56 | # print "readBlock past last lba", blockNum 57 | # return "\x00" * self.blockSize 58 | lpn = int(self.lbaoffset + blockNum * self.lbaToLpnFactor) 59 | d = self.nand.readLPN(lpn, self.key) 60 | for i in range(1, self.pagesPerLBA): 61 | d += self.nand.readLPN(lpn + i, self.key) 62 | if self.lbasPerPage: 63 | zz = blockNum % self.lbasPerPage 64 | return d[zz*self.blockSize:(zz+1)*self.blockSize] 65 | return d 66 | 67 | def write(self, offset, data): 68 | raise Exception("FTLBlockDevice write method not implemented") 69 | 70 | def writeBlock(self, lba, block): 71 | raise Exception("FTLBlockDevice writeBlock method not implemented") 72 | 73 | def dumpToFile(self, outputfilename): 74 | hs = sizeof_fmt((self.last_lba - self.lbaoffset) * self.pageSize) 75 | print("Dumping partition to %s (%s)" % (outputfilename, hs)) 76 | flags = os.O_CREAT | os.O_RDWR 77 | if sys.platform == "win32": 78 | flags |= os.O_BINARY 79 | fd=os.open(outputfilename, flags) 80 | 81 | pbar = ProgressBar(self.last_lba - self.lbaoffset - 1) 82 | pbar.start() 83 | for i in range(self.lbaoffset, self.last_lba): 84 | pbar.update(i-self.lbaoffset) 85 | d = self.nand.readLPN(i, self.key) 86 | if i == self.lbaoffset and d[0x400:0x402] != "HX": 87 | print("FAIL? Not HFS partition or wrong key") 88 | os.write(fd, d) 89 | pbar.finish() 90 | os.close(fd) 91 | 92 | class IMG3BlockDevice(object): 93 | def __init__(self, filename, key, iv, write=False): 94 | flag = os.O_RDONLY if not write else os.O_RDWR 95 | if sys.platform == 'win32': 96 | flag = flag | os.O_BINARY 97 | self.filename = filename 98 | self.fd = os.open(filename, flag) 99 | self.writeFlag = write 100 | d = os.read(self.fd, 8192) 101 | if d[:4] != "3gmI": 102 | raise Exception("IMG3BlockDevice bad magic %s" % d[:4]) 103 | if d[0x34:0x38] != "ATAD": 104 | raise Exception("Fu") 105 | self.encrypted = True 106 | self.key = key 107 | self.iv0 = iv 108 | self.offset = 0x40 109 | self.size = os.path.getsize(filename) 110 | self.setBlockSize(8192) 111 | 112 | def setBlockSize(self, bs): 113 | self.blockSize = bs 114 | self.nBlocks = self.size / bs 115 | self.ivs = {0: self.iv0} 116 | 117 | def getIVforBlock(self, blockNum): 118 | #read last 16 bytes of previous block to get IV 119 | if blockNum not in self.ivs: 120 | os.lseek(self.fd, self.offset + self.blockSize * 121 | blockNum - 16, os.SEEK_SET) 122 | self.ivs[blockNum] = os.read(self.fd, 16) 123 | return self.ivs[blockNum] 124 | 125 | def readBlock(self, blockNum): 126 | os.lseek(self.fd, self.offset + self.blockSize * blockNum, os.SEEK_SET) 127 | data = os.read(self.fd, self.blockSize) 128 | if self.encrypted: 129 | data = AESdecryptCBC(data, self.key, self.getIVforBlock(blockNum)) 130 | return data 131 | 132 | def _write(self, offset, data): 133 | if self.writeFlag: #fail silently for testing 134 | os.lseek(self.fd, self.offset + offset, os.SEEK_SET) 135 | return os.write(self.fd, data) 136 | 137 | def writeBlock(self, lba, data): 138 | if self.encrypted: 139 | data = AESencryptCBC(data, self.key, self.getIVforBlock(lba)) 140 | return self._write(lba*self.blockSize, data) 141 | -------------------------------------------------------------------------------- /pymobiledevice/util/bpatch.py: -------------------------------------------------------------------------------- 1 | from bsdiff import Patch 2 | import os, sys 3 | import bz2 4 | from array import array 5 | import struct 6 | from six import StringIO 7 | 8 | def patch(old_file_name, new_file_name, patch_file_name): 9 | 10 | patch_file = open(patch_file_name, "rb") 11 | patch_file.read(8) #magic number 12 | 13 | compressed_control_len = offtin(patch_file.read(8)) 14 | compressed_diff_len = offtin(patch_file.read(8)) 15 | new_file_len = offtin(patch_file.read(8)) 16 | 17 | compressed_control_block = patch_file.read(compressed_control_len) 18 | compressed_diff_block = patch_file.read(compressed_diff_len) 19 | compressed_extra_block = patch_file.read() 20 | 21 | control_stream = StringIO(bz2.decompress(compressed_control_block)) 22 | diff_string = bz2.decompress(compressed_diff_block) 23 | extra_string = bz2.decompress(compressed_extra_block) 24 | 25 | control_tuples_list = [] 26 | while True: 27 | r = control_stream.read(8) 28 | if not r: 29 | break 30 | x = offtin(r) 31 | y = offtin(control_stream.read(8)) 32 | z = offtin(control_stream.read(8)) 33 | control_tuples_list.append((x,y,z)) 34 | 35 | old_data = open(old_file_name, "rb").read() 36 | new_data = Patch(old_data, new_file_len, control_tuples_list, diff_string, extra_string) 37 | 38 | new_file = open(new_file_name, "wb") 39 | new_file.write(new_data) 40 | 41 | def offtin(buf): 42 | s = ord(buf[7]) 43 | y = (s & 0x7F) << 56 44 | y += ord(buf[6]) << 48 45 | y += ord(buf[5]) << 40 46 | y += ord(buf[4]) << 32 47 | y += ord(buf[3]) << 24 48 | y += ord(buf[2]) << 16 49 | y += ord(buf[1]) << 8 50 | y += ord(buf[0]) 51 | 52 | if (s & 0x80): 53 | y = -y 54 | 55 | return y 56 | 57 | 58 | if __name__ == '__main__': 59 | if(len(sys.argv) < 4): 60 | print("bpatch: usage: python bpatch.py oldfile newfile patchfile") 61 | else: 62 | old_file_name = sys.argv[1] 63 | new_file_name = sys.argv[2] 64 | patch_file_name = sys.argv[3] 65 | patch(old_file_name, new_file_name, patch_file_name) 66 | 67 | -------------------------------------------------------------------------------- /pymobiledevice/util/bplist.py: -------------------------------------------------------------------------------- 1 | """ 2 | http://github.com/farcaller/bplist-python/blob/master/bplist.py 3 | """ 4 | import struct 5 | import plistlib 6 | from datetime import datetime, timedelta 7 | 8 | class BPListWriter(object): 9 | def __init__(self, objects): 10 | self.bplist = "" 11 | self.objects = objects 12 | 13 | def binary(self): 14 | '''binary -> string 15 | 16 | Generates bplist 17 | ''' 18 | self.data = 'bplist00' 19 | 20 | # TODO: flatten objects and count max length size 21 | 22 | # TODO: write objects and save offsets 23 | 24 | # TODO: write offsets 25 | 26 | # TODO: write metadata 27 | 28 | return self.data 29 | 30 | def write(self, filename): 31 | ''' 32 | 33 | Writes bplist to file 34 | ''' 35 | if self.bplist != "": 36 | pass 37 | # TODO: save self.bplist to file 38 | else: 39 | raise Exception('BPlist not yet generated') 40 | 41 | class BPlistReader(object): 42 | def __init__(self, s): 43 | self.data = s 44 | self.objects = [] 45 | self.resolved = {} 46 | 47 | def __unpackIntStruct(self, sz, s): 48 | '''__unpackIntStruct(size, string) -> int 49 | 50 | Unpacks the integer of given size (1, 2 or 4 bytes) from string 51 | ''' 52 | if sz == 1: 53 | ot = '!B' 54 | elif sz == 2: 55 | ot = '!H' 56 | elif sz == 4: 57 | ot = '!I' 58 | elif sz == 8: 59 | ot = '!Q' 60 | else: 61 | raise Exception('int unpack size '+str(sz)+' unsupported') 62 | return struct.unpack(ot, s)[0] 63 | 64 | def __unpackInt(self, offset): 65 | '''__unpackInt(offset) -> int 66 | 67 | Unpacks int field from plist at given offset 68 | ''' 69 | return self.__unpackIntMeta(offset)[1] 70 | 71 | def __unpackIntMeta(self, offset): 72 | '''__unpackIntMeta(offset) -> (size, int) 73 | 74 | Unpacks int field from plist at given offset and returns its size and value 75 | ''' 76 | obj_header = struct.unpack('!B', self.data[offset])[0] 77 | obj_type, obj_info = (obj_header & 0xF0), (obj_header & 0x0F) 78 | int_sz = 2**obj_info 79 | return int_sz, self.__unpackIntStruct(int_sz, self.data[offset+1:offset+1+int_sz]) 80 | 81 | def __resolveIntSize(self, obj_info, offset): 82 | '''__resolveIntSize(obj_info, offset) -> (count, offset) 83 | 84 | Calculates count of objref* array entries and returns count and offset to first element 85 | ''' 86 | if obj_info == 0x0F: 87 | ofs, obj_count = self.__unpackIntMeta(offset+1) 88 | objref = offset+2+ofs 89 | else: 90 | obj_count = obj_info 91 | objref = offset+1 92 | return obj_count, objref 93 | 94 | def __unpackFloatStruct(self, sz, s): 95 | '''__unpackFloatStruct(size, string) -> float 96 | 97 | Unpacks the float of given size (4 or 8 bytes) from string 98 | ''' 99 | if sz == 4: 100 | ot = '!f' 101 | elif sz == 8: 102 | ot = '!d' 103 | else: 104 | raise Exception('float unpack size '+str(sz)+' unsupported') 105 | return struct.unpack(ot, s)[0] 106 | 107 | def __unpackFloat(self, offset): 108 | '''__unpackFloat(offset) -> float 109 | 110 | Unpacks float field from plist at given offset 111 | ''' 112 | obj_header = struct.unpack('!B', self.data[offset])[0] 113 | obj_type, obj_info = (obj_header & 0xF0), (obj_header & 0x0F) 114 | int_sz = 2**obj_info 115 | return int_sz, self.__unpackFloatStruct(int_sz, self.data[offset+1:offset+1+int_sz]) 116 | 117 | def __unpackDate(self, offset): 118 | td = int(struct.unpack(">d", self.data[offset+1:offset+9])[0]) 119 | return datetime(year=2001,month=1,day=1) + timedelta(seconds=td) 120 | 121 | def __unpackItem(self, offset): 122 | '''__unpackItem(offset) 123 | 124 | Unpacks and returns an item from plist 125 | ''' 126 | obj_header = struct.unpack('!B', self.data[offset])[0] 127 | obj_type, obj_info = (obj_header & 0xF0), (obj_header & 0x0F) 128 | if obj_type == 0x00: 129 | if obj_info == 0x00: # null 0000 0000 130 | return None 131 | elif obj_info == 0x08: # bool 0000 1000 // false 132 | return False 133 | elif obj_info == 0x09: # bool 0000 1001 // true 134 | return True 135 | elif obj_info == 0x0F: # fill 0000 1111 // fill byte 136 | raise Exception("0x0F Not Implemented") # this is really pad byte, FIXME 137 | else: 138 | raise Exception('unpack item type '+str(obj_header)+' at '+str(offset)+ 'failed') 139 | elif obj_type == 0x10: # int 0001 nnnn ... // # of bytes is 2^nnnn, big-endian bytes 140 | return self.__unpackInt(offset) 141 | elif obj_type == 0x20: # real 0010 nnnn ... // # of bytes is 2^nnnn, big-endian bytes 142 | return self.__unpackFloat(offset) 143 | elif obj_type == 0x30: # date 0011 0011 ... // 8 byte float follows, big-endian bytes 144 | return self.__unpackDate(offset) 145 | elif obj_type == 0x40: # data 0100 nnnn [int] ... // nnnn is number of bytes unless 1111 then int count follows, followed by bytes 146 | obj_count, objref = self.__resolveIntSize(obj_info, offset) 147 | return plistlib.Data(self.data[objref:objref+obj_count]) # XXX: we return data as str 148 | elif obj_type == 0x50: # string 0101 nnnn [int] ... // ASCII string, nnnn is # of chars, else 1111 then int count, then bytes 149 | obj_count, objref = self.__resolveIntSize(obj_info, offset) 150 | return self.data[objref:objref+obj_count] 151 | elif obj_type == 0x60: # string 0110 nnnn [int] ... // Unicode string, nnnn is # of chars, else 1111 then int count, then big-endian 2-byte uint16_t 152 | obj_count, objref = self.__resolveIntSize(obj_info, offset) 153 | return self.data[objref:objref+obj_count*2].decode('utf-16be') 154 | elif obj_type == 0x80: # uid 1000 nnnn ... // nnnn+1 is # of bytes 155 | # FIXME: Accept as a string for now 156 | obj_count, objref = self.__resolveIntSize(obj_info, offset) 157 | return plistlib.Data(self.data[objref:objref+obj_count]) 158 | elif obj_type == 0xA0: # array 1010 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows 159 | obj_count, objref = self.__resolveIntSize(obj_info, offset) 160 | arr = [] 161 | for i in range(obj_count): 162 | arr.append(self.__unpackIntStruct(self.object_ref_size, self.data[objref+i*self.object_ref_size:objref+i*self.object_ref_size+self.object_ref_size])) 163 | return arr 164 | elif obj_type == 0xC0: # set 1100 nnnn [int] objref* // nnnn is count, unless '1111', then int count follows 165 | # XXX: not serializable via apple implementation 166 | raise Exception("0xC0 Not Implemented") # FIXME: implement 167 | elif obj_type == 0xD0: # dict 1101 nnnn [int] keyref* objref* // nnnn is count, unless '1111', then int count follows 168 | obj_count, objref = self.__resolveIntSize(obj_info, offset) 169 | keys = [] 170 | for i in range(obj_count): 171 | keys.append(self.__unpackIntStruct(self.object_ref_size, self.data[objref+i*self.object_ref_size:objref+i*self.object_ref_size+self.object_ref_size])) 172 | values = [] 173 | objref += obj_count*self.object_ref_size 174 | for i in range(obj_count): 175 | values.append(self.__unpackIntStruct(self.object_ref_size, self.data[objref+i*self.object_ref_size:objref+i*self.object_ref_size+self.object_ref_size])) 176 | dic = {} 177 | for i in range(obj_count): 178 | dic[keys[i]] = values[i] 179 | return dic 180 | else: 181 | raise Exception('don\'t know how to unpack obj type '+hex(obj_type)+' at '+str(offset)) 182 | 183 | def __resolveObject(self, idx): 184 | try: 185 | return self.resolved[idx] 186 | except KeyError: 187 | obj = self.objects[idx] 188 | if type(obj) == list: 189 | newArr = [] 190 | for i in obj: 191 | newArr.append(self.__resolveObject(i)) 192 | self.resolved[idx] = newArr 193 | return newArr 194 | if type(obj) == dict: 195 | newDic = {} 196 | for k,v in obj.iteritems(): 197 | rk = self.__resolveObject(k) 198 | rv = self.__resolveObject(v) 199 | newDic[rk] = rv 200 | self.resolved[idx] = newDic 201 | return newDic 202 | else: 203 | self.resolved[idx] = obj 204 | return obj 205 | 206 | def parse(self): 207 | # read header 208 | if self.data[:8] != 'bplist00': 209 | raise Exception('Bad magic') 210 | 211 | # read trailer 212 | self.offset_size, self.object_ref_size, self.number_of_objects, self.top_object, self.table_offset = struct.unpack('!6xBB4xI4xI4xI', self.data[-32:]) 213 | #print "** plist offset_size:",self.offset_size,"objref_size:",self.object_ref_size,"num_objs:",self.number_of_objects,"top:",self.top_object,"table_ofs:",self.table_offset 214 | 215 | # read offset table 216 | self.offset_table = self.data[self.table_offset:-32] 217 | self.offsets = [] 218 | ot = self.offset_table 219 | for i in xrange(self.number_of_objects): 220 | offset_entry = ot[:self.offset_size] 221 | ot = ot[self.offset_size:] 222 | self.offsets.append(self.__unpackIntStruct(self.offset_size, offset_entry)) 223 | #print "** plist offsets:",self.offsets 224 | 225 | # read object table 226 | self.objects = [] 227 | k = 0 228 | for i in self.offsets: 229 | obj = self.__unpackItem(i) 230 | #print "** plist unpacked",k,type(obj),obj,"at",i 231 | k += 1 232 | self.objects.append(obj) 233 | 234 | # rebuild object tree 235 | #for i in range(len(self.objects)): 236 | # self.__resolveObject(i) 237 | 238 | # return root object 239 | return self.__resolveObject(self.top_object) 240 | 241 | @classmethod 242 | def plistWithString(cls, s): 243 | parser = cls(s) 244 | return parser.parse() 245 | 246 | @classmethod 247 | def plistWithFile(cls, f): 248 | file = open(f,"rb") 249 | parser = cls(file.read()) 250 | file.close() 251 | return parser.parse() -------------------------------------------------------------------------------- /pymobiledevice/util/ccl_bplist.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2012, CCL Forensics 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the CCL Forensics nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL CCL FORENSICS BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | """ 27 | 28 | import sys 29 | import os 30 | import struct 31 | import datetime 32 | 33 | __version__ = "0.11" 34 | __description__ = "Converts Apple binary PList files into a native Python data structure" 35 | __contact__ = "Alex Caithness" 36 | 37 | class BplistError(Exception): 38 | pass 39 | 40 | class BplistUID: 41 | def __init__(self, value): 42 | self.value = value 43 | 44 | def __repr__(self): 45 | return "UID: {0}".format(self.value) 46 | 47 | def __str__(self): 48 | return self.__repr__() 49 | 50 | def __decode_multibyte_int(b, signed=True): 51 | if len(b) == 1: 52 | fmt = ">B" # Always unsigned? 53 | elif len(b) == 2: 54 | fmt = ">h" 55 | elif len(b) == 3: 56 | if signed: 57 | return ((b[0] << 16) | struct.unpack(">H", b[1:])[0]) - ((b[0] >> 7) * 2 * 0x800000) 58 | else: 59 | return (b[0] << 16) | struct.unpack(">H", b[1:])[0] 60 | elif len(b) == 4: 61 | fmt = ">i" 62 | elif len(b) == 8: 63 | fmt = ">q" 64 | else: 65 | raise BplistError("Cannot decode multibyte int of length {0}".format(len(b))) 66 | 67 | if signed and len(b) > 1: 68 | return struct.unpack(fmt.lower(), b)[0] 69 | else: 70 | return struct.unpack(fmt.upper(), b)[0] 71 | 72 | def __decode_float(b, signed=True): 73 | if len(b) == 4: 74 | fmt = ">f" 75 | elif len(b) == 8: 76 | fmt = ">d" 77 | else: 78 | raise BplistError("Cannot decode float of length {0}".format(len(b))) 79 | 80 | if signed: 81 | return struct.unpack(fmt.lower(), b)[0] 82 | else: 83 | return struct.unpack(fmt.upper(), b)[0] 84 | 85 | def __decode_object(f, offset, collection_offset_size, offset_table): 86 | # Move to offset and read type 87 | #print("Decoding object at offset {0}".format(offset)) 88 | f.seek(offset) 89 | # A little hack to keep the script portable between py2.x and py3k 90 | if sys.version_info[0] < 3: 91 | type_byte = ord(f.read(1)[0]) 92 | else: 93 | type_byte = f.read(1)[0] 94 | #print("Type byte: {0}".format(hex(type_byte))) 95 | if type_byte == 0x00: # Null 0000 0000 96 | return None 97 | elif type_byte == 0x08: # False 0000 1000 98 | return False 99 | elif type_byte == 0x09: # True 0000 1001 100 | return True 101 | elif type_byte == 0x0F: # Fill 0000 1111 102 | raise BplistError("Fill type not currently supported at offset {0}".format(f.tell())) # Not sure what to return really... 103 | elif type_byte & 0xF0 == 0x10: # Int 0001 xxxx 104 | int_length = 2 ** (type_byte & 0x0F) 105 | int_bytes = f.read(int_length) 106 | return __decode_multibyte_int(int_bytes) 107 | elif type_byte & 0xF0 == 0x20: # Float 0010 nnnn 108 | float_length = 2 ** (type_byte & 0x0F) 109 | float_bytes = f.read(float_length) 110 | return __decode_float(float_bytes) 111 | elif type_byte & 0xFF == 0x33: # Date 0011 0011 112 | date_bytes = f.read(8) 113 | date_value = __decode_float(date_bytes) 114 | return datetime.datetime(2001,1,1) + datetime.timedelta(seconds = date_value) 115 | elif type_byte & 0xF0 == 0x40: # Data 0100 nnnn 116 | if type_byte & 0x0F != 0x0F: 117 | # length in 4 lsb 118 | data_length = type_byte & 0x0F 119 | else: 120 | # A little hack to keep the script portable between py2.x and py3k 121 | if sys.version_info[0] < 3: 122 | int_type_byte = ord(f.read(1)[0]) 123 | else: 124 | int_type_byte = f.read(1)[0] 125 | if int_type_byte & 0xF0 != 0x10: 126 | raise BplistError("Long Data field definition not followed by int type at offset {0}".format(f.tell())) 127 | int_length = 2 ** (int_type_byte & 0x0F) 128 | int_bytes = f.read(int_length) 129 | data_length = __decode_multibyte_int(int_bytes, False) 130 | return f.read(data_length) 131 | elif type_byte & 0xF0 == 0x50: # ASCII 0101 nnnn 132 | if type_byte & 0x0F != 0x0F: 133 | # length in 4 lsb 134 | ascii_length = type_byte & 0x0F 135 | else: 136 | # A little hack to keep the script portable between py2.x and py3k 137 | if sys.version_info[0] < 3: 138 | int_type_byte = ord(f.read(1)[0]) 139 | else: 140 | int_type_byte = f.read(1)[0] 141 | if int_type_byte & 0xF0 != 0x10: 142 | raise BplistError("Long ASCII field definition not followed by int type at offset {0}".format(f.tell())) 143 | int_length = 2 ** (int_type_byte & 0x0F) 144 | int_bytes = f.read(int_length) 145 | ascii_length = __decode_multibyte_int(int_bytes, False) 146 | return f.read(ascii_length).decode("ascii") 147 | elif type_byte & 0xF0 == 0x60: # UTF-16 0110 nnnn 148 | if type_byte & 0x0F != 0x0F: 149 | # length in 4 lsb 150 | utf16_length = (type_byte & 0x0F) * 2 # Length is characters - 16bit width 151 | else: 152 | # A little hack to keep the script portable between py2.x and py3k 153 | if sys.version_info[0] < 3: 154 | int_type_byte = ord(f.read(1)[0]) 155 | else: 156 | int_type_byte = f.read(1)[0] 157 | if int_type_byte & 0xF0 != 0x10: 158 | raise BplistError("Long UTF-16 field definition not followed by int type at offset {0}".format(f.tell())) 159 | int_length = 2 ** (int_type_byte & 0x0F) 160 | int_bytes = f.read(int_length) 161 | utf16_length = __decode_multibyte_int(int_bytes, False) * 2 162 | return f.read(utf16_length).decode("utf_16_be") 163 | elif type_byte & 0xF0 == 0x80: # UID 1000 nnnn 164 | uid_length = (type_byte & 0x0F) + 1 165 | uid_bytes = f.read(uid_length) 166 | return BplistUID(__decode_multibyte_int(uid_bytes, signed=False)) 167 | elif type_byte & 0xF0 == 0xA0: # Array 1010 nnnn 168 | if type_byte & 0x0F != 0x0F: 169 | # length in 4 lsb 170 | array_count = type_byte & 0x0F 171 | else: 172 | # A little hack to keep the script portable between py2.x and py3k 173 | if sys.version_info[0] < 3: 174 | int_type_byte = ord(f.read(1)[0]) 175 | else: 176 | int_type_byte = f.read(1)[0] 177 | if int_type_byte & 0xF0 != 0x10: 178 | raise BplistError("Long Array field definition not followed by int type at offset {0}".format(f.tell())) 179 | int_length = 2 ** (int_type_byte & 0x0F) 180 | int_bytes = f.read(int_length) 181 | array_count = __decode_multibyte_int(int_bytes, signed=False) 182 | array_refs = [] 183 | for i in range(array_count): 184 | array_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 185 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in array_refs] 186 | elif type_byte & 0xF0 == 0xC0: # Set 1010 nnnn 187 | if type_byte & 0x0F != 0x0F: 188 | # length in 4 lsb 189 | set_count = type_byte & 0x0F 190 | else: 191 | # A little hack to keep the script portable between py2.x and py3k 192 | if sys.version_info[0] < 3: 193 | int_type_byte = ord(f.read(1)[0]) 194 | else: 195 | int_type_byte = f.read(1)[0] 196 | if int_type_byte & 0xF0 != 0x10: 197 | raise BplistError("Long Set field definition not followed by int type at offset {0}".format(f.tell())) 198 | int_length = 2 ** (int_type_byte & 0x0F) 199 | int_bytes = f.read(int_length) 200 | set_count = __decode_multibyte_int(int_bytes, signed=False) 201 | set_refs = [] 202 | for i in range(set_count): 203 | set_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 204 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in set_refs] 205 | elif type_byte & 0xF0 == 0xD0: # Dict 1011 nnnn 206 | if type_byte & 0x0F != 0x0F: 207 | # length in 4 lsb 208 | dict_count = type_byte & 0x0F 209 | else: 210 | # A little hack to keep the script portable between py2.x and py3k 211 | if sys.version_info[0] < 3: 212 | int_type_byte = ord(f.read(1)[0]) 213 | else: 214 | int_type_byte = f.read(1)[0] 215 | #print("Dictionary length int byte: {0}".format(hex(int_type_byte))) 216 | if int_type_byte & 0xF0 != 0x10: 217 | raise BplistError("Long Dict field definition not followed by int type at offset {0}".format(f.tell())) 218 | int_length = 2 ** (int_type_byte & 0x0F) 219 | int_bytes = f.read(int_length) 220 | dict_count = __decode_multibyte_int(int_bytes, signed=False) 221 | key_refs = [] 222 | #print("Dictionary count: {0}".format(dict_count)) 223 | for i in range(dict_count): 224 | key_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 225 | value_refs = [] 226 | for i in range(dict_count): 227 | value_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False)) 228 | 229 | dict_result = {} 230 | for i in range(dict_count): 231 | #print("Key ref: {0}\tVal ref: {1}".format(key_refs[i], value_refs[i])) 232 | key = __decode_object(f, offset_table[key_refs[i]], collection_offset_size, offset_table) 233 | val = __decode_object(f, offset_table[value_refs[i]], collection_offset_size, offset_table) 234 | dict_result[key] = val 235 | return dict_result 236 | 237 | 238 | def load(f): 239 | """ 240 | Reads and converts a file-like object containing a binary property list. 241 | Takes a file-like object (must support reading and seeking) as an argument 242 | Returns a data structure representing the data in the property list 243 | """ 244 | # Check magic number 245 | if f.read(8) != b"bplist00": 246 | raise BplistError("Bad file header") 247 | 248 | # Read trailer 249 | f.seek(-32, os.SEEK_END) 250 | trailer = f.read(32) 251 | offset_int_size, collection_offset_size, object_count, top_level_object_index, offest_table_offset = struct.unpack(">6xbbQQQ", trailer) 252 | 253 | # Read offset table 254 | f.seek(offest_table_offset) 255 | offset_table = [] 256 | for i in range(object_count): 257 | offset_table.append(__decode_multibyte_int(f.read(offset_int_size), False)) 258 | 259 | return __decode_object(f, offset_table[top_level_object_index], collection_offset_size, offset_table) 260 | 261 | 262 | def NSKeyedArchiver_convert(o, object_table): 263 | if isinstance(o, list): 264 | return NsKeyedArchiverList(o, object_table) 265 | elif isinstance(o, dict): 266 | return NsKeyedArchiverDictionary(o, object_table) 267 | elif isinstance(o, BplistUID): 268 | return NSKeyedArchiver_convert(object_table[o.value], object_table) 269 | else: 270 | return o 271 | 272 | 273 | class NsKeyedArchiverDictionary(dict): 274 | def __init__(self, original_dict, object_table): 275 | super(NsKeyedArchiverDictionary, self).__init__(original_dict) 276 | self.object_table = object_table 277 | 278 | def __getitem__(self, index): 279 | o = super(NsKeyedArchiverDictionary, self).__getitem__(index) 280 | return NSKeyedArchiver_convert(o, self.object_table) 281 | 282 | class NsKeyedArchiverList(list): 283 | def __init__(self, original_iterable, object_table): 284 | super(NsKeyedArchiverList, self).__init__(original_iterable) 285 | self.object_table = object_table 286 | 287 | def __getitem__(self, index): 288 | o = super(NsKeyedArchiverList, self).__getitem__(index) 289 | return NSKeyedArchiver_convert(o, self.object_table) 290 | 291 | def __iter__(self): 292 | for o in super(NsKeyedArchiverList, self).__iter__(): 293 | yield NSKeyedArchiver_convert(o, self.object_table) 294 | 295 | 296 | def deserialise_NsKeyedArchiver(obj): 297 | """Deserialises an NSKeyedArchiver bplist rebuilding the structure. 298 | obj should usually be the top-level object returned by the load() 299 | function.""" 300 | 301 | # Check that this is an archiver and version we understand 302 | if not isinstance(obj, dict): 303 | raise TypeError("obj must be a dict") 304 | if "$archiver" not in obj or obj["$archiver"] != "NSKeyedArchiver": 305 | raise ValueError("obj does not contain an '$archiver' key or the '$archiver' is unrecognised") 306 | if "$version" not in obj or obj["$version"] != 100000: 307 | raise ValueError("obj does not contain a '$version' key or the '$version' is unrecognised") 308 | 309 | object_table = obj["$objects"] 310 | if "root" in obj["$top"]: 311 | return NSKeyedArchiver_convert(obj["$top"]["root"], object_table) 312 | else: 313 | return NSKeyedArchiver_convert(obj["$top"], object_table) 314 | 315 | # NSMutableDictionary convenience functions 316 | def is_nsmutabledictionary(obj): 317 | if not isinstance(obj, dict): 318 | #print("not dict") 319 | return False 320 | if "$class" not in obj.keys(): 321 | #print("no class") 322 | return False 323 | if obj["$class"].get("$classname") != "NSMutableDictionary": 324 | #print("wrong class") 325 | return False 326 | if "NS.keys" not in obj.keys(): 327 | #print("no keys") 328 | return False 329 | if "NS.objects" not in obj.keys(): 330 | #print("no objects") 331 | return False 332 | 333 | return True 334 | 335 | def convert_NSMutableDictionary(obj): 336 | """Converts a NSKeyedArchiver serialised NSMutableDictionary into 337 | a straight dictionary (rather than two lists as it is serialised 338 | as)""" 339 | 340 | # The dictionary is serialised as two lists (one for keys and one 341 | # for values) which obviously removes all convenience afforded by 342 | # dictionaries. This function converts this structure to an 343 | # actual dictionary so that values can be accessed by key. 344 | 345 | if not is_nsmutabledictionary(obj): 346 | raise ValueError("obj does not have the correct structure for a NSMutableDictionary serialised to a NSKeyedArchiver") 347 | keys = obj["NS.keys"] 348 | vals = obj["NS.objects"] 349 | 350 | # sense check the keys and values: 351 | if not isinstance(keys, list): 352 | raise TypeError("The 'NS.keys' value is an unexpected type (expected list; actual: {0}".format(type(keys))) 353 | if not isinstance(vals, list): 354 | raise TypeError("The 'NS.objects' value is an unexpected type (expected list; actual: {0}".format(type(vals))) 355 | if len(keys) != len(vals): 356 | raise ValueError("The length of the 'NS.keys' list ({0}) is not equal to that of the 'NS.objects ({1})".format(len(keys), len(vals))) 357 | 358 | result = {} 359 | for i,k in enumerate(keys): 360 | if "k" in result: 361 | raise ValueError("The 'NS.keys' list contains duplicate entries") 362 | result[k] = vals[i] 363 | 364 | return result 365 | -------------------------------------------------------------------------------- /pymobiledevice/util/cert.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | def chunks(l, n): 4 | return (l[i:i+n] for i in range(0, len(l), n)) 5 | 6 | def RSA_KEY_DER_to_PEM(data): 7 | a = ["-----BEGIN RSA PRIVATE KEY-----"] 8 | a.extend(chunks(base64.b64encode(data),64)) 9 | a.append("-----END RSA PRIVATE KEY-----") 10 | return "\n".join(a) -------------------------------------------------------------------------------- /pymobiledevice/util/cpio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf8 -*- 3 | # 4 | # $Id$ 5 | # 6 | # Copyright (c) 2012-2014 "dark[-at-]gotohack.org" 7 | # 8 | # This file is part of pymobiledevice 9 | # 10 | # pymobiledevice is free software: you can redistribute it and/or modify 11 | # it under the terms of the GNU General Public License as published by 12 | # the Free Software Foundation, either version 3 of the License, or 13 | # (at your option) any later version. 14 | # 15 | # This program is distributed in the hope that it will be useful, 16 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | # GNU General Public License for more details. 19 | # 20 | # You should have received a copy of the GNU General Public License 21 | # along with this program. If not, see . 22 | # 23 | # 24 | 25 | from pprint import pprint 26 | import sys 27 | from struct import unpack, pack 28 | import os 29 | 30 | #Values for mode, OR'd together: 31 | 32 | ISDIR = 0o040000 #Directory 33 | ISFIFO = 0o010000 #FIFO 34 | ISREG = 0o0100000 #Regular file 35 | ISBLK = 0o060000 #Block special file 36 | ISCHR = 0o020000 #Character special file 37 | ISCTG = 0o0110000 #Reserved for contiguous files 38 | ISLNK = 0o0120000 #Reserved for symbolic links 39 | ISOCK = 0o0140000 #Reserved for sockets 40 | IFMT = 0o0170000 #type of file 41 | 42 | MODEMASK = 0o0000777 43 | TRAILER = "TRAILER!!!" 44 | 45 | NEW_MAGIC = 0o070701 #New ASCII magic 46 | CRC_MAGIC = 0o070702 #New CRC magic 47 | OLD_MAGIC = 0o070707 #Old ASCII magic 48 | 49 | def version(): 50 | return '0.1' 51 | 52 | 53 | class CpioArchive(object): 54 | 55 | def __init__(self, cpiofile=None, fileobj=None, mode="rb"): 56 | if fileobj: 57 | self.ifile = fileobj 58 | else: 59 | self.ifile = open(cpiofile,mode) 60 | 61 | def is_cpiofile(self,cpiofile=None,fileobj=None): 62 | if fileobj: 63 | magic = int(fileobj.read(6),8) 64 | else: 65 | magic = int(open(cpiofile,'r').read(6),8) 66 | 67 | if magic in [NEW_MAGIC, CRC_MAGIC, OLD_MAGIC]: 68 | return True 69 | 70 | def read_old_ascii_cpio_record(self): 71 | f = {} 72 | try: 73 | f["dev"] = int(self.ifile.read(6),8) #device where file resides 74 | f["ino"] = int(self.ifile.read(6),8) #I-number of file 75 | f["mode"] = int(self.ifile.read(6),8) #Ifile mode 76 | f["uid"] = int(self.ifile.read(6),8) #owner user ID 77 | f["gid"] = int(self.ifile.read(6),8) #owner group ID 78 | f["nlink"] = int(self.ifile.read(6),8) #number of links to file 79 | f["rdev"] = int(self.ifile.read(6),8) #device major/minor for special file 80 | f["mtime"] = int(self.ifile.read(11),8) #modify time of file 81 | f["namesize"] = int(self.ifile.read(6),8) #length of file name 82 | f["filesize"] = int(self.ifile.read(11),8) #length of file to follow 83 | f["name"] = self.ifile.read(f.get("namesize"))[:-1] # Removing \x00 84 | f["data"] = self.ifile.read(f.get("filesize")) 85 | except: 86 | print('ERROR: cpio record trunked (incomplete archive)') 87 | return None 88 | return f 89 | 90 | def extract_files(self,files=None,outpath="."): 91 | print("Extracting files from CPIO archive" ) 92 | while 1: 93 | try: 94 | hdr = int(self.ifile.read(6),8) 95 | except: 96 | print('ERROR: cpio record trunked (incomplete archive)') 97 | break 98 | 99 | if hdr != OLD_MAGIC: 100 | raise NotImplementedError #FIXME Should implement new & Binary CPIO record 101 | 102 | f = self.read_old_ascii_cpio_record() 103 | if f and f.get("name") == TRAILER: 104 | break 105 | 106 | if files: 107 | if not f.get("name") in files: 108 | print("Skipped %s" % f.get("name")) 109 | continue 110 | 111 | fullOutPath = os.path.join(outpath,f.get("name").strip("../")) 112 | print("x %s" % fullOutPath) 113 | 114 | if (f.get("mode") & IFMT == ISFIFO):#FIFO 115 | if not os.path.isdir(os.path.dirname(fullOutPath)): 116 | os.makedirs(os.path.dirname(fullOutPath),0o0755) 117 | os.mkfifo(fullOutPath, f.get("mode") & MODEMASK) 118 | os.chmod(fullOutPath, f.get("mode") & MODEMASK) 119 | 120 | if (f.get("mode") & IFMT == ISDIR): #Directory 121 | if not os.path.isdir(fullOutPath): 122 | os.makedirs(fullOutPath, f.get("mode") & MODEMASK) 123 | 124 | if (f.get("mode") & IFMT == ISBLK): #Block special file 125 | raise NotImplementedError 126 | 127 | if (f.get("mode") & IFMT == ISCHR): #Character special file 128 | raise NotImplementedError 129 | 130 | if (f.get("mode") & IFMT == ISLNK): #Reserved for symbolic links 131 | raise NotImplementedError 132 | 133 | if (f.get("mode") & IFMT == ISOCK): #Reserved for sockets 134 | raise NotImplementedError 135 | 136 | if (f.get("mode") & IFMT == ISCTG) or (f.get("mode") & IFMT == ISREG): #Contiguous or Regular file 137 | if not os.path.isdir(os.path.dirname(fullOutPath)): 138 | os.makedirs(os.path.dirname(fullOutPath),0o0755) 139 | fd = open(fullOutPath,"wb") 140 | fd.write(f.get("data")) 141 | 142 | os.chmod(fullOutPath, f.get("mode") & MODEMASK) 143 | 144 | 145 | if __name__ == "__main__": 146 | a = CpioArchive(sys.argv[1],mode="rb",) 147 | a.extract_files() 148 | 149 | -------------------------------------------------------------------------------- /pymobiledevice/util/lzss.py: -------------------------------------------------------------------------------- 1 | """ 2 | /************************************************************** 3 | LZSS.C -- A Data Compression Program 4 | *************************************************************** 5 | 4/6/1989 Haruhiko Okumura 6 | Use, distribute, and modify this program freely. 7 | Please send me your improved versions. 8 | PC-VAN SCIENCE 9 | NIFTY-Serve PAF01022 10 | CompuServe 74050,1022 11 | 12 | **************************************************************/ 13 | /* 14 | * lzss.c - Package for decompressing lzss compressed objects 15 | * 16 | * Copyright (c) 2003 Apple Computer, Inc. 17 | * 18 | * DRI: Josh de Cesare 19 | */ 20 | """ 21 | from array import array 22 | import struct 23 | 24 | N = 4096 25 | F = 18 26 | THRESHOLD = 2 27 | NIL = N 28 | 29 | def decompress_lzss(str): 30 | if str[:8] !="complzss": 31 | print("decompress_lzss: complzss magic missing") 32 | return 33 | decompsize = struct.unpack(">L", str[12:16])[0] 34 | text_buf = array("B", " "*(N + F - 1)) 35 | src = array("B", str[0x180:]) 36 | srclen = len(src) 37 | dst = array("B", " "*decompsize) 38 | r = N - F 39 | srcidx, dstidx, flags, c = 0, 0, 0, 0 40 | 41 | while True: 42 | flags >>= 1 43 | if ((flags & 0x100) == 0): 44 | if (srcidx >= srclen): 45 | break 46 | c = src[srcidx] 47 | srcidx += 1 48 | flags = c | 0xFF00 49 | 50 | if (flags & 1): 51 | if (srcidx >= srclen): 52 | break 53 | c = src[srcidx] 54 | srcidx += 1 55 | dst[dstidx] = c 56 | dstidx += 1 57 | text_buf[r] = c 58 | r += 1 59 | r &= (N - 1) 60 | else: 61 | if (srcidx >= srclen): 62 | break 63 | i = src[srcidx] 64 | srcidx += 1 65 | if (srcidx >= srclen): 66 | break 67 | j = src[srcidx] 68 | srcidx += 1 69 | i |= ((j & 0xF0) << 4) 70 | j = (j & 0x0F) + THRESHOLD 71 | for k in range(j + 1): 72 | c = text_buf[(i + k) & (N - 1)] 73 | dst[dstidx] = c 74 | dstidx += 1 75 | text_buf[r] = c 76 | r += 1 77 | r &= (N - 1) 78 | return dst.tostring() 79 | 80 | -------------------------------------------------------------------------------- /pymobiledevice/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''pymobiledevice 3 | ''' 4 | VERSION = '1.3.1' 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | M2Crypto==0.38.0 ; sys_platform == 'darwin' 2 | construct>=2.9.29 3 | future 4 | pyasn1 5 | six 6 | biplist 7 | hexdump 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''package script 3 | ''' 4 | 5 | 6 | import os 7 | import platform 8 | import sys 9 | from pymobiledevice import version as pm 10 | from setuptools import setup, find_packages 11 | BASE_DIR = os.path.realpath(os.path.dirname(__file__)) 12 | VERSION = pm.VERSION 13 | 14 | def replace_version_py(version): 15 | content = """# -*- coding: utf-8 -*- 16 | '''pymobiledevice 17 | ''' 18 | VERSION = '%(version)s' 19 | """ 20 | version_py = os.path.join(BASE_DIR, 'pymobiledevice', 'version.py') 21 | with open(version_py, 'w') as fd: 22 | fd.write(content % {'version':version}) 23 | 24 | 25 | def generate_version(): 26 | version = VERSION 27 | if os.path.isfile(os.path.join(BASE_DIR, "version.txt")): 28 | with open("version.txt", "r") as fd: 29 | content = fd.read().strip() 30 | if content: 31 | version = content 32 | replace_version_py(version) 33 | return version 34 | 35 | 36 | def parse_requirements(): 37 | reqs = [] 38 | if os.path.isfile(os.path.join(BASE_DIR, "requirements.txt")): 39 | with open(os.path.join(BASE_DIR, "requirements.txt"), 'r') as fd: 40 | for line in fd.readlines(): 41 | line = line.strip() 42 | if line: 43 | reqs.append(line) 44 | if sys.platform == "win32": 45 | if "64" in platform.architecture()[0]: 46 | reqs.append('M2CryptoWin64') 47 | else: 48 | reqs.append('M2CryptoWin32') 49 | return reqs 50 | 51 | 52 | def get_description(): 53 | with open(os.path.join(BASE_DIR, "README.md"), "r", encoding="utf-8") as fh: 54 | return fh.read() 55 | 56 | 57 | if __name__ == "__main__": 58 | 59 | setup( 60 | version=generate_version(), 61 | name="pymobiledevice", 62 | description="python implementation for libimobiledevice library", 63 | long_description=get_description(), 64 | long_description_content_type='text/markdown', 65 | cmdclass={}, 66 | packages=find_packages(), 67 | package_data={'':['*.txt', '*.TXT'], }, 68 | data_files=[(".", ["requirements.txt"])], 69 | author="Mathieu Renard ", 70 | license="Copyright(c)2010-2023 Mathieu Renard All Rights Reserved. ", 71 | entry_points={'console_scripts': [ 72 | 'pymobiledevice-afc=pymobiledevice.afc:main', 73 | 'pymobiledevice-appsmanager=pymobiledevice.apps:main', 74 | 'pymobiledevice-diagnosticsrelay=pymobiledevice.diagnostic_relay:main', 75 | 'pymobiledevice-filerelay=pymobiledevice.file_relay:main', 76 | 'pymobiledevice-housearrest=pymobiledevice.house_arrest:main', 77 | 'pymobiledevice-lockdown=pymobiledevice.lockdown:main', 78 | 'pymobiledevice-mobileconfig=pymobiledevice.mobile_config:main', 79 | 'pymobiledevice-mobilebackup=pymobiledevice.mobilebackup:main', 80 | 'pymobiledevice-mobilebackup2=pymobiledevice.mobilebackup2:main', 81 | 'pymobiledevice-syslog=pymobiledevice.syslog:main', 82 | 'pymobiledevice-pcapd=pymobiledevice.pcapd:main', 83 | 'pymobiledevice-screenshotr=pymobiledevice.screenshotr:main']}, 84 | classifiers=[ 85 | "Programming Language :: Python :: 2.7", 86 | "Programming Language :: Python :: 3.6", 87 | "Programming Language :: Python :: 3.7", 88 | "Programming Language :: Python :: 3.11", 89 | ], 90 | install_requires=parse_requirements(), 91 | url="https://github.com/iOSForensics/pymobiledevice", 92 | project_urls={ 93 | "pymobiledevice Documentation":"https://github.com/iOSForensics/pymobiledevice" 94 | }, 95 | ) 96 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | import unittest 5 | import os 6 | import sys 7 | 8 | test_dir = os.path.dirname(os.path.abspath(__file__)) 9 | sys.path.insert(0, os.path.dirname(test_dir)) 10 | 11 | def main(): 12 | runner = unittest.TextTestRunner(verbosity=1 + sys.argv.count('-v')) 13 | suite = unittest.TestLoader().discover(test_dir, pattern='*test.py') 14 | raise SystemExit(not runner.run(suite).wasSuccessful()) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() -------------------------------------------------------------------------------- /test/afc_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''afc test case 3 | ''' 4 | 5 | import unittest 6 | 7 | from pymobiledevice.usbmux.usbmux import USBMux 8 | from pymobiledevice.lockdown import LockdownClient 9 | from pymobiledevice.afc import AFCShell 10 | 11 | 12 | 13 | class AfcTest(unittest.TestCase): 14 | 15 | def _get_device(self): 16 | retry_times = 5 17 | udid = None 18 | mux = USBMux() 19 | if not mux.devices: 20 | mux.process(0.1) 21 | while retry_times > 0: 22 | if len(mux.devices) > 0: 23 | udid = mux.devices[0].serial 24 | break 25 | mux.process(0.5) 26 | retry_times -= 1 27 | return udid 28 | 29 | def test_get_device_info(self): 30 | udid = self._get_device() 31 | print('udid:%s' % udid) 32 | if udid is None: 33 | print("no real device found") 34 | return 35 | lockdown = LockdownClient(udid) 36 | lockdown.startService("com.apple.afc") 37 | info = lockdown.allValues 38 | print(info) 39 | self.assertIsInstance(info, dict, 'Query device information error') 40 | 41 | def test_exec_cmd(self): 42 | udid = self._get_device() 43 | print('udid:%s' % udid) 44 | if udid is None: 45 | print("no real device found") 46 | return 47 | AFCShell().onecmd("Hello iPhone!") 48 | -------------------------------------------------------------------------------- /test/crashreportcopymobile_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''screenshotr test case 3 | ''' 4 | 5 | import unittest 6 | import os 7 | 8 | from pymobiledevice.usbmux.usbmux import USBMux 9 | from pymobiledevice.lockdown import LockdownClient 10 | from pymobiledevice.afc import AFCShell 11 | from pymobiledevice.afc import AFCClient 12 | 13 | 14 | class CrashReportTest(unittest.TestCase): 15 | 16 | def test_get_crash_log(self): 17 | mux = USBMux() 18 | if not mux.devices: 19 | mux.process(0.1) 20 | if len(mux.devices) == 0: 21 | print("no real device found") 22 | self.no_device = True 23 | return 24 | udid = mux.devices[0].serial 25 | 26 | procname = "QQ" 27 | lockdown = LockdownClient(udid) 28 | self.service = lockdown.startService("com.apple.crashreportcopymobile") 29 | client = AFCClient(lockdown,service=self.service) 30 | afc_shell = AFCShell(client=client) 31 | remote_crash_path = '/' 32 | dest_path = '/tmp' 33 | local_crashes =[] 34 | print('udid:', udid) 35 | for _dirname, _dirs, files in afc_shell.afc.dir_walk(remote_crash_path): 36 | 37 | for filename in files: 38 | if procname in filename: 39 | remote_crash_file = os.path.join(remote_crash_path, filename) 40 | data = afc_shell.afc.get_file_contents(remote_crash_file) 41 | local_crash_file = os.path.join(dest_path, filename) 42 | local_crashes.append(local_crash_file) 43 | with open(local_crash_file, 'wb') as fp: 44 | fp.write(data) 45 | print(local_crashes) 46 | 47 | def tearDown(self): 48 | if not self.no_device and self.service: 49 | self.service.close() 50 | -------------------------------------------------------------------------------- /test/diagnostics_relay_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''diagnostics_relay test case 3 | ''' 4 | 5 | import unittest 6 | import time 7 | 8 | from pymobiledevice.usbmux.usbmux import USBMux 9 | from pymobiledevice.lockdown import LockdownClient 10 | from pymobiledevice.diagnostics_relay import DIAGClient 11 | 12 | 13 | class DiagnosticsRelayTest(unittest.TestCase): 14 | 15 | def test_reboot_device(self): 16 | mux = USBMux() 17 | if not mux.devices: 18 | mux.process(0.1) 19 | if len(mux.devices) == 0: 20 | print("no real device found") 21 | return 22 | udid = mux.devices[0].serial 23 | lockdown = LockdownClient(udid) 24 | DIAGClient(lockdown).restart() 25 | time.sleep(10) 26 | for _ in range(20): 27 | mux.process(1) 28 | for dev in mux.devices: 29 | if udid == dev.serial: 30 | print('reboot successfully') 31 | return 32 | else: 33 | self.fail('reboot error: real device disconect') 34 | -------------------------------------------------------------------------------- /test/house_arrest_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''house_arrest test case 3 | ''' 4 | 5 | import unittest 6 | import os 7 | import pprint 8 | 9 | from pymobiledevice.usbmux.usbmux import USBMux 10 | from pymobiledevice.lockdown import LockdownClient 11 | from pymobiledevice.afc import AFCShell 12 | from pymobiledevice.afc import AFCClient 13 | from pymobiledevice.house_arrest import HouseArrestClient 14 | 15 | class HouseArrestTest(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.no_device = False 19 | mux = USBMux() 20 | if not mux.devices: 21 | mux.process(0.1) 22 | if len(mux.devices) == 0: 23 | print("no real device found") 24 | self.no_device = True 25 | return 26 | udid = mux.devices[0].serial 27 | self.house_arrest_client = HouseArrestClient(udid) 28 | app_bundle_id = "com.gotohack.testapp" 29 | result = self.house_arrest_client.send_command(app_bundle_id) 30 | if not result: 31 | raise RuntimeError("Launching HouseArrest failed for app:%s" % app_bundle_id) 32 | 33 | def test_list_files_in_sandbox(self): 34 | if self.no_device: 35 | return 36 | sandbox_tree =[] 37 | file_path = '/Documents' 38 | for l in self.house_arrest_client.read_directory(file_path): 39 | if l not in ('.', '..'): 40 | tmp_dict = {} 41 | tmp_dict['path'] = os.path.join(file_path, l) 42 | info = self.house_arrest_client.get_file_info(tmp_dict['path']) 43 | tmp_dict['is_dir'] = (info is not None and info['st_ifmt'] == 'S_IFDIR') 44 | sandbox_tree.append(tmp_dict) 45 | pprint.pprint(sandbox_tree) 46 | 47 | def test_push_file_to_sandbox(self): 48 | if self.no_device: 49 | return 50 | data = b"hello sandbox!" 51 | self.house_arrest_client.set_file_contents('/Documents/test.log', data) 52 | 53 | def test_pull_file_from_sandbox(self): 54 | if self.no_device: 55 | return 56 | content = self.house_arrest_client.get_file_contents('/Documents/test.log') 57 | print(content) 58 | 59 | def test_remove_file_in_sandbox(self): 60 | if self.no_device: 61 | return 62 | shell = AFCShell(client=self.house_arrest_client) 63 | shell.do_rm('/Documents/test.log') 64 | 65 | def tearDown(self): 66 | if not self.no_device and self.house_arrest_client: 67 | self.house_arrest_client.service.close() 68 | -------------------------------------------------------------------------------- /test/installation_proxy_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''installation_proxy test case 3 | ''' 4 | 5 | import unittest 6 | import time 7 | import os 8 | 9 | from pymobiledevice.usbmux.usbmux import USBMux 10 | from pymobiledevice.lockdown import LockdownClient 11 | from pymobiledevice.afc import AFCClient 12 | 13 | class InstallationProxyTest(unittest.TestCase): 14 | 15 | def setUp(self): 16 | self.no_device = False 17 | mux = USBMux() 18 | if not mux.devices: 19 | mux.process(0.1) 20 | if len(mux.devices) == 0: 21 | print("no real device found") 22 | self.no_device = True 23 | return 24 | self.udid = mux.devices[0].serial 25 | self.lockdownclient = LockdownClient(self.udid) 26 | self.service = self.lockdownclient.startService("com.apple.mobile.installation_proxy") 27 | 28 | def wait_completion(self, handler=None, *args): 29 | while True: 30 | z = self.service.recvPlist() 31 | print(type(z), z) 32 | if not z: 33 | break 34 | completion = z.get("PercentComplete") 35 | if completion: 36 | if handler: 37 | handler(completion, *args) 38 | print("%s: %s%% Complete" % (z.get("Status"), completion)) 39 | else: 40 | if z.get("Status") == "Complete" or ("Status" not in z and "CFBundleIdentifier" in z): 41 | return (True, "") 42 | else: 43 | return (False, z.get("ErrorDescription")) 44 | 45 | def test_install_app(self): 46 | if self.no_device: 47 | return 48 | ipa_path = os.path.join(os.path.expanduser("~"), "Downloads/app/DemoApp.ipa") 49 | tmp_ipa = "/t%d.ipa" % time.time() 50 | with open(ipa_path, "rb") as f: 51 | ipa_content = f.read() 52 | afc = AFCClient(self.lockdownclient) 53 | afc.set_file_contents(tmp_ipa, ipa_content) 54 | print("Upload completed") 55 | print("Starting installation") 56 | cmd = {"Command":"Install", "PackagePath": tmp_ipa} 57 | self.lockdownclient = LockdownClient(self.udid) 58 | self.service = self.lockdownclient.startService("com.apple.mobile.installation_proxy") 59 | self.service.sendPlist(cmd) 60 | result, err = self.wait_completion() 61 | self.assertTrue(result, 'install_app failed: %s' % err) 62 | 63 | def test_uninstall_app(self): 64 | if self.no_device: 65 | return 66 | bundle_id = "com.gotohack.test.demo" 67 | cmd = {"Command": "Uninstall", "ApplicationIdentifier": bundle_id} 68 | self.service.sendPlist(cmd) 69 | result, err = self.wait_completion() 70 | self.assertTrue(result, 'uninstall_app failed: %s' % err) 71 | 72 | def test_apps_info(self): 73 | if self.no_device: 74 | return 75 | self.service.sendPlist({"Command": "Lookup"}) 76 | print(self.service.recvPlist()) 77 | 78 | def test_list_apps(self, app_type='user'): 79 | if self.no_device: 80 | return 81 | options = {} 82 | if app_type == 'system': 83 | options["ApplicationType"] = "System" 84 | elif app_type == 'user': 85 | options["ApplicationType"] = "User" 86 | options["ReturnAttributes"] = ["CFBundleIdentifier", 87 | "CFBundleName", ] 88 | self.service.sendPlist({"Command": "Browse", "ClientOptions": options}) 89 | apps = [] 90 | while True: 91 | z = self.service.recvPlist() 92 | if z.get("Status") == "BrowsingApplications": 93 | apps.extend(z["CurrentList"]) 94 | elif z.get("Status") == "Complete": 95 | break 96 | else: 97 | raise Exception(z.get("ErrorDescription")) 98 | print(apps) 99 | 100 | def tearDown(self): 101 | if not self.no_device and self.service: 102 | self.service.close() 103 | -------------------------------------------------------------------------------- /test/listdevice_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''Device Query test case 3 | ''' 4 | 5 | import unittest 6 | 7 | from pymobiledevice.usbmux.usbmux import USBMux 8 | 9 | 10 | class ListDeviceTest(unittest.TestCase): 11 | 12 | 13 | def test_list_devices(self): 14 | mux = USBMux() 15 | if not mux.devices: 16 | mux.process(0.1) 17 | self.assertTrue(len(mux.devices)>=0, 'usbmuxd communication error') 18 | -------------------------------------------------------------------------------- /test/screenshotr_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''screenshotr test case 3 | ''' 4 | 5 | import unittest 6 | import tempfile 7 | 8 | from pymobiledevice.usbmux.usbmux import USBMux 9 | from pymobiledevice.lockdown import LockdownClient 10 | from pymobiledevice.screenshotr import screenshotr 11 | 12 | 13 | class ScreenshotrTest(unittest.TestCase): 14 | 15 | def test_screenshot(self): 16 | mux = USBMux() 17 | if not mux.devices: 18 | mux.process(0.1) 19 | if len(mux.devices) == 0: 20 | print("no real device found") 21 | return 22 | udid = mux.devices[0].serial 23 | lockdownclient = LockdownClient(udid) 24 | tiff_file = tempfile.NamedTemporaryFile(suffix='.tiff') 25 | tiff_file_path = tiff_file.name 26 | screenshot = screenshotr(lockdownclient) 27 | data = screenshot.take_screenshot() 28 | with open(tiff_file_path, "wb") as fd: 29 | fd.write(data) 30 | screenshot.stop_session() 31 | tiff_file.close() 32 | -------------------------------------------------------------------------------- /test/syslog_relay_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''syslog_relay test case 3 | ''' 4 | 5 | import unittest 6 | 7 | from pymobiledevice.usbmux.usbmux import USBMux 8 | from pymobiledevice.syslog import Syslog 9 | 10 | 11 | class ListDeviceTest(unittest.TestCase): 12 | 13 | def test_list_devices(self): 14 | mux = USBMux() 15 | if not mux.devices: 16 | mux.process(0.1) 17 | if len(mux.devices) == 0: 18 | print("no real device found") 19 | return 20 | syslog = Syslog() 21 | syslog.watch(10, '/tmp/sys.log', 'QQ') 22 | --------------------------------------------------------------------------------