Skip to content

Instantly share code, notes, and snippets.

@svanheule
Created October 21, 2020 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save svanheule/273fd573c43e3cb0300c4ea31e6bfed0 to your computer and use it in GitHub Desktop.
Save svanheule/273fd573c43e3cb0300c4ea31e6bfed0 to your computer and use it in GitHub Desktop.
TP-Link OC200 image validation
#!/usr/bin/python3
import argparse
import binascii
import hashlib
import struct
# Image header starts with four uint32_be, followed by an MD5 digest
# * version (?)
# * magic number (0xaa55d98f)
# * header length (0x1000)
# * total file size, including header
# * MD5 digest
#
# The MD5 digest in the header must be calculated as follows:
# 1. Set [0x10:0x20] to zero (storage area for the MD5 digest)
# 2. Set [0x130:0x1b0] to zero (storage area for the RSA signature)
#
# At an offset of 0x130, an RSA signature is present (in reversed byte order).
# The public key is:
# BgIAAACkAABSU0ExAAQAAAEAAQD9lxDCQ5DFNSYJBriTmTmZlE
# MYVgGcZTO+AIwmdVjhaeJI6wWtN7DqCaHQlOqJ2xvKNrLB+wA1
# NxUh7VDViymotq/+9QDf7qEtJHmesjirvPN6Hfrf+FO4/hmjbV
# XgytHORxGta5KW4QHVIwyMSVPOvMC4A5lFIh+D1kJW5GXWtA==
# The image signature is a SHA-1 digest calculated the following way:
# 1. Set 0x80 bytes starting at 0x130 to zero; this is the storage area for the signature
# 2. Drop the first 0x14 bytes of the image
# 3. Calculate the MD5 digest of the remaining data
# 4. Calculate the SHA-1 digest of the MD5 digest
struct_header = struct.Struct('>4I16s')
IMAGE_MAGIC = 0xaa55d98f
struct_img_part = struct.Struct('>32s5I')
parser = argparse.ArgumentParser('parse TP-Link mvebu image')
parser.add_argument('file', help='path to file')
parser.add_argument('-x', '--extract', action='store_true', help='extract data parts')
parser.add_argument('-v', '--verbose', action='store_true', help='print more info')
parser.add_argument('-s', '--signature', action='store_true', help='extract rsa signature')
parser.add_argument('-d', '--digest', action='store_true', help='calculate md5 digest')
args = parser.parse_args()
def read_at(f, offset, length):
f.seek(offset)
return f.read(length)
def print_part_table_header():
print('NAME | NAND? | OFFSET | PART SIZE | SEEK | LENGTH ')
print('---------------------------------+-------+----------+-----------+----------+----------')
def print_part_table_line(part):
print('{:32s} | {:1d} | {:8x} | {:8x} | {:8x} | {:8x}'.format(
part[0].decode('ascii').strip('\0'),
part[5],
part[1],
part[2],
part[3],
part[4]
))
with open(args.file, 'rb') as image:
header = struct_header.unpack(read_at(image, 0, struct_header.size))
signature = read_at(image, 0x130, 0x80)
version, magic, part_table_offset, file_size, checksum = header
rsa_signature = read_at(image, 0x130, 0x80)
image_data = bytearray(read_at(image, 0, file_size))
image_data[0x130:0x130+0x80] = bytearray(0x80)
signature_digest = hashlib.sha1(hashlib.md5(image_data[0x14:]).digest()).digest()
image_data[0x10:0x10+0x10] = bytearray(0x10)
image_digest = hashlib.md5(image_data).digest()
if args.verbose:
print(f'Header start: {version:08x} {magic:08x}')
print(f'File size: {file_size:x}')
print(f'Payload table offset: {part_table_offset:x}')
print('Checksum:', binascii.hexlify(checksum).decode('ascii'))
if checksum == image_digest:
print('Checksum correct')
else:
print('Checksum incorrect! Calculated {}'.format(
binascii.hexlify(image_digest).decode('ascii')
))
if args.signature:
print(binascii.hexlify(rsa_signature[::-1]).decode('ascii'))
if args.digest:
print(binascii.hexlify(signature_digest).decode('ascii'))
parts = dict()
end_of_table = False
img_parts_start = part_table_offset
offset_entry = img_parts_start
while not end_of_table:
img_part = read_at(image, offset_entry, struct_img_part.size)
offset_entry += struct_img_part.size
img_part = struct_img_part.unpack(img_part)
name = img_part[0].decode('ascii').strip('\0')
part_offset, part_size = img_part[1:3]
payload_offset, payload_size = img_part[3:5]
store_in_nand = bool(img_part[5])
if len(name) > 0:
parts[name] = img_part
end_of_table = (len(name) == 0) or (offset_entry == img_parts_start+32*struct_img_part.size)
if args.verbose:
print('Found {} parts'.format(len(parts)))
if len(parts):
print_part_table_header()
for name in parts:
print_part_table_line(parts[name])
if args.extract:
for name in parts:
offset, size = parts[name][3:5]
payload_data = read_at(image, offset, size)
with open(f'{name}.bin', 'wb') as part:
part.write(payload_data)
#!/bin/bash
DIGEST_CALC=$(./parse.py -d $1)
DIGEST_SIGN=$(./parse.py -s $1 | xxd -revert -plain | openssl rsautl -verify -pubin -inkey rsa.pem | xxd -s 15 -plain)
echo "Image contains $DIGEST_CALC"
echo "Calculated $DIGEST_SIGN"
@minanagehsalalma
Copy link

any write up that maybe help recreating this for other models ?

@svanheule
Copy link
Author

It's been a while since I've looked at this device, and I don't even have one anymore. I also don't know about any other devices that use the same image format.

If you have a firmware image using the same format as the OC200, verify.sh should be able to tell if the checksum is correct. parse.py can also extract the different data partitions embedded inside the firmware image, using the -x argument; ./parsy.py -h should provide you with a help output. Other than that, there's little more you can do with these scripts.

@minanagehsalalma
Copy link

@svanheule

I was able to extract the public key
BgIAAACkAABSU0ExAAQAAAEAAQCtdVIi5h5+e4v16PPyGj8o10hKva+bycG1F5TW7abW1RDK6PEanVepTEs0hVZgTL09z7taV3JyD7m2Mtfj6JTK0+U9VsPg61mvOmoHR0ibiy6mehK0KTqPO2gAUjmpAZhX32BKBFG8LPEJVNN0e/eeN1UDHpFwNzqYHdEF7tu4wA==
by opening /lib/libcmm.so from the fw upgrade file and ctrl + f for ==

trying parse.py TD-W9960V1_1.3.0_0.8.0_up_boot210831_2021-09-0_1631785075811.bin

shows this error

Traceback (most recent call last):
 File "C:\Users\ddddd\Folder45\parse.py", line 105, in <module>
   img_part = struct_img_part.unpack(img_part)
struct.error: unpack requires a buffer of 52 bytes

the firmware upgrade file right out of the configuration file
<Url val=http://download.tplinkcloud.com/firmware/TD-W9960V1_1.3.0_0.8.0_up_boot210831_2021-09-0_1631785075811.bin />

what's next ?

Thanks a lot for your reply : )

@svanheule
Copy link
Author

Probably you need to check if the image you're trying to process actually uses the same format as the OC200 does. This script mainly exists to illustrate how the image checksum works for OC200 firmware files. If you want to use this for something else, you're on your own to figure out if it's applicable or not.

@minanagehsalalma
Copy link

Probably you need to check if the image you're trying to process actually uses the same format as the OC200 does

@svanheule How Would i do that ?

@minanagehsalalma
Copy link

minanagehsalalma commented Jul 7, 2022

Probably you need to check if the image you're trying to process actually uses the same format as the OC200 does

@svanheule How Would i do that ?

all i need is a starting point

it can't be that no one on the whole internet explored modifying tp link images :_/

@svanheule
Copy link
Author

OpenWrt supports at least three TP-Link specific image formats, aside from a whole range of other image formats. I suggest you start exploring the firmware-utils repository, and use a hex editor or a tool like binwalk to explore the binary image.
https://git.openwrt.org/?p=project/firmware-utils.git;a=tree;f=src;hb=HEAD

If you need more support, please post your question elsewhere. I don't have time to look into some random device for you, and posting request for help here isn't going to draw much attention from anyone besides me.

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