-
-
Save pexcn/009a66cc2a0edc8f06aa to your computer and use it in GitHub Desktop.
Script to patch an Android recovery.img file, changing the qcom,qpnp-rtc-write Device Tree property (tested with an Android 5.1 image based on kernel 3.4 for a Nexus 5 (rev_11)).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
""" | |
Patch a recovery image to enable RTC writing | |
Unfortunately it has not the intended effect (tested from recovery): | |
~ # /system/xbin/hwclock -w | |
hwclock: RTC_SET_TIME: Operation not permitted | |
<3>[ 25.821247] spmi_pmic_arb fc4cf000.qcom,spmi: pmic_arb_wait_for_done: transaction denied (0x5) | |
<3>[ 25.821338] qcom,qpnp-rtc qpnp-rtc-ee162000: SPMI write failed | |
<3>[ 25.821408] qcom,qpnp-rtc qpnp-rtc-ee162000: Write to RTC reg failed | |
""" | |
__author__ = "Peter Wu" | |
__email__ = "[email protected]" | |
__license__ = "MIT" | |
import argparse, hashlib, logging, struct, sys | |
_logger = logging.getLogger(__name__) | |
def check(actual, expected, message): | |
if expected != actual: | |
raise RuntimeError("CHECK (%s == %s) failed: %s" % \ | |
(expected, actual, message)) | |
# Expected page size | |
PAGE_SIZE = 2048 | |
uint32_le_type = struct.Struct("<I") # For boot header | |
uint32_be_type = struct.Struct(">I") # For FDT | |
# unsigned 32-bit integers | |
KERNEL_SIZE_OFFSET = 8 | |
RAMDISK_SIZE_OFFSET = 16 | |
SECOND_SIZE_OFFSET = 24 | |
PAGE_SIZE_OFFSET = 36 | |
CHECKSUM_OFFSET = 576 | |
CHECKSUM_LENGTH = 20 | |
# Node name containing the qcom,qpnp-rtc-write property. | |
QCOM_RTC_NODE_NAME = b"qcom,pm8941_rtc" | |
# offset in structure for qcom,qpnp-rtc-write int property | |
QPNP_RTC_WRITE_PROP_NAMEOFF = 0x00001fa7 | |
# https://github.com/we3des/psychic-octo-robot/wiki/Android-boot.img-zImage-format | |
# https://github.com/android/platform_system_core/blob/master/mkbootimg/bootimg.h | |
def parse_boot_img_header(block): | |
check(block.startswith(b"ANDROID!"), True, "Boot magic") | |
kernel_size, = uint32_le_type.unpack_from(block, KERNEL_SIZE_OFFSET) | |
ramdisk_size, = uint32_le_type.unpack_from(block, RAMDISK_SIZE_OFFSET) | |
second_size, = uint32_le_type.unpack_from(block, SECOND_SIZE_OFFSET) | |
page_size, = uint32_le_type.unpack_from(block, PAGE_SIZE_OFFSET) | |
check(page_size, PAGE_SIZE, "page size") | |
checksum = block[CHECKSUM_OFFSET:CHECKSUM_OFFSET+CHECKSUM_LENGTH] | |
return kernel_size, ramdisk_size, second_size, checksum | |
# SHA1(kernel | kernel_size | initrd | initrd_size | extra | extra_size) | |
def calculate_checksum(kernel, ramdisk, second): | |
h = hashlib.sha1() | |
for item in (kernel, ramdisk, second): | |
h.update(item) | |
h.update(uint32_le_type.pack(len(item))) | |
return h.digest() | |
def calc_padding_size(size, block_size=4): | |
"""Returns the needed padding count to align data at word size.""" | |
return (block_size - (size % block_size)) % block_size | |
def print_size(label, size): | |
_logger.info("%s", "{0:<19} {1:#010x} ({1})".format(label + ":", size)) | |
def read_aligned(f, size): | |
data = f.read(size) | |
padding_size = calc_padding_size(size, PAGE_SIZE) | |
return data, f.read(padding_size) | |
def read_image(image_filename): | |
with open(image_filename, "rb") as f: | |
header = f.read(PAGE_SIZE) | |
kernel_size, ramdisk_size, second_size, checksum = \ | |
parse_boot_img_header(header) | |
print_size("Kernel size", kernel_size) | |
print_size("Ramdisk size", ramdisk_size) | |
print_size("Second stage size", second_size) | |
kernel, kernel_padding = read_aligned(f, kernel_size) | |
ramdisk, ramdisk_padding = read_aligned(f, ramdisk_size) | |
second, second_padding = read_aligned(f, second_size) | |
checksum_expected = calculate_checksum(kernel, ramdisk, second) | |
check(checksum, checksum_expected, "Invalid checksum") | |
trailer = f.read() | |
check(0, len(trailer), "expected no trailing data") | |
return [header, kernel, kernel_padding, ramdisk, ramdisk_padding, | |
second, second_padding] | |
def write_image(image_filename, blocks): | |
with open(image_filename, "wb") as f: | |
for block in blocks: | |
f.write(block) | |
# Scan for structure block in FDT | |
# https://www.power.org/wp-content/uploads/2012/06/Power_ePAPR_APPROVED_v1.1.pdf#page=91 | |
def find_structure_block(fdt, node_name, pos=0): | |
assert pos % 4 == 0 | |
byteseq = b'\x00\x00\x00\x01' | |
byteseq += node_name | |
byteseq += b'\x00' * calc_padding_size(len(node_name)) | |
_logger.debug("Looking for %s in %d bytes", byteseq, len(fdt)) | |
# For each possible FDT_BEGIN_NODE, find properties. | |
while True: | |
pos = fdt.find(byteseq, pos) | |
_logger.debug("FDT_BEGIN_NODE query result: %d", pos) | |
if pos == -1: | |
break | |
pos += len(byteseq) # skip FDT_BEGIN_NODE | |
while True: | |
token_type, = uint32_be_type.unpack_from(fdt, pos) | |
_logger.debug("FDT token type: %#010x", token_type) | |
pos += 4 # skip token type | |
if token_type == 2: # FDT_END_NODE | |
break # Nothing interesting, look for next node. | |
elif token_type == 4: # FDT_NOP | |
pass # ignore, no data | |
elif token_type == 3: # FDT_PROP | |
# Scan for property (uint32_t len, namelen; uint8_t value[len]) | |
prop_len, = uint32_be_type.unpack_from(fdt, pos) | |
prop_nameoff, = uint32_be_type.unpack_from(fdt, pos + 4) | |
pos += 8 | |
_logger.debug("Prop len=%d, nameoff=%d", prop_len, prop_nameoff) | |
if prop_nameoff == QPNP_RTC_WRITE_PROP_NAMEOFF: | |
return pos | |
# skip value and padding | |
pos += prop_len + calc_padding_size(prop_len) | |
else: | |
# Perhaps we did not run into a FDT node... | |
_logger.warning("Unexpected token type %d", token_type) | |
break | |
return pos | |
def iter_props(kernel): | |
pos = 0 | |
while pos != -1: | |
pos = find_structure_block(kernel, QCOM_RTC_NODE_NAME, pos) | |
if pos != -1: | |
_logger.info("Found property value at offset %d", pos) | |
yield pos | |
def patch_kernel(kernel, new_value): | |
kernel = bytearray(kernel) | |
for pos in iter_props(kernel): | |
value, = uint32_be_type.unpack_from(kernel, pos) | |
if value != new_value: | |
_logger.info("Setting qcom,qpnp-rtc-write = <%#x>", 1) | |
kernel[pos + 3] = new_value | |
else: | |
_logger.info("Not changing qcom,qpnp-rtc-write = <%#x>", value) | |
return kernel | |
parser = argparse.ArgumentParser() | |
parser.add_argument("-d", "--debug", action="store_true", | |
help="Enable verbose debug logging") | |
parser.add_argument("-n", "--dry-run", action="store_true", | |
help="Avoid writing the file") | |
parser.add_argument("image_file", help="recovery.img file") | |
parser.add_argument("enable_write", type=int, choices=(0,1), | |
help="Enable or disable RTC writing") | |
def main(): | |
args = parser.parse_args() | |
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO, | |
format="%(name)s: %(message)s") | |
image_filename = args.image_file | |
new_value = args.enable_write | |
blocks = read_image(image_filename) | |
header = blocks[0] | |
kernel = blocks[1] | |
ramdisk = blocks[3] | |
second = blocks[5] | |
kernel = patch_kernel(kernel, new_value) | |
checksum = calculate_checksum(kernel, ramdisk, second) | |
header = header[0:CHECKSUM_OFFSET] + checksum + \ | |
header[CHECKSUM_OFFSET+CHECKSUM_LENGTH:] | |
blocks[0:2] = header, kernel | |
if not args.dry_run: | |
write_image(image_filename, blocks) | |
else: | |
_logger.info("Would write new image %s", image_filename) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment