How it started
The people from the EEVBlog forum already did a lot of work on this topic. They figured out how to unlock functionality with just some simple commands.
This should work well for people who are not that familiar with command line tools, but want to use their oscilloscope to its full potential.
What did I do?
I ported all the windows batch files to a single bash script. It also has new functionality like using md5sum
to check if the backup needs to be recreated (checking if local copy is same as the one on the oszi).
I also worked on a script for breaking up a disk image into multiple filesystem images. This is useful for making a complete backup.
The full backup
The idea behind it was simple. We want to have a exact copy of the microSD card, but without the need of removing it. This means not breaking the warranty seal!
We used netcat to transfer the data to my laptop. Just run nc -l 1337 > backup.img
on the laptop and nc <ip> 1337 < /dev/block/mmcblk1
on the oszi. This will then send the data to the laptop and save it as backup.img
. This is a raw disk image, but it cannot be mounted directly, as it contains all 16 partitions.
The python script
This is where my partitions.py script comes into play. We did not find any partition table on the scope… Except, we found one. But it was… weird.
The place to look for was actually /proc/cmdline
.
Take a look:
rk3399_rigol:/ $ cat /proc/cmdline
/system/bin/sh: cat: /proc/cmdline: Permission denied
Oh, oops, I forgot.
rk3399_rigol:/ $ su # we do not need a password :)
rk3399_rigol:/ # cat /proc/cmdline
earlycon=uart8250,mmio32,0xff1a0000 swiotlb=1 coherent_pool=1m cma=257M androidboot.baseband=N/A androidboot.selinux=disabled androidboot.hardware=rk30board androidboot.console=ttyFIQ0 init=/init mtdparts=rk29xxnand:0x00002000@0x00002000(uboot),0x00002000@0x00004000(trust),0x00002000@0x00006000(misc),0x00008000@0x00008000(resource),0x0000C000@0x00010000(kernel),0x00010000@0x0001C000(boot),0x00020000@0x0002C000(recovery),0x00038000@0x0004C000(backup),0x00040000@0x00084000(cache),0x00400000@0x000C4000(system),0x00008000@0x004C4000(metadata),0x00000040@0x004CC000(verity_mode),0x00002000@0x004CC040(baseparamer),0x00000400@0x004CE040(frp),0x000FA000@0x004CE440(rigol),-@0x00600000(userdata) storagemedia=sd androidboot.oem_unlocked=0 uboot_logo=0x02000000@0xf5c00000 loader.timestamp=2023-08-23_11:38:38 SecureBootCheckOk=0 androidboot.mode=emmc
Nice.
We can now format this long string into this table (the format my script likes):
0x00002000@0x00002000(uboot)
0x00002000@0x00004000(trust)
0x00002000@0x00006000(misc)
0x00008000@0x00008000(resource)
0x0000C000@0x00010000(kernel)
0x00010000@0x0001C000(boot)
0x00020000@0x0002C000(recovery)
0x00038000@0x0004C000(backup)
0x00040000@0x00084000(cache)
0x00400000@0x000C4000(system)
0x00008000@0x004C4000(metadata)
0x00000040@0x004CC000(verity_mode)
0x00002000@0x004CC040(baseparamer)
0x00000400@0x004CE040(frp)
0x000FA000@0x004CE440(rigol)
-@0x00600000(userdata)
Here is my Code, but you can also find it on Github:
#!/usr/bin/env python
"""
This file is intended to be used after making a block-by-block copy of the micro sd card.
The partitions.txt file can be obtained by running `cat /proc/cmdline` on the oszi and then formatting it a bit.
"""
import os
import re
BACKUP_PATH = 'backups/backup.img'
PARTS_FOLDER = 'parts'
MULTIPLE_OF = 0x200 # 512 bytes
OFFSET_OFFSET = 0x2000
SKIP_NAMES = ['userdata']
# check if backup exists
if not os.path.exists(BACKUP_PATH):
raise FileNotFoundError(f'Backup file not found: {BACKUP_PATH}')
# check if parts folder exists
if not os.path.exists(PARTS_FOLDER):
os.mkdir(PARTS_FOLDER)
with open(BACKUP_PATH, 'rb') as img:
idx = 1
with open('partitions.txt', 'r') as f:
for line in f:
stripped = line.strip()
if stripped.startswith('#'):
continue
if not stripped:
continue
# format: hex@hex(str)
match = re.match(r'(0x[0-9a-fA-F]+|-)@(0x[0-9a-fA-F]+)\((.+)\)', stripped)
if not match:
raise ValueError(f'Invalid line: {stripped}')
size = match.group(1)
offset = match.group(2)
name = match.group(3)
if name in SKIP_NAMES:
print(f'Skipping {name}')
continue
try:
size = int(size, 16)
except ValueError:
if size != '-':
raise ValueError(f'Invalid size: {size}')
size = None
try:
offset = int(offset, 16)
except ValueError:
raise ValueError(f'Invalid offset: {offset}')
# split backup according to partitions.
img.seek((offset + OFFSET_OFFSET) * MULTIPLE_OF)
size = size * MULTIPLE_OF if size else None
with open(os.path.join(PARTS_FOLDER, f"{idx:02d}_{name}.bin"), 'wb') as part:
print(f'Writing partition {name} to {os.path.join(PARTS_FOLDER, name)} (pos: {img.tell()})')
while True:
if size is not None:
if size <= 0:
break
chunk = img.read(min(size, 1024 * 1024))
size -= len(chunk)
else:
chunk = img.read(1024 * 1024)
if not chunk:
break
part.write(chunk)
print(f'Partition {name} written to {os.path.join(PARTS_FOLDER, name)}')
idx += 1