Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
safeloader patching script for OpenWrt
#!/usr/bin/python3
# This file allows replacing or adding a TP-Link factory image partition to an OpenWrt
# safeloader factory image.
# The procedure to calculate the hash and the required salt are described at:
# https://github.com/openwrt/openwrt/blob/master/tools/firmware-utils/src/tplink-safeloader.c
import argparse
import binascii
import collections
import hashlib
import struct
class SafeloaderFirmware:
DESCRIPTION_OFFSET = 0x14
DESCRIPTION_SIZE = 0x1000
PART_TABLE_OFFSET = 0x1014
PART_TABLE_SIZE = 0x800
def __init__(self, description=b'', padding=None):
self.parts = collections.OrderedDict()
self.description = description
self.padding = padding
def set_part(self, name, data):
if name in self.parts:
self.parts[name] = data
else:
self.parts[name] = data
self.parts.move_to_end(name)
def get_part(self, name):
return self.parts[name]
def part_table(self):
template = 'fwup-ptn {name:s} base 0x{offset:05x} size 0x{size:05x}\t\r\n'
offset = self.PART_TABLE_SIZE
table = b''
for part_name,part_data in self.parts.items():
table += template.format(name=part_name, offset=offset, size=len(part_data)).encode('ascii')
offset += len(part_data)
table += b'\0'
data = bytearray([0xff]*self.PART_TABLE_SIZE)
data[0:len(table)] = table
return bytes(data)
def description_data(self):
data = bytearray([0xff]*self.DESCRIPTION_SIZE)
desc = self.description
if desc is not None:
data[0:4] = struct.pack('>I', len(desc))
data[4:4+len(desc)] = desc
return bytes(data)
def checksum(self):
# The calculated hash is seeded with a salt, followed by the FW image body
m = hashlib.md5()
m.update(bytes([
0x7a, 0x2b, 0x15, 0xed, 0x9b, 0x98, 0x59, 0x6d,
0xe5, 0x04, 0xab, 0x44, 0xac, 0x2a, 0x9f, 0x4e
]))
m.update(self.description_data())
m.update(self.part_table())
for name,part in self.parts.items():
m.update(part)
return m.digest()
def save_firmware(self, output_file):
checksum = self.checksum()
data = self.description_data()
data += self.part_table()
for name,part in self.parts.items():
data += part
data = self.checksum() + data
data = struct.pack('>I', len(data) + 4) + data
if self.padding is not None:
data += self.padding
output_file.write(data)
@classmethod
def from_file(cls, input_file):
def read_fw_ptn_list(fw):
fwup_end = fw.find(b'\0', 0)
if fwup_end > 0:
return [p.strip() for p in fw[:fwup_end].decode('ascii').splitlines()]
else:
return None
def parse_ptn(ptn):
tokens = ptn.split()
if len(tokens) != 6:
return None
elif tokens[0] != 'fwup-ptn' or tokens[2] != 'base' or tokens[4] != 'size':
return None
else:
return (tokens[1], int(tokens[3], base=16), int(tokens[5], base=16))
(fw_size,) = struct.unpack('>I', input_file.read(4))
checksum = input_file.read(0x10)
input_file.seek(cls.DESCRIPTION_OFFSET)
(desc_size,) = struct.unpack('>I', input_file.read(4))
if desc_size <= cls.DESCRIPTION_SIZE - 4:
desc = input_file.read(desc_size)
else:
desc = None
input_file.seek(cls.PART_TABLE_OFFSET)
bulk = input_file.read(fw_size-cls.PART_TABLE_OFFSET)
padding = input_file.read()
if len(padding) == 0:
padding = None
fw = cls(desc, padding)
part_list = read_fw_ptn_list(bulk)
for ptn in part_list:
name, base, size = parse_ptn(ptn)
fw.set_part(name, bulk[base:base+size])
if fw.checksum() != checksum:
print('invalid file checksum')
return fw
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Patch OpenWrt safeloader factory image')
parser.add_argument('-f', '--factory',
type=argparse.FileType('rb'),
required=True,
help='OpenWrt factory image to be patched')
parser.add_argument('-i', '--input',
type=argparse.FileType('rb'),
required=True,
help='Safeloader image to read patch partition from')
parser.add_argument('-p', '--patch',
type=str,
required=True,
help='Partion to patch into OpenWrt factory image')
parser.add_argument('-o', '--output',
type=argparse.FileType('wb'),
required=True,
help='output path for the patched file')
args = parser.parse_args()
# Read OpenWrt factory image
factory = SafeloaderFirmware.from_file(args.factory)
# Read patch source input file
patch_input = SafeloaderFirmware.from_file(args.input)
if args.patch is not None:
if args.patch in patch_input.parts:
print(f'Patching "{args.patch}" into OpenWrt factory image...')
factory.set_part(args.patch, patch_input.get_part(args.patch))
else:
print(f'Could not find firmware part "{args.patch}" in input image')
# Write updated factory image
if args.output:
factory.save_firmware(args.output)
else:
print('Patched factory image not saved, resulting partition table:')
for name, data in factory.parts.items():
print(f'\t{name:s} +0x{len(data):06x}')
@billzhong
Copy link

billzhong commented May 24, 2021

Hi,
Is it possible to patch tp-link's US stock firmware? I accidentally flashed CA stock firmware to US device.
Thanks.

@svanheule
Copy link
Author

svanheule commented May 24, 2021

This script isn't really intended to produce variants of TP-Link's images. Instead, the goal is to adapt the (universal) OpenWrt factory-install images for other regions.

You could (in theory) use this script to create a CA version of the OpenWrt factory-install image, replace the "product-info" partition by the US version while running OpenWrt, and then revert back to TP-Link's US firmware. Note that I would rather recommend you to just trade in your device, since this is an advanced procedure, requiring quite a few steps you need to get right.

@beneficadoramocaba
Copy link

beneficadoramocaba commented Feb 26, 2022

@svanheule Greetings. I got here by way of the "bad file" thread you linked above.

  • Are you aware of whether anyone has succeeded in the procedure you describe, to return the device to it's stock US firmware?
  • Do the links you have provided above, contain sufficient information for me to attempt this? (Especially, the partition replacement you mentioned)

Sadly, the CA releases are significantly delayed (even though there are no functional differences that I can see) - for example, US v5.0.4 was released over 4 months ago, and contains security patches, but is still unavailable from the CA site.

Thanks for your time.

@svanheule
Copy link
Author

svanheule commented Feb 26, 2022

Due to the way partitions on NOR flash work in Linux, it's a bit trickier than I suggested here. I actually had a more detailed look a few days later. You would need to modify mtd2, which also contains device-specific info like the MAC address of your device.

I think you can also use this script to modify the US stock firmware instead of OpenWrt. Just use the latest US firmware instead of the OpenWrt factory image, and the other steps should be identical.

@beneficadoramocaba
Copy link

beneficadoramocaba commented Feb 28, 2022

That looks like just what I need, thanks!

@kaelbashir
Copy link

kaelbashir commented May 4, 2022

hey @beneficadoramocaba; were you able to modify the latest US firmware as suggested by @svanheule ?

@beneficadoramocaba
Copy link

beneficadoramocaba commented May 6, 2022

@kaelbashir I did not make the attempt. The problem that led me to flash the "possessive" regional firmware seems to have resolved itself, so the device is back in use for now.

@Elabajaba
Copy link

Elabajaba commented Sep 6, 2022

I'm running into an issue with TPLink EAP235-wall firmware (v3.1.0) where it crashes when I'm trying to patch the openwrt factory image.

Running python patch-safeloader.py -f openwrt-factory.bin -i temp.bin -p soft-version -o patched.bin on Fedora 36

Traceback (most recent call last):
  File "/home/redacted/openwrt-temp/patch-safeloader.py", line 152, in <module>
    patch_input = SafeloaderFirmware.from_file(args.input)
  File "/home/redacted/openwrt-temp/patch-safeloader.py", line 118, in from_file
    part_list = read_fw_ptn_list(bulk)
  File "/home/redacted/openwrt-temp/patch-safeloader.py", line 87, in read_fw_ptn_list
    return [p.strip() for p in fw[:fwup_end].decode('ascii').splitlines()]
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe3 in position 1: ordinal not in range(128)

I ran ./tplink-safeloader -z 'EAP235-WALLv1_3.1.0_[20210721-rel46004]_up_signed.bin' -o temp.bin to generate the safeloader image from the official tplink US firmware.

edit: Python version is 3.10.6

@svanheule
Copy link
Author

svanheule commented Sep 7, 2022

I ran ./tplink-safeloader -z 'EAP235-WALLv1_3.1.0_[20210721-rel46004]_up_signed.bin' -o temp.bin to generate the safeloader image from the official tplink US firmware.

There's your problem. The patching script takes the original firmware image as input. TP-Link provides these images in the safeloader format, so there's nothing you need to do before feeding it to the script. You converted it into an OpenWrt sysupgrade image first, which has the wrong format.

@Elabajaba
Copy link

Elabajaba commented Sep 7, 2022

Running it directly on the TPLink firmware gives an invalid file checksum error (tested on v3.0 and v3.1, on both US and Canadian firmware). Attempting to update the AP with the output patched-factory.bin also fails.

$ python patch-safeloader.py -f openwrt-factory.bin -i EAP235-30.bin  -p soft-version -o patched.bin
invalid file checksum
Patching "soft-version" into OpenWrt factory image...

@muellerj
Copy link

muellerj commented Sep 15, 2022

I get the same error message:

$ bin/patch-safeloader -f src/openwrt-firmware-eap235wall.bin -i src/official-firmware-3p1p0-eap235wall.bin -p soft-version -o out/openwrt-firmware-eap235wall-patched.bin
invalid file checksum
Patching "soft-version" into OpenWrt factory image...

The successfull version mirror (https://svanheule.net/files/eap235-wall-v1-squashfs-factory-v3-test.bin) also seems to be down, any chance this could be hosted on openwrt in the meanwhile?

Thanks in advance!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment