v380 IPcam: Firmware patching

I made some progress:

  • Running code from a micro SD card as root
  • Downloading camera firmware
  • Writing custom firmware patches
  • Finding the root password hash
  • Changing the root password

To see what’s going on, you’ll need to pop open the camera casing and connect USB UART to screen or similar as in #1. Also you need a micro SD card.


To actually get RTSP working on the camera, I had to patch the firmware and enable RTSP via the ceshi.ini file. The patch file was provided on a forum from someone who emailed v380 tech support.

Structure of patch zip

Reverse-engineering that patch AK3918E-V200_V. .zip was my first step to understanding the camera. The zip file looks like this:

$ als AK3918E-V200_V.\ .zip
Archive:  AK3918E-V200_V. .zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       50  2019-07-13 10:18   local_update.conf
        0  2019-07-13 10:18   patch_reuse
        0  2019-07-13 10:18   updatepatch/
  3451562  2019-07-13 10:18   updatepatch/5d4315195544f84f54a52ac757ce200e.patch

where local_update.conf contains the name of the patch file (its MD5 hash)


To upgrade the camera, unzip to the root of an sd card, put the card in the camera and boot it up. You’ll hear 固件更新開始 (jiàn gēngxīn kāishǐ) and after a few minutes it should reboot itself.

Binwalk patch

The structure of the patch file wasn’t clear (file just said it was data), so I ran binwalk -e on it. The most interesting file it extracted was a squashfs file system containing some ARM32 ELFs.

as9nvserver     gpiotest    log_server  prerun
as9updatednsip  gzip        motor_test  recorder
daemon          hu_updater  mqtt_test   vsipbroadcast
eventhub_core   hwwtd       mvrtsp      wpa_cli_lite

libcurl.so.4                 libmvs_ctk.so         libmvs_tls.so
libHYWideAreaObjectTrack.so  libmvs_lch_client.so  libonvif.so
libmval_eventhub.so          libmvs_lch.so         libopencv_core.so
libmvs_assisttools.so        libmvs_mdq.so         libopencv_imgproc.so
libmvs_clog.so               libmvs_memp.so
libmvs_core.so               libmvs_pcc.so

akcamera.ko        aw_gpio_moto_driver.ko  sensor_h42.ko     sensor_sc1135.ko
akcamera_n1.ko     i2c-gpio-soft.ko        sensor_h62.ko     sensor_sc1145.ko
akcamera_n2.ko     mv_motor_driver.ko      sensor_sc1035.ko  sensor_sc1235.ko
ak_gpio_i2c.ko     otg-hs.ko               sensor_sc1037.ko  sensor_sc1245.ko
ak_sar_adc_drv.ko  sensor_gc1034.ko        sensor_sc1045.ko

I then spent some time looking through strings with rabin2 -zzz and grepping for things.

hu_updater / prerun

I loaded hu_updater into Ghidra and it did really well at disassembling ARM code. (I should really have looked at prerun, but the relevant code is similar).

Patch file format

From this I was able to reverse-engineer the patch file format. It uses little-endian 32-bit integers and null-terminated strings.

The header of size 0x80 needs the following parts:

  • 0x00: 0x0a
  • 0x04: V380E2_C / V380E2_CA This is the hwname defined in /mnt/mtd/mvconf/patchmanage.conf which depends on the firmware version you have. If this is wrong, there will be a log message containing the expected value: patch not match <V380E2_CA>:<V380E2_C> failed..
  • 0x14: 0x1f4b59 a version
  • 0x18: Number of files in the patch

Each file contains a header of size 0x40 before the file contents:

  • The filename
  • 0x38: File size

As an example, the patch file from the zip file looks like:

00000000  0a 00 00 00 56 33 38 30  45 32 5f 43 00 00 00 00  |....V380E2_C....|
00000010  00 00 00 00 59 4b 1f 00  15 00 00 00 aa aa 34 00  |....YK........4.|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000080  65 78 73 68 65 6c 6c 5f  61 66 75 2e 73 68 00 00  |exshell_afu.sh..|
00000090  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000000b0  00 00 00 00 00 00 00 00  1b 05 00 00 87 7f 00 00  |................| # Length 0x051b
000000c0  23 21 2f 62 69 6e 2f 73  68 0a 0a 6e 65 65 64 5f  |#!/bin/sh..need_|
... Contents of exshell_afu.sh ...
000005c0  6d 74 64 2f 6d 76 63 6f  6e 66 2f 70 61 74 63 68  |mtd/mvconf/patch|
000005d0  6d 61 6e 61 67 65 2e 63  6f 6e 66 65 78 73 68 65  |manage.confexshe| # EOF / next header
000005e0  6c 6c 5f 62 66 75 2e 73  68 00 00 00 00 00 00 00  |ll_bfu.sh.......|

I wrote a tool bcaller/v380-ipcam-firmware-patch so you can see the files in your patch.

Updater sequence

  1. The patch MD5 and hwname are checked
  2. Files are extracted from the patch to subfolders of /tmp/hu_files_tmpdir/ depending on their prefix
  3. If it exists, exshell_bfu.sh is executed (bfu = before update)
  4. Generic files and sound files are copied over to /mnt/mtd/ and /mnt/mtd/mvsound/ respectively
  5. Kernel and MTD images are flashed using /sbin/updater
  6. If it exists, exshell_afu.sh is executed

Run code as root

Let’s have fun and make a patch containing exshell_bfu.sh e.g.:

set -x
echo HELLO
/mnt/mtd/audiofile_player /mnt/mtd/mvsound/sf_sysstarting_en.wav 0
ls /
ls /etc/
ls /mnt/
cat /etc/passwd
cat /etc/shadow
ps waxfu
sleep 30

Use patchv380, copy the patch to /sdcard/updatepatch/ and alter the patchmd5 in local_update.conf. Then boot up the camera. The sleep 30 is there so that you have time to unplug the camera before it runs post-update code (probably not harmful either way).

IFCReadStringOnce Warning:no such file(/tmp/hu_files_tmpdir/exshells/patchrule.sh)
+ echo HELLO
+ /mnt/mtd/audiofile_player /mnt/mtd/mvsound/sf_sysstarting_en.wav 0
=== play type : 0 ===
--AudioFilter Version V1.8.00_svn5047, type:8000--
## ERROR: CHIP(106552) unsupported
can't open the sd filter!
open sdfilter failed!!!!
Play Finished
+ whoami
+ ls /
bin   dev   etc   ext   init  lib   mnt   mvs   proc  sbin  sys   tmp   usr   var
+ ls /etc/
bak            hosts          ld.so.conf     profile        sysconfig
fstab          init.d         mdev.conf      resolv.conf
group          inittab        nsswitch.conf  services
host.conf      jffs2          passwd         shadow
+ ls /mnt/
mtd     nand    sdcard
+ cat /etc/passwd

+ cat /etc/shadow

+ps waxfu
    1 root       0:01 init
...  # I removed the kernel threads in [square brackets] for brevity
  362 root       0:00 syslogd -n -O /var/log/messages -s 200 -b 3
  363 root       0:00 klogd -n
  371 root       0:00 {vg_boot.sh} /bin/sh /mnt/mtd/vg_boot.sh
  375 root       0:00 /tmp/prerun
  388 root       0:00 sh -c /tmp/hu_files_tmpdir/exshells/exshell_bfu.sh
  389 root       0:00 {exshell_bfu.sh} /bin/sh /tmp/hu_files_tmpdir/exshells/ex
  402 root       0:00 /sbin/getty -L ttySAK0 115200 vt100
  404 root       0:00 ps waxfu

That sha256crypt password in the shadow file ($5$EvgtGUo1zRnZRW$Ge399ZNp3EYQP1NJt7MF1fbYjfnhtloG5m1N2KCp9l0) looks tough, so I tried to change the password but failed.

Changing password for root
New password: 
Retype password: 
passwd: /etc/passwd: Read-only file system
passwd: can't update password file /etc/passwd

Not too upset as I have root anyway. We’ll come back to it.

Firmware flasher

I extracted the hardware flasher cp /sbin/updater /mnt/sdcard/updater and loaded it into Ghidra to see how firmware updates work. Updating the kernel, nand, serial number, LOGO (not sure what that is) and mac address use ioctl magic, whereas updating the MTDs looks simpler.

There are 5 mtd parts:

MTD Update file prefix Type
1 IMG_RFS SquashFS
2 IMG_USR SquashFS
3 IMG_MVS SquashFS
4 IMG_EXT SquashFS
5 IMG_JFS jffs2

To update mtd1, the patch must contain a squashfs file with filename beginning with IMG_RFS. /dev/mtd1 is erased using an ioctl, but then we just write the squashfs file system to /dev/mtd1.

Grabbing the firmware

+ ls /dev/
akfha_char          mem                 mtdblock0           random
akgpio              mmcblk0             mtdblock1           rfkill
akpcm_cdev0         mmcblk0p1           mtdblock2           root
akpcm_cdev1         mtd0                mtdblock3           rtc0
console             mtd0ro              mtdblock4           tty
cpu_dma_latency     mtd1                mtdblock5           ttySAK0
full                mtd1ro              network_latency     ttyp0
i2c-0               mtd2                network_throughput  ttyp1
ion                 mtd2ro              null                ttyprintk
isp_char            mtd3                ptmx                uio0
kmsg                mtd3ro              pts                 urandom
log                 mtd4                ptyp0               watchdog
loop-control        mtd4ro              ptyp1               zero
loop0               mtd5                ram0
loop1               mtd5ro              ram1

The “files” /dev/mtd? are character devices, so function like byte streams. We can make an exshell_bfu.sh patch e.g. cat /dev/mtd4ro > /mnt/sdcard/mtd4 to extract the contents of the MTD parts.

Changing the root password

If I want to set the root password to Hello.123, The shadow file hash can be changed to:

$ mkpasswd --method=sha256crypt --salt=EvgtGUo1zRnZRW Hello.123

Looking through the files in the MTDs downloaded to the sdcard (with unsquashfs -lls), I found /etc/password in mtd1. Now we can make a cheeky update patch:

$ sudo unsquashfs -d mtdA mtd1
Parallel unsquashfs: Using 4 processors
206 inodes (230 blocks) to write
created 58 files
created 26 directories
created 148 symlinks
created 0 devices
created 0 fifos
$ sed -i 's/^root:[^:]*:/root:$5$EvgtGUo1zRnZRW$2A2sE5yjjsR2K6QJH0Te2rKOUGaCRXiEIgdr9e5KlO0:/' mtdA/etc/shadow
$ mksquashfs mtdA IMG_RFS_mtd1.squashfs -b 128K -comp xz

We used sudo so that we unsquash files with the correct owners (e.g. uid=0). Check that the output of unsquashfs -s looks the same for the old mtd1 and patched IMG_RFS_mtd1.squashfs.

$ patchv380 IMG_RFS_mtd1.squashfs -h V380E2_CA

Let’s run it

Patch copied over to the sdcard correctly (as explained in the zip section above). Camera booting up.

copy finished normally.
copy finished normally.
==== access [/dev/mtd1]
[updater]erase mtd start...........................erase success!

write mtd start.........................................write OK!
hardware update finished normally === has img:[1] , has spec:[2]
=== update success! ===
sdcard update finished!
Restarting system.

Now we’ve successfully flashed our system! Time for some fun…

V380E login: root

welcome to file system
[root@V380E ~]$