├── 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 | [](LICENSE)
4 | [](https://travis-ci.org/qtacore/pymobiledevice)
5 | [](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("