-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpatch_preloader.py
More file actions
executable file
·222 lines (190 loc) · 8.6 KB
/
Copy pathpatch_preloader.py
File metadata and controls
executable file
·222 lines (190 loc) · 8.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
#!/usr/bin/env python3
"""
patch_preloader.py — produce preloader_k6835v1_64_patched.bin from the
stock BraX3 preloader.
This script applies four byte-level patches to the stock Mediatek
preloader (file preloader_k6835v1_64.bin) that is shipped on the
BraX3 device. The output is byte-for-byte the stock preloader with
exactly 16 bytes changed — no other modifications, no embedded data.
The resulting file lets mtkclient keep the preloader alive past the
DRAM training failure (which would otherwise trigger an immediate
watchdog reset) and lets the USB read32 handler succeed without the
HACC crypto engine being operational.
Two categories of patch:
1. Three ASSERT NOPs — neutralise the watchdog-reset calls inside
the post-DRAM-init calibration loop. The stock preloader
watchdog-resets the SoC back to brom when the DRAM training
fails (which happens on the bricked BraX3 because the dram_para
partition is empty). Replacing the four-byte Thumb-2 `bl`
instructions with two Thumb-2 NOPs each keeps the preloader
alive long enough to answer USB commands.
2. Crypto bypass — the USB read32 handler at file 0x7488 calls a
validator at 0x35f18, which calls a deeper auth check at
0x35e98. That deeper check returns -1 (= "auth failed") on
this device because the seclib init never populates the
authorised-ranges pool (the secure-monitor firmware blob in
seccfg is missing or corrupted). Replacing the
`mov.w r0, #0xffffffff` failure-return at file 0x35ef0 with
`movs r0, #0; movs r0, #0` forces the auth check to always
succeed.
Usage:
python3 patch_preloader.py \
--input preloader_k6835v1_64.bin \
--output preloader_k6835v1_64_patched.bin
"""
import argparse
import hashlib
import os
import sys
# ---------------------------------------------------------------------------
# Patch sites
# ---------------------------------------------------------------------------
# Three Thumb-2 `bl mtk_wdt_reset` calls inside the post-DRAM-init
# calibration asserts (dramc_top.c lines 1778, 1791, 1798).
#
# Each site is 4 bytes. The expected original bytes encode the
# specific branch to the watchdog reset function at runtime 0x2f4d0.
# The script refuses to run if any of the three expected byte
# sequences is not present — a blind patch on a different preloader
# revision would be unsafe, so a mismatch is a hard error.
#
# (file_offset, expected_original_bytes_hex, source_location)
ASSERT_CALL_SITES = [
(0x1c9d4, bytes.fromhex("12f07cfd"), "dramc_top.c:1778"),
(0x1cb88, bytes.fromhex("12f0a2fc"), "dramc_top.c:1791"),
(0x1cbaa, bytes.fromhex("12f091fc"), "dramc_top.c:1798"),
]
# The replacement bytes: two Thumb-2 NOPs (each is `bf00`), packed
# into 4 bytes. Same length as the original `bl` so the file size
# and every subsequent offset are preserved.
NOP4 = b"\x00\xbf\x00\xbf"
# The deeper auth check at file 0x35e98 has two return paths:
#
# file 0x35eec: movs r0, #0 ; success
# file 0x35ef0: mov.w r0, #0xffffffff ; failure
#
# Patching only the failure return preserves the success path and
# the function epilogue that immediately follows.
#
# The original bytes are the LE encoding of the 32-bit word
# 0xf04fff30 (i.e. the disassembly `f04f 30ff mov.w r0, #-1` shown
# in big-endian halfword order).
#
# The replacement is two 16-bit Thumb `movs r0, #0` instructions
# (each 0x2000), back-to-back in 4 bytes. Same length, same total
# file size.
CRYPTO_BYPASS_OFFSET = 0x35ef0
CRYPTO_BYPASS_ORIG = b"\x4f\xf0\xff\x30" # mov.w r0, #0xffffffff
CRYPTO_BYPASS_PATCHED = b"\x00\x20\x00\x20" # movs r0, #0; movs r0, #0
# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------
def apply_patches(data: bytes) -> bytes:
"""Return a copy of `data` with the four patches applied.
Raises ValueError if any expected original byte sequence is not
present — the script refuses to produce a partial / mismatched
output rather than silently corrupting the binary.
"""
out = bytearray(data)
for offset, expected_orig, label in ASSERT_CALL_SITES:
actual = bytes(out[offset:offset + 4])
if actual == NOP4:
# already patched (idempotent re-run on previous output)
continue
if actual != expected_orig:
raise ValueError(
f"ASSERT site at file offset 0x{offset:x} ({label}) does "
f"not contain the expected original bytes. Got "
f"{actual.hex()}, expected {expected_orig.hex()} (or "
f"already-patched {NOP4.hex()}). The input is not the "
f"preloader revision this script was designed for; "
f"refusing to patch."
)
out[offset:offset + 4] = NOP4
actual = bytes(out[CRYPTO_BYPASS_OFFSET:CRYPTO_BYPASS_OFFSET + 4])
if actual == CRYPTO_BYPASS_PATCHED:
# already patched (idempotent re-run on previous output)
return bytes(out)
if actual != CRYPTO_BYPASS_ORIG:
raise ValueError(
f"Crypto-bypass site at file offset 0x{CRYPTO_BYPASS_OFFSET:x} "
f"does not contain the expected original bytes. Got "
f"{actual.hex()}, expected {CRYPTO_BYPASS_ORIG.hex()} (or "
f"already-patched {CRYPTO_BYPASS_PATCHED.hex()}). The input "
f"is not the preloader revision this script was designed "
f"for; refusing to patch."
)
out[CRYPTO_BYPASS_OFFSET:CRYPTO_BYPASS_OFFSET + 4] = CRYPTO_BYPASS_PATCHED
return bytes(out)
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
ap.add_argument("--input", "-i", required=True,
help="Stock BraX3 preloader "
"(preloader_k6835v1_64.bin, 386,004 bytes).")
ap.add_argument("--output", "-o", required=True,
help="Path to write the patched preloader "
"(e.g. preloader_k6835v1_64_patched.bin).")
ap.add_argument("--dry-run", action="store_true",
help="Validate the patch sites and print what would "
"change, but do not write the output file.")
args = ap.parse_args()
if not os.path.exists(args.input):
print(f"ERROR: input file not found: {args.input}", file=sys.stderr)
return 1
with open(args.input, "rb") as f:
original = f.read()
# Sanity: every patch site must fit inside the file.
all_offsets = [o for o, _, _ in ASSERT_CALL_SITES] + [CRYPTO_BYPASS_OFFSET]
end = max(all_offsets) + 4
if end > len(original):
print(
f"ERROR: input is {len(original)} bytes but the highest patch "
f"site extends to 0x{end:x}. Refusing to patch.",
file=sys.stderr,
)
return 2
print(f"Input: {args.input}")
print(f" size: {len(original)} bytes")
print(f" SHA256: {hashlib.sha256(original).hexdigest()}")
print()
print("Patches:")
for offset, expected_orig, label in ASSERT_CALL_SITES:
actual = bytes(original[offset:offset + 4])
if actual == NOP4:
state = "already patched"
elif actual == expected_orig:
state = "will patch"
else:
state = "MISMATCH"
print(f" file 0x{offset:x} ({label}) [{state}]")
print(f" {expected_orig.hex()} bl 0x2f4d0 (mtk_wdt_reset)")
print(f" -> {NOP4.hex()} nop.n nop.n")
crypto_actual = bytes(original[CRYPTO_BYPASS_OFFSET:CRYPTO_BYPASS_OFFSET + 4])
if crypto_actual == CRYPTO_BYPASS_PATCHED:
crypto_state = "already patched"
elif crypto_actual == CRYPTO_BYPASS_ORIG:
crypto_state = "will patch"
else:
crypto_state = "MISMATCH"
print(f" file 0x{CRYPTO_BYPASS_OFFSET:x} (read32 deeper-auth failure return) [{crypto_state}]")
print(f" {CRYPTO_BYPASS_ORIG.hex()} mov.w r0, #0xffffffff")
print(f" -> {CRYPTO_BYPASS_PATCHED.hex()} movs r0, #0; movs r0, #0")
print()
patched = apply_patches(original)
if args.dry_run:
print("Dry run — output file not written.")
return 0
with open(args.output, "wb") as f:
f.write(patched)
print(f"Output: {args.output}")
print(f" size: {len(patched)} bytes (unchanged)")
print(f" SHA256: {hashlib.sha256(patched).hexdigest()}")
print()
print("Total bytes changed: 16 (4 patches x 4 bytes each).")
print("Every other byte in the file is identical to the input.")
return 0
if __name__ == "__main__":
sys.exit(main())