Skip to content

Dump your backup firmware

roleo edited this page Jun 15, 2020 · 14 revisions

To save a restorable backup of your cam you need 2 files:

  • sys_xxx
  • home_xxx

where xxx is the suffix of your model (i.e. y203c)

The sys file is the root file system and the home file is the home partition (mounted on /home). Both files are jffs2 images. To save these files without a previous hack, you need to extract them through a serial connection. So you need some manual skills because you have to solder 3 wires.

Physical connection

  1. Open the cam removing screws.
  2. Find the 3 uart pads. Where are they? It depends on the model and it's not simple to identify them. Example: uart pads
  3. Connect the pads to a ttl-rs232/usb adapter (rx, tx and GND).
  4. Open putty with logging enabled.
  5. Switch on the cam. Now, if the connection is ok, you will see the uboot and kernel logs on the screen. Otherwise, restart from point 2.
  6. Wait for loading to complete.
  7. Stop the logging and save the boot log (eventually cleaned up from your data, if present).

Now you have a physical connection to the cam and you have a dump of the boot log (you could analyze it to get some info).

Analyze the log

In the log you will find the mtd partitions list. Example:

Creating 6 MTD partitions on "NOR_FLASH":
0x000000000000-0x000000050000 : "BOOT"
0x000000050000-0x0000001d0000 : "KERNEL"
0x0000001d0000-0x0000003b0000 : "ROOTFS"
0x0000003b0000-0x000000fe0000 : "HOME"
0x000000fe0000-0x000000ff0000 : "vd1"
0x000000ff0000-0x000001000000 : "conf"

The partitions that interest us are obviously "ROOTFS" (mtd2) and "HOME" (mtd3) so we have to write down the informations regarding the starting address (1st column), the ending address (2nd column) and the length (the difference) because we will need them later.

Prepare the software

Before you proceed to dump the partitions, prepare a python script named dump.py with the following content (thanks to @IonAgorria):

#!/usr/bin/env python3
__author__ = 'Ion Agorria'
import argparse
from datetime import datetime
from time import time
from serial import Serial

def visible(text):
    text.replace("\n", "\\n")
    text.replace("\r", "\\r")
    return text

def parse_line(line, size):
    # Break down each part
    if len(line) != size:
        raise Exception("Line size %s doesn't match %s: %s" % (len(line), size, visible(line)))
    i = 0
    addr = line[i:i + 8]
    i += 8
    assert line[i:i + 2] == ": "
    i += 2
    hex_data = line[i:i + 8]
    i += 8
    assert line[i:i + 1] == " "
    i += 1
    hex_data += line[i:i + 8]
    i += 8
    assert line[i:i + 1] == " "
    i += 1
    hex_data += line[i:i + 8]
    i += 8
    assert line[i:i + 1] == " "
    i += 1
    hex_data += line[i:i + 8]
    i += 8
    assert line[i:i + 4] == "    "
    i += 4
    text = line[i:i + 16]
    i += 16
    assert line[i:i + 2] == "\r\n"

    #Convert hex string into actual data
    data = []
    for i in range(0, len(hex_data), 2):
        piece = hex_data[i:i + 2]
        data.append(int(piece, 16))

    #For redundancy check if data matches text, but only for printable characters (non dot)
#    for i, piece in enumerate(data):
#        if text[i] != ".":
#            assert chr(piece) == text[i]

    return int(addr, 16), data, text


def write(serial, opts, data):
    if opts.debug:
        print("Debug write: " + visible(data))
    data = bytes(data, "ascii")
    serial.write(data)


def dump(serial, opts, log):
    data = []
    finish = False
    start_addr = opts.start
    last_addr = start_addr - 0x10
    do_reset = False

    # Restore the previous dump
    if opts.previous:
        loaded = False
        last_percentage = 0
        print("Info: loading from " + opts.previous)
        with open(opts.previous, 'r') as previous:
            lines = previous.readlines()
            count = len(lines)
            i = 0
            for line in lines:
                #Fix lines with only \n
                if line[-2:] != "\r\n":
                    line = line[:-1] + "\r\n"

                addr, line_data, text = parse_line(line, opts.size)

                #Check if we skipped some line
                if last_addr != addr - 0x10:
                    raise Exception("Possible skip or repetition, last address 0x%x doesn't match with previous address 0x%x" % (last_addr, addr - 0x10))
                last_addr = addr

                #Check if we reach end
                if addr > opts.end:
                    print("Info: Reached specified end address")
                    finish = True
                    break

                #Discard if not start
                if addr < opts.start:
                    print("Warning: address %s is lower than start address! discarding" % addr)
                    continue

                #Store line data and log
                data.append(bytes(line_data))
                if not opts.ignore_log:
                    log.write(line)
                    log.flush()

                #Print percentage
                i += 1
                percentage = int(round(i / count * 100))
                if last_percentage != percentage:
                    print("Info: (%i%%) %i/%i " % (percentage, count, i))
                last_percentage = percentage

            #If we reach here everything went fine
            loaded = True

        if not loaded:
            raise Exception("Dump was not restored completely, something went wrong")

        start_addr = last_addr + 0x10
        last_addr = start_addr - 0x10
        print("Info: finished loading previous")

    if not finish:
        # Send the initial command
        write(serial, opts, "md %s %s\n" % (hex(start_addr), hex(opts.step * 4)))
        do_reset = opts.reset

    while not finish:
        #Read response
        chunk = serial.readlines()
        if opts.debug:
            print("Debug read: " + str(chunk))

        assert len(chunk) > 0

        #Remove first one, its the command that we sent
        chunk = chunk[1:]

        #Iterate each line in chunk
        for line in chunk:
            line = line.decode("ascii")

            if opts.debug:
                print("line: " + str(line))
				
            #Adquire new chunk by sending newline
            if line == 'MStar # ':
                if opts.debug:
                    print("Debug: detected prompt, sending newline")
                write(serial, opts, "\n")
                continue

            addr, line_data, text = parse_line(line, opts.size)

            #Check if we skipped some line
            if last_addr != addr - 0x10:
                raise Exception("Possible skip or repetition, last address 0x%x doesn't match with previous address 0x%x" % (last_addr, addr - 0x10))
            last_addr = addr

            #Check if we reach end
            if addr > opts.end:
                print("Info: Reached specified end address")
                finish = True
                break

            #Discard if not start
            if addr < opts.start:
                print("Warning: address %s is lower than start address! discarding" % addr)
                continue

            #Print current line
            hex_addr = hex(addr).upper()[2:]
            while not len(hex_addr) == 8:
                hex_addr = "0" + hex_addr
            hex_data = ""
            for line_byte in line_data:
                line_byte = hex(line_byte).upper()[2:]
                if len(line_byte) == 1:
                    line_byte = "0" + line_byte
                hex_data += " " + line_byte
            print("0x%s %s |%s|" % (hex_addr, hex_data, text))

            #Store line data and log
            data.append(bytes(line_data))
            log.write(line)
            log.flush()

        if do_reset:
            print("Info: sending reset as requested")
            write(serial, opts, "reset\n")

    return data


def main():
    # Args parse
    parser = argparse.ArgumentParser()
    parser.add_argument("port", help="Serial port of device")
    parser.add_argument("baud", type=int, help="Serial baud rate")
    parser.add_argument("start",  help="Start address in dec or hex (with 0x), must be multiple of 16")
    parser.add_argument("end", help="End address in dec or hex (with 0x), must be multiple of 16")
    parser.add_argument("--step", type=int, default=256, help="Number of lines per dump chunk")
    parser.add_argument("--size", type=int, default=67, help="Total size of each line including spaces and newlines")
    parser.add_argument("--timeout", type=float, default=0.1, help="Timeout in secs for serial")
    parser.add_argument("--previous", help="Previous log to continue from")
    parser.add_argument("--ignore-log", action='store_true', help="Previous log is not saved on new log")
    parser.add_argument('--debug', action='store_true', help='Enables debug mode')
    parser.add_argument('--reset', action='store_true', help='Sends "reset" after finishing')
    opts = parser.parse_args()

    # Args conversion
    if opts.start[0:2] == "0x":
        opts.start = int(opts.start, 16)
    else:
        opts.start = int(opts.start)
    if opts.end[0:2] == "0x":
        opts.end = int(opts.end, 16)
    else:
        opts.end = int(opts.end)

    # Args check
    if opts.start % 16 != 0:
        raise Exception("start argument is not multiple of 16")
    if opts.start < 0:
        raise Exception("start argument is too low")
    if opts.end % 16 != 0:
        raise Exception("end argument is not multiple of 16")
    if opts.end <= opts.start:
        raise Exception("end argument is too low")
    if opts.step <= 0:
        raise Exception("step argument is too low")
    if opts.size <= 0:
        raise Exception("size argument is too low")
    if opts.timeout <= 0:
        raise Exception("timeout argument is too low")
    if opts.debug:
        print("Debug mode enabled")
    name = datetime.fromtimestamp(time()).strftime("%Y-%m-%dT%H%M%S") + "_" + hex(opts.start) + "_" + hex(opts.end)

    #Prepare to dump
    serial = Serial(port=opts.port, baudrate=opts.baud, timeout=opts.timeout)
    log = open(name + ".log", "w")
    try:
        data = dump(serial, opts, log)
    except Exception as e:
        raise e
    finally:
        serial.close()
        log.close()

    #Write to file
    file = open(name + ".img", "wb")
    try:
        for line in data:
            file.write(line)
    except Exception as e:
        raise e
    finally:
        file.close()


main()

And another python script named swap.py with the following content:

#!/usr/bin/env python3
import os
import sys, getopt

def main(argv):
	inputfile = ''
	outputfile = ''
	try:
		opts, args = getopt.getopt(argv,"hi:o:",["ifile=","ofile="])
	except getopt.GetoptError:
		print('swap.py -i <inputfile> -o <outputfile>')
		sys.exit(2)
	for opt, arg in opts:
		if opt == '-h':
			print('swap.py -i <inputfile> -o <outputfile>')
			sys.exit()
		elif opt in ("-i", "--ifile"):
			inputfile = arg
		elif opt in ("-o", "--ofile"):
			outputfile = arg
	print('Input file is "', inputfile)
	print('Output file is "', outputfile)

	fi = open(inputfile, 'rb')
	fo = open(outputfile, 'wb')

	while True:
		a = fi.read(4)
		if (not a):
			break
		fo.write(a[::-1])

	fi.close
	fo.close

if __name__ == "__main__":
   main(sys.argv[1:])

Dump sys_xxx (rootfs):

  1. Open putty with logging enabled.
  2. Switch on the cam.
  3. Stop the boot at the U-Boot prompt (should be "MStar #") pressing "Enter".
  4. Run the following commands to initialize the SPI bus and copy the partition content to RAM. Copy from offset 0x1d0000, length 0x1e0000 to RAM at address 0x21000000:
sf probe
sf read 0x21000000 0x1d0000 0x1e0000
  1. Close putty.
  2. Open a python 3.x console and run the script with the following options:
py dump.py --timeout 1 --step 64 comX 115200 0x21000000 0x211dfff0

Where comX is the port of your serial adapter, 0x21000000 is the RAM start address and 0x211dfff0 is the RAM end address (-16 bytes).

  1. Save the file as sys_xxx, where xxx is the suffix of your model (i.e. y203c)

Dump home_xxx (home partition):

  1. Open putty with logging enabled.
  2. Switch on the cam.
  3. Stop the boot at the U-Boot prompt (should be "MStar #") pressing "Enter".
  4. Run the following commands to initialize the SPI bus and copy the partition content to RAM. Copy from offset 0x3b0000, length 0xc30000 to RAM at address 0x21000000:
sf probe
sf read 0x21000000 0x3b0000 0xc30000
  1. Close putty.
  2. Open a python 3.x console and run the script with the following options:
py dump.py --timeout 1 --step 64 com4 115200 0x21000000 0x21c2fff0

Where comX is the port of your serial adapter, 0x21000000 is the RAM start address and 0x21c2fff0 is the RAM end address (-16 bytes).

  1. Save the file as home_xxx, where xxx is the suffix of your model (i.e. y203c)

Swap endianness

The files dumped with the previous procedure have one last problem. Before they can be mounted as jffs2 filesystem or used for a restore, they must be reordered because the endianness is wrong. Run the swap.py script:

py swap.py -i sys_xxx -o sys_xxx.swapped
py swap.py -i home_xxx -o home_xxx.swapped

Notes

Please note that rootfs partition contains a file /etc/back.bin with a backup of your configuration (WiFi SSID, password, etc...). If you want to clean it, unpack the file system using jefferson and repack it using mkfs.jffs2.

  • jefferson sys_xxx.swapped -d outdir
  • Clean...
  • mkfs.jffs2 -l -e 0x10000 -r outdir -o sys_xxx.final

If you want to reduce the file size unpack and repack home_xxx also.

  • jefferson home_xxx.swapped -d outdir
  • mkfs.jffs2 -l -e 0x10000 -r outdir -o home_xxx.final