iphonetool 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iphonetool/__init__.py +0 -0
- iphonetool/__main__.py +62 -0
- iphonetool/config.py +11 -0
- iphonetool/dfu.py +243 -0
- iphonetool/helpers.py +87 -0
- iphonetool/normal.py +89 -0
- iphonetool/recovery.py +112 -0
- iphonetool/usbliter8ctl.py +60 -0
- iphonetool-0.1.0.dist-info/METADATA +26 -0
- iphonetool-0.1.0.dist-info/RECORD +13 -0
- iphonetool-0.1.0.dist-info/WHEEL +4 -0
- iphonetool-0.1.0.dist-info/entry_points.txt +3 -0
- iphonetool-0.1.0.dist-info/licenses/LICENSE +674 -0
iphonetool/__init__.py
ADDED
|
File without changes
|
iphonetool/__main__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.13"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "pymobiledevice3>=10.1.0",
|
|
6
|
+
# "pyusb>=1.3.1",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import usb.core
|
|
12
|
+
except ImportError:
|
|
13
|
+
print("pyusb not installed and is needed for all operations. Please install pyusb")
|
|
14
|
+
raise
|
|
15
|
+
import argparse
|
|
16
|
+
import asyncio
|
|
17
|
+
import sys
|
|
18
|
+
from enum import IntEnum
|
|
19
|
+
|
|
20
|
+
import dfu
|
|
21
|
+
import helpers
|
|
22
|
+
import normal
|
|
23
|
+
import recovery
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def main() -> int:
|
|
27
|
+
if "-w" in sys.argv or "--wait" in sys.argv:
|
|
28
|
+
print("Waiting for device...")
|
|
29
|
+
dev = await helpers.wait_device()
|
|
30
|
+
print("Found device")
|
|
31
|
+
else:
|
|
32
|
+
dev = helpers.get_device()
|
|
33
|
+
|
|
34
|
+
# If this fails, it will fall through to a "normal" device below
|
|
35
|
+
if dev is None and ("-h" not in sys.argv and "--help" not in sys.argv):
|
|
36
|
+
print("No idevice found :(")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
parser = argparse.ArgumentParser()
|
|
40
|
+
# This is only for showing up in help
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
"-w", "--wait", help="Wait for a device to appear", action="store_true"
|
|
43
|
+
)
|
|
44
|
+
subparsers = parser.add_subparsers(dest="action")
|
|
45
|
+
# List is supported in all modes
|
|
46
|
+
subparsers.add_parser("info")
|
|
47
|
+
|
|
48
|
+
if dev is None:
|
|
49
|
+
return await normal.main(None, parser, subparsers)
|
|
50
|
+
|
|
51
|
+
# Figure out what mode the device is in
|
|
52
|
+
match helpers.classify_mode(dev):
|
|
53
|
+
case helpers.DeviceMode.NORMAL:
|
|
54
|
+
return await normal.main(dev, parser, subparsers)
|
|
55
|
+
case helpers.DeviceMode.RECOVERY:
|
|
56
|
+
return await recovery.main(dev, parser, subparsers)
|
|
57
|
+
case helpers.DeviceMode.DFU:
|
|
58
|
+
return await dfu.main(dev, parser, subparsers)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
exit(asyncio.run(main()))
|
iphonetool/config.py
ADDED
iphonetool/dfu.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import glob
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from enum import IntEnum
|
|
11
|
+
from typing import Any, Optional
|
|
12
|
+
|
|
13
|
+
import usb.core
|
|
14
|
+
|
|
15
|
+
import config
|
|
16
|
+
import helpers
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def main(
|
|
20
|
+
dev: usb.core.Device,
|
|
21
|
+
parser: argparse.ArgumentParser,
|
|
22
|
+
subparsers: argparse._SubParsersAction,
|
|
23
|
+
):
|
|
24
|
+
subparsers.add_parser("exit_dfu_helper", help="Help exiting DFU mode")
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
serial = dev.serial_number
|
|
28
|
+
except ValueError as e:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"This script must be run as root for iPhones in recovery or dfu mode."
|
|
31
|
+
) from e
|
|
32
|
+
|
|
33
|
+
if helpers.serial_info_get(serial, "PWND") == "[usbliter8]":
|
|
34
|
+
pwned_device = True
|
|
35
|
+
else:
|
|
36
|
+
pwned_device = False
|
|
37
|
+
|
|
38
|
+
if pwned_device:
|
|
39
|
+
subparsers.add_parser(
|
|
40
|
+
"demote", help="Demote this device to a development device"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
boot_parser = subparsers.add_parser(
|
|
44
|
+
"boot", help='Low-level device exploit booting. You probably want "linux"'
|
|
45
|
+
)
|
|
46
|
+
boot_subparsers = boot_parser.add_subparsers(dest="boot_action")
|
|
47
|
+
|
|
48
|
+
boot_raw_parser = boot_subparsers.add_parser(
|
|
49
|
+
"raw", help="Boot a raw iBoot file"
|
|
50
|
+
)
|
|
51
|
+
boot_raw_parser.add_argument(
|
|
52
|
+
"iBoot", type=pathlib.Path, help="The raw iBoot to boot"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
boot_remote_parser = boot_subparsers.add_parser(
|
|
56
|
+
"remote", help="Remote boot m1n1, linux, etc."
|
|
57
|
+
)
|
|
58
|
+
boot_remote_parser.add_argument(
|
|
59
|
+
"-1",
|
|
60
|
+
"--m1n1",
|
|
61
|
+
type=pathlib.Path,
|
|
62
|
+
help="m1n1-idevice.macho + any addons (linux, dtbs, etc)",
|
|
63
|
+
required=True,
|
|
64
|
+
)
|
|
65
|
+
boot_remote_parser.add_argument(
|
|
66
|
+
"-m", "--monitor", type=pathlib.Path, help="m1n1 monitor stub"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# High-level linux booter
|
|
70
|
+
linux_parser = subparsers.add_parser("linux", help="Boot linux on device")
|
|
71
|
+
linux_parser.add_argument(
|
|
72
|
+
"-1",
|
|
73
|
+
"--m1n1",
|
|
74
|
+
type=pathlib.Path,
|
|
75
|
+
help="Path to m1n1-idevice.macho",
|
|
76
|
+
required=True,
|
|
77
|
+
)
|
|
78
|
+
linux_parser_dtb_group = linux_parser.add_mutually_exclusive_group(
|
|
79
|
+
required=True
|
|
80
|
+
)
|
|
81
|
+
linux_parser_dtb_group.add_argument(
|
|
82
|
+
"-d", "--dtb", type=pathlib.Path, help="Path to devicetree for device"
|
|
83
|
+
)
|
|
84
|
+
linux_parser_dtb_group.add_argument(
|
|
85
|
+
"-D", "--dtbs", type=pathlib.Path, help="Path to directory of devicetrees"
|
|
86
|
+
)
|
|
87
|
+
linux_parser.add_argument(
|
|
88
|
+
"-k", "--kernel", type=pathlib.Path, help="Path to kernel", required=True
|
|
89
|
+
)
|
|
90
|
+
linux_parser.add_argument(
|
|
91
|
+
"-c", "--commandline", help="Linux commandline to boot"
|
|
92
|
+
)
|
|
93
|
+
linux_parser.add_argument(
|
|
94
|
+
"-i", "--initramfs", type=pathlib.Path, help="Linux initramfs to boot"
|
|
95
|
+
)
|
|
96
|
+
linux_parser.add_argument(
|
|
97
|
+
"-m", "--monitor", type=pathlib.Path, help="m1n1 monitor stub"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
args = parser.parse_args()
|
|
101
|
+
|
|
102
|
+
return await main_real(dev, pwned_device, serial, args.action, vars(args))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Usbliter8Command(IntEnum):
|
|
106
|
+
DFU_DNLOAD = 1
|
|
107
|
+
DFU_ABORT = 4
|
|
108
|
+
CUSTOM_DEMOTE = 7
|
|
109
|
+
CUSTOM_BOOT = 8
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def send_usbliter8_command(
|
|
113
|
+
dev: usb.core.Device, command: Usbliter8Command, data: Any, timeout: int
|
|
114
|
+
) -> None:
|
|
115
|
+
dev.ctrl_transfer(0x21, command, 0, 0, data, timeout)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def usbliter8_download(dev: usb.core.Device, data: bytes) -> True:
|
|
119
|
+
offset = 0
|
|
120
|
+
left = len(data)
|
|
121
|
+
|
|
122
|
+
while left:
|
|
123
|
+
current_block_length = min(config.USBLITER8_TRANSFER_SIZE, left)
|
|
124
|
+
|
|
125
|
+
send_usbliter8_command(
|
|
126
|
+
dev,
|
|
127
|
+
Usbliter8Command.DFU_DNLOAD,
|
|
128
|
+
data[offset : offset + current_block_length],
|
|
129
|
+
1000,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
offset += current_block_length
|
|
133
|
+
left -= current_block_length
|
|
134
|
+
|
|
135
|
+
print(f"\rUploaded: 0x{offset:x}/0x{len(data):x} bytes.", end="")
|
|
136
|
+
|
|
137
|
+
print()
|
|
138
|
+
|
|
139
|
+
send_usbliter8_command(dev, Usbliter8Command.DFU_DNLOAD, None, 100)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def linux_remote_boot(m1n1_blob: pathlib.Path, monitor_stub: Optional[pathlib.Path]):
|
|
143
|
+
subprocess.check_call(["./remote_boot/remoteboot.sh", "build"])
|
|
144
|
+
if monitor_stub is not None:
|
|
145
|
+
subprocess.check_call(
|
|
146
|
+
[
|
|
147
|
+
helpers.base_directory() / "./remote_boot/remoteboot.sh",
|
|
148
|
+
"boot",
|
|
149
|
+
m1n1_blob,
|
|
150
|
+
monitor_stub,
|
|
151
|
+
],
|
|
152
|
+
env={
|
|
153
|
+
"USBLITER8CTL": helpers.base_directory() / "usbliter8ctl.py",
|
|
154
|
+
"PYTHON": sys.executable,
|
|
155
|
+
"HOME": os.getenv("HOME"),
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
subprocess.check_call(
|
|
160
|
+
["./remote_boot/remoteboot.sh", "boot", m1n1_blob],
|
|
161
|
+
env={
|
|
162
|
+
"USBLITER8CTL": helpers.base_directory() / "usbliter8ctl.py",
|
|
163
|
+
"PYTHON": sys.executable,
|
|
164
|
+
"HOME": os.getenv("HOME"),
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
async def main_real(
|
|
170
|
+
dev: usb.core.Device, pwned: bool, serial: str, action: str, args: dict
|
|
171
|
+
) -> int:
|
|
172
|
+
ecid = int(helpers.serial_info(serial, "ECID"), 16)
|
|
173
|
+
|
|
174
|
+
irecovery = subprocess.check_output(["irecovery", "-q", "-i", hex(ecid)]).decode()
|
|
175
|
+
|
|
176
|
+
print(args)
|
|
177
|
+
|
|
178
|
+
match action:
|
|
179
|
+
case "info":
|
|
180
|
+
if pwned:
|
|
181
|
+
print(
|
|
182
|
+
f"Detected PWNED DFU mode {helpers.irecovery_info(irecovery, "NAME")}:"
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
print(f"Detected DFU mode {helpers.irecovery_info(irecovery, "NAME")}:")
|
|
186
|
+
print("iPhone ID:", ecid)
|
|
187
|
+
print(
|
|
188
|
+
"iPhone internal version:", helpers.irecovery_info(irecovery, "PRODUCT")
|
|
189
|
+
)
|
|
190
|
+
print("Codename:", helpers.irecovery_info(irecovery, "MODEL"))
|
|
191
|
+
print("CPU:", helpers.serial_info(serial, "CPID"))
|
|
192
|
+
case "exit_dfu_helper":
|
|
193
|
+
# TODO: This
|
|
194
|
+
print("Idk. You figure it out")
|
|
195
|
+
case "demote":
|
|
196
|
+
print(f"Telling device {ecid} to demote to development.")
|
|
197
|
+
send_usbliter8_command(dev, Usbliter8Command.CUSTOM_DEMOTE, None, 100)
|
|
198
|
+
print(f"Device {ecid} has demoted to development.")
|
|
199
|
+
case "boot":
|
|
200
|
+
match args["boot_action"]:
|
|
201
|
+
case "raw":
|
|
202
|
+
iboot_file = args["iBoot"]
|
|
203
|
+
print(f"Uploading {iboot_file} to device...")
|
|
204
|
+
usbliter8_download(dev, iboot_file.read_bytes())
|
|
205
|
+
|
|
206
|
+
send_usbliter8_command(dev, Usbliter8Command.CUSTOM_BOOT, None, 100)
|
|
207
|
+
send_usbliter8_command(dev, Usbliter8Command.DFU_ABORT, None, 100)
|
|
208
|
+
case "remote":
|
|
209
|
+
linux_remote_boot(args["m1n1"], args.get("monitor"))
|
|
210
|
+
case "linux":
|
|
211
|
+
print("Preapring iboot...")
|
|
212
|
+
with tempfile.NamedTemporaryFile(mode="wb") as m1n1_blob_file:
|
|
213
|
+
print("adding m1n1")
|
|
214
|
+
with args["m1n1"].open("rb") as f:
|
|
215
|
+
shutil.copyfileobj(f, m1n1_blob_file)
|
|
216
|
+
if args["commandline"] is not None:
|
|
217
|
+
print("adding commandline")
|
|
218
|
+
m1n1_blob_file.write(
|
|
219
|
+
f'chosen.bootargs={args["commandline"]}\n'.encode()
|
|
220
|
+
)
|
|
221
|
+
if args["dtb"] is not None:
|
|
222
|
+
if not args["dtb"].is_file():
|
|
223
|
+
raise ValueError("Specified DTB is not a file.")
|
|
224
|
+
print("adding dtb")
|
|
225
|
+
with args["dtb"].open("rb") as f:
|
|
226
|
+
shutil.copyfileobj(f, m1n1_blob_file)
|
|
227
|
+
else:
|
|
228
|
+
if not args["dtbs"].is_dir():
|
|
229
|
+
raise ValueError("Specified DTB directory is not a dir.")
|
|
230
|
+
print("adding dtbs")
|
|
231
|
+
for dtb in glob.iglob("./*.dtb", root_dir=args["dtbs"]):
|
|
232
|
+
with (args["dtbs"] / dtb).open("rb") as f:
|
|
233
|
+
shutil.copyfileobj(f, m1n1_blob_file)
|
|
234
|
+
print("adding kernel")
|
|
235
|
+
with args["kernel"].open("rb") as f:
|
|
236
|
+
shutil.copyfileobj(f, m1n1_blob_file)
|
|
237
|
+
if args["initramfs"] is not None:
|
|
238
|
+
print("adding initramfs")
|
|
239
|
+
with args["initramfs"].open("rb") as f:
|
|
240
|
+
shutil.copyfileobj(f, m1n1_blob_file)
|
|
241
|
+
linux_remote_boot(
|
|
242
|
+
pathlib.Path(m1n1_blob_file.name), args.get("monitor")
|
|
243
|
+
)
|
iphonetool/helpers.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import pathlib
|
|
4
|
+
import subprocess
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import usb.core
|
|
9
|
+
|
|
10
|
+
from config import APPLE_VENDORID, AppleProductId
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DeviceMode(Enum):
|
|
14
|
+
NORMAL = auto()
|
|
15
|
+
RECOVERY = auto()
|
|
16
|
+
DFU = auto()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_device():
|
|
20
|
+
return usb.core.find(idVendor=APPLE_VENDORID)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def wait_device():
|
|
24
|
+
while True:
|
|
25
|
+
dev = get_device()
|
|
26
|
+
if dev is not None:
|
|
27
|
+
return dev
|
|
28
|
+
|
|
29
|
+
await asyncio.sleep(0.1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def wait_disconnect(dev: usb.core.Device):
|
|
33
|
+
while True:
|
|
34
|
+
try:
|
|
35
|
+
device = usb.core.find(idVendor=dev.idVendor, idProduct=dev.idProduct)
|
|
36
|
+
if device is not None and device.serial_number == dev.serial_number:
|
|
37
|
+
await asyncio.sleep(0.1)
|
|
38
|
+
continue
|
|
39
|
+
except ValueError:
|
|
40
|
+
continue
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def classify_mode(dev: usb.core.Device) -> DeviceMode:
|
|
45
|
+
match dev.idProduct:
|
|
46
|
+
case AppleProductId.RECOVERY:
|
|
47
|
+
return DeviceMode.RECOVERY
|
|
48
|
+
case AppleProductId.DFU:
|
|
49
|
+
return DeviceMode.DFU
|
|
50
|
+
case _:
|
|
51
|
+
return DeviceMode.NORMAL
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def serial_info_get(serial, key) -> Optional[str]:
|
|
55
|
+
value = dict(
|
|
56
|
+
list(map(functools.partial(str.split, sep=":", maxsplit=1), serial.split(" ")))
|
|
57
|
+
).get(key.upper())
|
|
58
|
+
if value is None:
|
|
59
|
+
return value
|
|
60
|
+
return value.strip()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def serial_info(serial, key) -> str:
|
|
64
|
+
value = serial_info_get(serial, key)
|
|
65
|
+
if value is None:
|
|
66
|
+
raise ValueError(f"Could not find key {key} in recovery serial {serial}")
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def irecovery_info(irecovery, key) -> str:
|
|
71
|
+
try:
|
|
72
|
+
return dict(list(map(functools.partial(str.split, sep=":", maxsplit=1), irecovery.splitlines())))[key.upper()].strip() # type: ignore
|
|
73
|
+
except IndexError as e:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Could not find key {key} in irecovery data {irecovery}"
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def irecovery_command(cmd: str, ecid: Optional[int] = None) -> None:
|
|
80
|
+
if ecid is not None:
|
|
81
|
+
subprocess.run(["irecovery", "-i", hex(ecid), "-c", cmd], check=True)
|
|
82
|
+
else:
|
|
83
|
+
subprocess.run(["irecovery", "-c", cmd], check=True)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def base_directory() -> pathlib.Path:
|
|
87
|
+
return pathlib.Path(__file__).parent
|
iphonetool/normal.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
import usb.core
|
|
5
|
+
|
|
6
|
+
import helpers
|
|
7
|
+
import recovery
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import pymobiledevice3.exceptions
|
|
11
|
+
import pymobiledevice3.lockdown
|
|
12
|
+
except ImportError:
|
|
13
|
+
print(
|
|
14
|
+
"pymobiledevice3 not installed and is needed for normal/recovery mode operation. Please install pymobiledevice3"
|
|
15
|
+
)
|
|
16
|
+
raise
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def main(
|
|
20
|
+
dev: usb.core.Device,
|
|
21
|
+
parser: argparse.ArgumentParser,
|
|
22
|
+
subparsers: argparse._SubParsersAction,
|
|
23
|
+
) -> int:
|
|
24
|
+
subparsers.add_parser("dfu_helper", help="Help put device into DFU mode")
|
|
25
|
+
subparsers.add_parser("enter_recovery", help="Enter recovery mode")
|
|
26
|
+
|
|
27
|
+
args = parser.parse_args()
|
|
28
|
+
|
|
29
|
+
return await main_real(dev, args.action)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# This can be called easily from other python files
|
|
33
|
+
async def main_real(dev: usb.core.Device, action: str) -> int:
|
|
34
|
+
serial = dev.serial_number.rstrip("\x00")
|
|
35
|
+
|
|
36
|
+
while True:
|
|
37
|
+
try:
|
|
38
|
+
connector = await pymobiledevice3.lockdown.create_using_usbmux(
|
|
39
|
+
connection_type="USB", serial=serial
|
|
40
|
+
)
|
|
41
|
+
break
|
|
42
|
+
except (
|
|
43
|
+
pymobiledevice3.exceptions.DeviceNotFoundError,
|
|
44
|
+
pymobiledevice3.exceptions.ConnectionFailedToUsbmuxdError,
|
|
45
|
+
):
|
|
46
|
+
# usbmuxd needs a little time sometimes
|
|
47
|
+
print("Failed to connect. Retrying...")
|
|
48
|
+
await asyncio.sleep(0.3)
|
|
49
|
+
continue
|
|
50
|
+
except pymobiledevice3.exceptions.MissingValueError as e:
|
|
51
|
+
# THis can occur if the attempt to connect is made while the device is shutting down
|
|
52
|
+
raise ValueError("Failed to connect to device") from e
|
|
53
|
+
|
|
54
|
+
async with connector as lockdown:
|
|
55
|
+
match action:
|
|
56
|
+
case "info":
|
|
57
|
+
print(f"Detected normal mode {lockdown.display_name}:")
|
|
58
|
+
print("iPhone ID:", lockdown.udid)
|
|
59
|
+
print("iPhone internal version:", lockdown.product_type)
|
|
60
|
+
print("Device name:", lockdown.all_values["DeviceName"])
|
|
61
|
+
print("iOS Version:", lockdown.product_version)
|
|
62
|
+
print("Codename:", lockdown.hardware_model.upper())
|
|
63
|
+
print("CPU:", lockdown.all_values["HardwarePlatform"].upper())
|
|
64
|
+
case "enter_recovery":
|
|
65
|
+
print(f"Telling device {lockdown.udid} to enter recovery.")
|
|
66
|
+
await lockdown.enter_recovery()
|
|
67
|
+
print(f"Device {lockdown.udid} has entered recovery.")
|
|
68
|
+
case "dfu_helper":
|
|
69
|
+
# Put device into recovery
|
|
70
|
+
print(f"Telling device {lockdown.udid} to enter recovery.")
|
|
71
|
+
await lockdown.enter_recovery()
|
|
72
|
+
print(f"Device {lockdown.udid} has entered recovery.")
|
|
73
|
+
|
|
74
|
+
# Wait for the device to disconnect
|
|
75
|
+
print("Waiting for device disconnect.")
|
|
76
|
+
await helpers.wait_disconnect(dev)
|
|
77
|
+
print(f"Device {lockdown.udid} disconnected")
|
|
78
|
+
|
|
79
|
+
# Release the old device
|
|
80
|
+
del dev
|
|
81
|
+
|
|
82
|
+
# Get a new device
|
|
83
|
+
print("Waiting for device to reconnect...")
|
|
84
|
+
dev = await helpers.wait_device()
|
|
85
|
+
|
|
86
|
+
# Continue dfu helper there
|
|
87
|
+
return await recovery.main_real(dev, "dfu_helper")
|
|
88
|
+
|
|
89
|
+
return 0
|
iphonetool/recovery.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import subprocess
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import usb.core
|
|
8
|
+
|
|
9
|
+
import helpers
|
|
10
|
+
import normal
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import pymobiledevice3
|
|
14
|
+
except ImportError:
|
|
15
|
+
print(
|
|
16
|
+
"pymobiledevice3 not installed and is needed for normal/recovery mode operation. Please install pymobiledevice3"
|
|
17
|
+
)
|
|
18
|
+
raise
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def print_in(secs: float, message: str) -> None:
|
|
22
|
+
try:
|
|
23
|
+
await asyncio.sleep(secs)
|
|
24
|
+
print(message)
|
|
25
|
+
except asyncio.CancelledError:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def main(
|
|
30
|
+
dev: usb.core.Device,
|
|
31
|
+
parser: argparse.ArgumentParser,
|
|
32
|
+
subparsers: argparse._SubParsersAction,
|
|
33
|
+
):
|
|
34
|
+
subparsers.add_parser("dfu_helper", help="Help put device into DFU mode")
|
|
35
|
+
subparsers.add_parser("exit_recovery", help="Exit recovery mode")
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
return await main_real(dev, args.action)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# This can be called easily from other python files
|
|
43
|
+
async def main_real(dev: usb.core.Device, action: str) -> int:
|
|
44
|
+
try:
|
|
45
|
+
serial = dev.serial_number
|
|
46
|
+
except ValueError as e:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"This script must be run as root for iPhones in recovery or dfu mode."
|
|
49
|
+
) from e
|
|
50
|
+
ecid = int(helpers.serial_info(serial, "ECID"), 16)
|
|
51
|
+
|
|
52
|
+
irecovery = subprocess.check_output(["irecovery", "-q", "-i", hex(ecid)]).decode()
|
|
53
|
+
|
|
54
|
+
match action:
|
|
55
|
+
case "info":
|
|
56
|
+
print(
|
|
57
|
+
f"Detected recovery mode {helpers.irecovery_info(irecovery, "NAME")}:"
|
|
58
|
+
)
|
|
59
|
+
print("iPhone ID:", ecid)
|
|
60
|
+
print(
|
|
61
|
+
"iPhone internal version:", helpers.irecovery_info(irecovery, "PRODUCT")
|
|
62
|
+
)
|
|
63
|
+
print("Codename:", helpers.irecovery_info(irecovery, "MODEL"))
|
|
64
|
+
print("CPU:", helpers.serial_info(serial, "CPID"))
|
|
65
|
+
case "exit_recovery":
|
|
66
|
+
print(f"Telling device {ecid} to exit recovery.", flush=True)
|
|
67
|
+
helpers.irecovery_command("setenv auto-boot true", ecid)
|
|
68
|
+
helpers.irecovery_command("saveenv", ecid)
|
|
69
|
+
helpers.irecovery_command("reboot", ecid)
|
|
70
|
+
print(f"Device {ecid} has exited recovery", flush=True)
|
|
71
|
+
case "dfu_helper":
|
|
72
|
+
helpers.irecovery_command("setenv auto-boot true", ecid)
|
|
73
|
+
helpers.irecovery_command("saveenv", ecid)
|
|
74
|
+
|
|
75
|
+
input(
|
|
76
|
+
"Hold down buttons 1 & 2 on the device (and keep holding until this program says stop) and press enter."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
helpers.irecovery_command("reset", ecid)
|
|
80
|
+
|
|
81
|
+
await asyncio.sleep(2.5)
|
|
82
|
+
|
|
83
|
+
print("Release button 1. Keep holding button 2")
|
|
84
|
+
|
|
85
|
+
info_task = asyncio.create_task(
|
|
86
|
+
print_in(
|
|
87
|
+
8, "Whoops. Device did not enter DFU mode. Waiting for re-connect."
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
dev = await helpers.wait_device()
|
|
91
|
+
canceled = info_task.cancel()
|
|
92
|
+
|
|
93
|
+
# Now actually check if the device entered DFU
|
|
94
|
+
mode = helpers.classify_mode(dev)
|
|
95
|
+
|
|
96
|
+
match mode:
|
|
97
|
+
case helpers.DeviceMode.NORMAL:
|
|
98
|
+
if not canceled:
|
|
99
|
+
# Print the message now if it wasn't printed before
|
|
100
|
+
print("Whoops. Device did not enter DFU mode.")
|
|
101
|
+
print("Device re-connected in normal mode.")
|
|
102
|
+
return await normal.main_real(dev, "dfu_helper")
|
|
103
|
+
case helpers.DeviceMode.RECOVERY:
|
|
104
|
+
if not canceled:
|
|
105
|
+
# Print the message now if it wasn't printed before
|
|
106
|
+
print("Whoops. Device did not enter DFU mode.")
|
|
107
|
+
print("Device re-connected in recovery mode.")
|
|
108
|
+
return await main_real(dev, "dfu_helper")
|
|
109
|
+
case helpers.DeviceMode.DFU:
|
|
110
|
+
print("Device entered DFU mode successfully!")
|
|
111
|
+
|
|
112
|
+
return 0
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.13"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "pyusb>=1.3.1",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
# This is a clone of usbliter8ctl using the helpers in this module.
|
|
10
|
+
# Used for remote_boot
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import pathlib
|
|
14
|
+
|
|
15
|
+
import usb
|
|
16
|
+
|
|
17
|
+
from dfu import Usbliter8Command, send_usbliter8_command, usbliter8_download
|
|
18
|
+
from helpers import get_device
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def do_boot(args, dev):
|
|
22
|
+
usbliter8_download(dev, args.iboot.read_bytes())
|
|
23
|
+
|
|
24
|
+
send_usbliter8_command(dev, Usbliter8Command.CUSTOM_BOOT, None, 100)
|
|
25
|
+
send_usbliter8_command(dev, Usbliter8Command.DFU_ABORT, None, 100)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def do_demote(args, dev):
|
|
29
|
+
send_usbliter8_command(dev, Usbliter8Command.CUSTOM_DEMOTE, None, 100)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
parser = argparse.ArgumentParser(description="Love is Control")
|
|
34
|
+
|
|
35
|
+
subparsers = parser.add_subparsers()
|
|
36
|
+
|
|
37
|
+
boot_parser = subparsers.add_parser("boot", help="boot raw iBoot")
|
|
38
|
+
boot_parser.set_defaults(func=do_boot)
|
|
39
|
+
boot_parser.add_argument("iboot", type=pathlib.Path)
|
|
40
|
+
|
|
41
|
+
demote_parser = subparsers.add_parser("demote", help="demote production mode")
|
|
42
|
+
demote_parser.set_defaults(func=do_demote)
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
if not hasattr(args, "func"):
|
|
46
|
+
parser.print_help()
|
|
47
|
+
exit(-1)
|
|
48
|
+
|
|
49
|
+
dev = get_device()
|
|
50
|
+
|
|
51
|
+
srnm = dev.serial_number
|
|
52
|
+
|
|
53
|
+
if "PWND:[" not in srnm:
|
|
54
|
+
raise RuntimeError("this is not Pwned DFU device")
|
|
55
|
+
|
|
56
|
+
args.func(args, dev)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if __name__ == "__main__":
|
|
60
|
+
main()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iphonetool
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: All of the modern iOS tools. Unified
|
|
5
|
+
Author: Jacob Freeman
|
|
6
|
+
Author-email: Jacob Freeman <t00niyf5@duck.com>
|
|
7
|
+
License-Expression: GPL-3.0-or-later
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: pymobiledevice3>=10.1.0
|
|
10
|
+
Requires-Dist: pyusb>=1.3.1
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# iphonetool
|
|
15
|
+
|
|
16
|
+
All of the modern iOS tools. Unified.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Usage
|
|
20
|
+
|
|
21
|
+
- Run `iphonetool.py info` to give info on connected devices
|
|
22
|
+
- Run `iphonetool.py -h` to show help for the current device (this help changes per-device)
|
|
23
|
+
|
|
24
|
+
# License
|
|
25
|
+
|
|
26
|
+
`iphonetool` is currently licensed under the GPLv3.
|