├── pymobiledevice ├── __init__.py ├── usbmux │ ├── __init__.py │ ├── tcprelay.py │ └── usbmux.py ├── version.py ├── util │ ├── cert.py │ ├── asciitables.py │ ├── bpatch.py │ ├── lzss.py │ ├── struct2.py │ ├── __init__.py │ ├── ca.py │ ├── bdev.py │ ├── cpio.py │ └── ccl_bplist.py ├── exceptions.py ├── house_arrest.py ├── screenshotr.py ├── syslog.py ├── mobile_config.py ├── file_relay.py ├── plist_service.py ├── constants.py ├── pcapd.py ├── apps.py ├── notification_proxy.py ├── diagnostics_relay.py ├── mobilebackup.py ├── installation_proxy.py ├── lockdown.py ├── mobilebackup2.py └── afc.py ├── CREDITS.txt ├── .gitattributes ├── .gitignore ├── requirements.txt ├── test ├── listdevice_test.py ├── __init__.py ├── syslog_relay_test.py ├── screenshotr_test.py ├── diagnostics_relay_test.py ├── afc_test.py ├── crashreportcopymobile_test.py ├── house_arrest_test.py └── installation_proxy_test.py ├── .github └── workflows │ └── publish.yml ├── .travis.yml ├── setup.py └── README.md /pymobiledevice/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pymobiledevice/usbmux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pymobiledevice/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''pymobiledevice-qta版本 3 | ''' 4 | VERSION = '1.0.2' 5 | -------------------------------------------------------------------------------- /CREDITS.txt: -------------------------------------------------------------------------------- 1 | Jan0 2 | chronic dev team 3 | idroid/openiboot team 4 | iphone dev team 5 | Jonathan Zdziarski 6 | msftguy 7 | planetbeing 8 | -------------------------------------------------------------------------------- /.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 | # python编译文件 2 | *.pyc 3 | # 打包自动生成的目录 4 | build/ 5 | dist/ 6 | pymobiledevice2.egg-info/ 7 | version.txt 8 | # 调试目录 9 | debug/ 10 | # Mac系统自动生成的文件 11 | *.DS_Store 12 | 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | M2Crypto==0.32.0; python_version < '3.9' 2 | M2Crypto; python_version >= '3.9' 3 | construct>=2.9.29 4 | pyasn1 5 | future 6 | six 7 | biplist 8 | attrs 9 | typing 10 | enum34 11 | packaging -------------------------------------------------------------------------------- /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) -------------------------------------------------------------------------------- /test/listdevice_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''查询设备的测试用例 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通信异常') -------------------------------------------------------------------------------- /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() -------------------------------------------------------------------------------- /pymobiledevice/exceptions.py: -------------------------------------------------------------------------------- 1 | from errno import ENOENT, ENOTDIR 2 | 3 | from .constants import AFC_E_UNKNOWN_ERROR, AFC_ERROR_NAMES, AFC_E_OBJECT_NOT_FOUND, AFC_E_OBJECT_IS_DIR 4 | 5 | AFC_TO_OS_ERROR_CODES = { 6 | AFC_E_OBJECT_NOT_FOUND: ENOENT, 7 | AFC_E_OBJECT_IS_DIR: ENOTDIR, 8 | } 9 | 10 | 11 | class PyMobileDeviceException(Exception): 12 | pass 13 | 14 | class MuxError(PyMobileDeviceException): 15 | pass 16 | 17 | 18 | class ServiceError(PyMobileDeviceException): 19 | pass 20 | 21 | 22 | class ConnectionError(PyMobileDeviceException): 23 | pass 24 | -------------------------------------------------------------------------------- /test/syslog_relay_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''syslog_relay的测试用例 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') -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/screenshotr_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''screenshotr的测试用例 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() -------------------------------------------------------------------------------- /test/diagnostics_relay_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''diagnostics_relay的测试用例 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') -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | release=${{ github.ref }} 31 | echo ${release: 10} > version.txt 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/* -------------------------------------------------------------------------------- /test/afc_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''afc的测试用例 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, '查询设备信息错误') 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 | -------------------------------------------------------------------------------- /pymobiledevice/house_arrest.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | import logging 5 | 6 | from .afc import AFCClient, AFCShell 7 | from .lockdown import LockdownClient 8 | 9 | 10 | class HouseArrestService(AFCClient): 11 | SERVICE_NAME = "com.apple.mobile.house_arrest" 12 | RSD_SERVICE_NAME = 'com.apple.mobile.house_arrest.shim.remote' 13 | 14 | def __init__(self, lockdown=None, udid=None, logger=None): 15 | self.logger = logger or logging.getLogger(__name__) 16 | self.lockdown = lockdown or LockdownClient(udid=udid) 17 | SERVICE_NAME = self.SERVICE_NAME 18 | 19 | super(HouseArrestService, self).__init__(self.lockdown, SERVICE_NAME) 20 | 21 | def stop_session(self): 22 | self.logger.info("Disconecting...") 23 | self.service.close() 24 | 25 | def send_command(self, applicationId, cmd="VendContainer"): 26 | self.service.sendPlist({"Command": cmd, "Identifier": applicationId}) 27 | res = self.service.recvPlist() 28 | if res.get("Error"): 29 | self.logger.error("%s : %s", applicationId, res.get("Error")) 30 | return False 31 | else: 32 | return True 33 | 34 | def shell(self, applicationId, cmd="VendContainer"): 35 | res = self.send_command(applicationId, cmd) 36 | if res: 37 | AFCShell(client=self).cmdloop() 38 | 39 | 40 | if __name__ == '__main__': 41 | HouseArrestService().shell('cn.rongcloud.rce.autotest.xctrunner') 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | matrix: 3 | include: 4 | - os: linux 5 | python: '2.7' 6 | language: generic 7 | dist: xenial 8 | - os: linux 9 | python: '3.6' 10 | language: generic 11 | dist: xenial 12 | install: 13 | - pip install -r requirements.txt --user 14 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then 15 | pip install m2crypto --user; 16 | sudo apt-get install -y usbmuxd; 17 | fi 18 | script: 19 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then 20 | python test/__init__.py; 21 | fi 22 | before_deploy: 23 | - echo $TRAVIS_TAG > version.txt 24 | - export PATH=/Users/travis/Library/Python/2.7/bin:$PATH 25 | deploy: 26 | skip_cleanup: true 27 | skip_existing: true 28 | provider: pypi 29 | user: qta 30 | password: 31 | secure: tzAoq5UnwaqQGHcH4qSnN0hfFop7f/Gjgf3DTWrblpn+vBIOBk4umJIAAF2u5tUHuhxm14UmDEuDciQM9j6DEoy+7VPhe8KY8cVhBjIYpEIBjYr4WeRsl2rhDPlYnp5wDHdsY4bsJgtOSjVCbdKZ9VhV0cjUegKU3oEX5k1qu/XPDNbiZhWs/zjyXwKAbnTBqcJlcdZphVeEgC9E8DMCQRRTfNXtqSmZj8EkjSwlbIAldZflqX7/Xg7/pFPaKjwQW1BvIaPUqvvwQLGvyqr4zovF4SQ8UpUPHMuhYY/0jJijgRe0VkI9SsCXbWFmMZrgkyfvjOmGLc3dYaaINh1jf6zoVWddtgeRW9mNs0GzcPz9PefefeNgIQ9yyM99Uv0nqrAdARO87SrtGCQ4CNrAOwaXJElzKcTJOfpbrLedbnqczzC5wa5LZYsMZudiULpiPOEK6MZ4spOnt8D2/8tSIOQMfY3BF5vUXYBEM+uuj38lKfZGXT2Ox9XzMZZPjkV5F5ky73oAdMEPTBJWUAI2xXeESBMSfh7NP0019kTloKOpDsgFknGQdLfGWdcONWmJWP1yreavcz9fLEWJXz+mdTzUcVKg2JLouUmhAdKtlU9oS6n9R3T0OZ0QlltLqnkp+IC0KouaTLrIBZ16IoYjnxJ3/pc/+FN8lV3nY/XFsnY= 32 | distributions: "sdist" 33 | on: 34 | tags: true 35 | branch: master 36 | condition: $TRAVIS_TAG =~ [0-9]+\.[0-9]+\.[0-9]+ 37 | -------------------------------------------------------------------------------- /test/crashreportcopymobile_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''screenshotr的测试用例 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() -------------------------------------------------------------------------------- /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 | 44 | y = (s & 0x7F) << 56 45 | y += ord(buf[6]) << 48 46 | y += ord(buf[5]) << 40 47 | y += ord(buf[4]) << 32 48 | y += ord(buf[3]) << 24 49 | y += ord(buf[2]) << 16 50 | y += ord(buf[1]) << 8 51 | y += ord(buf[0]) 52 | 53 | if (s & 0x80): 54 | y = -y 55 | 56 | return y 57 | 58 | 59 | if __name__ == '__main__': 60 | if(len(sys.argv) < 4): 61 | print("bpatch: usage: python bpatch.py oldfile newfile patchfile") 62 | else: 63 | old_file_name = sys.argv[1] 64 | new_file_name = sys.argv[2] 65 | patch_file_name = sys.argv[3] 66 | patch(old_file_name, new_file_name, patch_file_name) 67 | 68 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/house_arrest_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''house_arrest的测试用例 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.tencent.AirSandboxDemo" 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() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''package script 3 | ''' 4 | 5 | 6 | import os 7 | import platform 8 | import sys 9 | from setuptools import setup, find_packages 10 | 11 | BASE_DIR = os.path.realpath(os.path.dirname(__file__)) 12 | VERSION = "1.0.2" 13 | 14 | def replace_version_py(version): 15 | content = """# -*- coding: utf-8 -*- 16 | '''pymobiledevice-qta版本 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") as fh: 54 | return fh.read() 55 | 56 | 57 | if __name__ == "__main__": 58 | 59 | setup( 60 | version=generate_version(), 61 | name="pymobiledevice-qta", 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", "version.txt"])], 69 | author="QTA", 70 | license="Copyright(c)2010-2018 Tencent All Rights Reserved. ", 71 | install_requires=parse_requirements(), 72 | entry_points={}, 73 | classifiers=[ 74 | "Programming Language :: Python :: 2.7", 75 | "Programming Language :: Python :: 3.6", 76 | "Programming Language :: Python :: 3.7", 77 | ], 78 | url="https://github.com/qtacore/pymobiledevice", 79 | project_urls={ 80 | "pymobiledevice-qta Documentation":"https://github.com/qtacore/pymobiledevice" 81 | }, 82 | ) 83 | -------------------------------------------------------------------------------- /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 | 26 | from pymobiledevice.lockdown import LockdownClient 27 | from six import PY3 28 | from time import gmtime, strftime 29 | from optparse import OptionParser 30 | import os 31 | 32 | class screenshotr(object): 33 | def __init__(self, lockdown=None, serviceName='com.apple.mobile.screenshotr'): 34 | if lockdown: 35 | self.lockdown = lockdown 36 | else: 37 | self.lockdown = LockdownClient() 38 | #Starting Screenshot service 39 | self.service = self.lockdown.startService(serviceName) 40 | 41 | #hand check 42 | DLMessageVersionExchange = self.service.recvPlist() 43 | #assert len(DLMessageVersionExchange) == 2 44 | version_major = DLMessageVersionExchange[1] 45 | self.service.sendPlist(["DLMessageVersionExchange", "DLVersionsOk", version_major ]) 46 | DLMessageDeviceReady = self.service.recvPlist() 47 | 48 | def stop_session(self): 49 | self.service.close() 50 | 51 | def take_screenshot(self): 52 | self.service.sendPlist(['DLMessageProcessMessage', {'MessageType': 'ScreenShotRequest'}]) 53 | res = self.service.recvPlist() 54 | 55 | assert len(res) == 2 56 | assert res[0] == "DLMessageProcessMessage" 57 | 58 | if res[1].get('MessageType') == 'ScreenShotReply': 59 | if PY3: 60 | screen_data = res[1]['ScreenShotData'] 61 | else: 62 | screen_data = res[1]['ScreenShotData'].data 63 | return screen_data 64 | return None 65 | 66 | if __name__ == '__main__': 67 | parser = OptionParser(usage='%prog') 68 | parser.add_option('-p', '--path', dest='outDir', default=False, 69 | help='Output Directory (default: . )', type='string') 70 | (options, args) = parser.parse_args() 71 | 72 | outPath = '.' 73 | if options.outDir: 74 | outPath = options.outDir 75 | 76 | screenshotr = screenshotr() 77 | data = screenshotr.take_screenshot() 78 | if data: 79 | filename = strftime('screenshot-%Y-%m-%d-%H-%M-%S.tif',gmtime()) 80 | outPath = os.path.join(outPath, filename) 81 | print('Saving Screenshot at %s' % outPath) 82 | o = open(outPath,'wb') 83 | o.write(data) -------------------------------------------------------------------------------- /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 | from pymobiledevice.lockdown import LockdownClient 26 | from six import PY3 27 | from sys import exit 28 | import re 29 | import time 30 | 31 | TIME_FORMAT = '%H:%M:%S' 32 | 33 | 34 | class Syslog(object): 35 | ''' 36 | 查看系统日志 37 | ''' 38 | def __init__(self, lockdown=None): 39 | if lockdown: 40 | self.lockdown = lockdown 41 | else: 42 | self.lockdown = LockdownClient() 43 | self.c = self.lockdown.startService("com.apple.syslog_relay") 44 | if self.c: 45 | self.c.send("watch") 46 | else: 47 | exit(1) 48 | 49 | def watch(self, watchtime, logFile=None, procName=None): 50 | '''查看日志 51 | :param watchtime: 时间(秒) 52 | :type watchtime: int 53 | :param logFile: 日志文件完整路径 54 | :type logFile: str 55 | :param procName: 进程名 56 | :type proName: str 57 | ''' 58 | 59 | begin = time.strftime(TIME_FORMAT) 60 | 61 | while True: 62 | d = self.c.recv(4096) 63 | if PY3: 64 | d = d.decode('utf-8') 65 | if procName: 66 | procFilter = re.compile(procName,re.IGNORECASE) 67 | if len(d.split(" ")) > 4 and not procFilter.search(d): 68 | continue 69 | s = d.strip("\n\x00\x00") 70 | print(s) 71 | if logFile: 72 | with open(logFile, 'a') as f: 73 | f.write(d.replace("\x00", "")) 74 | now = self.time_match(s[7:15]) 75 | if now: 76 | time_spend = self.time_caculate(str(begin), now) 77 | if time_spend > watchtime : 78 | break 79 | 80 | def time_match(self, str_time): 81 | '''判断时间格式是否匹配 82 | ''' 83 | pattern = re.compile(r'\d{2}:\d{2}:\d{2}') 84 | match = pattern.match(str_time) 85 | if match: 86 | return str_time 87 | else: 88 | return False 89 | 90 | def time_caculate(self, a, b): 91 | ''' 92 | 计算两个字符串的时间差 93 | ''' 94 | time_a = int(a[6:8])+60*int(a[3:5])+3600*int(a[0:2]) 95 | time_b = int(b[6:8])+60*int(b[3:5])+3600*int(b[0:2]) 96 | return time_b - time_a 97 | 98 | if __name__ == "__main__": 99 | syslog = Syslog() 100 | syslog.watch(10,'/tmp/sys.log','QQ') -------------------------------------------------------------------------------- /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 | 26 | from pymobiledevice.lockdown import LockdownClient 27 | from optparse import OptionParser 28 | from pprint import pprint 29 | import plistlib 30 | from pymobiledevice.util import read_file 31 | 32 | class MobileConfigService(object): 33 | def __init__(self, lockdown): 34 | self.lockdown = lockdown 35 | self.service = lockdown.startService("com.apple.mobile.MCInstall") 36 | 37 | def GetProfileList(self): 38 | self.service.sendPlist({"RequestType":"GetProfileList"}) 39 | res = self.service.recvPlist() 40 | if res.get("Status") != "Acknowledged": 41 | print("GetProfileList error") 42 | pprint(res) 43 | return 44 | return res 45 | 46 | def InstallProfile(self, s): 47 | #s = plistlib.writePlistToString(payload) 48 | self.service.sendPlist({"RequestType":"InstallProfile", "Payload": plistlib.Data(s)}) 49 | return self.service.recvPlist() 50 | 51 | def RemoveProfile(self, ident): 52 | profiles = self.GetProfileList() 53 | if not profiles: 54 | return 55 | if not profiles["ProfileMetadata"].has_key(ident): 56 | print("Trying to remove not installed profile %s" % ident) 57 | return 58 | meta = profiles["ProfileMetadata"][ident] 59 | pprint(meta) 60 | data = plistlib.writePlistToString({"PayloadType": "Configuration", 61 | "PayloadIdentifier": ident, 62 | "PayloadUUID": meta["PayloadUUID"], 63 | "PayloadVersion": meta["PayloadVersion"] 64 | }) 65 | self.service.sendPlist({"RequestType":"RemoveProfile", "ProfileIdentifier": plistlib.Data(data)}) 66 | return self.service.recvPlist() 67 | 68 | def main(): 69 | parser = OptionParser(usage="%prog") 70 | parser.add_option("-l", "--list", dest="list", action="store_true", 71 | default=False, help="List installed profiles") 72 | parser.add_option("-i", "--install", dest="install", action="store", 73 | metavar="FILE", help="Install profile") 74 | parser.add_option("-r", "--remove", dest="remove", action="store", 75 | metavar="IDENTIFIER", help="Remove profile") 76 | (options, args) = parser.parse_args() 77 | 78 | if not options.list and not options.install and not options.remove: 79 | parser.print_help() 80 | return 81 | lockdown = LockdownClient() 82 | mc = MobileConfigService(lockdown) 83 | 84 | if options.list: 85 | pprint(mc.GetProfileList()) 86 | elif options.install: 87 | mc.InstallProfile(read_file(options.install)) 88 | elif options.remove: 89 | pprint(mc.RemoveProfile(options.remove)) 90 | 91 | if __name__ == "__main__": 92 | main() 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pymobiledevice 2 | 3 | [![GitHub license](https://img.shields.io/cran/l/devtools.svg)](LICENSE) 4 | [![Build Status](https://travis-ci.org/qtacore/pymobiledevice.svg?branch=master)](https://travis-ci.org/qtacore/pymobiledevice) 5 | [![PyPi version](https://img.shields.io/pypi/v/pymobiledevice-qta.svg)](https://pypi.python.org/pypi/pymobiledevice-qta/) 6 | 7 | pymobiledevice is a cross-platform implementation of the mobiledevice library 8 | that talks the protocols to support iPhone®, iPod Touch®, iPad® and Apple TV® devices. 9 | 10 | 11 | ## Requirements 12 | 13 | * Python 2.7 and 3.x 14 | * M2Crypto 15 | * construct >= 2.9.29 16 | * pyasn1 17 | * future 18 | * six 19 | * biplist 20 | 21 | 22 | ## Lockdownd.py [com.apple.lockownd] 23 | 24 | This script can be used in order to pair with the device & starts other services. 25 | 26 | */!\ Others services can only being accessed after succesful pairing. 27 | Succesful pairing requiert the device to be unlocked and user to click on 28 | "Trust this device" on its phone screen.* 29 | 30 | 31 | ## afc.py [com.apple.afc] 32 | 33 | This service is responsible for things such as copying music and photos. AFC Clients like iTunes 34 | are allowed accessing to a “jailed” or limited area of the device filesystem. Actually, AFC clients can 35 | only access certain files, namely those located in the Media folder. 36 | 37 | 38 | ## house_arrest.py [com.apple.mobile.house_arrest] 39 | 40 | This service allows accessing to AppStore applications folders and their content. 41 | In other words, by using an AFC client, a user/attacker can download the application resources and data. 42 | It also includes the “default preferences” file where credentials are sometimes stored. 43 | 44 | 45 | ## installation_proxy.py [com.apple.mobile.installation_proxy] 46 | 47 | The installation proxy manages applications on a device. 48 | It allows execution of the following commands: 49 | - List installed applications 50 | - List archived applications 51 | - ... 52 | 53 | 54 | ## mobilebackup.py & mobilebackup2.py [ com.apple.mobilebackup & com.apple.mobilebackup2 ] 55 | 56 | Those services are used by iTunes to backup the device. 57 | 58 | 59 | ## diagnostics_relay.py [com.apple.mobile.diagnostics_relay] 60 | 61 | The diagnostic relay allows requesting iOS diagnostic information. 62 | The service handles the following actions: 63 | - [ Sleep ]Puts the device into deep sleep mode and disconnects from host. 64 | - [ Restart ] Restart the device and optionally show a user notification. 65 | - [ Shutdown ] Shutdown of the device and optionally show a user notification. 66 | - [ NAND, IORegistry, GasGauge, MobileGestalt ] Querry diagnostic informations. 67 | - ... 68 | 69 | 70 | ## filerelay.py [com.apple.mobile.file_relay] 71 | 72 | Depending of the iOS version, the file relay service may support the following commands: 73 | Accounts, AddressBook, AppleSupport, AppleTV, Baseband, Bluetooth, CrashReporter, CLTM 74 | Caches, CoreLocation, DataAccess, DataMigrator, demod, Device-o-Matic, EmbeddedSocial, FindMyiPhone 75 | GameKitLogs, itunesstored, IORegUSBDevice, HFSMeta, Keyboard, Lockdown, MapsLogs, MobileAsset, 76 | MobileBackup, MobileCal, MobileDelete, MobileInstallation, MobileMusicPlayer, MobileNotes, NANDDebugInfo 77 | Network, Photos, SafeHarbor, SystemConfiguration, tmp, Ubiquity, UserDatabases, VARFS, VPN, Voicemail 78 | WiFi, WirelessAutomation. 79 | 80 | All the files returned by the iPhone are stored in clear text in a gziped CPIO archive. 81 | 82 | 83 | ## pcapd.py [com.apple.pcapd] 84 | 85 | Starting iOS 5, apple added a remote virtual interface (RVI) facility that allows mirroring networks trafic from an iOS device. 86 | On Mac OSX the virtual interface can be enabled with the rvictl command. This script allows to use this service on other systems. 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /test/installation_proxy_test.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | '''installation_proxy的测试用例 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("上传完毕") 55 | print("开始安装") 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.tencent.qt4i.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() -------------------------------------------------------------------------------- /pymobiledevice/util/struct2.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Created: codeskyblue 2020/06/01 4 | # 5 | 6 | import struct 7 | from collections import namedtuple 8 | from functools import partial 9 | import typing 10 | 11 | 12 | class Field(object): 13 | def __init__(self, name, default=None, format=None): 14 | assert format, "format should not empty" 15 | self._name = name 16 | self._format = format 17 | self._default = default 18 | self._struct = struct.Struct(format) 19 | 20 | @property 21 | def name(self): 22 | return self._name 23 | 24 | @property 25 | def format(self): 26 | return self._format 27 | 28 | @property 29 | def default(self): 30 | return self._default 31 | 32 | @property 33 | def size(self): 34 | return self._struct.size 35 | 36 | def to_field(self): 37 | return Field(self._name, self._format, self._default) 38 | 39 | 40 | class Byte(Field): 41 | """ 42 | Byte("padding")[6] 43 | """ 44 | def __init__(self, name, default=None, format=None): 45 | super().__init__(name, default, format="s") 46 | 47 | def __item__(self, n): 48 | """ 49 | Args: 50 | n (int): 51 | """ 52 | self._format = str(n)+"s" 53 | return self 54 | 55 | Bool = partial(Field, format="?") 56 | U8 = UInt8 = partial(Field, format="B") 57 | U16 = UInt16 = partial(Field, format="H") 58 | U32 = UInt32 = partial(Field, format="I") 59 | U64 = UInt64 = partial(Field, format="Q") 60 | 61 | 62 | class Struct(object): 63 | def __init__(self, typename, *fields, **kwargs): 64 | """ 65 | Args: 66 | typename (str): 67 | """ 68 | byteorder = kwargs.pop('byteorder', '<') 69 | 70 | self._fields = [self._convert_field(f) for f in fields] 71 | self._typename = typename 72 | self._fmt = byteorder + ''.join([f.format for f in self._fields]) 73 | self._field_names = [] 74 | for f in self._fields: 75 | if f.name in self._field_names: 76 | raise ValueError("Struct has duplicated name", f.name) 77 | self._field_names.append(f.name) 78 | self._size = sum([f.size for f in self._fields]) 79 | 80 | @property 81 | def size(self): 82 | return struct.Struct(self._fmt).size 83 | 84 | def _convert_field(self, fvalue): 85 | if isinstance(fvalue, Field): 86 | return fvalue 87 | else: 88 | raise ValueError("Unknown type:", fvalue) 89 | 90 | def parse_stream(self, reader): 91 | """ 92 | Args: 93 | reader (typing.BinaryIO): 94 | """ 95 | return self.parse(reader.read(self.size)) 96 | 97 | def parse(self, buffer): 98 | """ 99 | Args: 100 | buffer (bytes): 101 | """ 102 | values = struct.unpack(self._fmt, buffer) 103 | return namedtuple(self._typename, self._field_names)(*values) 104 | 105 | def build(self, *args, **kwargs): 106 | """ 107 | Returns: 108 | bytearray: 109 | """ 110 | if args: 111 | assert len(args) == 1 112 | assert isinstance(args[0], dict) 113 | kwargs = args[0] 114 | 115 | buffer = bytearray() 116 | for f in self._fields: 117 | value = kwargs.get(f.name) 118 | if value is None: 119 | if f.default is None: 120 | raise ValueError("missing required field", f.name, value, 121 | f.default) 122 | value = f.default 123 | buffer.extend(struct.pack(f.format, value)) 124 | return buffer 125 | 126 | 127 | def _example(): 128 | Message = Struct("Message", 129 | U32("length"), 130 | U16("magic", 0x1234)) 131 | m = Message.parse(b"\x12\x00\x00\x00\x12\x35") 132 | assert m.length == 0x12 133 | assert m.magic == 0x3512 134 | 135 | buf = Message.build(length=7) 136 | assert buf == b'\x07\x00\x00\x00\x34\x12' 137 | 138 | 139 | if __name__ == "__main__": 140 | _example() -------------------------------------------------------------------------------- /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 | 8 | try: 9 | import cPickle 10 | except ImportError: 11 | import pickle as cPickle 12 | plistlib.readPlistFromString = plistlib.loads 13 | plistlib.readPlist = plistlib.load 14 | 15 | def read_file(filename): 16 | f = open(filename, "rb") 17 | data = f.read() 18 | f.close() 19 | return data 20 | 21 | def write_file(filename,data): 22 | f = open(filename, "wb") 23 | f.write(data) 24 | f.close() 25 | 26 | def makedirs(dirs): 27 | try: 28 | os.makedirs(dirs) 29 | except: 30 | pass 31 | 32 | def getHomePath(foldername, filename): 33 | home = os.path.expanduser('~') 34 | folderpath = os.path.join(home, foldername) 35 | if not os.path.exists(folderpath): 36 | makedirs(folderpath) 37 | return os.path.join(folderpath, filename) 38 | 39 | def readHomeFile(foldername, filename): 40 | path = getHomePath(foldername, filename) 41 | if not os.path.exists(path): 42 | return None 43 | return read_file(path) 44 | 45 | #return path to HOME+foldername+filename 46 | def writeHomeFile(foldername, filename, data): 47 | filepath = getHomePath(foldername, filename) 48 | write_file(filepath, data) 49 | return filepath 50 | 51 | def readPlist(filename): 52 | return plistlib.readPlist(filename) 53 | 54 | def parsePlist(s): 55 | return plistlib.readPlistFromString(s) 56 | 57 | #http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size 58 | def sizeof_fmt(num): 59 | for x in ['bytes','KB','MB','GB','TB']: 60 | if num < 1024.0: 61 | return "%d%s" % (num, x) 62 | num /= 1024.0 63 | 64 | #http://www.5dollarwhitebox.org/drupal/node/84 65 | def convert_bytes(bytes): 66 | bytes = float(bytes) 67 | if bytes >= 1099511627776: 68 | terabytes = bytes / 1099511627776 69 | size = '%.2fT' % terabytes 70 | elif bytes >= 1073741824: 71 | gigabytes = bytes / 1073741824 72 | size = '%.2fG' % gigabytes 73 | elif bytes >= 1048576: 74 | megabytes = bytes / 1048576 75 | size = '%.2fM' % megabytes 76 | elif bytes >= 1024: 77 | kilobytes = bytes / 1024 78 | size = '%.2fK' % kilobytes 79 | else: 80 | size = '%.2fb' % bytes 81 | return size 82 | 83 | def xor_strings(a,b): 84 | r="" 85 | for i in xrange(len(a)): 86 | r+= chr(ord(a[i])^ord(b[i])) 87 | return r 88 | 89 | hex = lambda data: " ".join("%02X" % ord(i) for i in data) 90 | ascii = lambda data: "".join(c if 31 < ord(c) < 127 else "." for c in data) 91 | 92 | def hexdump(d): 93 | for i in xrange(0,len(d),16): 94 | data = d[i:i+16] 95 | print("%08X | %s | %s" % (i, hex(data).ljust(47), ascii(data))) 96 | 97 | def search_plist(directory, matchDict): 98 | for p in map(os.path.normpath, glob.glob(directory + "/*.plist")): 99 | try: 100 | d = plistlib.readPlist(p) 101 | ok = True 102 | for k,v in matchDict.items(): 103 | if d.get(k) != v: 104 | ok = False 105 | break 106 | if ok: 107 | print("Using plist file %s" % p) 108 | return d 109 | except: 110 | continue 111 | 112 | def save_pickle(filename,data): 113 | f = gzip.open(filename,"wb") 114 | cPickle.dump(data, f, cPickle.HIGHEST_PROTOCOL) 115 | f.close() 116 | 117 | def load_pickle(filename): 118 | f = gzip.open(filename,"rb") 119 | data = cPickle.load(f) 120 | f.close() 121 | return data 122 | 123 | 124 | class MultipleOption(Option): 125 | ACTIONS = Option.ACTIONS + ("extend",) 126 | STORE_ACTIONS = Option.STORE_ACTIONS + ("extend",) 127 | TYPED_ACTIONS = Option.TYPED_ACTIONS + ("extend",) 128 | ALWAYS_TYPED_ACTIONS = Option.ALWAYS_TYPED_ACTIONS + ("extend",) 129 | 130 | def take_action(self, action, dest, opt, value, values, parser): 131 | if action == "extend": 132 | values.ensure_value(dest, []).append(value) 133 | else: 134 | Option.take_action(self, action, dest, opt, value, values, parser) 135 | -------------------------------------------------------------------------------- /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 | 25 | from pymobiledevice.lockdown import LockdownClient 26 | from pymobiledevice.util.cpio import CpioArchive 27 | from pymobiledevice.util import MultipleOption 28 | import zlib 29 | import gzip 30 | from pprint import pprint 31 | from tempfile import mkstemp 32 | from optparse import OptionParser 33 | from io import BytesIO 34 | import os 35 | 36 | SRCFILES = """Baseband 37 | CrashReporter 38 | MobileAsset 39 | VARFS 40 | HFSMeta 41 | Lockdown 42 | MobileBackup 43 | MobileDelete 44 | MobileInstallation 45 | MobileNotes 46 | Network 47 | UserDatabases 48 | WiFi 49 | WirelessAutomation 50 | NANDDebugInfo 51 | SystemConfiguration 52 | Ubiquity 53 | tmp 54 | WirelessAutomation""" 55 | 56 | class DeviceVersionNotSupported(Exception): 57 | pass 58 | 59 | class FileRelay(object): 60 | def __init__(self, lockdown=None, serviceName="com.apple.mobile.file_relay"): 61 | if lockdown: 62 | self.lockdown = lockdown 63 | else: 64 | self.lockdown = LockdownClient() 65 | 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 | print("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 | if __name__ == "__main__": 98 | 99 | parser = OptionParser(option_class=MultipleOption,usage="%prog") 100 | parser.add_option("-s", "--sources", 101 | action="extend", 102 | dest="sources", 103 | metavar='SOURCES', 104 | choices=SRCFILES.split("\n"), 105 | help="comma separated list of file relay source to dump") 106 | parser.add_option("-e", "--extract",dest="extractpath" , default=False, 107 | help="Extract archive to specified location", type="string") 108 | parser.add_option("-o", "--output", dest="outputfile", default=False, 109 | help="Output location", type="string") 110 | 111 | (options, args) = parser.parse_args() 112 | 113 | sources = [] 114 | if options.sources: 115 | sources = options.sources 116 | else: 117 | sources = ["UserDatabases"] 118 | print("Downloading: %s" % ''.join([str(item)+" " for item in sources])) 119 | 120 | fc = None 121 | try: 122 | fc = FileRelay() 123 | except: 124 | print("Device with product vertion >= 8.0 does not allow access to fileRelay service") 125 | exit() 126 | 127 | data = fc.request_sources(sources) 128 | 129 | if data: 130 | if options.outputfile: 131 | path = options.outputfile 132 | else: 133 | _,path = mkstemp(prefix="fileRelay_dump_",suffix=".gz",dir=".") 134 | 135 | open(path,'wb').write(data) 136 | print("Data saved to: %s " % path) 137 | 138 | if options.extractpath: 139 | with open(path, 'r') as f: 140 | gz = gzip.GzipFile(mode='rb', fileobj=f) 141 | cpio = CpioArchive(fileobj=BytesIO(gz.read())) 142 | cpio.extract_files(files=None,outpath=options.extractpath) 143 | -------------------------------------------------------------------------------- /pymobiledevice/plist_service.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 plistlib 27 | import ssl 28 | import struct 29 | from re import sub 30 | from six import PY3 31 | 32 | from pymobiledevice.usbmux import usbmux 33 | 34 | if PY3: 35 | plistlib.readPlistFromString = plistlib.loads 36 | plistlib.writePlistToString = plistlib.dumps 37 | 38 | 39 | class PlistService(object): 40 | 41 | def __init__(self, port, udid=None): 42 | self.port = port 43 | self.connect(udid) 44 | 45 | def connect(self, udid=None): 46 | mux = usbmux.USBMux() 47 | mux.process(1.0) 48 | dev = None 49 | 50 | while not dev and mux.devices : 51 | mux.process(1.0) 52 | if udid: 53 | for d in mux.devices: 54 | if d.serial == udid: 55 | dev = d 56 | print("Connecting to device: " + dev.serial) 57 | else: 58 | dev = mux.devices[0] 59 | print("Connecting to device: " + dev.serial) 60 | 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 Exception as e: 77 | import traceback 78 | print("Sending data to device failled: ", e) 79 | print(traceback.format_exc()) 80 | return -1 81 | return 0 82 | 83 | def sendRequest(self, data): 84 | res = None 85 | if self.sendPlist(data) >= 0: 86 | res = self.recvPlist() 87 | return res 88 | 89 | 90 | def recv_exact(self, l): 91 | data = "" 92 | if PY3: 93 | data = b"" 94 | while l > 0: 95 | d = self.recv(l) 96 | if not d or len(d) == 0: 97 | break 98 | data += d 99 | l -= len(d) 100 | return data 101 | 102 | def recv_raw(self): 103 | l = self.recv_exact(4) 104 | if not l or len(l) != 4: 105 | return 106 | l = struct.unpack(">L", l)[0] 107 | return self.recv_exact(l) 108 | 109 | def send_raw(self, data): 110 | return self.send(struct.pack(">L", len(data)) + data) 111 | 112 | def recvPlist(self): 113 | payload = self.recv_raw() 114 | if not payload: 115 | return 116 | bplist_header = "bplist00" 117 | xml_header = "\/ \-_0-9\"\'\\=\.\?\!\+]+','', payload.decode('utf-8')).encode('utf-8') 130 | return plistlib.readPlistFromString(payload) 131 | else: 132 | raise Exception("recvPlist invalid data : %s" % payload[:100].encode("hex")) 133 | 134 | def sendPlist(self, d): 135 | payload = plistlib.writePlistToString(d) 136 | l = struct.pack(">L", len(payload)) 137 | return self.send(l + payload) 138 | 139 | def ssl_start(self, keyfile, certfile): 140 | self._socket_back = self.s.dup() 141 | self.s = ssl.wrap_socket(self.s, keyfile, certfile, ssl_version=ssl.PROTOCOL_TLS) 142 | 143 | def ssl_stop(self): 144 | self.s.close() 145 | self.s = self._socket_back 146 | self._socket_back = None 147 | -------------------------------------------------------------------------------- /pymobiledevice/constants.py: -------------------------------------------------------------------------------- 1 | 2 | # region operations 3 | AFC_OP_STATUS = 0x00000001 4 | AFC_OP_DATA = 0x00000002 # Data */ 5 | AFC_OP_READ_DIR = 0x00000003 # ReadDir */ 6 | AFC_OP_READ_FILE = 0x00000004 # ReadFile */ 7 | AFC_OP_WRITE_FILE = 0x00000005 # WriteFile */ 8 | AFC_OP_WRITE_PART = 0x00000006 # WritePart */ 9 | AFC_OP_TRUNCATE = 0x00000007 # TruncateFile */ 10 | AFC_OP_REMOVE_PATH = 0x00000008 # RemovePath */ 11 | AFC_OP_MAKE_DIR = 0x00000009 # MakeDir */ 12 | AFC_OP_GET_FILE_INFO = 0x0000000a # GetFileInfo */ 13 | AFC_OP_GET_DEVINFO = 0x0000000b # GetDeviceInfo */ 14 | AFC_OP_WRITE_FILE_ATOM = 0x0000000c # WriteFileAtomic (tmp file+rename) */ 15 | AFC_OP_FILE_OPEN = 0x0000000d # FileRefOpen */ 16 | AFC_OP_FILE_OPEN_RES = 0x0000000e # FileRefOpenResult */ 17 | AFC_OP_READ = 0x0000000f # FileRefRead */ 18 | AFC_OP_WRITE = 0x00000010 # FileRefWrite */ 19 | AFC_OP_FILE_SEEK = 0x00000011 # FileRefSeek */ 20 | AFC_OP_FILE_TELL = 0x00000012 # FileRefTell */ 21 | AFC_OP_FILE_TELL_RES = 0x00000013 # FileRefTellResult */ 22 | AFC_OP_FILE_CLOSE = 0x00000014 # FileRefClose */ 23 | AFC_OP_FILE_SET_SIZE = 0x00000015 # FileRefSetFileSize (ftruncate) */ 24 | AFC_OP_GET_CON_INFO = 0x00000016 # GetConnectionInfo */ 25 | AFC_OP_SET_CON_OPTIONS = 0x00000017 # SetConnectionOptions */ 26 | AFC_OP_RENAME_PATH = 0x00000018 # RenamePath */ 27 | AFC_OP_SET_FS_BS = 0x00000019 # SetFSBlockSize (0x800000) */ 28 | AFC_OP_SET_SOCKET_BS = 0x0000001A # SetSocketBlockSize (0x800000) */ 29 | AFC_OP_FILE_LOCK = 0x0000001B # FileRefLock */ 30 | AFC_OP_MAKE_LINK = 0x0000001C # MakeLink */ 31 | AFC_OP_GET_FILE_HASH = 0x0000001D # GetFileHash */ 32 | AFC_OP_SET_FILE_TIME = 0x0000001E # set st_mtime */ 33 | AFC_OP_GET_FILE_HASH_RANGE = 0x0000001F # GetFileHashWithRange */ 34 | # iOS 6+ */ 35 | AFC_OP_FILE_SET_IMMUTABLE_HINT = 0x00000020 # FileRefSetImmutableHint */ 36 | AFC_OP_GET_SIZE_OF_PATH_CONTENTS = 0x00000021 # GetSizeOfPathContents */ 37 | AFC_OP_REMOVE_PATH_AND_CONTENTS = 0x00000022 # RemovePathAndContents */ 38 | AFC_OP_DIR_OPEN = 0x00000023 # DirectoryEnumeratorRefOpen */ 39 | AFC_OP_DIR_OPEN_RESULT = 0x00000024 # DirectoryEnumeratorRefOpenResult */ 40 | AFC_OP_DIR_READ = 0x00000025 # DirectoryEnumeratorRefRead */ 41 | AFC_OP_DIR_CLOSE = 0x00000026 # DirectoryEnumeratorRefClose */ 42 | # iOS 7+ */ 43 | AFC_OP_FILE_READ_OFFSET = 0x00000027 # FileRefReadWithOffset */ 44 | AFC_OP_FILE_WRITE_OFFSET = 0x00000028 # FileRefWriteWithOffset */ 45 | # endregion 46 | 47 | # region error codes 48 | AFC_E_SUCCESS = 0 49 | AFC_E_UNKNOWN_ERROR = 1 50 | AFC_E_OP_HEADER_INVALID = 2 51 | AFC_E_NO_RESOURCES = 3 52 | AFC_E_READ_ERROR = 4 53 | AFC_E_WRITE_ERROR = 5 54 | AFC_E_UNKNOWN_PACKET_TYPE = 6 55 | AFC_E_INVALID_ARG = 7 56 | AFC_E_OBJECT_NOT_FOUND = 8 57 | AFC_E_OBJECT_IS_DIR = 9 58 | AFC_E_PERM_DENIED = 10 59 | AFC_E_SERVICE_NOT_CONNECTED = 11 60 | AFC_E_OP_TIMEOUT = 12 61 | AFC_E_TOO_MUCH_DATA = 13 62 | AFC_E_END_OF_DATA = 14 63 | AFC_E_OP_NOT_SUPPORTED = 15 64 | AFC_E_OBJECT_EXISTS = 16 65 | AFC_E_OBJECT_BUSY = 17 66 | AFC_E_NO_SPACE_LEFT = 18 67 | AFC_E_OP_WOULD_BLOCK = 19 68 | AFC_E_IO_ERROR = 20 69 | AFC_E_OP_INTERRUPTED = 21 70 | AFC_E_OP_IN_PROGRESS = 22 71 | AFC_E_INTERNAL_ERROR = 23 72 | 73 | AFC_E_MUX_ERROR =30 74 | AFC_E_NO_MEM =31 75 | AFC_E_NOT_ENOUGH_DATA =32 76 | AFC_E_DIR_NOT_EMPTY =33 77 | # endregion 78 | 79 | AFC_FOPEN_RDONLY = 0x00000001 #/**< r O_RDONLY */ 80 | AFC_FOPEN_RW = 0x00000002 #/**< r+ O_RDWR | O_CREAT */ 81 | AFC_FOPEN_WRONLY = 0x00000003 #/**< w O_WRONLY | O_CREAT | O_TRUNC */ 82 | AFC_FOPEN_WR = 0x00000004 #/**< w+ O_RDWR | O_CREAT | O_TRUNC */ 83 | AFC_FOPEN_APPEND = 0x00000005 #/**< a O_WRONLY | O_APPEND | O_CREAT */ 84 | AFC_FOPEN_RDAPPEND = 0x00000006 #/**< a+ O_RDWR | O_APPEND | O_CREAT */ 85 | 86 | AFC_HARDLINK = 1 87 | AFC_SYMLINK = 2 88 | 89 | AFC_LOCK_SH = 1 | 4 #/**< shared lock */ 90 | AFC_LOCK_EX = 2 | 4 #/**< exclusive lock */ 91 | AFC_LOCK_UN = 8 | 4 #/**< unlock */ 92 | 93 | AFCMAGIC = b'CFA6LPAA' 94 | 95 | AFC_ERRORS = {k: v for k, v in locals().items() if k.startswith('AFC_E_')} 96 | AFC_FILE_MODES = {k: v for k, v in locals().items() if k.startswith('AFC_FOPEN_')} 97 | AFC_OPERATIONS = {k: v for k, v in locals().items() if k.startswith('AFC_OP_')} 98 | 99 | AFC_ERROR_NAMES = {v: k for k, v in AFC_ERRORS.items()} 100 | AFC_FILE_MODE_NAMES = {v: k for k, v in AFC_FILE_MODES.items()} 101 | AFC_OPERATION_NAMES = {v: k for k, v in AFC_OPERATIONS.items()} 102 | -------------------------------------------------------------------------------- /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 | 25 | import struct 26 | import time 27 | import sys 28 | from tempfile import mkstemp 29 | from pymobiledevice.lockdown import LockdownClient 30 | from optparse import OptionParser 31 | 32 | """ 33 | struct pcap_hdr_s { 34 | guint32 magic_number; /* magic number */ 35 | guint16 version_major; /* major version number */ 36 | guint16 version_minor; /* minor version number */ 37 | gint32 thiszone; /* GMT to local correction */ 38 | guint32 sigfigs; /* accuracy of timestamps */ 39 | guint32 snaplen; /* max length of captured packets, in octets */ 40 | guint32 network; /* data link type */ 41 | } pcap_hdr_t; 42 | typedef struct pcaprec_hdr_s { 43 | guint32 ts_sec; /* timestamp seconds */ 44 | guint32 ts_usec; /* timestamp microseconds */ 45 | guint32 incl_len; /* number of octets of packet saved in file */ 46 | guint32 orig_len; /* actual length of packet */ 47 | } pcaprec_hdr_t; 48 | """ 49 | LINKTYPE_ETHERNET = 1 50 | LINKTYPE_RAW = 101 51 | 52 | class PcapOut(object): 53 | 54 | def __init__(self, pipename=r'test.pcap'): 55 | self.pipe = open(pipename,'wb') 56 | self.pipe.write(struct.pack("LBL", data[:9]) 119 | flags1, flags2, offset_to_ip_data, zero = struct.unpack(">LLLL", data[9:0x19]) 120 | 121 | assert hdrsize >= 0x19 122 | interfacetype= data[0x19:hdrsize].strip("\x00") 123 | t = time.time() 124 | print(interfacetype, packet_size, t) 125 | 126 | packet = data[hdrsize:] 127 | assert packet_size == len(packet) 128 | 129 | if offset_to_ip_data == 0: 130 | #add fake ethernet header for pdp packets 131 | packet = "\xBE\xEF" * 6 + "\x08\x00" + packet 132 | if not output.writePacket(packet): 133 | break 134 | 135 | -------------------------------------------------------------------------------- /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 | 26 | from pymobiledevice.lockdown import LockdownClient 27 | from pymobiledevice.afc import AFCClient, AFCShell 28 | from optparse import OptionParser 29 | import os 30 | import warnings 31 | 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 | print("Unable to Lookup the selected application: You probably trying to access to a system app...") 52 | return None 53 | return AFCClient(lockdown, service=mis) 54 | 55 | def house_arrest_shell(lockdown, applicationId): 56 | afc = house_arrest(lockdown, applicationId) 57 | if afc: AFCShell(client=afc).cmdloop() 58 | 59 | """ 60 | "Install" 61 | "Upgrade" 62 | "Uninstall" 63 | "Lookup" 64 | "Browse" 65 | "Archive" 66 | "Restore" 67 | "RemoveArchive" 68 | "LookupArchives" 69 | "CheckCapabilitiesMatch" 70 | 71 | installd 72 | if stat("/var/mobile/tdmtanf") => "TDMTANF Bypass" => SignerIdentity bypass 73 | """ 74 | 75 | def mobile_install(lockdown,ipaPath): 76 | #Start afc service & upload ipa 77 | afc = AFCClient(lockdown) 78 | afc.set_file_contents("/" + os.path.basename(ipaPath), open(ipaPath,'rb').read()) 79 | mci = lockdown.startService("com.apple.mobile.installation_proxy") 80 | #print mci.sendPlist({"Command":"Archive","ApplicationIdentifier": "com.joystickgenerals.STActionPig"}) 81 | mci.sendPlist({"Command":"Install", 82 | #"ApplicationIdentifier": "com.gotohack.JBChecker", 83 | "PackagePath": os.path.basename(ipaPath)}) 84 | while True: 85 | z = mci.recvPlist() 86 | if not z: 87 | break 88 | completion = z.get('PercentComplete') 89 | if completion: 90 | print('Installing, %s: %s %% Complete' % (ipaPath, z['PercentComplete'])) 91 | if z.get('Status') == 'Complete': 92 | print("Installation %s\n" % z['Status']) 93 | break 94 | 95 | def list_apps(lockdown): 96 | mci = lockdown.startService("com.apple.mobile.installation_proxy") 97 | mci.sendPlist({"Command":"Lookup"}) 98 | res = mci.recvPlist() 99 | for app in res["LookupResult"].values(): 100 | if app.get("ApplicationType") != "System": 101 | print(app["CFBundleIdentifier"], "=>", app.get("Container")) 102 | else: 103 | print(app["CFBundleIdentifier"], "=> N/A") 104 | 105 | 106 | def get_apps_BundleID(lockdown,appType="User"): 107 | appList = [] 108 | mci = lockdown.startService("com.apple.mobile.installation_proxy") 109 | mci.sendPlist({"Command":"Lookup"}) 110 | res = mci.recvPlist() 111 | for app in res["LookupResult"].values(): 112 | if app.get("ApplicationType") == appType: 113 | appList.append(app["CFBundleIdentifier"]) 114 | #else: #FIXME 115 | # appList.append(app["CFBundleIdentifier"]) 116 | mci.close() 117 | #pprint(appList) 118 | return appList 119 | 120 | 121 | if __name__ == "__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 | -------------------------------------------------------------------------------- /pymobiledevice/util/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/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/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/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/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 | from pymobiledevice.lockdown import LockdownClient 26 | from pprint import pprint 27 | import time 28 | from six.moves import _thread 29 | 30 | # NP Client to device Notifications (post_notification) 31 | NP_SYNC_WILL_START = "com.apple.itunes-mobdev.syncWillStart" 32 | NP_SYNC_DID_START = "com.apple.itunes-mobdev.syncDidStart" 33 | NP_SYNC_DID_FINISH = "com.apple.itunes-mobdev.syncDidFinish" 34 | NP_SYNC_LOCK_REQUEST = "com.apple.itunes-mobdev.syncLockRequest" 35 | 36 | # Device to NP Client Notifications (get_notification) 37 | NP_SYNC_CANCEL_REQUEST = "com.apple.itunes-client.syncCancelRequest" 38 | NP_SYNC_SUSPEND_REQUEST = "com.apple.itunes-client.syncSuspendRequest" 39 | NP_SYNC_RESUME_REQUEST = "com.apple.itunes-client.syncResumeRequest" 40 | NP_PHONE_NUMBER_CHANGED = "com.apple.mobile.lockdown.phone_number_changed" 41 | NP_DEVICE_NAME_CHANGED = "com.apple.mobile.lockdown.device_name_changed" 42 | NP_TIMEZONE_CHANGED = "com.apple.mobile.lockdown.timezone_changed" 43 | NP_TRUSTED_HOST_ATTACHED = "com.apple.mobile.lockdown.trusted_host_attached" 44 | NP_HOST_DETACHED = "com.apple.mobile.lockdown.host_detached" 45 | NP_HOST_ATTACHED = "com.apple.mobile.lockdown.host_attached" 46 | NP_REGISTRATION_FAILED = "com.apple.mobile.lockdown.registration_failed" 47 | NP_ACTIVATION_STATE = "com.apple.mobile.lockdown.activation_state" 48 | NP_BRICK_STATE = "com.apple.mobile.lockdown.brick_state" 49 | NP_DISK_USAGE_CHANGED = "com.apple.mobile.lockdown.disk_usage_changed" 50 | NP_DS_DOMAIN_CHANGED = "com.apple.mobile.data_sync.domain_changed" 51 | NP_BACKUP_DOMAIN_CHANGED = "com.apple.mobile.backup.domain_changed" 52 | NP_APP_INSTALLED = "com.apple.mobile.application_installed" 53 | NP_APP_UNINSTALLED = "com.apple.mobile.application_uninstalled" 54 | NP_DEV_IMAGE_MOUNTED = "com.apple.mobile.developer_image_mounted" 55 | NP_ATTEMPTACTIVATION = "com.apple.springboard.attemptactivation" 56 | NP_ITDBPREP_DID_END = "com.apple.itdbprep.notification.didEnd" 57 | NP_LANGUAGE_CHANGED = "com.apple.language.changed" 58 | NP_ADDRESS_BOOK_PREF_CHANGED = "com.apple.AddressBook.PreferenceChanged" 59 | 60 | 61 | class NPClient(object): 62 | def __init__(self, lockdown=None, serviceName="com.apple.mobile.notification_proxy"): 63 | if lockdown: 64 | self.lockdown = lockdown 65 | else: 66 | self.lockdown = LockdownClient() 67 | self.service = self.lockdown.startService(serviceName) 68 | 69 | 70 | def stop_session(self): 71 | print("Disconecting...") 72 | self.service.close() 73 | 74 | 75 | def post_notification(self, notification): 76 | #Sends a notification to the device's notification_proxy. 77 | 78 | self.service.sendPlist({"Command": "PostNotification", 79 | "Name": notification}) 80 | 81 | self.service.sendPlist({"Command": "Shutdown"}) 82 | res = self.service.recvPlist() 83 | pprint(res) 84 | if res: 85 | if res.get("Command") == "ProxyDeath": 86 | return res.get("Command") 87 | else: 88 | print("Got unknown NotificationProxy command %s" % res.get("Command")) 89 | pprint(res) 90 | return None 91 | 92 | 93 | def observe_notification(self, notification): 94 | #Tells the device to send a notification on the specified event 95 | 96 | print("Observing %s" % notification) 97 | self.service.sendPlist({"Command": "ObserveNotification", 98 | "Name": notification}) 99 | 100 | 101 | def get_notification(self, notification): 102 | #Checks if a notification has been sent by the device 103 | 104 | res = self.service.recvPlist() 105 | if res: 106 | if res.get("Command") == "RelayNotification": 107 | if res.get("Name"): 108 | return res.get("Name") 109 | 110 | elif res.get("Command") == "ProxyDeath": 111 | print("NotificationProxy died!") 112 | else: 113 | print("Got unknown NotificationProxy command %s" % res.get("Command")) 114 | pprint(res) 115 | return None 116 | 117 | 118 | def notifier(self, name, args=None): 119 | 120 | if args == None: 121 | return None 122 | 123 | self.observe_notification(args.get("notification")) 124 | 125 | while args.get("running") == True: 126 | np_name = self.get_notification(args.get("notification")) 127 | if np_name: 128 | userdata = args.get("userdata") 129 | try: 130 | thread.start_new_thread( args.get("callback") , (np_name, userdata, ) ) 131 | except: 132 | print("Error: unable to start thread") 133 | 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 | try: 145 | import threading 146 | _thread.start_new_thread( self.notifier, ("NotificationProxyNotifier_"+notification, np_data, ) ) 147 | except: 148 | print("Error: unable to start thread") 149 | 150 | while(1): 151 | time.sleep(1) 152 | 153 | 154 | 155 | def cb_test(name,data=None): 156 | print("Got Notification >> %s" % name) 157 | print("Data:") 158 | pprint(data) 159 | 160 | 161 | if __name__ == "__main__": 162 | np = NPClient() 163 | np.subscribe(NP_DEVICE_NAME_CHANGED, cb_test, data=None) 164 | 165 | -------------------------------------------------------------------------------- /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 | if __name__ == "__main__": 214 | 215 | parser = OptionParser(usage="%prog") 216 | parser.add_option("-c", "--cmd", dest="cmd", default=False, 217 | help="Launch diagnostic command", type="string") 218 | parser.add_option("-m", "--mobilegestalt", dest="mobilegestalt_key", default=False, 219 | help="Request mobilegestalt key", type="string") 220 | parser.add_option("-i", "--ioclass", dest="ioclass", default=False, 221 | help="Request ioclass", type="string") 222 | parser.add_option("-n", "--ioname", dest="ioname", default=False, 223 | help="Request ionqme", type="string") 224 | 225 | (options, args) = parser.parse_args() 226 | 227 | diag = DIAGClient() 228 | if not options.cmd: 229 | res = diag.diagnostics() 230 | 231 | elif options.cmd == "IORegistry": 232 | res = diag.ioregistry_plane() 233 | 234 | elif options.cmd == "MobileGestalt": 235 | 236 | if not options.mobilegestalt_key or options.mobilegestalt_key not in MobileGestaltKeys.split("\n"): 237 | res = diag.query_mobilegestalt() 238 | 239 | else: 240 | res = diag.query_mobilegestalt([options.mobilegestalt_key]) 241 | 242 | else: 243 | res = diag.action(options.cmd) 244 | 245 | if res: 246 | for k in res.keys(): 247 | print(" %s \t: %s" % (k,res[k])) 248 | 249 | -------------------------------------------------------------------------------- /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 | from pymobiledevice.lockdown import LockdownClient 27 | import plistlib 28 | from pprint import pprint 29 | import os 30 | import datetime 31 | from pymobiledevice.afc import AFCClient 32 | 33 | # 34 | # Fix plistlib.py line 364 35 | # def asBase64(self, maxlinelength=76): 36 | # if self.data != None: 37 | # return _encodeBase64(self.data, maxlinelength) 38 | # return "" 39 | # 40 | # 41 | 42 | 43 | MOBILEBACKUP_E_SUCCESS = 0 44 | MOBILEBACKUP_E_INVALID_ARG = -1 45 | MOBILEBACKUP_E_PLIST_ERROR = -2 46 | MOBILEBACKUP_E_MUX_ERROR = -3 47 | MOBILEBACKUP_E_BAD_VERSION = -4 48 | MOBILEBACKUP_E_REPLY_NOT_OK = -5 49 | MOBILEBACKUP_E_UNKNOWN_ERROR = -256 50 | 51 | DEVICE_LINK_FILE_STATUS_NONE = 0 52 | DEVICE_LINK_FILE_STATUS_HUNK = 1 53 | DEVICE_LINK_FILE_STATUS_LAST_HUNK = 2 54 | 55 | class DeviceVersionNotSupported(Exception): 56 | def __str__(self): 57 | return "Device version not supported, please use mobilebackup2" 58 | 59 | 60 | class MobileBackup(object): 61 | def __init__(self, lockdown=None): 62 | if lockdown: 63 | self.lockdown = lockdown 64 | else: 65 | self.lockdown = LockdownClient() 66 | 67 | ProductVersion = self.lockdown.getValue("", "ProductVersion") 68 | if ProductVersion[0] >= "5": 69 | raise DeviceVersionNotSupported 70 | 71 | self.service = self.lockdown.startService("com.apple.mobilebackup") 72 | self.udid = self.lockdown.udid 73 | DLMessageVersionExchange = self.service.recvPlist() 74 | version_major = DLMessageVersionExchange[1] 75 | self.service.sendPlist(["DLMessageVersionExchange", "DLVersionsOk", version_major]) 76 | DLMessageDeviceReady = self.service.recvPlist() 77 | if DLMessageDeviceReady and DLMessageDeviceReady[0] == "DLMessageDeviceReady": 78 | print("Got DLMessageDeviceReady") 79 | 80 | def check_filename(self, name): 81 | if name.find("../") != -1: 82 | raise Exception("HAX, sneaky dots in path %s" % name) 83 | if not name.startswith(self.backupPath): 84 | if name.startswith(self.udid): 85 | name = os.path.join(self.backupPath, name) 86 | return name 87 | name = os.path.join(self.backupPath, self.udid, name) 88 | return name 89 | return name 90 | 91 | 92 | def read_file(self, filename): 93 | filename = self.check_filename(filename) 94 | if os.path.isfile(filename): 95 | with open(filename, 'rb') as f: 96 | data = f.read() 97 | f.close() 98 | return data 99 | return None 100 | 101 | 102 | def write_file(self, filename, data): 103 | filename = self.check_filename(filename) 104 | with open(filename, 'wb') as f: 105 | f.write(data) 106 | f.close() 107 | 108 | def create_info_plist(self): 109 | root_node = self.lockdown.allValues 110 | #print pprint(root_node) 111 | info = {"BuildVersion": root_node.get("BuildVersion") or "", 112 | "DeviceName": root_node.get("DeviceName") or "", 113 | "Display Name": root_node.get("DeviceName") or "", 114 | "GUID": "---", 115 | "ProductType": root_node.get("ProductType") or "", 116 | "ProductVersion": root_node.get("ProductVersion") or "", 117 | "Serial Number": root_node.get("SerialNumber") or "", 118 | "Unique Identifier": self.udid.upper(), 119 | "Target Identifier": self.udid, 120 | "Target Type": "Device", 121 | "iTunes Version": "10.0.1" 122 | } 123 | info["ICCID"] = root_node.get("IntegratedCircuitCardIdentity") or "" 124 | info["IMEI"] = root_node.get("InternationalMobileEquipmentIdentity") or "" 125 | info["Last Backup Date"] = datetime.datetime.now() 126 | 127 | afc = AFCClient(self.lockdown) 128 | iTunesFilesDict = {} 129 | iTunesFiles = afc.read_directory("/iTunes_Control/iTunes/") 130 | 131 | for i in iTunesFiles: 132 | data = afc.get_file_contents("/iTunes_Control/iTunes/" + i) 133 | if data: 134 | iTunesFilesDict[i] = plistlib.Data(data) 135 | info["iTunesFiles"] = iTunesFilesDict 136 | 137 | iBooksData2 = afc.get_file_contents("/Books/iBooksData2.plist") 138 | if iBooksData2: 139 | info["iBooks Data 2"] = plistlib.Data(iBooksData2) 140 | 141 | info["iTunes Settings"] = self.lockdown.getValue("com.apple.iTunes") 142 | print("Creating %s" % os.path.join(self.udid,"Info.plist")) 143 | self.write_file(os.path.join(self.udid,"Info.plist"), plistlib.writePlistToString(info)) 144 | 145 | def ping(self, message): 146 | self.service.sendPlist(["DLMessagePing", message]) 147 | print("ping response", self.service.recvPlist()) 148 | 149 | def device_link_service_send_process_message(self, msg): 150 | return self.service.sendPlist(["DLMessageProcessMessage", msg]) 151 | 152 | def device_link_service_receive_process_message(self): 153 | req = self.service.recvPlist() 154 | if req: 155 | assert req[0] == "DLMessageProcessMessage" 156 | return req[1] 157 | 158 | def send_file_received(self): 159 | return self.device_link_service_send_process_message({"BackupMessageTypeKey": "kBackupMessageBackupFileReceived"}) 160 | 161 | def request_backup(self): 162 | req = {"BackupComputerBasePathKey": "/", 163 | "BackupMessageTypeKey": "BackupMessageBackupRequest", 164 | "BackupProtocolVersion": "1.6" 165 | } 166 | self.create_info_plist() 167 | self.device_link_service_send_process_message(req) 168 | res = self.device_link_service_receive_process_message() 169 | if not res: 170 | return 171 | if res["BackupMessageTypeKey"] != "BackupMessageBackupReplyOK": 172 | print(res) 173 | return 174 | self.device_link_service_send_process_message(res) 175 | 176 | filedata = "" 177 | f = None 178 | outpath = None 179 | while True: 180 | res = self.service.recvPlist() 181 | if not res or res[0] != "DLSendFile": 182 | if res[0] == "DLMessageProcessMessage": 183 | if res[1].get("BackupMessageTypeKey") == "BackupMessageBackupFinished": 184 | print("Backup finished OK !") 185 | #TODO BackupFilesToDeleteKey 186 | plistlib.writePlist(res[1]["BackupManifestKey"], self.check_filename("Manifest.plist")) 187 | break 188 | data = res[1].data 189 | info = res[2] 190 | if not f: 191 | outpath = self.check_filename(info.get("DLFileDest")) 192 | print(info["DLFileAttributesKey"]["Filename"], info.get("DLFileDest")) 193 | f = open(outpath + ".mddata", "wb") 194 | f.write(data) 195 | if info.get("DLFileStatusKey") == DEVICE_LINK_FILE_STATUS_LAST_HUNK: 196 | self.send_file_received() 197 | f.close() 198 | if not info.get("BackupManifestKey", False): 199 | plistlib.writePlist(info.get("BackupFileInfo"), outpath + ".mdinfo") 200 | f = None 201 | 202 | if __name__ == "__main__": 203 | mb = MobileBackup() 204 | mb.request_backup() 205 | 206 | -------------------------------------------------------------------------------- /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 | from pymobiledevice.lockdown import LockdownClient 26 | from optparse import OptionParser 27 | import os 28 | from pymobiledevice.afc import AFCClient 29 | 30 | 31 | client_options = { 32 | "SkipUninstall" : False, 33 | "ApplicationSINF" : False, 34 | "iTunesMetadata" : False, 35 | "ReturnAttributes" : False 36 | } 37 | 38 | 39 | 40 | class installation_proxy(object): 41 | 42 | def __init__(self,lockdown=None): 43 | 44 | if lockdown: 45 | self.lockdown = lockdown 46 | else: 47 | self.lockdown = LockdownClient() 48 | 49 | self.service = self.lockdown.startService("com.apple.mobile.installation_proxy") 50 | 51 | 52 | def watch_completion(self,handler=None,*args): 53 | while True: 54 | z = self.service.recvPlist() 55 | if not z: 56 | break 57 | completion = z.get("PercentComplete") 58 | if completion: 59 | if handler: 60 | print("calling handler") 61 | handler(completion,*args) 62 | print("%s %% Complete" % z.get("PercentComplete")) 63 | if z.get("Status") == "Complete": 64 | return z.get("Status") 65 | return "Error" 66 | 67 | def send_cmd_for_bid(self,bundleID, cmd="Archive", options=None, handler=None, *args): 68 | cmd = {"Command": cmd, "ApplicationIdentifier": bundleID } 69 | if options: 70 | cmd.update(options) 71 | self.service.sendPlist(cmd) 72 | print("%s : %s\n" % (cmd, self.watch_completion(handler, *args))) 73 | 74 | def uninstall(self,bundleID, options=None, handler=None, *args): 75 | self.send_cmd_for_bid(bundleID, "Uninstall", options, handler, args) 76 | 77 | def install_or_upgrade(self, ipaPath, cmd="Install", options=None, handler=None, *args): 78 | afc = AFCClient(self.lockdown) 79 | afc.set_file_contents("/" + os.path.basename(ipaPath), open(ipaPath,"rb").read()) 80 | cmd = {"Command":cmd, "PackagePath": os.path.basename(ipaPath)} 81 | if options: 82 | cmd.update(options) 83 | self.service.sendPlist(cmd) 84 | print("%s : %s\n" % (cmd, self.watch_completion(handler, args))) 85 | 86 | def install(self,ipaPath, options=None, handler=None, *args): 87 | return self.install_or_upgrade(ipaPath, "Install", client_options, handler, args) 88 | 89 | def upgrade(self,ipaPath, options=None, handler=None, *args): 90 | return self.install_or_upgrade(ipaPath, "Upgrade", client_options, handler, args) 91 | 92 | def apps_info(self): 93 | self.service.sendPlist({"Command": "Lookup"}) 94 | return self.service.recvPlist().get('LookupResult') 95 | 96 | def archive(self,bundleID, options=None, handler=None, *args): 97 | self.send_cmd_for_bid(bundleID, "Archive", options, handler, args) 98 | 99 | def restore_archive(self,bundleID, options=None, handler=None, *args): 100 | self.send_cmd_for_bid(bundleID, "Restore", client_options, handler, args) 101 | 102 | def remove_archive(self,bundleID, options={}, handler=None, *args): 103 | self.send_cmd_for_bid(bundleID, "RemoveArchive", options, handler, args) 104 | 105 | def archives_info(self): 106 | return self.service.sendRequest({"Command": "LookupArchive"}).get("LookupResult") 107 | 108 | def search_path_for_bid(self, bid): 109 | path = None 110 | for a in self.get_apps(appTypes=["User","System"]): 111 | if a.get("CFBundleIdentifier") == bid: 112 | path = a.get("Path")+"/"+a.get("CFBundleExecutable") 113 | return path 114 | 115 | def get_apps(self, appTypes=["User"]): 116 | return [app for app in self.apps_info().values() 117 | if app.get("ApplicationType") in appTypes] 118 | 119 | def find_bundle_id(self, bundle_id): 120 | for app in self.get_apps(): 121 | if app.get('CFBundleIdentifier') == bundle_id: 122 | return app 123 | 124 | def print_apps(self, appType=["User"]): 125 | for app in self.get_apps(appType): 126 | print(("%s : %s => %s" % (app.get("CFBundleDisplayName"), 127 | app.get("CFBundleIdentifier"), 128 | app.get("Path") if app.get("Path") 129 | else app.get("Container"))).encode('utf-8')) 130 | 131 | def get_apps_bid(self, appTypes=["User"]): 132 | return [app["CFBundleIdentifier"] 133 | for app in self.get_apps() 134 | if app.get("ApplicationType") in appTypes] 135 | 136 | def iter_installed(self, appType= "User", attrs=None): 137 | options = {} 138 | if appType: 139 | options["ApplicationType"] = appType 140 | if attrs: 141 | options['ReturnAttributes'] = attrs 142 | 143 | self.service.sendPlist({ 144 | "Command": "Browse", 145 | "ClientOptions": options, 146 | }) 147 | 148 | while True: 149 | data = self.service.recvPlist() 150 | if data['Status'] == 'Complete': 151 | break 152 | for appinfo in data['CurrentList']: 153 | yield appinfo 154 | 155 | def close(self): 156 | self.service.close() 157 | 158 | def __del__(self): 159 | self.close() 160 | 161 | 162 | if __name__ == "__main__": 163 | parser = OptionParser(usage="%prog cmd ") 164 | parser.add_option("-l", "--listapps", 165 | default=False, 166 | action="store_true", 167 | help="List installed applications") 168 | parser.add_option("-i", "--install", 169 | default=False, 170 | action="store", 171 | dest="install_ipapath", 172 | metavar="FILE", 173 | help="Install application on device") 174 | parser.add_option("-r", "--remove", 175 | default=False, 176 | action="store", 177 | dest="remove_bundleid", 178 | metavar="BUNDLE_ID", 179 | help="Remove (uninstall) application from device") 180 | parser.add_option("-u", "--upgrade", 181 | default=False, 182 | action="store", 183 | dest="upgrade_bundleid", 184 | metavar="BUNDLE_ID", 185 | help="Upgrade application on device") 186 | parser.add_option("-a", "--archive", 187 | default=False, 188 | action="store", 189 | dest="archive_bundleid", 190 | metavar="BUNDLE_ID", 191 | help="Archive application on device") 192 | 193 | (options, args) = parser.parse_args() 194 | #FIXME We should handle all installation proxy commands 195 | if options.listapps: 196 | instpxy = installation_proxy() 197 | instpxy.print_apps(["User","System"]) 198 | elif options.install_ipapath: 199 | instpxy = installation_proxy() 200 | instpxy.install(options.install_ipapath) 201 | elif options.remove_bundleid: 202 | instpxy = installation_proxy() 203 | instpxy.remove(options.remove_bundleid) 204 | elif options.upgrade_bundleid: 205 | instpxy = installation_proxy() 206 | instpxy.upgrade(options.upgrade_bundleid) 207 | elif options.archive_bundleid: 208 | instpxy = installation_proxy() 209 | instpxy.archive(options.archive_bundleid) 210 | else: 211 | parser.error("Incorrect number of arguments") 212 | -------------------------------------------------------------------------------- /pymobiledevice/lockdown.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 sys 28 | import uuid 29 | import platform 30 | import re 31 | 32 | from pymobiledevice.plist_service import PlistService 33 | from pymobiledevice.util.ca import ca_do_everything 34 | from pymobiledevice.util import readHomeFile, writeHomeFile 35 | from pymobiledevice.usbmux import usbmux 36 | 37 | from six import PY3 38 | 39 | if PY3: 40 | plistlib.readPlistFromString = plistlib.loads 41 | plistlib.writePlistToString = plistlib.dumps 42 | plistlib.readPlist = plistlib.load 43 | 44 | 45 | class NotTrustedError(Exception): 46 | pass 47 | 48 | 49 | class PairingError(Exception): 50 | pass 51 | 52 | 53 | class NotPairedError(Exception): 54 | pass 55 | 56 | 57 | class CannotStopSessionError(Exception): 58 | pass 59 | 60 | 61 | class StartServiceError(Exception): 62 | def __init__(self, message): 63 | print("[ERROR] %s" % message) 64 | 65 | 66 | class FatalPairingError(Exception): 67 | pass 68 | 69 | 70 | # we store pairing records and ssl keys in ~/.pymobiledevice 71 | HOMEFOLDER = ".pymobiledevice" 72 | MAXTRIES = 20 73 | 74 | 75 | def list_devices(): 76 | mux = usbmux.USBMux() 77 | mux.process(1) 78 | return [d.serial for d in mux.devices] 79 | 80 | 81 | class LockdownClient(object): 82 | 83 | def __init__(self, udid=None): 84 | self.paired = False 85 | self.SessionID = None 86 | self.c = PlistService(62078, udid) 87 | self.hostID = self.generate_hostID() 88 | self.SystemBUID = self.generate_hostID() 89 | self.paired = False 90 | self.label = "pyMobileDevice" 91 | 92 | assert self.queryType() == "com.apple.mobile.lockdown" 93 | 94 | self.allValues = self.getValue() 95 | self.udid = self.allValues.get("UniqueDeviceID") 96 | self.UniqueChipID = self.allValues.get("UniqueChipID") 97 | self.DevicePublicKey = self.allValues.get("DevicePublicKey") 98 | self.ios_version = self.allValues.get("ProductVersion") 99 | self.identifier = self.udid 100 | if not self.identifier: 101 | if self.UniqueChipID: 102 | self.identifier = "%x" % self.UniqueChipID 103 | else: 104 | raise Exception("Could not get UDID or ECID, failing") 105 | 106 | if not self.validate_pairing(): 107 | self.pair() 108 | self.c = PlistService(62078, udid) 109 | if not self.validate_pairing(): 110 | raise FatalPairingError 111 | self.paired = True 112 | return 113 | 114 | def compare_ios_version(self, ios_version): 115 | """ 116 | currrent_version > ios_version return 1 117 | currrent_version = ios_version return 0 118 | currrent_version < ios_version return -1 119 | :param ios_version: 120 | :return: int 121 | """ 122 | version_reg = r'^\d*\.\d*\.?\d*$' 123 | if not re.match(version_reg, ios_version): 124 | raise Exception('ios_version invalid:%s' % ios_version) 125 | a = self.ios_version.split('.') 126 | b = ios_version.split('.') 127 | length = min(len(a), len(b)) 128 | for i in range(length): 129 | if int(a[i]) < int(b[i]): 130 | return -1 131 | if int(a[i]) > int(b[i]): 132 | return 1 133 | if len(a) > len(b): 134 | return 1 135 | elif len(a) < len(b): 136 | return -1 137 | else: 138 | return 0 139 | 140 | def queryType(self): 141 | self.c.sendPlist({"Request": "QueryType"}) 142 | res = self.c.recvPlist() 143 | return res.get("Type") 144 | 145 | def generate_hostID(self): 146 | hostname = platform.node() 147 | hostid = uuid.uuid3(uuid.NAMESPACE_DNS, hostname) 148 | return str(hostid).upper() 149 | 150 | def enter_recovery(self): 151 | self.c.sendPlist({"Request": "EnterRecovery"}) 152 | print(self.c.recvPlist()) 153 | 154 | def stop_session(self): 155 | if self.SessionID and self.c: 156 | self.c.sendPlist({"Label": self.label, "Request": "StopSession", "SessionID": self.SessionID}) 157 | self.SessionID = None 158 | res = self.c.recvPlist() 159 | if not res or res.get("Result") != "Success": 160 | raise CannotStopSessionError 161 | return res 162 | 163 | def validate_pairing(self): 164 | pair_record = None 165 | certPem = None 166 | privateKeyPem = None 167 | 168 | if sys.platform == "win32": 169 | folder = os.environ["ALLUSERSPROFILE"] + "/Apple/Lockdown/" 170 | elif sys.platform == "darwin": 171 | folder = "/var/db/lockdown/" 172 | elif len(sys.platform) >= 5: 173 | if sys.platform[0:5] == "linux": 174 | folder = "/var/lib/lockdown/" 175 | try: 176 | pair_record = plistlib.readPlist(folder + "%s.plist" % self.identifier) 177 | print("Using iTunes pair record: %s.plist" % self.identifier) 178 | except: 179 | print("No iTunes pairing record found for device %s" % self.identifier) 180 | if self.compare_ios_version("13.0") >= 0: 181 | print("Getting pair record from usbmuxd") 182 | client = usbmux.UsbmuxdClient() 183 | pair_record = client.get_pair_record(self.udid) 184 | else: 185 | print("Looking for pymobiledevice pairing record") 186 | record = readHomeFile(HOMEFOLDER, "%s.plist" % self.identifier) 187 | if record: 188 | pair_record = plistlib.readPlistFromString(record) 189 | print("Found pymobiledevice pairing record for device %s" % self.udid) 190 | else: 191 | print("No pymobiledevice pairing record found for device %s" % self.identifier) 192 | return False 193 | self.record = pair_record 194 | 195 | if PY3: 196 | certPem = pair_record["HostCertificate"] 197 | privateKeyPem = pair_record["HostPrivateKey"] 198 | else: 199 | certPem = pair_record["HostCertificate"].data 200 | privateKeyPem = pair_record["HostPrivateKey"].data 201 | 202 | if self.compare_ios_version("11.0") < 0: 203 | ValidatePair = {"Label": self.label, "Request": "ValidatePair", "PairRecord": pair_record} 204 | self.c.sendPlist(ValidatePair) 205 | r = self.c.recvPlist() 206 | if not r or r.has_key("Error"): 207 | pair_record = None 208 | print("ValidatePair fail", ValidatePair) 209 | return False 210 | 211 | self.hostID = pair_record.get("HostID", self.hostID) 212 | self.SystemBUID = pair_record.get("SystemBUID", self.SystemBUID) 213 | d = {"Label": self.label, "Request": "StartSession", "HostID": self.hostID, 'SystemBUID': self.SystemBUID} 214 | self.c.sendPlist(d) 215 | startsession = self.c.recvPlist() 216 | self.SessionID = startsession.get("SessionID") 217 | if startsession.get("EnableSessionSSL"): 218 | self.sslfile = self.identifier + "_ssl.txt" 219 | lf = "\n" 220 | if PY3: 221 | lf = b"\n" 222 | self.sslfile = writeHomeFile(HOMEFOLDER, self.sslfile, certPem + lf + privateKeyPem) 223 | self.c.ssl_start(self.sslfile, self.sslfile) 224 | 225 | self.paired = True 226 | return True 227 | 228 | def pair(self): 229 | self.DevicePublicKey = self.getValue("", "DevicePublicKey") 230 | if self.DevicePublicKey == '': 231 | print("Unable to retreive DevicePublicKey") 232 | return False 233 | 234 | print("Creating host key & certificate") 235 | certPem, privateKeyPem, DeviceCertificate = ca_do_everything(self.DevicePublicKey) 236 | 237 | pair_record = {"DevicePublicKey": plistlib.Data(self.DevicePublicKey), 238 | "DeviceCertificate": plistlib.Data(DeviceCertificate), 239 | "HostCertificate": plistlib.Data(certPem), 240 | "HostID": self.hostID, 241 | "RootCertificate": plistlib.Data(certPem), 242 | "SystemBUID": "30142955-444094379208051516"} 243 | 244 | pair = {"Label": self.label, "Request": "Pair", "PairRecord": pair_record} 245 | self.c.sendPlist(pair) 246 | pair = self.c.recvPlist() 247 | 248 | if pair and pair.get("Result") == "Success" or "EscrowBag" in pair: 249 | pair_record["HostPrivateKey"] = plistlib.Data(privateKeyPem) 250 | pair_record["EscrowBag"] = pair.get("EscrowBag") 251 | writeHomeFile(HOMEFOLDER, "%s.plist" % self.identifier, plistlib.writePlistToString(pair_record)) 252 | self.paired = True 253 | return True 254 | 255 | elif pair and pair.get("Error") == "PasswordProtected": 256 | self.c.close() 257 | raise NotTrustedError 258 | 259 | else: 260 | print(pair.get("Error")) 261 | self.c.close() 262 | raise PairingError 263 | 264 | def getValue(self, domain=None, key=None): 265 | 266 | if (isinstance(key, str) and hasattr(self, 'record') and hasattr(self.record, key)): 267 | return self.record[key] 268 | 269 | req = {"Request": "GetValue", "Label": self.label} 270 | 271 | if domain: 272 | req["Domain"] = domain 273 | if key: 274 | req["Key"] = key 275 | 276 | self.c.sendPlist(req) 277 | res = self.c.recvPlist() 278 | if res: 279 | r = res.get("Value") 280 | if hasattr(r, "data"): 281 | return r.data 282 | return r 283 | 284 | def setValue(self, value, domain=None, key=None): 285 | 286 | req = {"Request": "SetValue", "Label": self.label} 287 | 288 | if domain: 289 | req["Domain"] = domain 290 | if key: 291 | req["Key"] = key 292 | 293 | req["Value"] = value 294 | self.c.sendPlist(req) 295 | res = self.c.recvPlist() 296 | print(res) 297 | return res 298 | 299 | def startService(self, name): 300 | if not self.paired: 301 | print("NotPaired") 302 | raise NotPairedError 303 | 304 | self.c.sendPlist({"Label": self.label, "Request": "StartService", "Service": name}) 305 | startService = self.c.recvPlist() 306 | ssl_enabled = startService.get("EnableServiceSSL", False) 307 | if not startService or startService.get("Error"): 308 | raise StartServiceError(startService.get("Error")) 309 | plist_service = PlistService(startService.get("Port"), self.udid) 310 | if ssl_enabled: 311 | plist_service.ssl_start(self.sslfile, self.sslfile) 312 | 313 | ssl_dial_only = False 314 | if name in ("com.apple.instruments.remoteserver", 315 | "com.apple.accessibility.axAuditDaemon.remoteserver", 316 | "com.apple.testmanagerd.lockdown", 317 | "com.apple.debugserver"): 318 | ssl_dial_only = True 319 | if ssl_dial_only: 320 | plist_service.ssl_stop() 321 | return plist_service 322 | 323 | def startServiceWithEscrowBag(self, name, escrowBag=None): 324 | if not self.paired: 325 | print("NotPaired") 326 | raise NotPairedError 327 | 328 | if (not escrowBag): 329 | escrowBag = self.record['EscrowBag'] 330 | 331 | self.c.sendPlist({"Label": self.label, "Request": "StartService", "Service": name, 'EscrowBag': escrowBag}) 332 | StartService = self.c.recvPlist() 333 | if not StartService or StartService.get("Error"): 334 | if StartService.get("Error", "") == 'PasswordProtected': 335 | raise StartServiceError( 336 | 'your device is protected with password, please enter password in device and try again') 337 | raise StartServiceError(StartService.get("Error")) 338 | ssl_enabled = StartService.get("EnableServiceSSL", False) 339 | plist_service = PlistService(StartService.get("Port"), self.udid) 340 | if ssl_enabled: 341 | plist_service.ssl_start(self.sslfile, self.sslfile) 342 | return plist_service 343 | 344 | 345 | if __name__ == "__main__": 346 | l = LockdownClient() 347 | # if l: 348 | # n = writeHomeFile(HOMEFOLDER, "%s_infos.plist" % l.udid, plistlib.writePlistToString(l.allValues)) 349 | # print("Wrote infos to %s" % n) 350 | # else: 351 | # print("Unable to connect to device") 352 | print(l.getValue()) 353 | -------------------------------------------------------------------------------- /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 | try: 25 | import plistlib 26 | 27 | haveplist = True 28 | except: 29 | haveplist = False 30 | 31 | 32 | class MuxError(Exception): 33 | pass 34 | 35 | 36 | class MuxVersionError(MuxError): 37 | pass 38 | 39 | 40 | class SafeStreamSocket: 41 | def __init__(self, address, family): 42 | self.sock = socket.socket(family, socket.SOCK_STREAM) 43 | self.sock.connect(address) 44 | 45 | def send(self, msg): 46 | totalsent = 0 47 | while totalsent < len(msg): 48 | sent = self.sock.send(msg[totalsent:]) 49 | if sent == 0: 50 | raise MuxError("socket connection broken") 51 | totalsent = totalsent + sent 52 | 53 | def recv(self, size): 54 | msg = "" 55 | if PY3: 56 | msg = b"" 57 | while len(msg) < size: 58 | chunk = self.sock.recv(size - len(msg)) 59 | empty_chunk = "" 60 | if PY3: 61 | empty_chunk = b"" 62 | if chunk == empty_chunk: 63 | raise MuxError("socket connection broken") 64 | msg = msg + chunk 65 | return msg 66 | 67 | 68 | class MuxDevice(object): 69 | def __init__(self, devid, usbprod, serial, location): 70 | self.devid = devid 71 | self.usbprod = usbprod 72 | self.serial = serial 73 | self.location = location 74 | 75 | def __str__(self): 76 | return "" % ( 77 | self.devid, 78 | self.usbprod, 79 | self.serial, 80 | self.location, 81 | ) 82 | 83 | 84 | class BinaryProtocol(object): 85 | TYPE_RESULT = 1 86 | TYPE_CONNECT = 2 87 | TYPE_LISTEN = 3 88 | TYPE_DEVICE_ADD = 4 89 | TYPE_DEVICE_REMOVE = 5 90 | VERSION = 0 91 | 92 | def __init__(self, socket): 93 | self.socket = socket 94 | self.connected = False 95 | 96 | def _pack(self, req, payload): 97 | if req == self.TYPE_CONNECT: 98 | connect_data = "\x00\x00" 99 | if PY3: 100 | connect_data = b"\x00\x00" 101 | return ( 102 | struct.pack("IH", payload["DeviceID"], payload["PortNumber"]) 103 | + connect_data 104 | ) 105 | elif req == self.TYPE_LISTEN: 106 | if PY3: 107 | return b"" 108 | else: 109 | return "" 110 | else: 111 | raise ValueError("Invalid outgoing request type %d" % req) 112 | 113 | def _unpack(self, resp, payload): 114 | if resp == self.TYPE_RESULT: 115 | return {"Number": struct.unpack("I", payload)[0]} 116 | elif resp == self.TYPE_DEVICE_ADD: 117 | devid, usbpid, serial, pad, location = struct.unpack("IH256sHI", payload) 118 | serial = serial.split("\0")[0] 119 | return { 120 | "DeviceID": devid, 121 | "Properties": { 122 | "LocationID": location, 123 | "SerialNumber": serial, 124 | "ProductID": usbpid, 125 | }, 126 | } 127 | elif resp == self.TYPE_DEVICE_REMOVE: 128 | devid = struct.unpack("I", payload)[0] 129 | return {"DeviceID": devid} 130 | else: 131 | raise MuxError("Invalid incoming response type %d" % resp) 132 | 133 | def sendpacket(self, req, tag, payload={}): 134 | payload = self._pack(req, payload) 135 | if self.connected: 136 | raise MuxError("Mux is connected, cannot issue control packets") 137 | length = 16 + len(payload) 138 | data = struct.pack("IIII", length, self.VERSION, req, tag) + payload 139 | self.socket.send(data) 140 | 141 | def getpacket(self): 142 | if self.connected: 143 | raise MuxError("Mux is connected, cannot issue control packets") 144 | dlen = self.socket.recv(4) 145 | dlen = struct.unpack("I", dlen)[0] 146 | body = self.socket.recv(dlen - 4) 147 | version, resp, tag = struct.unpack("III", body[:0xC]) 148 | if version != self.VERSION: 149 | raise MuxVersionError( 150 | "Version mismatch: expected %d, got %d" % (self.VERSION, version) 151 | ) 152 | payload = self._unpack(resp, body[0xC:]) 153 | return (resp, tag, payload) 154 | 155 | 156 | class PlistProtocol(BinaryProtocol): 157 | TYPE_RESULT = "Result" 158 | TYPE_CONNECT = "Connect" 159 | TYPE_LISTEN = "Listen" 160 | TYPE_DEVICE_ADD = "Attached" 161 | TYPE_DEVICE_REMOVE = "Detached" # ??? 162 | TYPE_PLIST = 8 163 | VERSION = 1 164 | 165 | def __init__(self, socket): 166 | if not haveplist: 167 | raise Exception("You need the plistlib module") 168 | BinaryProtocol.__init__(self, socket) 169 | 170 | def _pack(self, req, payload): 171 | return payload 172 | 173 | def _unpack(self, resp, payload): 174 | return payload 175 | 176 | def sendpacket(self, req, tag, payload={}): 177 | payload["ClientVersionString"] = "qt4i-usbmuxd" 178 | if isinstance(req, int): 179 | req = [self.TYPE_CONNECT, self.TYPE_LISTEN][req - 2] 180 | payload["MessageType"] = req 181 | payload["ProgName"] = "tcprelay" 182 | if PY3: 183 | wrapped_payload = plistlib.dumps(payload) 184 | else: 185 | wrapped_payload = plistlib.writePlistToString(payload) 186 | BinaryProtocol.sendpacket(self, self.TYPE_PLIST, tag, wrapped_payload) 187 | 188 | def getpacket(self): 189 | resp, tag, payload = BinaryProtocol.getpacket(self) 190 | if resp != self.TYPE_PLIST: 191 | raise MuxError("Received non-plist type %d" % resp) 192 | if PY3: 193 | payload = plistlib.loads(payload) 194 | else: 195 | payload = plistlib.readPlistFromString(payload) 196 | return payload.get("MessageType", ""), tag, payload 197 | 198 | 199 | class MuxConnection(object): 200 | def __init__(self, socketpath, protoclass): 201 | self.socketpath = socketpath 202 | if sys.platform in ["win32", "cygwin"]: 203 | family = socket.AF_INET 204 | address = ("127.0.0.1", 27015) 205 | else: 206 | family = socket.AF_UNIX 207 | address = self.socketpath 208 | self.socket = SafeStreamSocket(address, family) 209 | self.proto = protoclass(self.socket) 210 | self.pkttag = 1 211 | self.devices = [] 212 | 213 | def _getreply(self): 214 | while True: 215 | resp, tag, data = self.proto.getpacket() 216 | if resp == self.proto.TYPE_RESULT: 217 | return tag, data 218 | else: 219 | raise MuxError("Invalid packet type received: %d" % resp) 220 | 221 | def _processpacket(self): 222 | resp, tag, data = self.proto.getpacket() 223 | if resp == self.proto.TYPE_DEVICE_ADD: 224 | self.devices.append( 225 | MuxDevice( 226 | data["DeviceID"], 227 | data["Properties"]["ProductID"], 228 | data["Properties"]["SerialNumber"], 229 | data["Properties"]["LocationID"], 230 | ) 231 | ) 232 | elif resp == self.proto.TYPE_DEVICE_REMOVE: 233 | for dev in self.devices: 234 | if dev.devid == data["DeviceID"]: 235 | self.devices.remove(dev) 236 | elif resp == self.proto.TYPE_RESULT: 237 | raise MuxError("Unexpected result: %d" % resp) 238 | else: 239 | raise MuxError("Invalid packet type received: %d" % resp) 240 | 241 | def _exchange(self, req, payload={}): 242 | mytag = self.pkttag 243 | self.pkttag += 1 244 | self.proto.sendpacket(req, mytag, payload) 245 | recvtag, data = self._getreply() 246 | if recvtag != mytag: 247 | raise MuxError("Reply tag mismatch: expected %d, got %d" % (mytag, recvtag)) 248 | return data["Number"] 249 | 250 | def listen(self): 251 | ret = self._exchange(self.proto.TYPE_LISTEN) 252 | if ret != 0: 253 | raise MuxError("Listen failed: error %d" % ret) 254 | 255 | def process(self, timeout=None): 256 | if self.proto.connected: 257 | raise MuxError("Socket is connected, cannot process listener events") 258 | rlo, wlo, xlo = select.select( 259 | [self.socket.sock], [], [self.socket.sock], timeout 260 | ) 261 | if xlo: 262 | self.socket.sock.close() 263 | raise MuxError("Exception in listener socket") 264 | if rlo: 265 | self._processpacket() 266 | 267 | def connect(self, device, port): 268 | ret = self._exchange( 269 | self.proto.TYPE_CONNECT, 270 | { 271 | "DeviceID": device.devid, 272 | "PortNumber": ((port << 8) & 0xFF00) | (port >> 8), 273 | }, 274 | ) 275 | if ret != 0: 276 | raise MuxError("Connect failed: error %d" % ret) 277 | self.proto.connected = True 278 | return self.socket.sock 279 | 280 | def close(self): 281 | self.socket.sock.close() 282 | 283 | 284 | class USBMux(object): 285 | def __init__(self, socketpath=None): 286 | if socketpath is None: 287 | if sys.platform == "darwin": 288 | socketpath = "/var/run/usbmuxd" 289 | else: 290 | socketpath = "/var/run/usbmuxd" 291 | self.socketpath = socketpath 292 | self.listener = MuxConnection(socketpath, PlistProtocol) 293 | try: 294 | self.listener.listen() 295 | self.version = 0 296 | self.protoclass = PlistProtocol 297 | except MuxVersionError: 298 | self.listener = MuxConnection(socketpath, BinaryProtocol) 299 | self.listener.listen() 300 | self.protoclass = BinaryProtocol 301 | self.version = 1 302 | self.devices = self.listener.devices 303 | 304 | def process(self, timeout=10): 305 | self.listener.process(timeout) 306 | 307 | def connect(self, device, port): 308 | connector = MuxConnection(self.socketpath, self.protoclass) 309 | return connector.connect(device, port) 310 | 311 | 312 | class UsbmuxdClient(MuxConnection): 313 | def __init__(self): 314 | super(UsbmuxdClient, self).__init__("/var/run/usbmuxd", PlistProtocol) 315 | 316 | def get_pair_record(self, udid): 317 | tag = self.pkttag 318 | self.pkttag += 1 319 | payload = {"PairRecordID": udid} 320 | self.proto.sendpacket("ReadPairRecord", tag, payload) 321 | _, recvtag, data = self.proto.getpacket() 322 | if recvtag != tag: 323 | raise MuxError("Reply tag mismatch: expected %d, got %d" % (tag, recvtag)) 324 | if PY3: 325 | pair_record = data["PairRecordData"] 326 | pair_record = plistlib.loads(pair_record) 327 | else: 328 | pair_record = data["PairRecordData"].data 329 | try: 330 | pair_record = plistlib.readPlistFromString(pair_record) 331 | except Exception: 332 | pair_record = self.parse_pair_record(pair_record) 333 | return pair_record 334 | 335 | def parse_pair_record(self, pair_record): 336 | import re 337 | 338 | desc = {} 339 | system_buid = r".*?SystemBUID.*?(\w{8}\-\w{4}\-\w{4}\-\w{4}\-\w{12}).*" 340 | wifi_mac_addr = ( 341 | r".*?WiFiMACAddress.*?(\w{2}\:\w{2}\:\w{2}\:\w{2}\:\w{2}\:\w{2}).*" 342 | ) 343 | host_id = r".*?HostID.*?(\w{8}\-\w{4}\-\w{4}\-\w{4}\-\w{12}).*" 344 | EscrowBag = r".*?EscrowBag.+?(.*?)\^" 345 | content = re.findall(system_buid, pair_record) 346 | if content: 347 | desc["SystemBUID"] = content[0] 348 | wifi_mac_addr = re.findall(wifi_mac_addr, pair_record) 349 | if wifi_mac_addr: 350 | desc["WiFiMACAddress"] = wifi_mac_addr[0] 351 | host_id = re.findall(host_id, pair_record) 352 | if host_id: 353 | desc["HostID"] = host_id[0] 354 | EscrowBag = re.findall(EscrowBag, pair_record) 355 | if EscrowBag: 356 | desc["EscrowBag"] = EscrowBag[0] 357 | 358 | in_func = False 359 | for line in pair_record.split("\n"): 360 | if "DeviceCertificate" in line and not in_func: 361 | in_func = "DeviceCertificate" 362 | desc["DeviceCertificate"] = "-----BEGIN CERTIFICATE-----\n" 363 | elif "HostCertificate" in line and not in_func: 364 | in_func = "HostCertificate" 365 | desc["HostCertificate"] = "-----BEGIN CERTIFICATE-----\n" 366 | elif "HostPrivateKey" in line and not in_func: 367 | in_func = "HostPrivateKey" 368 | desc["HostPrivateKey"] = "-----BEGIN PRIVATE KEY-----\n" 369 | elif "RootCertificate" in line and not in_func: 370 | in_func = "RootCertificate" 371 | desc["RootCertificate"] = "-----BEGIN CERTIFICATE-----\n" 372 | elif "RootPrivateKey" in line and not in_func: 373 | in_func = "RootPrivateKey" 374 | desc["RootPrivateKey"] = "-----BEGIN PRIVATE KEY-----\n" 375 | elif "-----END" in line and in_func: 376 | desc[in_func] += line + "\n" 377 | in_func = False 378 | elif in_func: 379 | desc[in_func] += line + "\n" 380 | for key in desc: 381 | if "PrivateKey" in key or "Certificate" in key or "EscrowBag" in key: 382 | desc[key] = plistlib.Data(desc[key]) 383 | return desc 384 | 385 | 386 | if __name__ == "__main__": 387 | mux = USBMux() 388 | print("Waiting for devices...") 389 | if not mux.devices: 390 | mux.process(0.1) 391 | while True: 392 | for dev in mux.devices: 393 | print("Device:", dev) 394 | mux.process() 395 | -------------------------------------------------------------------------------- /pymobiledevice/mobilebackup2.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 os 27 | import datetime 28 | from optparse import OptionParser 29 | from pprint import pprint 30 | from biplist import writePlist, readPlist 31 | from time import mktime, gmtime 32 | 33 | from pymobiledevice.lockdown import LockdownClient 34 | from pymobiledevice.mobilebackup import MobileBackup 35 | from uuid import uuid4 36 | CODE_SUCCESS = 0x00 37 | CODE_ERROR_LOCAL = 0x06 38 | CODE_ERROR_REMOTE = 0x0b 39 | CODE_FILE_DATA = 0x0c 40 | 41 | ERROR_ENOENT = -6 42 | ERROR_EEXIST = -7 43 | 44 | 45 | class DeviceVersionNotSupported(Exception): 46 | def __str__(self): 47 | return "Device version not supported, please use mobilebackup" 48 | 49 | 50 | class MobileBackup2(MobileBackup): 51 | 52 | service = None 53 | def __init__(self, lockdown = None,backupPath = None, password=""): 54 | if lockdown: 55 | self.lockdown = lockdown 56 | else: 57 | self.lockdown = LockdownClient() 58 | 59 | ProductVersion = self.lockdown.getValue("", "ProductVersion") 60 | if ProductVersion and int(ProductVersion[:ProductVersion.find('.')]) < 5: 61 | raise DeviceVersionNotSupported 62 | 63 | self.udid = lockdown.getValue("", "UniqueDeviceID") 64 | self.willEncrypt = lockdown.getValue("com.apple.mobile.backup", "WillEncrypt") 65 | self.escrowBag = lockdown.getValue('', 'EscrowBag') 66 | 67 | self.service = self.lockdown.startServiceWithEscrowBag("com.apple.mobilebackup2", self.escrowBag) 68 | if not self.service: 69 | raise Exception("MobileBackup2 init error : Could not start com.apple.mobilebackup2") 70 | 71 | if backupPath: 72 | self.backupPath = backupPath 73 | else: 74 | self.backupPath = "backups" 75 | if not os.path.isdir(self.backupPath): 76 | os.makedirs(self.backupPath,0o0755) 77 | 78 | print("Starting new com.apple.mobilebackup2 service with working dir: %s" % self.backupPath) 79 | 80 | self.password = password 81 | DLMessageVersionExchange = self.service.recvPlist() 82 | version_major = DLMessageVersionExchange[1] 83 | self.service.sendPlist(["DLMessageVersionExchange", "DLVersionsOk", version_major]) 84 | DLMessageDeviceReady = self.service.recvPlist() 85 | if DLMessageDeviceReady and DLMessageDeviceReady[0] == "DLMessageDeviceReady": 86 | self.version_exchange() 87 | else: 88 | raise Exception("MobileBackup2 init error %s" % DLMessageDeviceReady) 89 | 90 | def __del__(self): 91 | if self.service: 92 | self.service.sendPlist(["DLMessageDisconnect", "___EmptyParameterString___"]) 93 | 94 | def internal_mobilebackup2_send_message(self, name, data): 95 | data["MessageName"] = name 96 | self.device_link_service_send_process_message(data) 97 | 98 | def internal_mobilebackup2_receive_message(self, name=None): 99 | res = self.device_link_service_receive_process_message() 100 | if res: 101 | if name and res["MessageName"] != name: 102 | print("MessageName does not match %s %s" % (name, str(res))) 103 | return res 104 | 105 | def version_exchange(self): 106 | self.internal_mobilebackup2_send_message("Hello", {"SupportedProtocolVersions": [2.0,2.1]}) 107 | return self.internal_mobilebackup2_receive_message("Response") 108 | 109 | def mobilebackup2_send_request(self, request, target, source, options={}): 110 | d = {"TargetIdentifier": target, 111 | "SourceIdentifier": source, 112 | "Options": options} 113 | self.internal_mobilebackup2_send_message(request, d) 114 | 115 | def mobilebackup2_receive_message(self): 116 | return self.service.recvPlist() 117 | 118 | def mobilebackup2_send_status_response(self, status_code, status1="___EmptyParameterString___", status2={}): 119 | a = ["DLMessageStatusResponse", status_code, status1, status2] 120 | self.service.sendPlist(a) 121 | 122 | def mb2_handle_free_disk_space(self,msg): 123 | s = os.statvfs(self.backupPath) 124 | freeSpace = s.f_bsize * s.f_bavail 125 | a = ["DLMessageStatusResponse", 0, freeSpace] 126 | self.service.sendPlist(a) 127 | 128 | def mb2_multi_status_add_file_error(self, errplist, path, error_code, error_message): 129 | errplist[path] = {"DLFileErrorCode": error_code, "DLFileErrorString": error_message} 130 | 131 | def mb2_handle_copy_item(self, msg): 132 | src = self.check_filename(msg[1]) 133 | dst = self.check_filename(msg[2]) 134 | if os.path.isfile(src): 135 | data = self.read_file(src) 136 | self.write_file(dst, data) 137 | else: 138 | os.makedirs(dst) 139 | self.mobilebackup2_send_status_response(0) 140 | 141 | def mb2_handle_send_file(self, filename, errplist): 142 | self.service.send_raw(filename) 143 | if not filename.startswith(self.udid): 144 | filename = self.udid + "/" + filename 145 | 146 | data = self.read_file(self.check_filename(filename)) 147 | if data != None: 148 | print("Sending %s to device" % filename) 149 | self.service.send_raw(chr(CODE_FILE_DATA) + data) 150 | self.service.send_raw(chr(CODE_SUCCESS)) 151 | else: 152 | print("File %s requested from device not found" % filename) 153 | self.service.send_raw(chr(CODE_ERROR_LOCAL)) 154 | self.mb2_multi_status_add_file_error(errplist, filename, ERROR_ENOENT, "Could not find the droid you were looking for ;)") 155 | 156 | def mb2_handle_send_files(self, msg): 157 | errplist = {} 158 | for f in msg[1]: 159 | self.mb2_handle_send_file(f, errplist) 160 | self.service.send("\x00\x00\x00\x00") 161 | if len(errplist): 162 | self.mobilebackup2_send_status_response(-13, "Multi status", errplist) 163 | else: 164 | self.mobilebackup2_send_status_response(0) 165 | 166 | def mb2_handle_list_directory(self, msg): 167 | path = msg[1] 168 | dirlist = {} 169 | self.mobilebackup2_send_status_response(0, status2=dirlist); 170 | 171 | def mb2_handle_make_directory(self, msg): 172 | dirname = self.check_filename(msg[1]) 173 | print("Creating directory %s" % dirname) 174 | if not os.path.isdir(dirname): 175 | os.makedirs(dirname) 176 | self.mobilebackup2_send_status_response(0, "") 177 | 178 | def mb2_handle_receive_files(self, msg): 179 | done = 0 180 | while not done: 181 | device_filename = self.service.recv_raw() 182 | if device_filename == "": 183 | break 184 | backup_filename = self.service.recv_raw() 185 | filedata = "" 186 | while True: 187 | stuff = self.service.recv_raw() 188 | if ord(stuff[0]) == CODE_FILE_DATA: 189 | filedata += stuff[1:] 190 | elif ord(stuff[0]) == CODE_SUCCESS: 191 | self.write_file(self.check_filename(backup_filename), filedata) 192 | break 193 | else: 194 | print("Unknown code", ord(stuff[0])) 195 | break 196 | self.mobilebackup2_send_status_response(0) 197 | 198 | def mb2_handle_move_files(self, msg): 199 | for k,v in msg[1].items(): 200 | print("Renaming %s to %s" % (self.check_filename(k),self.check_filename(v))) 201 | os.rename(self.check_filename(k),self.check_filename(v)) 202 | self.mobilebackup2_send_status_response(0) 203 | 204 | def mb2_handle_remove_files(self, msg): 205 | for filename in msg[1]: 206 | print("Removing ", self.check_filename(filename)) 207 | try: 208 | filename = self.check_filename(filename) 209 | if os.path.isfile(filename): 210 | os.unlink(filename) 211 | except Exception as e: 212 | print(e) 213 | self.mobilebackup2_send_status_response(0) 214 | 215 | def work_loop(self): 216 | while True: 217 | msg = self.mobilebackup2_receive_message() 218 | if not msg: 219 | break 220 | 221 | assert(msg[0] in ["DLMessageDownloadFiles", 222 | "DLContentsOfDirectory", 223 | "DLMessageCreateDirectory", 224 | "DLMessageUploadFiles", 225 | "DLMessageMoveFiles","DLMessageMoveItems", 226 | "DLMessageRemoveFiles", "DLMessageRemoveItems", 227 | "DLMessageCopyItem", 228 | "DLMessageProcessMessage", 229 | "DLMessageGetFreeDiskSpace", 230 | "DLMessageDisconnect"]) 231 | 232 | if msg[0] == "DLMessageDownloadFiles": 233 | self.mb2_handle_send_files(msg) 234 | elif msg[0] == "DLContentsOfDirectory": 235 | self.mb2_handle_list_directory(msg) 236 | elif msg[0] == "DLMessageCreateDirectory": 237 | self.mb2_handle_make_directory(msg) 238 | elif msg[0] == "DLMessageUploadFiles": 239 | self.mb2_handle_receive_files(msg) 240 | elif msg[0] in ["DLMessageMoveFiles","DLMessageMoveItems"]: 241 | self.mb2_handle_move_files(msg) 242 | elif msg[0] in ["DLMessageRemoveFiles", "DLMessageRemoveItems"]: 243 | self.mb2_handle_remove_files(msg) 244 | elif msg[0] == "DLMessageCopyItem": 245 | self.mb2_handle_copy_item(msg) 246 | elif msg[0] == "DLMessageProcessMessage": 247 | errcode = msg[1].get("ErrorCode") 248 | if errcode == 0: 249 | m = msg[1].get("MessageName") 250 | if m != "Response": 251 | print(m) 252 | if errcode == 1: 253 | raise Exception("Please unlock your device and retry...") 254 | if errcode == 211: 255 | raise Exception('Please go to Settings->iClould->Find My iPhone and disable it') 256 | if errcode == 105: 257 | raise Exception('Not enough free space on device for restore') 258 | if errcode == 17: 259 | raise Exception('please press \'trust this computer\' in your device') 260 | if errcode == 102: 261 | raise Exception('Please reboot your device and try again') 262 | raise Exception('Unknown error ' + str(errcode) + msg[1].get("ErrorDescription", "")) 263 | elif msg[0] == "DLMessageGetFreeDiskSpace": 264 | self.mb2_handle_free_disk_space(msg) 265 | elif msg[0] == "DLMessageDisconnect": 266 | break 267 | 268 | def create_status_plist(self,fullBackup=True): 269 | #Creating Status file for backup 270 | statusDict = { 'UUID': str(uuid4()).upper(), 271 | 'BackupState': 'new', 272 | 'IsFullBackup': fullBackup, 273 | 'Version': '2.4', 274 | 'Date': datetime.datetime.fromtimestamp(mktime(gmtime())), 275 | 'SnapshotState': 'finished' 276 | } 277 | writePlist(statusDict,self.check_filename("Status.plist")) 278 | 279 | 280 | def backup(self,fullBackup=True): 281 | print("Starting%sbackup..." % (" Encrypted " if self.willEncrypt else "")) 282 | options = {} 283 | if not os.path.isdir(os.path.join(self.backupPath,self.udid)): 284 | os.makedirs(os.path.join(self.backupPath,self.udid)) 285 | 286 | self.create_info_plist() 287 | 288 | options["ForceFullBackup"] = fullBackup 289 | self.mobilebackup2_send_request("Backup", self.udid, options) 290 | self.work_loop() 291 | 292 | 293 | def restore(self, options = {"RestoreSystemFiles": True, 294 | "RestoreShouldReboot": True, 295 | "RestorePreserveCameraRoll": True, 296 | "RemoveItemsNotRestored": False, 297 | "RestoreDontCopyBackup": True, 298 | "RestorePreserveSettings": True}, 299 | password=None): 300 | 301 | print("Starting restoration...") 302 | m = os.path.join(self.backupPath,self.udid,"Manifest.plist") 303 | try: 304 | manifest = readPlist(m) 305 | except IOError: 306 | print('not a valid backup folder') 307 | return -1 308 | if manifest.get("IsEncrypted"): 309 | print("Backup is encrypted, enter password : ") 310 | if password: 311 | self.password = password 312 | else: 313 | self.password = raw_input() 314 | options["Password"] = self.password 315 | self.mobilebackup2_send_request("Restore", self.udid, self.udid, options) 316 | self.work_loop() 317 | 318 | 319 | def info(self,options={}): 320 | self.mobilebackup2_send_request("Info", self.udid, options) 321 | info = self.work_loop() 322 | if info: 323 | pprint(info.get("Content")) 324 | return info 325 | 326 | 327 | def list(self,options={}): 328 | self.mobilebackup2_send_request("List", self.udid, options) 329 | z = self.work_loop() 330 | if z: 331 | print(z["Content"]) 332 | return z 333 | 334 | def changepw(self,oldpw,newpw): 335 | options = { "OldPassword" : oldpw, 336 | "NewPassword" : newpw } 337 | 338 | self.mobilebackup2_send_request("ChangePassword", self.udid, "", options) 339 | z = self.work_loop() 340 | if z: 341 | print(z) 342 | return z 343 | 344 | def unback(self,options={"Password": None}): 345 | self.mobilebackup2_send_request("Unback", self.udid, options) 346 | print(self.work_loop()) 347 | 348 | def enableCloudBackup(self,options={"CloudBackupState": False}): 349 | self.mobilebackup2_send_request("EnableCloudBackup", self.udid, options) 350 | print(self.work_loop()) 351 | 352 | 353 | if __name__ == "__main__": 354 | parser = OptionParser(usage="%prog") 355 | parser.add_option("-b", "--backup", dest="backup", action="store_true", default=True, 356 | help="Backup device") 357 | parser.add_option("-r", "--restore", dest="restore", action="store_true", default=False, 358 | help="Restore device") 359 | parser.add_option("-i", "--info", dest="info", action="store_true", default=False, 360 | help="Show backup info") 361 | parser.add_option("-l", "--list", dest="list", action="store_true", default=False, 362 | help="Show backup info") 363 | parser.add_option("-u", "--uuid", dest="uuid", action="store", default=None, 364 | help="uuid of device to backup/restore") 365 | parser.add_option("-p", "--path", dest="path", action="store", default=None, 366 | help="path to backup/restore to") 367 | (options, args) = parser.parse_args() 368 | 369 | lockdown = LockdownClient(options.uuid) 370 | mb = MobileBackup2(lockdown, options.path) 371 | 372 | if options.backup: 373 | mb.backup() 374 | elif options.restore: 375 | mb.restore() 376 | if options.info: 377 | mb.info() 378 | if options.list: 379 | mb.list() 380 | 381 | -------------------------------------------------------------------------------- /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/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 | 25 | from past.builtins import xrange 26 | import os 27 | from construct.core import Struct 28 | from construct.lib.containers import Container 29 | from construct import Bytes, Int64ul 30 | import struct 31 | from cmd import Cmd 32 | from six import PY3 33 | from pprint import pprint 34 | import plistlib 35 | import posixpath 36 | 37 | from pymobiledevice.lockdown import LockdownClient 38 | from pymobiledevice.util import hexdump, parsePlist 39 | 40 | MODEMASK = 0o0000777 41 | 42 | AFC_OP_STATUS = 0x00000001 43 | AFC_OP_DATA = 0x00000002 #Data */ 44 | AFC_OP_READ_DIR = 0x00000003 #ReadDir */ 45 | AFC_OP_READ_FILE = 0x00000004 #ReadFile */ 46 | AFC_OP_WRITE_FILE = 0x00000005 #WriteFile */ 47 | AFC_OP_WRITE_PART = 0x00000006 #WritePart */ 48 | AFC_OP_TRUNCATE = 0x00000007 #TruncateFile */ 49 | AFC_OP_REMOVE_PATH = 0x00000008 #RemovePath */ 50 | AFC_OP_MAKE_DIR = 0x00000009 #MakeDir */ 51 | AFC_OP_GET_FILE_INFO = 0x0000000a #GetFileInfo */ 52 | AFC_OP_GET_DEVINFO = 0x0000000b #GetDeviceInfo */ 53 | AFC_OP_WRITE_FILE_ATOM = 0x0000000c #WriteFileAtomic (tmp file+rename) */ 54 | AFC_OP_FILE_OPEN = 0x0000000d #FileRefOpen */ 55 | AFC_OP_FILE_OPEN_RES = 0x0000000e #FileRefOpenResult */ 56 | AFC_OP_READ = 0x0000000f #FileRefRead */ 57 | AFC_OP_WRITE = 0x00000010 #FileRefWrite */ 58 | AFC_OP_FILE_SEEK = 0x00000011 #FileRefSeek */ 59 | AFC_OP_FILE_TELL = 0x00000012 #FileRefTell */ 60 | AFC_OP_FILE_TELL_RES = 0x00000013 #FileRefTellResult */ 61 | AFC_OP_FILE_CLOSE = 0x00000014 #FileRefClose */ 62 | AFC_OP_FILE_SET_SIZE = 0x00000015 #FileRefSetFileSize (ftruncate) */ 63 | AFC_OP_GET_CON_INFO = 0x00000016 #GetConnectionInfo */ 64 | AFC_OP_SET_CON_OPTIONS = 0x00000017 #SetConnectionOptions */ 65 | AFC_OP_RENAME_PATH = 0x00000018 #RenamePath */ 66 | AFC_OP_SET_FS_BS = 0x00000019 #SetFSBlockSize (0x800000) */ 67 | AFC_OP_SET_SOCKET_BS = 0x0000001A #SetSocketBlockSize (0x800000) */ 68 | AFC_OP_FILE_LOCK = 0x0000001B #FileRefLock */ 69 | AFC_OP_MAKE_LINK = 0x0000001C #MakeLink */ 70 | AFC_OP_SET_FILE_TIME = 0x0000001E #set st_mtime */ 71 | 72 | AFC_E_SUCCESS = 0 73 | AFC_E_UNKNOWN_ERROR = 1 74 | AFC_E_OP_HEADER_INVALID = 2 75 | AFC_E_NO_RESOURCES = 3 76 | AFC_E_READ_ERROR = 4 77 | AFC_E_WRITE_ERROR = 5 78 | AFC_E_UNKNOWN_PACKET_TYPE = 6 79 | AFC_E_INVALID_ARG = 7 80 | AFC_E_OBJECT_NOT_FOUND = 8 81 | AFC_E_OBJECT_IS_DIR = 9 82 | AFC_E_PERM_DENIED =10 83 | AFC_E_SERVICE_NOT_CONNECTED =11 84 | AFC_E_OP_TIMEOUT =12 85 | AFC_E_TOO_MUCH_DATA =13 86 | AFC_E_END_OF_DATA =14 87 | AFC_E_OP_NOT_SUPPORTED =15 88 | AFC_E_OBJECT_EXISTS =16 89 | AFC_E_OBJECT_BUSY =17 90 | AFC_E_NO_SPACE_LEFT =18 91 | AFC_E_OP_WOULD_BLOCK =19 92 | AFC_E_IO_ERROR =20 93 | AFC_E_OP_INTERRUPTED =21 94 | AFC_E_OP_IN_PROGRESS =22 95 | AFC_E_INTERNAL_ERROR =23 96 | 97 | AFC_E_MUX_ERROR =30 98 | AFC_E_NO_MEM =31 99 | AFC_E_NOT_ENOUGH_DATA =32 100 | AFC_E_DIR_NOT_EMPTY =33 101 | 102 | AFC_FOPEN_RDONLY = 0x00000001 #/**< r O_RDONLY */ 103 | AFC_FOPEN_RW = 0x00000002 #/**< r+ O_RDWR | O_CREAT */ 104 | AFC_FOPEN_WRONLY = 0x00000003 #/**< w O_WRONLY | O_CREAT | O_TRUNC */ 105 | AFC_FOPEN_WR = 0x00000004 #/**< w+ O_RDWR | O_CREAT | O_TRUNC */ 106 | AFC_FOPEN_APPEND = 0x00000005 #/**< a O_WRONLY | O_APPEND | O_CREAT */ 107 | AFC_FOPEN_RDAPPEND = 0x00000006 #/**< a+ O_RDWR | O_APPEND | O_CREAT */ 108 | 109 | AFC_HARDLINK = 1 110 | AFC_SYMLINK = 2 111 | 112 | AFC_LOCK_SH = 1 | 4 #/**< shared lock */ 113 | AFC_LOCK_EX = 2 | 4 #/**< exclusive lock */ 114 | AFC_LOCK_UN = 8 | 4 #/**< unlock */ 115 | 116 | if PY3: 117 | AFCMAGIC = b"CFA6LPAA" 118 | else: 119 | AFCMAGIC = "CFA6LPAA" 120 | 121 | AFCPacket = Struct("magic" / Bytes(8), 122 | "entire_length" / Int64ul, 123 | "this_length" / Int64ul, 124 | "packet_num" / Int64ul, 125 | "operation" / Int64ul 126 | ) 127 | 128 | 129 | class AFCClient(object): 130 | def __init__(self, lockdown=None, serviceName="com.apple.afc", service=None): 131 | self.serviceName = serviceName 132 | self.lockdown = lockdown if lockdown else LockdownClient() 133 | self.service = service if service else self.lockdown.startService(self.serviceName) 134 | self.packet_num = 0 135 | 136 | 137 | def stop_session(self): 138 | print("Disconecting...") 139 | self.service.close() 140 | 141 | 142 | def dispatch_packet(self, operation, data, this_length=0): 143 | afcpack = Container(magic=AFCMAGIC, 144 | entire_length=40 + len(data), 145 | this_length=40 + len(data), 146 | packet_num=self.packet_num, 147 | operation=operation) 148 | if this_length: 149 | afcpack.this_length = this_length 150 | header = AFCPacket.build(afcpack) 151 | self.packet_num += 1 152 | if PY3 and isinstance(data, str): 153 | data = data.encode('utf-8') 154 | self.service.send(header + data) 155 | 156 | 157 | def receive_data(self): 158 | res = self.service.recv_exact(40) 159 | status = AFC_E_SUCCESS 160 | data = "" 161 | if res: 162 | res = AFCPacket.parse(res) 163 | assert res["entire_length"] >= 40 164 | length = res["entire_length"] - 40 165 | data = self.service.recv_exact(length) 166 | if res.operation == AFC_OP_STATUS: 167 | if length != 8: 168 | print("Status length != 8") 169 | status = struct.unpack(" 0: 297 | if sz > MAXIMUM_READ_SIZE: 298 | toRead = MAXIMUM_READ_SIZE 299 | else: 300 | toRead = sz 301 | try: 302 | self.dispatch_packet(AFC_OP_READ, struct.pack("